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 CHANGED
@@ -7,3 +7,5 @@ part 103 ("General requirements - Control devices") and parts 3xx
7
7
  import dali.device.general
8
8
  import dali.device.sequences
9
9
  import dali.device.pushbutton # noqa: F401
10
+ import dali.device.occupancy # noqa: F401
11
+ import dali.device.light # noqa: F401
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.input_device_error
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
- os.write(self._f, self._cmd(
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 %x", 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._SEND_MODE_DALI16 if len(frame) == 16
380
- else self._SEND_MODE_DALI24,
443
+ mode=self._command_mode(frame),
381
444
  frame=frame.pack_len(4))
382
445
  try:
383
- os.write(self._f, data)
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
- await event.wait()
398
- event.clear()
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 mode={mode:02x} rtype={rtype:02x} frame={frame} interval={interval:04x} seq={seq:02x}")
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("Bus watch waiting with timeout")
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("Bus watch waiting for data, no timeout")
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:9]))
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.debug("bus_watch: unexpected packet mode, ignoring")
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._RESPONSE_BUS_STATUS \
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.debug("Failed sendtwice command: %s", current_command)
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.debug("Failed config command (second frame didn't match): %s", current_comment)
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.debug("Failed config command %s with backward frame",
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.debug("Unexpected response waiting for retransmit of config command")
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:9]))
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
- os.write(self._f, self._cmd(
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
- os.write(self._f, frame.pack_len(2))
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.