Skip to content

Commit 9e68881

Browse files
committed
DenkoviRelay: add resource, driver and export
Add support for denkovi usb serial relay boards (https://denkovi.com/) Signed-off-by: Diogo Silva <[email protected]>
1 parent 0288e4b commit 9e68881

File tree

11 files changed

+254
-1
lines changed

11 files changed

+254
-1
lines changed

doc/configuration.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,26 @@ Arguments:
547547
Used by:
548548
- `HIDRelayDriver`_
549549

550+
DenkoviRelay
551+
++++++++
552+
An :any:`DenkoviRelay` resource describes a single output of a Denkovi relay board.
553+
554+
.. code-block:: yaml
555+
556+
DenkoviRelay:
557+
index: 2
558+
invert: false
559+
match:
560+
ID_PATH: 'pci-0000:00:14.0-usb-0:2:1.0'
561+
562+
Arguments:
563+
- index (int, default=1): number of the relay to use
564+
- invert (bool, default=False): whether to invert the relay
565+
- match (dict): key and value pairs for a udev match, see `udev Matching`_
566+
567+
Used by:
568+
- `DenkoviRelayDriver`_
569+
550570
HttpDigitalOutput
551571
+++++++++++++++++
552572
An :any:`HttpDigitalOutput` resource describes a generic digital output that
@@ -2415,6 +2435,26 @@ Implements:
24152435
Arguments:
24162436
- None
24172437

2438+
DenkoviRelayDriver
2439+
~~~~~~~~~~~~~~
2440+
An :any:`DenkoviRelayDriver` controls an `DenkoviRelay`_ or `NetworkDenkoviRelay`_ resource.
2441+
It can set and get the current state of the resource.
2442+
2443+
Binds to:
2444+
relay:
2445+
- `DenkoviRelay`_
2446+
- `NetworkDenkoviRelay`_
2447+
2448+
Implements:
2449+
- :any:`DigitalOutputProtocol`
2450+
2451+
.. code-block:: yaml
2452+
2453+
DenkoviRelayDriver: {}
2454+
2455+
Arguments:
2456+
- None
2457+
24182458
ManualSwitchDriver
24192459
~~~~~~~~~~~~~~~~~~
24202460
A :any:`ManualSwitchDriver` requires the user to control a switch or jumper on

labgrid/driver/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from .lxausbmuxdriver import LXAUSBMuxDriver
3636
from .pyvisadriver import PyVISADriver
3737
from .usbhidrelay import HIDRelayDriver
38+
from .denkovirelay import DenkoviRelayDriver
3839
from .flashscriptdriver import FlashScriptDriver
3940
from .usbaudiodriver import USBAudioInputDriver
4041
from .usbvideodriver import USBVideoDriver

labgrid/driver/denkovirelay.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import attr
2+
3+
from .common import Driver
4+
from ..factory import target_factory
5+
from ..resource.remote import NetworkDenkoviRelay
6+
from ..step import step
7+
from ..protocol import DigitalOutputProtocol
8+
from ..util.agentwrapper import AgentWrapper
9+
10+
11+
@target_factory.reg_driver
12+
@attr.s(eq=False)
13+
class DenkoviRelayDriver(Driver, DigitalOutputProtocol):
14+
bindings = {
15+
"relay": {"DenkoviRelay", NetworkDenkoviRelay},
16+
}
17+
18+
def __attrs_post_init__(self):
19+
super().__attrs_post_init__()
20+
self.wrapper = None
21+
22+
def on_activate(self):
23+
if isinstance(self.relay, NetworkDenkoviRelay):
24+
host = self.relay.host
25+
else:
26+
host = None
27+
self.wrapper = AgentWrapper(host)
28+
self.proxy = self.wrapper.load("denkovi_relay")
29+
30+
def on_deactivate(self):
31+
self.wrapper.close()
32+
self.wrapper = None
33+
self.proxy = None
34+
35+
@Driver.check_active
36+
@step(args=["status"])
37+
def set(self, status):
38+
if self.relay.invert:
39+
status = not status
40+
self.proxy.set(self.relay.busnum, self.relay.devnum, self.relay.index, status)
41+
42+
@Driver.check_active
43+
@step(result=True)
44+
def get(self):
45+
status = self.proxy.get(self.relay.busnum, self.relay.devnum, self.relay.index)
46+
if self.relay.invert:
47+
status = not status
48+
return status

labgrid/remote/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,7 @@ def digital_io(self):
925925
name = self.args.name
926926
target = self._get_target(place)
927927
from ..resource import ModbusTCPCoil, OneWirePIO, HttpDigitalOutput, WaveshareModbusTCPCoil
928-
from ..resource.remote import NetworkDeditecRelais8, NetworkSysfsGPIO, NetworkLXAIOBusPIO, NetworkHIDRelay
928+
from ..resource.remote import NetworkDeditecRelais8, NetworkSysfsGPIO, NetworkLXAIOBusPIO, NetworkHIDRelay, NetworkDenkoviRelay
929929

930930
drv = None
931931
try:
@@ -950,6 +950,8 @@ def digital_io(self):
950950
drv = self._get_driver_or_new(target, "LXAIOBusPIODriver", name=name)
951951
elif isinstance(resource, NetworkHIDRelay):
952952
drv = self._get_driver_or_new(target, "HIDRelayDriver", name=name)
953+
elif isinstance(resource, NetworkDenkoviRelay):
954+
drv = self._get_driver_or_new(target, "DenkoviRelayDriver", name=name)
953955
if drv:
954956
break
955957

labgrid/remote/exporter.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,26 @@ def _get_params(self):
526526
}
527527

