Skip to content

Commit 05910e8

Browse files
committed
Add support for LinkPi SmartHUB power port
LinkPi SmartHUB is a 12-port USB3.0 HUB utilizing four RTS5411 USB3.0 4-port HUB controllers, a FT232R USB UART IC and a STM32F103RB MCU for port power control. Add a LinkPiSmartHUBPowerPort resource and a LinkPiSmartHUBPowerDriver with an accompanying linkpismarthub agent to support power on/off the ports of the LinkPi SmartHUB using a simple line-based protocol over a serial port. Signed-off-by: Jonas Karlman <[email protected]>
1 parent 22f4b81 commit 05910e8

File tree

12 files changed

+316
-4
lines changed

12 files changed

+316
-4
lines changed

doc/configuration.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,31 @@ NetworkUSBPowerPort
356356
A :any:`NetworkUSBPowerPort` describes a `USBPowerPort`_ resource available on
357357
a remote computer.
358358

359+
LinkPiSmartHUBPowerPort
360+
+++++++++++++++++++++++
361+
A :any:`LinkPiSmartHUBPowerPort` describes a LinkPi SmartHUB port with power switching.
362+
363+
.. code-block:: yaml
364+
365+
LinkPiSmartHUBPowerPort:
366+
match:
367+
ID_SERIAL_SHORT: 'ABCD1234'
368+
index: 5
369+
370+
The example describes port '1' on the SmartHUB with the ID_SERIAL_SHORT ``ABCD1234``.
371+
372+
Arguments:
373+
- index (str or int): port name ('1' to '12') or internal port index (0 to 11)
374+
- match (dict): key and value pairs for a udev match, see `udev Matching`_
375+
376+
Used by:
377+
- `LinkPiSmartHUBPowerDriver`_
378+
379+
NetworkLinkPiSmartHUBPowerPort
380+
++++++++++++++++++++++++++++++
381+
A :any:`NetworkLinkPiSmartHUBPowerPort` describes a `LinkPiSmartHUBPowerPort`_
382+
resource available on a remote computer.
383+
359384
SiSPMPowerPort
360385
++++++++++++++
361386
A :any:`SiSPMPowerPort` describes a *GEMBIRD SiS-PM* as supported by
@@ -2271,6 +2296,28 @@ Implements:
22712296
Arguments:
22722297
- delay (float, default=2.0): delay in seconds between off and on
22732298

2299+
LinkPiSmartHUBPowerDriver
2300+
~~~~~~~~~~~~~~~~~~~~~~~~~
2301+
A :any:`LinkPiSmartHUBPowerDriver` controls a `LinkPiSmartHUBPowerPort`_,
2302+
allowing control of the target power state without user interaction.
2303+
2304+
Binds to:
2305+
port:
2306+
- `LinkPiSmartHUBPowerPort`_
2307+
- `NetworkLinkPiSmartHUBPowerPort`_
2308+
2309+
Implements:
2310+
- :any:`PowerProtocol`
2311+
- :any:`ResetProtocol`
2312+
2313+
.. code-block:: yaml
2314+
2315+
LinkPiSmartHUBPowerDriver:
2316+
delay: 5.0
2317+
2318+
Arguments:
2319+
- delay (float, default=2.0): delay in seconds between off and on
2320+
22742321
SiSPMPowerDriver
22752322
~~~~~~~~~~~~~~~~
22762323
A :any:`SiSPMPowerDriver` controls a `SiSPMPowerPort`_, allowing control of the

labgrid/driver/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
from .onewiredriver import OneWirePIODriver
1515
from .powerdriver import ManualPowerDriver, ExternalPowerDriver, \
1616
DigitalOutputPowerDriver, YKUSHPowerDriver, \
17-
USBPowerDriver, SiSPMPowerDriver, NetworkPowerDriver, \
18-
PDUDaemonDriver
17+
USBPowerDriver, LinkPiSmartHUBPowerDriver, SiSPMPowerDriver, \
18+
NetworkPowerDriver, PDUDaemonDriver
1919
from .usbloader import MXSUSBDriver, IMXUSBDriver, BDIMXUSBDriver, RKUSBDriver, UUUDriver
2020
from .usbsdmuxdriver import USBSDMuxDriver
2121
from .usbsdwiredriver import USBSDWireDriver

