python-dali 0.10__py3-none-any.whl → 0.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dali/device/__init__.py +2 -0
- dali/device/helpers.py +1 -2
- dali/device/sequences.py +50 -0
- dali/driver/atxled.py +154 -0
- dali/driver/base.py +0 -114
- dali/driver/hid.py +110 -31
- dali/driver/serial.py +636 -7
- dali/driver/tridonic.py +2 -2
- dali/driver/unipi.py +18 -8
- dali/driver/usb.py +114 -0
- dali/frame.py +5 -20
- dali/gear/__init__.py +1 -0
- dali/gear/colour.py +31 -0
- dali/gear/converter.py +173 -0
- dali/gear/general.py +4 -0
- dali/gear/led.py +19 -2
- dali/gear/sequences.py +30 -2
- dali/tests/test_device_sequences.py +97 -3
- dali/tests/test_dummy.py +3 -2
- dali/tests/test_frame.py +0 -27
- {python_dali-0.10.dist-info → python_dali-0.12.dist-info}/METADATA +28 -18
- {python_dali-0.10.dist-info → python_dali-0.12.dist-info}/RECORD +27 -25
- {python_dali-0.10.dist-info → python_dali-0.12.dist-info}/WHEEL +1 -1
- dali/version.py +0 -1
- {python_dali-0.10.dist-info → python_dali-0.12.dist-info/licenses}/COPYING +0 -0
- {python_dali-0.10.dist-info → python_dali-0.12.dist-info/licenses}/COPYING.LESSER +0 -0
- {python_dali-0.10.dist-info → python_dali-0.12.dist-info/licenses}/LICENSE +0 -0
- {python_dali-0.10.dist-info → python_dali-0.12.dist-info}/top_level.txt +0 -0
dali/device/__init__.py
CHANGED
dali/device/helpers.py
CHANGED
|
@@ -127,8 +127,7 @@ class DeviceInstanceTypeMapper:
|
|
|
127
127
|
if isinstance(rsp, QueryDeviceStatusResponse):
|
|
128
128
|
# Make sure the status is OK
|
|
129
129
|
if (
|
|
130
|
-
rsp.
|
|
131
|
-
or rsp.short_address_is_mask
|
|
130
|
+
rsp.short_address_is_mask
|
|
132
131
|
or rsp.reset_state
|
|
133
132
|
):
|
|
134
133
|
continue
|
dali/device/sequences.py
CHANGED
|
@@ -14,6 +14,9 @@ from dali.device.general import (
|
|
|
14
14
|
DTR2,
|
|
15
15
|
EventScheme,
|
|
16
16
|
InstanceEventFilter,
|
|
17
|
+
QueryResolution,
|
|
18
|
+
QueryInputValue,
|
|
19
|
+
QueryInputValueLatch,
|
|
17
20
|
QueryEventFilterH,
|
|
18
21
|
QueryEventFilterL,
|
|
19
22
|
QueryEventFilterM,
|
|
@@ -23,6 +26,7 @@ from dali.device.general import (
|
|
|
23
26
|
SetEventScheme,
|
|
24
27
|
)
|
|
25
28
|
from dali.device.helpers import check_bad_rsp
|
|
29
|
+
from dali.exceptions import DALISequenceError
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
def SetEventSchemes(
|
|
@@ -225,3 +229,49 @@ def QueryEventFilters(
|
|
|
225
229
|
hi = rsp.value
|
|
226
230
|
|
|
227
231
|
return filter_type(int.from_bytes((lo, md, hi), "little"))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def query_input_value(
|
|
235
|
+
device: DeviceShort,
|
|
236
|
+
instance: InstanceNumber,
|
|
237
|
+
resolution: Optional[int] = None
|
|
238
|
+
) -> Generator[Command, Optional[Response], Optional[int]]:
|
|
239
|
+
"""
|
|
240
|
+
A generator sequence to retrieve full sensor value from a part-103 control device.
|
|
241
|
+
Use with an appropriate DALI driver instance, through the `run_sequence()`
|
|
242
|
+
method.
|
|
243
|
+
|
|
244
|
+
:param device: A DeviceShort address to target
|
|
245
|
+
:param instance: An InstanceNumber to target
|
|
246
|
+
:param resolution: Number of valid bits that the device provides
|
|
247
|
+
"""
|
|
248
|
+
# Although the proper types are expected, ints are common enough for
|
|
249
|
+
# addresses and their meaning is unambiguous in this context
|
|
250
|
+
if isinstance(device, int):
|
|
251
|
+
device = DeviceShort(device)
|
|
252
|
+
if isinstance(instance, int):
|
|
253
|
+
instance = InstanceNumber(instance)
|
|
254
|
+
|
|
255
|
+
if resolution is None:
|
|
256
|
+
resolution = yield QueryResolution(device, instance)
|
|
257
|
+
if check_bad_rsp(resolution):
|
|
258
|
+
raise DALISequenceError("query_input_value: QueryResolution failed")
|
|
259
|
+
resolution = resolution.value
|
|
260
|
+
|
|
261
|
+
value = yield QueryInputValue(device, instance)
|
|
262
|
+
if check_bad_rsp(value):
|
|
263
|
+
raise DALISequenceError("query_input_value: QueryInputValue failed")
|
|
264
|
+
value = value.value
|
|
265
|
+
while resolution > 8:
|
|
266
|
+
resolution -= 8
|
|
267
|
+
value <<= 8
|
|
268
|
+
chunk = yield QueryInputValueLatch(device, instance)
|
|
269
|
+
if check_bad_rsp(chunk):
|
|
270
|
+
raise DALISequenceError("query_input_value: QueryInputValueLatch failed")
|
|
271
|
+
value += chunk.value
|
|
272
|
+
|
|
273
|
+
if resolution > 0:
|
|
274
|
+
# Strip the repeated trailing bytes as per IEC 62386-103:2014, part 9.7.2
|
|
275
|
+
value >>= 8 - resolution
|
|
276
|
+
|
|
277
|
+
return value
|
dali/driver/atxled.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from dali.command import Command
|
|
2
|
+
from dali.driver.base import SyncDALIDriver, DALIDriver
|
|
3
|
+
from dali.frame import BackwardFrame
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import serial
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
DALI_PACKET_SIZE = {"j": 8, "h": 16, "l": 24, "m": 25}
|
|
12
|
+
DALI_PACKET_PREFIX = {v: k for k, v in DALI_PACKET_SIZE.items()}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DaliHatSerialDriver(DALIDriver):
|
|
16
|
+
"""Driver for communicating with DALI devices over a serial connection."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, port="/dev/ttyS0", LOG=None):
|
|
19
|
+
"""Initialize the serial connection to the DALI interface."""
|
|
20
|
+
self.port = port
|
|
21
|
+
self.lock = threading.RLock()
|
|
22
|
+
self.buffer = []
|
|
23
|
+
if not LOG:
|
|
24
|
+
self.LOG = logging.getLogger("AtxLedDaliDriver")
|
|
25
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
26
|
+
self.LOG.addHandler(handler)
|
|
27
|
+
else:
|
|
28
|
+
self.LOG = LOG
|
|
29
|
+
try:
|
|
30
|
+
self.conn = serial.Serial(
|
|
31
|
+
port=self.port,
|
|
32
|
+
baudrate=19200,
|
|
33
|
+
parity=serial.PARITY_NONE,
|
|
34
|
+
stopbits=serial.STOPBITS_ONE,
|
|
35
|
+
bytesize=serial.EIGHTBITS,
|
|
36
|
+
timeout=0.2,
|
|
37
|
+
)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
self.LOG.exception("Could not open serial connection: %s", e)
|
|
40
|
+
self.conn = None
|
|
41
|
+
|
|
42
|
+
def read_line(self):
|
|
43
|
+
"""Read the next line from the buffer, refilling the buffer if necessary."""
|
|
44
|
+
with self.lock:
|
|
45
|
+
while not self.buffer:
|
|
46
|
+
line = self.conn.read_until(b"\n").decode("ascii")
|
|
47
|
+
if not line:
|
|
48
|
+
return ""
|
|
49
|
+
self.buffer.append(line)
|
|
50
|
+
return self.buffer.pop(0)
|
|
51
|
+
|
|
52
|
+
def construct(self, command):
|
|
53
|
+
"""Construct a DALI command to be sent over the serial connection."""
|
|
54
|
+
assert isinstance(command, Command)
|
|
55
|
+
f = command.frame
|
|
56
|
+
packet_size = len(f)
|
|
57
|
+
prefix = DALI_PACKET_PREFIX[packet_size]
|
|
58
|
+
if command.sendtwice and packet_size == 16:
|
|
59
|
+
prefix = "t"
|
|
60
|
+
data = "".join(["{:02X}".format(byte) for byte in f.pack])
|
|
61
|
+
command_str = (f"{prefix}{data}\n").encode("ascii")
|
|
62
|
+
return command_str
|
|
63
|
+
|
|
64
|
+
def extract(self, data):
|
|
65
|
+
"""Parse the response from the serial device and return the corresponding frame."""
|
|
66
|
+
if data.startswith("J"):
|
|
67
|
+
try:
|
|
68
|
+
data = int(data[1:], 16)
|
|
69
|
+
return BackwardFrame(data)
|
|
70
|
+
except ValueError as e:
|
|
71
|
+
self.LOG.error(f"Failed to parse response '{data}': {e}")
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def close(self):
|
|
75
|
+
"""Close the serial connection."""
|
|
76
|
+
if self.conn:
|
|
77
|
+
self.conn.close()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SyncDaliHatDriver(DaliHatSerialDriver, SyncDALIDriver):
|
|
81
|
+
"""Synchronous DALI driver."""
|
|
82
|
+
|
|
83
|
+
def send(self, command: Command):
|
|
84
|
+
"""Send a command to the DALI interface and wait for a response."""
|
|
85
|
+
with self.lock:
|
|
86
|
+
lines = []
|
|
87
|
+
last_resp = None
|
|
88
|
+
send_twice = command.sendtwice
|
|
89
|
+
cmd = self.construct(command)
|
|
90
|
+
self.LOG.debug("command string sent: %r", cmd)
|
|
91
|
+
self.conn.write(cmd)
|
|
92
|
+
REPS = 5
|
|
93
|
+
i = 0
|
|
94
|
+
already_resent = False
|
|
95
|
+
resent_times = 0
|
|
96
|
+
resp = None
|
|
97
|
+
while i < REPS:
|
|
98
|
+
i += 1
|
|
99
|
+
resp = self.read_line()
|
|
100
|
+
self.LOG.debug("raw response received: %r", resp)
|
|
101
|
+
resend = False
|
|
102
|
+
if cmd[:3] not in ["hB1", "hB3", "hB5"]:
|
|
103
|
+
if resp and resp[0] in {"N", "J"}:
|
|
104
|
+
if send_twice:
|
|
105
|
+
if last_resp:
|
|
106
|
+
if last_resp == resp:
|
|
107
|
+
resp = self.extract(resp)
|
|
108
|
+
break
|
|
109
|
+
resend = True
|
|
110
|
+
last_resp = None
|
|
111
|
+
else:
|
|
112
|
+
last_resp = resp
|
|
113
|
+
else:
|
|
114
|
+
resp = self.extract(resp)
|
|
115
|
+
break
|
|
116
|
+
elif resp and resp[0] in {"X", "Z", ""}:
|
|
117
|
+
time.sleep(0.1)
|
|
118
|
+
collision_bytes = None
|
|
119
|
+
while collision_bytes != "":
|
|
120
|
+
collision_bytes = self.read_line()
|
|
121
|
+
if resp[0] == "X":
|
|
122
|
+
break
|
|
123
|
+
self.LOG.info(
|
|
124
|
+
"got conflict (%s) sending %r, sending again", resp, cmd
|
|
125
|
+
)
|
|
126
|
+
last_resp = None
|
|
127
|
+
resend = True
|
|
128
|
+
elif resp:
|
|
129
|
+
lines.append(resp)
|
|
130
|
+
|
|
131
|
+
resp = None
|
|
132
|
+
if resend and not already_resent:
|
|
133
|
+
self.conn.write((cmd).encode("ascii"))
|
|
134
|
+
REPS += 1 + send_twice
|
|
135
|
+
already_resent = True
|
|
136
|
+
else:
|
|
137
|
+
if resp and resp[0] == "N":
|
|
138
|
+
resp = self.extract(resp)
|
|
139
|
+
break
|
|
140
|
+
elif resp and resp[0] in {"X", "Z", ""}:
|
|
141
|
+
time.sleep(0.1)
|
|
142
|
+
collision_bytes = None
|
|
143
|
+
while collision_bytes != "":
|
|
144
|
+
collision_bytes = self.read_line()
|
|
145
|
+
elif resp:
|
|
146
|
+
last_resp = None
|
|
147
|
+
resend = True
|
|
148
|
+
if resend and resent_times < 5:
|
|
149
|
+
self.conn.write(cmd.encode("ascii"))
|
|
150
|
+
REPS += 1 + send_twice
|
|
151
|
+
resent_times += 1
|
|
152
|
+
if command.is_query:
|
|
153
|
+
return command.response(resp)
|
|
154
|
+
return resp
|
dali/driver/base.py
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import threading
|
|
2
|
-
import usb
|
|
3
|
-
|
|
4
|
-
|
|
5
1
|
###############################################################################
|
|
6
2
|
# driver contracts
|
|
7
3
|
###############################################################################
|
|
@@ -124,113 +120,3 @@ class Listener(Backend):
|
|
|
124
120
|
"""
|
|
125
121
|
raise NotImplementedError(
|
|
126
122
|
'Abstract ``Listener`` does not implement ``listen``')
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
###############################################################################
|
|
130
|
-
# USB backends
|
|
131
|
-
###############################################################################
|
|
132
|
-
|
|
133
|
-
class USBBackend(Backend):
|
|
134
|
-
"""Backend implementation for communicating with USB devices.
|
|
135
|
-
"""
|
|
136
|
-
|
|
137
|
-
def __init__(self, vendor, product, bus=None,
|
|
138
|
-
port_numbers=None, interface=0):
|
|
139
|
-
self._device = None
|
|
140
|
-
# lookup devices by vendor and product
|
|
141
|
-
devices = [dev for dev in usb.core.find(
|
|
142
|
-
find_all=True,
|
|
143
|
-
idVendor=vendor,
|
|
144
|
-
idProduct=product
|
|
145
|
-
)]
|
|
146
|
-
# use first device if bus or port_numers not defined
|
|
147
|
-
if bus is None or port_numbers is None:
|
|
148
|
-
self._device = devices[0]
|
|
149
|
-
else:
|
|
150
|
-
for dev in devices:
|
|
151
|
-
if dev.bus == bus and dev.port_numbers == port_numbers:
|
|
152
|
-
self._device = dev
|
|
153
|
-
break
|
|
154
|
-
# if queried device not found, raise
|
|
155
|
-
if not self._device:
|
|
156
|
-
raise usb.core.USBError('Device not found')
|
|
157
|
-
# detach kernel driver if necessary
|
|
158
|
-
if self._device.is_kernel_driver_active(interface) is True:
|
|
159
|
-
self._device.detach_kernel_driver(interface)
|
|
160
|
-
# set device configuration
|
|
161
|
-
self._device.set_configuration()
|
|
162
|
-
# claim interface
|
|
163
|
-
usb.util.claim_interface(self._device, interface)
|
|
164
|
-
# get active configuration
|
|
165
|
-
cfg = self._device.get_active_configuration()
|
|
166
|
-
intf = cfg[(0, 0)]
|
|
167
|
-
# get write end point
|
|
168
|
-
def match_ep_out(e):
|
|
169
|
-
return usb.util.endpoint_direction(e.bEndpointAddress) == \
|
|
170
|
-
usb.util.ENDPOINT_OUT
|
|
171
|
-
self._ep_write = usb.util.find_descriptor(
|
|
172
|
-
intf, custom_match=match_ep_out)
|
|
173
|
-
# get read end point
|
|
174
|
-
def match_ep_in(e):
|
|
175
|
-
return usb.util.endpoint_direction(e.bEndpointAddress) == \
|
|
176
|
-
usb.util.ENDPOINT_IN
|
|
177
|
-
self._ep_read = usb.util.find_descriptor(
|
|
178
|
-
intf, custom_match=match_ep_in)
|
|
179
|
-
|
|
180
|
-
def read(self, timeout=None):
|
|
181
|
-
"""Read data from USB device.
|
|
182
|
-
"""
|
|
183
|
-
return self._ep_read.read(self._ep_read.wMaxPacketSize, timeout=timeout)
|
|
184
|
-
|
|
185
|
-
def write(self, data):
|
|
186
|
-
"""Write data to USB device.
|
|
187
|
-
"""
|
|
188
|
-
return self._ep_write.write(data)
|
|
189
|
-
|
|
190
|
-
def close(self):
|
|
191
|
-
"""Close connection to USB device.
|
|
192
|
-
"""
|
|
193
|
-
usb.util.dispose_resources(self._device)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
class USBListener(USBBackend, Listener):
|
|
197
|
-
"""Listener implementation for communicating with USB devices.
|
|
198
|
-
"""
|
|
199
|
-
|
|
200
|
-
def __init__(self, driver, vendor, product, bus=None,
|
|
201
|
-
port_numbers=None, interface=0):
|
|
202
|
-
super(USBListener, self).__init__(
|
|
203
|
-
vendor,
|
|
204
|
-
product,
|
|
205
|
-
bus=bus,
|
|
206
|
-
port_numbers=port_numbers,
|
|
207
|
-
interface=interface
|
|
208
|
-
)
|
|
209
|
-
self.driver = driver
|
|
210
|
-
# flag whether actually disconnecting from device
|
|
211
|
-
self._disconnecting = False
|
|
212
|
-
# event to stop listening
|
|
213
|
-
self._stop_listening = threading.Event()
|
|
214
|
-
# create and start listener thread
|
|
215
|
-
self._listener = threading.Thread(target=self.listen)
|
|
216
|
-
self._listener.start()
|
|
217
|
-
|
|
218
|
-
def listen(self):
|
|
219
|
-
"""Poll data from USB device.
|
|
220
|
-
"""
|
|
221
|
-
while not self._stop_listening.is_set():
|
|
222
|
-
try:
|
|
223
|
-
self.driver.receive(self.read())
|
|
224
|
-
except usb.core.USBError as e:
|
|
225
|
-
# read timeout
|
|
226
|
-
if e.errno == 110:
|
|
227
|
-
continue
|
|
228
|
-
if not self._disconnecting:
|
|
229
|
-
self.close()
|
|
230
|
-
|
|
231
|
-
def close(self):
|
|
232
|
-
"""Close connection to USB device.
|
|
233
|
-
"""
|
|
234
|
-
self._disconnecting = True
|
|
235
|
-
self._stop_listening.set()
|
|
236
|
-
super(USBListener, self).close()
|
dali/driver/hid.py
CHANGED
|
@@ -123,18 +123,20 @@ class hid:
|
|
|
123
123
|
path = glob.glob(self._path)
|
|
124
124
|
else:
|
|
125
125
|
path = [self._path]
|
|
126
|
+
ex = None
|
|
126
127
|
if path:
|
|
127
128
|
try:
|
|
128
129
|
if self._glob:
|
|
129
130
|
self._log.debug("trying concrete path %s", path[0])
|
|
130
131
|
self._f = os.open(path[0], os.O_RDWR | os.O_NONBLOCK)
|
|
131
|
-
except:
|
|
132
|
+
except Exception as e:
|
|
132
133
|
self._f = None
|
|
134
|
+
ex = e
|
|
133
135
|
else:
|
|
134
136
|
self._log.debug("path %s not found", self._path)
|
|
135
137
|
if not self._f:
|
|
136
138
|
# It didn't work. Schedule a reconnection attempt if we can.
|
|
137
|
-
self._log.debug("hid failed to open %s - waiting to try again", self._path)
|
|
139
|
+
self._log.debug("hid failed to open %s (%s) - waiting to try again", self._path, ex)
|
|
138
140
|
self._reconnect_task = asyncio.create_task(self._reconnect())
|
|
139
141
|
return False
|
|
140
142
|
self._reconnect_count = 0
|
|
@@ -208,6 +210,29 @@ class hid:
|
|
|
208
210
|
if not in_transaction:
|
|
209
211
|
self.transaction_lock.release()
|
|
210
212
|
|
|
213
|
+
async def power_supply(self, supply_on, in_transaction=False, exceptions=None):
|
|
214
|
+
"""
|
|
215
|
+
TODO
|
|
216
|
+
"""
|
|
217
|
+
if exceptions is None:
|
|
218
|
+
exceptions = self.exceptions_on_send
|
|
219
|
+
|
|
220
|
+
if not in_transaction:
|
|
221
|
+
await self.transaction_lock.acquire()
|
|
222
|
+
try:
|
|
223
|
+
command_sent = False
|
|
224
|
+
while not command_sent:
|
|
225
|
+
try:
|
|
226
|
+
response = await self._power_supply(supply_on)
|
|
227
|
+
command_sent = True
|
|
228
|
+
except CommunicationError:
|
|
229
|
+
if exceptions:
|
|
230
|
+
raise
|
|
231
|
+
return response
|
|
232
|
+
finally:
|
|
233
|
+
if not in_transaction:
|
|
234
|
+
self.transaction_lock.release()
|
|
235
|
+
|
|
211
236
|
async def run_sequence(self, seq, progress=None):
|
|
212
237
|
"""Run a command sequence as a transaction
|
|
213
238
|
"""
|
|
@@ -268,6 +293,8 @@ class tridonic(hid):
|
|
|
268
293
|
# Commands sent to the interface
|
|
269
294
|
# cmd, seq, ctrl, mode, frame (4 bytes), dtr, prio, devtype
|
|
270
295
|
_cmdtmpl = struct.Struct(">4B4s3B53x")
|
|
296
|
+
# length of the "useful part", without the trailing padding
|
|
297
|
+
_cmdtmpl_useful_size = _cmdtmpl.size - 53
|
|
271
298
|
|
|
272
299
|
# _CMD send in byte 0
|
|
273
300
|
_CMD_INIT = 0x01
|
|
@@ -277,6 +304,7 @@ class tridonic(hid):
|
|
|
277
304
|
_CMD_SET_IOPINS = 0x20
|
|
278
305
|
_CMD_READ_IOPINS = 0x21
|
|
279
306
|
_CMD_IDENTIFY = 0x22
|
|
307
|
+
_CMD_POWERSUPPLY = 0x40
|
|
280
308
|
|
|
281
309
|
# For _CMD_INIT, send in byte 1:
|
|
282
310
|
_CMD_INIT_READVERSION = 0x00
|
|
@@ -298,9 +326,16 @@ class tridonic(hid):
|
|
|
298
326
|
_SEND_MODE_DALI24 = 6
|
|
299
327
|
_SEND_MODE_HELVAR17 = 8
|
|
300
328
|
|
|
329
|
+
# For _CMD_POWERSUPPLY, send in byte 1;
|
|
330
|
+
_POWER_SUPPLY_OFF = 0x00
|
|
331
|
+
_POWER_SUPPLY_ON = 0x01
|
|
332
|
+
_POWER_SUPPLY_TBD = 0xFF # APPLY?
|
|
333
|
+
|
|
301
334
|
# Responses received from the interface
|
|
302
335
|
# Decodes to mode, response type, frame, interval, seq
|
|
303
336
|
_resptmpl = struct.Struct(">BB4sHB55x")
|
|
337
|
+
# length of the "useful part", without the trailing padding
|
|
338
|
+
_resptmpl_useful_size = _resptmpl.size - 55
|
|
304
339
|
_MODE_INFO = 0x01 # Response to an init command
|
|
305
340
|
_MODE_OBSERVE = 0x11 # Other traffic observed on the bus
|
|
306
341
|
_MODE_RESPONSE = 0x12 # Response to a send command
|
|
@@ -353,11 +388,40 @@ class tridonic(hid):
|
|
|
353
388
|
self._bus_watch_data_available = asyncio.Event()
|
|
354
389
|
self._bus_watch_data = []
|
|
355
390
|
|
|
391
|
+
def _raw_write(self, buf: bytes):
|
|
392
|
+
self._log.debug(f"_raw_write: {_hex(buf[0:self._cmdtmpl_useful_size])}")
|
|
393
|
+
os.write(self._f, buf)
|
|
394
|
+
|
|
356
395
|
def _initialise_device(self):
|
|
357
396
|
# Read firmware version; pick up the reply in _handle_read
|
|
358
|
-
|
|
397
|
+
self._raw_write(self._cmd(
|
|
359
398
|
tridonic._CMD_INIT, tridonic._CMD_INIT_READVERSION))
|
|
360
399
|
|
|
400
|
+
async def _power_supply(self, supply_on):
|
|
401
|
+
await self.connected.wait()
|
|
402
|
+
async with self._command_semaphore:
|
|
403
|
+
data = self._cmd(self._CMD_POWERSUPPLY,
|
|
404
|
+
self._POWER_SUPPLY_ON if supply_on else self._POWER_SUPPLY_OFF)
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
self._raw_write(data)
|
|
408
|
+
except OSError:
|
|
409
|
+
# The device has failed. Disconnect, schedule a
|
|
410
|
+
# reconnection, and report this command as failed.
|
|
411
|
+
self._log.debug("fail on transmit, disconnecting")
|
|
412
|
+
self.disconnect(reconnect=True)
|
|
413
|
+
raise CommunicationError
|
|
414
|
+
|
|
415
|
+
@staticmethod
|
|
416
|
+
def _command_mode(frame):
|
|
417
|
+
if len(frame) == 8:
|
|
418
|
+
return tridonic._SEND_MODE_DALI8
|
|
419
|
+
if len(frame) == 16:
|
|
420
|
+
return tridonic._SEND_MODE_DALI16
|
|
421
|
+
if len(frame) == 24:
|
|
422
|
+
return tridonic._SEND_MODE_DALI24
|
|
423
|
+
raise UnsupportedFrameTypeError
|
|
424
|
+
|
|
361
425
|
async def _send_raw(self, command):
|
|
362
426
|
frame = command.frame
|
|
363
427
|
if len(frame) not in (16, 24):
|
|
@@ -365,7 +429,7 @@ class tridonic(hid):
|
|
|
365
429
|
await self.connected.wait()
|
|
366
430
|
async with self._command_semaphore:
|
|
367
431
|
seq = next(self._cmd_seq)
|
|
368
|
-
self._log.debug("Sending with seq
|
|
432
|
+
self._log.debug(f"Sending with {seq=:02x}")
|
|
369
433
|
event = asyncio.Event()
|
|
370
434
|
messages = []
|
|
371
435
|
# If seq is in self._outstanding this means we've wrapped
|
|
@@ -376,11 +440,10 @@ class tridonic(hid):
|
|
|
376
440
|
data = self._cmd(
|
|
377
441
|
self._CMD_SEND, seq,
|
|
378
442
|
ctrl=self._SEND_CTRL_SENDTWICE if command.sendtwice else 0,
|
|
379
|
-
mode=self.
|
|
380
|
-
else self._SEND_MODE_DALI24,
|
|
443
|
+
mode=self._command_mode(frame),
|
|
381
444
|
frame=frame.pack_len(4))
|
|
382
445
|
try:
|
|
383
|
-
|
|
446
|
+
self._raw_write(data)
|
|
384
447
|
except OSError:
|
|
385
448
|
# The device has failed. Disconnect, schedule a
|
|
386
449
|
# reconnection, and report this command as failed.
|
|
@@ -391,11 +454,17 @@ class tridonic(hid):
|
|
|
391
454
|
outstanding_transmissions = 2 if command.sendtwice else 1
|
|
392
455
|
response = None
|
|
393
456
|
while outstanding_transmissions or response is None:
|
|
394
|
-
self._log.debug(f"waiting for {outstanding_transmissions=} "
|
|
395
|
-
"{response
|
|
457
|
+
self._log.debug(f"_send_raw waiting for {outstanding_transmissions=} "
|
|
458
|
+
f"{response=!s}")
|
|
396
459
|
if len(messages) == 0:
|
|
397
|
-
|
|
398
|
-
|
|
460
|
+
try:
|
|
461
|
+
await asyncio.wait_for(event.wait(), timeout=0.2)
|
|
462
|
+
except TimeoutError:
|
|
463
|
+
self._log.error(f"_send_raw timed out {seq=:02x}, faking an error response")
|
|
464
|
+
response = dali.frame.BackwardFrameError(255)
|
|
465
|
+
break
|
|
466
|
+
finally:
|
|
467
|
+
event.clear()
|
|
399
468
|
message = messages.pop(0)
|
|
400
469
|
if message == "fail":
|
|
401
470
|
# The device has gone away, possibly in the middle
|
|
@@ -406,11 +475,12 @@ class tridonic(hid):
|
|
|
406
475
|
# The message mode is guaranteed to be _MODE_RESPONSE
|
|
407
476
|
mode, rtype, frame, interval, seq = self._resptmpl.unpack(
|
|
408
477
|
message)
|
|
409
|
-
self._log.debug(f"message
|
|
478
|
+
self._log.debug(f"_send_raw got message: {mode=:02x} {rtype=:02x} frame={_hex(frame)} {interval=:04x} {seq=:02x}")
|
|
410
479
|
if rtype in (self._RESPONSE_FRAME_DALI16,
|
|
411
480
|
self._RESPONSE_FRAME_DALI24):
|
|
412
481
|
# XXX check the frame contents?
|
|
413
482
|
outstanding_transmissions -= 1
|
|
483
|
+
self._log.debug(f"_send_raw rtype DALI16 or DALI24, new {outstanding_transmissions=}")
|
|
414
484
|
elif rtype == self._RESPONSE_FRAME_DALI8:
|
|
415
485
|
response = dali.frame.BackwardFrame(frame)
|
|
416
486
|
elif rtype == self._RESPONSE_INFO \
|
|
@@ -419,7 +489,8 @@ class tridonic(hid):
|
|
|
419
489
|
elif rtype == self._RESPONSE_NO_FRAME:
|
|
420
490
|
response = "no"
|
|
421
491
|
else:
|
|
422
|
-
self._log.debug(f"didn't understand {rtype
|
|
492
|
+
self._log.debug(f"didn't understand {rtype=:02x}")
|
|
493
|
+
self._log.debug(f"_send_raw finished {seq=:02x}")
|
|
423
494
|
del self._outstanding[seq], event, messages
|
|
424
495
|
if command.response:
|
|
425
496
|
# Construct response and return it
|
|
@@ -443,13 +514,13 @@ class tridonic(hid):
|
|
|
443
514
|
# Wait for data
|
|
444
515
|
if len(self._bus_watch_data) == 0:
|
|
445
516
|
if current_command:
|
|
446
|
-
self._log.debug("
|
|
517
|
+
self._log.debug("bus_watch waiting with timeout")
|
|
447
518
|
try:
|
|
448
519
|
await asyncio.wait_for(self._bus_watch_data_available.wait(), 0.2)
|
|
449
520
|
except asyncio.TimeoutError:
|
|
450
521
|
pass
|
|
451
522
|
else:
|
|
452
|
-
self._log.debug("
|
|
523
|
+
self._log.debug("bus_watch waiting for data, no timeout")
|
|
453
524
|
await self._bus_watch_data_available.wait()
|
|
454
525
|
self._bus_watch_data_available.clear()
|
|
455
526
|
|
|
@@ -460,10 +531,10 @@ class tridonic(hid):
|
|
|
460
531
|
else:
|
|
461
532
|
timeout = False
|
|
462
533
|
message = self._bus_watch_data.pop(0)
|
|
463
|
-
self._log.debug("bus_watch message %s", _hex(message[0:
|
|
534
|
+
self._log.debug("bus_watch message %s", _hex(message[0:self._resptmpl_useful_size]))
|
|
464
535
|
origin, rtype, raw_frame, interval, seq = self._resptmpl.unpack(message)
|
|
465
536
|
if origin not in (self._MODE_OBSERVE, self._MODE_RESPONSE):
|
|
466
|
-
self._log.
|
|
537
|
+
self._log.warning("bus_watch: unexpected packet mode, ignoring")
|
|
467
538
|
continue
|
|
468
539
|
if rtype == self._RESPONSE_FRAME_DALI16:
|
|
469
540
|
frame = dali.frame.ForwardFrame(16, raw_frame)
|
|
@@ -473,7 +544,7 @@ class tridonic(hid):
|
|
|
473
544
|
frame = dali.frame.BackwardFrame(raw_frame)
|
|
474
545
|
elif rtype == self._RESPONSE_NO_FRAME:
|
|
475
546
|
frame = "no"
|
|
476
|
-
elif rtype == self.
|
|
547
|
+
elif rtype == self._RESPONSE_INFO \
|
|
477
548
|
and message[5] == self._BUS_STATUS_FRAMING_ERROR:
|
|
478
549
|
frame = dali.frame.BackwardFrameError(255)
|
|
479
550
|
else:
|
|
@@ -490,7 +561,7 @@ class tridonic(hid):
|
|
|
490
561
|
# We are waiting for a repeat of the command
|
|
491
562
|
if timeout:
|
|
492
563
|
# We didn't get it: report a failed command
|
|
493
|
-
self._log.
|
|
564
|
+
self._log.warning("Failed sendtwice command: %s", current_command)
|
|
494
565
|
self.bus_traffic._invoke(current_command, None, True)
|
|
495
566
|
current_command = None
|
|
496
567
|
continue
|
|
@@ -502,18 +573,23 @@ class tridonic(hid):
|
|
|
502
573
|
current_command = None
|
|
503
574
|
continue
|
|
504
575
|
else:
|
|
505
|
-
self._log.
|
|
576
|
+
self._log.warning("Failed config command (second frame didn't match): %s", current_command)
|
|
506
577
|
self.bus_traffic._invoke(current_command, None, True)
|
|
507
578
|
current_command = None
|
|
508
579
|
# Fall through to continue processing frame
|
|
509
580
|
elif isinstance(frame, dali.frame.BackwardFrame):
|
|
510
581
|
# Error: config commands don't get backward frames.
|
|
511
|
-
self._log.
|
|
582
|
+
self._log.error("Failed config command %s with backward frame",
|
|
512
583
|
current_command)
|
|
513
584
|
self.bus_traffic._invoke(current_command, None, True)
|
|
514
585
|
current_command = None
|
|
586
|
+
elif frame == "no":
|
|
587
|
+
self._log.warning("No frame in response to a sendtwice command: %s", current_command)
|
|
588
|
+
self.bus_traffic._invoke(current_command, None, True)
|
|
589
|
+
current_command = None
|
|
590
|
+
continue
|
|
515
591
|
else:
|
|
516
|
-
self._log.
|
|
592
|
+
self._log.error("Unexpected response waiting for retransmit of config command, frame = %s", frame)
|
|
517
593
|
elif current_command.response:
|
|
518
594
|
# We are waiting for a response
|
|
519
595
|
if timeout or frame == "no":
|
|
@@ -565,13 +641,13 @@ class tridonic(hid):
|
|
|
565
641
|
# self._log.debug("End of loop")
|
|
566
642
|
|
|
567
643
|
def _handle_read(self, data):
|
|
568
|
-
self._log.debug("_handle_read %s", _hex(data[0:
|
|
644
|
+
self._log.debug("_handle_read %s", _hex(data[0:self._resptmpl_useful_size]))
|
|
569
645
|
if data[0] == self._MODE_INFO:
|
|
570
646
|
# Response to initialisation command
|
|
571
647
|
if not self.firmware_version:
|
|
572
648
|
self.firmware_version = f"{data[3]}.{data[4]}"
|
|
573
649
|
# Now read the serial number
|
|
574
|
-
|
|
650
|
+
self._raw_write(self._cmd(
|
|
575
651
|
tridonic._CMD_INIT, tridonic._CMD_INIT_READSERIAL))
|
|
576
652
|
elif not self.serial:
|
|
577
653
|
self.serial = _hex(data[1:5])
|
|
@@ -589,17 +665,20 @@ class tridonic(hid):
|
|
|
589
665
|
self._bus_watch_data.append(data)
|
|
590
666
|
self._bus_watch_data_available.set()
|
|
591
667
|
seq = data[8]
|
|
592
|
-
# The Tridonic DALI USB has a firmware bug. When it
|
|
593
|
-
# observes a frame on the bus, not generated by itself,
|
|
594
|
-
# that matches the most recent frame it transmitted, it
|
|
595
|
-
# reports it as if it had transmitted it itself. We
|
|
596
|
-
# ignore this - the sequence number will have been removed
|
|
597
|
-
# from self._outstanding.
|
|
598
668
|
if seq in self._outstanding:
|
|
669
|
+
self._log.debug(f"_handle_read: signaling for {seq=:02x}")
|
|
599
670
|
event, messages = self._outstanding[seq]
|
|
600
671
|
messages.append(data)
|
|
601
672
|
event.set()
|
|
602
673
|
del event, messages
|
|
674
|
+
else:
|
|
675
|
+
# The Tridonic DALI USB has a firmware bug. When it
|
|
676
|
+
# observes a frame on the bus, not generated by itself,
|
|
677
|
+
# that matches the most recent frame it transmitted, it
|
|
678
|
+
# reports it as if it had transmitted it itself. We
|
|
679
|
+
# ignore this - the sequence number will have been removed
|
|
680
|
+
# from self._outstanding.
|
|
681
|
+
self._log.debug(f"_handle_read: nothing to do for {seq=:02x}")
|
|
603
682
|
|
|
604
683
|
else:
|
|
605
684
|
self._log.debug("Unknown response mode %x", data[0])
|
|
@@ -662,7 +741,7 @@ class hasseb(hid):
|
|
|
662
741
|
async with self._command_lock:
|
|
663
742
|
times = 2 if command.sendtwice else 1
|
|
664
743
|
for rep in range(times):
|
|
665
|
-
|
|
744
|
+
self._raw_write(frame.pack_len(2))
|
|
666
745
|
# Earlier commands may have left a response available that
|
|
667
746
|
# we need to ignore. We're only interested in responses
|
|
668
747
|
# that become available in the future.
|