528528

529+
@attr.s(eq=False)
530+
class DenkoviRelayExport(USBGenericExport):
531+
"""ResourceExport for outputs on Denkovi relays"""
532+
533+
def __attrs_post_init__(self):
534+
super().__attrs_post_init__()
535+
536+
def _get_params(self):
537+
"""Helper function to return parameters"""
538+
return {
539+
"host": self.host,
540+
"busnum": self.local.busnum,
541+
"devnum": self.local.devnum,
542+
"path": self.local.path,
543+
"vendor_id": self.local.vendor_id,
544+
"model_id": self.local.model_id,
545+
"index": self.local.index,
546+
}
547+
548+
529549
@attr.s(eq=False)
530550
class USBFlashableExport(USBGenericExport):
531551
"""ResourceExport for Flashable USB devices"""

labgrid/resource/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
DFUDevice,
1414
DeditecRelais8,
1515
HIDRelay,
16+
DenkoviRelay,
1617
IMXUSBLoader,
1718
LXAUSBMux,
1819
MatchedSysfsGPIO,

labgrid/resource/remote.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,17 @@ def __attrs_post_init__(self):
333333
super().__attrs_post_init__()
334334

335335

336+
@target_factory.reg_resource
337+
@attr.s(eq=False)
338+
class NetworkDenkoviRelay(RemoteUSBResource):
339+
"""The NetworkDenkoviRelay describes a remotely accessible USB relay port"""
340+
index = attr.ib(default=1, validator=attr.validators.instance_of(int))
341+
invert = attr.ib(default=False, validator=attr.validators.instance_of(bool))
342+
def __attrs_post_init__(self):
343+
self.timeout = 10.0
344+
super().__attrs_post_init__()
345+
346+
336347
@target_factory.reg_resource
337348
@attr.s(eq=False)
338349
class NetworkSysfsGPIO(NetworkResource, ManagedResource):

labgrid/resource/suggest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
USBAudioInput,
2222
LXAUSBMux,
2323
HIDRelay,
24+
DenkoviRelay,
2425
USBDebugger,
2526
USBPowerPort,
2627
MatchedSysfsGPIO
@@ -55,6 +56,7 @@ def __init__(self, args):
5556
self.resources.append(USBAudioInput(**args))
5657
self.resources.append(LXAUSBMux(**args))
5758
self.resources.append(HIDRelay(**args))
59+
self.resources.append(DenkoviRelay(**args))
5860
self.resources.append(USBDebugger(**args))
5961
self.resources.append(USBPowerPort(**args, index=0))
6062
self.resources.append(MatchedSysfsGPIO(**args, pin=0))