labgrid/driver/powerdriver.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from ..factory import target_factory
99
from ..protocol import PowerProtocol, DigitalOutputProtocol, ResetProtocol
1010
from ..resource import NetworkPowerPort
11+
from ..resource.remote import NetworkLinkPiSmartHUBPowerPort
1112
from ..step import step
13+
from ..util.agentwrapper import AgentWrapper
1214
from ..util.proxy import proxymanager
1315
from ..util.helper import processwrapper
1416
from .common import Driver
@@ -57,6 +59,53 @@ def cycle(self):
5759
)
5860

5961

62+
@target_factory.reg_driver
63+
@attr.s(eq=False)
64+
class LinkPiSmartHUBPowerDriver(Driver, PowerResetMixin, PowerProtocol):
65+
bindings = {"port": {"LinkPiSmartHUBPowerPort", NetworkLinkPiSmartHUBPowerPort}, }
66+
delay = attr.ib(default=2.0, validator=attr.validators.instance_of(float))
67+
68+
def __attrs_post_init__(self):
69+
super().__attrs_post_init__()
70+
self.wrapper = None
71+
72+
def on_activate(self):
73+
if isinstance(self.port, NetworkLinkPiSmartHUBPowerPort):
74+
host = self.port.host
75+
else:
76+
host = None
77+
self.wrapper = AgentWrapper(host)
78+
self.proxy = self.wrapper.load('linkpismarthub')
79+
80+
def on_deactivate(self):
81+
self.proxy = None
82+
if self.wrapper:
83+
self.wrapper.close()
84+
self.wrapper = None
85+
86+
@Driver.check_active
87+
@step()
88+
def on(self):
89+
self.proxy.set(self.port.path, self.port.index, 1)
90+
91+
@Driver.check_active
92+
@step()
93+
def off(self):
94+
self.proxy.set(self.port.path, self.port.index, 0)
95+
96+
@Driver.check_active
97+
@step()
98+
def cycle(self):
99+
self.off()
100+
time.sleep(self.delay)
101+
self.on()
102+
103+
@Driver.check_active
104+
@step()
105+
def get(self):
106+
return self.proxy.get(self.port.path, self.port.index)
107+
108+
60109
@target_factory.reg_driver
61110
@attr.s(eq=False)
62111
class SiSPMPowerDriver(Driver, PowerResetMixin, PowerProtocol):

labgrid/remote/client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,12 @@ def power(self):
883883
name = self.args.name
884884
target = self._get_target(place)
885885
from ..resource.power import NetworkPowerPort, PDUDaemonPort
886-
from ..resource.remote import NetworkUSBPowerPort, NetworkSiSPMPowerPort, NetworkSysfsGPIO
886+
from ..resource.remote import (
887+
NetworkUSBPowerPort,
888+
NetworkLinkPiSmartHUBPowerPort,
889+
NetworkSiSPMPowerPort,
890+
NetworkSysfsGPIO,
891+
)
887892
from ..resource import TasmotaPowerPort, NetworkYKUSHPowerPort
888893

889894
drv = None
@@ -897,6 +902,8 @@ def power(self):
897902
drv = self._get_driver_or_new(target, "NetworkPowerDriver", name=name)
898903
elif isinstance(resource, NetworkUSBPowerPort):
899904
drv = self._get_driver_or_new(target, "USBPowerDriver", name=name)
905+
elif isinstance(resource, NetworkLinkPiSmartHUBPowerPort):
906+
drv = self._get_driver_or_new(target, "LinkPiSmartHUBPowerDriver", name=name)
900907
elif isinstance(resource, NetworkSiSPMPowerPort):
901908
drv = self._get_driver_or_new(target, "SiSPMPowerDriver", name=name)
902909
elif isinstance(resource, PDUDaemonPort):

labgrid/remote/exporter.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,26 @@ def _get_params(self):
446446
}
447447

448448

449+
@attr.s(eq=False)
450+
class LinkPiSmartHUBPowerPortExport(USBGenericExport):
451+
"""ResourceExport for ports on power switchable LinkPi SmartHUBs"""
452+
453+
def __attrs_post_init__(self):
454+
super().__attrs_post_init__()
455+
456+
def _get_params(self):
457+
"""Helper function to return parameters"""
458+
return {
459+
"host": self.host,
460+
"busnum": self.local.busnum,
461+
"devnum": self.local.devnum,
462+
"path": self.local.path,
463+
"vendor_id": self.local.vendor_id,
464+
"model_id": self.local.model_id,
465+
"index": self.local.index,
466+
}
467+
468+
449469
@attr.s(eq=False)
450470
class SiSPMPowerPortExport(USBGenericExport):
451471
"""ResourceExport for ports on GEMBRID switches"""
@@ -563,6 +583,7 @@ def __attrs_post_init__(self):
563583
exports["USBVideo"] = USBGenericExport
564584
exports["USBAudioInput"] = USBAudioInputExport
565585
exports["USBTMC"] = USBGenericExport
586+
exports["LinkPiSmartHUBPowerPort"] = LinkPiSmartHUBPowerPortExport
566587
exports["SiSPMPowerPort"] = SiSPMPowerPortExport
567588
exports["USBPowerPort"] = USBPowerPortExport
568589
exports["DeditecRelais8"] = USBDeditecRelaisExport

labgrid/resource/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
DeditecRelais8,
1515
HIDRelay,
1616
IMXUSBLoader,
17+
LinkPiSmartHUBPowerPort,
1718
LXAUSBMux,
1819
MatchedSysfsGPIO,
1920
MXSUSBLoader,

labgrid/resource/remote.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,17 @@ def __attrs_post_init__(self):
250250
self.timeout = 10.0
251251
super().__attrs_post_init__()
252252

253+
254+
@target_factory.reg_resource
255+
@attr.s(eq=False)
256+
class NetworkLinkPiSmartHUBPowerPort(RemoteUSBResource):
257+
"""The NetworkLinkPiSmartHUBPowerPort describes a remotely accessible LinkPi SmartHUB port with power switching"""
258+
index = attr.ib(default=None, validator=attr.validators.instance_of((str, int)))
259+
def __attrs_post_init__(self):
260+
self.timeout = 10.0
261+
super().__attrs_post_init__()
262+
263+
253264
@target_factory.reg_resource
254265
@attr.s(eq=False)
255266
class NetworkSiSPMPowerPort(RemoteUSBResource):

labgrid/resource/suggest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
AlteraUSBBlaster,
1818
RKUSBLoader,
1919
USBNetworkInterface,
20+
LinkPiSmartHUBPowerPort,
2021
SiSPMPowerPort,
2122
USBAudioInput,
2223
LXAUSBMux,
@@ -51,6 +52,7 @@ def __init__(self, args):
5152
self.resources.append(AlteraUSBBlaster(**args))
5253
self.resources.append(RKUSBLoader(**args))
5354
self.resources.append(USBNetworkInterface(**args))
55+
self.resources.append(LinkPiSmartHUBPowerPort(**args))
5456
self.resources.append(SiSPMPowerPort(**args))
5557
self.resources.append(USBAudioInput(**args))
5658
self.resources.append(LXAUSBMux(**args))

labgrid/resource/udev.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,30 @@ def __attrs_post_init__(self):
601601
self.match['DRIVER'] = 'hub'
602602
super().__attrs_post_init__()
603603