labgrid/resource/udev.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,19 @@ def filter_match(self, device):
706706

707707
return super().filter_match(device)
708708

709+
710+
@target_factory.reg_resource
711+
@attr.s(eq=False)
712+
class DenkoviRelay(USBResource):
713+
index = attr.ib(default=1, validator=attr.validators.instance_of(int))
714+
invert = attr.ib(default=False, validator=attr.validators.instance_of(bool))
715+
716+
def __attrs_post_init__(self):
717+
self.match['ID_VENDOR'] = 'FTDI'
718+
self.match['ID_MODEL'] = 'FT245R_USB_FIFO'
719+
super().__attrs_post_init__()
720+
721+
709722
@target_factory.reg_resource
710723
@attr.s(eq=False)
711724
class USBFlashableDevice(USBResource):

labgrid/util/agents/denkovi_relay.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""
2+
This module implements the communication protocol to switch the digital outputs
3+
on a Denkovi USB Relay.
4+
5+
Supported Functionality:
6+
7+
- Turn digital output on and off
8+
"""
9+
10+
import usb.core
11+
import usb.util
12+
13+
from pylibftdi import BitBangDevice
14+
from threading import Lock
15+
16+
17+
class DenkoviRelay:
18+
lock = Lock()
19+
20+
def __init__(self, **args):
21+
self._dev = usb.core.find(**args)
22+
23+
if self._dev is None:
24+
raise ValueError("Device not found")
25+
26+
self._serialNumber = usb.util.get_string(self._dev, self._dev.iSerialNumber)
27+
28+
if self._serialNumber is None:
29+
raise ValueError("Failed to get device serial number")
30+
31+
self.lock.acquire()
32+
33+
bitbangDev = BitBangDevice(self._serialNumber)
34+
35+
if bitbangDev is None:
36+
raise ValueError("Failed to instantiate bitbang device")
37+
38+
bitbangDev.direction = 0xFF
39+
40+
bitbangDev.close()
41+
42+
self.lock.release()
43+
44+
def set_output(self, number, status):
45+
assert 1 <= number <= 8
46+
number = number - 1
47+
48+
self.lock.acquire()
49+
50+
bitbangDev = BitBangDevice(self._serialNumber)
51+
52+
if bitbangDev is None:
53+
raise ValueError("Failed to instantiate bitbang device")
54+
55+
if status:
56+
bitbangDev.port = bitbangDev.port | (1 << number)
57+
else:
58+
bitbangDev.port = bitbangDev.port & ~(1 << number)
59+
60+
bitbangDev.close()
61+
62+
self.lock.release()
63+
64+
def get_output(self, number):
65+
assert 1 <= number <= 8
66+
number = number - 1
67+
68+
val = None
69+
70+
self.lock.acquire()
71+
72+
bitbangDev = BitBangDevice(self._serialNumber)
73+
74+
if bitbangDev is None:
75+
raise ValueError("Failed to instantiate bitbang device")
76+
77+
if bitbangDev.port & (1 << number):
78+
val = True
79+
else:
80+
val = False
81+
82+
bitbangDev.close()
83+
84+
self.lock.release()
85+
86+
return val
87+
88+
def __del__(self):
89+
usb.util.release_interface(self._dev, 0)
90+
91+
92+
_relays = {}
93+
94+
95+
def _get_relay(busnum, devnum):
96+
if (busnum, devnum) not in _relays:
97+
_relays[(busnum, devnum)] = DenkoviRelay(bus=busnum, address=devnum)
98+
return _relays[(busnum, devnum)]
99+
100+
101+
def handle_set(busnum, devnum, number, status):
102+
relay = _get_relay(busnum, devnum)
103+
relay.set_output(number, status)
104+
105+
106+
def handle_get(busnum, devnum, number):
107+
relay = _get_relay(busnum, devnum)
108+
return relay.get_output(number)
109+
110+
111+
methods = {
112+
"set": handle_set,
113+
"get": handle_get,
114+
}

0 commit comments

Comments
 (0)