604+
@target_factory.reg_resource
605+
@attr.s(eq=False)
606+
class LinkPiSmartHUBPowerPort(USBResource):
607+
"""The LinkPiSmartHUBPowerPort describes a LinkPi SmartHUB port with power switching.
608+
609+
Args:
610+
index (str or int): port name ('1' to '12') or internal port index (0 to 11)
611+
"""
612+
index = attr.ib(default=None, validator=attr.validators.instance_of((str, int)))
613+
614+
def __attrs_post_init__(self):
615+
self.match['SUBSYSTEM'] = 'tty'
616+
self.match['@SUBSYSTEM'] = 'usb'
617+
self.match.setdefault('ID_VENDOR_ID', '0403')
618+
self.match.setdefault('ID_MODEL_ID', '6001')
619+
super().__attrs_post_init__()
620+
621+
@property
622+
def path(self):
623+
if self.device is not None:
624+
return self.device.device_node
625+
626+
return None
627+
604628
@target_factory.reg_resource
605629
@attr.s(eq=False)
606630
class SiSPMPowerPort(USBResource):
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""
2+
This module implements the communication protocol to power on/off ports on a
3+
LinkPi SmartHUB, a 12-port USB3.0 HUB utilizing four RTS5411 USB3.0 4-port HUB
4+
controllers, a FT232R USB UART IC and a STM32F103RB MCU for port power control.
5+
6+
The protocol is a simple line-based protocol over a serial port.
7+
8+
Known commands:
9+
- onoff <port> <1|0> - switch port power on/off
10+
- state - get current power state of all ports
11+
- SetOWP <1|0> <1|0> ... - set the power-on state of all ports
12+
- GetOWP - get the power-on state of all ports
13+
14+
Responses are in JSON format, e.g.:
15+
16+
.. code-block:: text
17+
18+
> onoff 5 1
19+
< {"Cmd":"OnOffResp","SeqNum":1,"ret":0}
20+
> state
21+
< {"Cmd":"StateResp","SeqNum":2,"state":[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]}
22+
> SetOWP 0 0 0 0 0 0 0 0 0 0 0 1
23+
< {"Cmd":"SetOWPResp","SeqNum":3,"ret":0}
24+
> GetOWP
25+
< {"Cmd":"GetOWPResp","SeqNum":4,"owp":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]}
26+
27+
A version announcement is continuously sent every second:
28+
29+
.. code-block:: text
30+
31+
< {"Cmd":"VerResp","ver":"SmartHUB_<ver>","uid":"<uid>"}
32+
"""
33+
import json
34+
import serial
35+
36+
37+
PORT_INDEX_MAP = {
38+
"1": 5, "2": 4, "3": 3, "4": 2, "5": 1, "6": 0,
39+
"7": 11, "8": 10, "9": 9, "10": 8, "11": 7, "12": 6,
40+
}
41+
42+
43+
# Port names printed on the device do not match the internal port index
44+
def name_to_index(name_or_index):
45+
return PORT_INDEX_MAP.get(name_or_index, name_or_index)
46+
47+
48+
class LinkPiSmartHUB:
49+
def __init__(self, path):
50+
if not path:
51+
raise ValueError("Device not found")
52+
self.path = path
53+
54+
def _command(self, command):
55+
with serial.Serial(self.path, 115200, timeout=2) as s:
56+
# wait on next version announcement
57+
s.readline()
58+
# send the command
59+
s.write(f"{command}\r\n".encode())
60+
# read and return the response
61+
return s.readline().decode().strip()
62+
63+
def onoff(self, index, state):
64+
return self._command(f"onoff {index} {state}")
65+
66+
def state(self):
67+
return self._command("state")
68+
69+
70+
def handle_set(path, name_or_index, state):
71+
smarthub = LinkPiSmartHUB(path)
72+
smarthub.onoff(name_to_index(name_or_index), state)
73+
74+
75+
def handle_get(path, name_or_index):
76+
smarthub = LinkPiSmartHUB(path)
77+
state = smarthub.state()
78+
return bool(json.loads(state).get("state")[name_to_index(name_or_index)])
79+
80+
81+
methods = {
82+
"set": handle_set,
83+
"get": handle_get,
84+
}

0 commit comments

Comments
 (0)