Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
python-version: ['3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v3
Expand Down
38 changes: 33 additions & 5 deletions dali/device/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
)
from dali.exceptions import MissingResponse, ResponseError
from dali.frame import BackwardFrameError
from dali.sequences import progress as seq_progress
from dali.sequences import progress as seq_progress, sleep


def check_bad_rsp(r: Response | None) -> bool:
Expand Down Expand Up @@ -85,7 +85,10 @@ def mapping(self) -> dict[(int, int), int]:
return self._mapping

def autodiscover(
self, addresses: int | tuple[int, int] | Iterable[int] = (0, 63)
self,
addresses: int | tuple[int, int] | Iterable[int] = (0, 63),
quiescent_settle: float = 0.2,
status_attempts: int = 3,
) -> Generator[Command, Response, None]:
"""
A generator sequence to scan a DALI bus for control device instances,
Expand All @@ -99,6 +102,11 @@ def autodiscover(
which case all addresses between the provided values will be scanned;
or finally can be an iterable of ints in which case each address, in
the iterator will be scanned.
:param quiescent_settle: Seconds to wait after entering quiescent mode
before probing, so an event frame already in flight does not corrupt
the first address probe.
:param status_attempts: How many times to probe an address whose status
reply collides with a spontaneous event before treating it as empty.
:return: A generator function, to use with e.g. `driver.run_sequence()`

Needs to be used through an appropriate driver, with `run_sequence()`,
Expand All @@ -111,6 +119,12 @@ def autodiscover(

# Use quiescent mode to reduce bus contention from input devices
yield StartQuiescentMode(DeviceBroadcast())
# Quiescent mode is not instant and an input device may already have an
# event frame in flight. Let the bus settle so the first probe is not
# corrupted by a colliding backward frame, which would otherwise make a
# present device look absent.
if quiescent_settle:
yield sleep(quiescent_settle)

if isinstance(addresses, int):
addresses = (n for n in range(0, addresses))
Expand All @@ -120,8 +134,16 @@ def autodiscover(
for addr_int in addresses:
addr = DeviceShort(addr_int)

# Check that the device exists and responds
rsp = yield QueryDeviceStatus(device=addr)
# Check that the device exists and responds. A framing error means a
# device answered but its backward frame collided with a spontaneous
# event, so retry; a missing response means the address is empty, so
# stop probing it immediately.
for _attempt in range(status_attempts):
rsp = yield QueryDeviceStatus(device=addr)
if not check_bad_rsp(rsp):
break
if not isinstance(getattr(rsp, "raw_value", None), BackwardFrameError):
break
if check_bad_rsp(rsp):
continue
if isinstance(rsp, QueryDeviceStatusResponse):
Expand All @@ -140,7 +162,13 @@ def autodiscover(
rsp = yield QueryNumberOfInstances(device=addr)
if check_bad_rsp(rsp):
continue
num_inst = rsp.value
# Instance numbers are 0..31 (IEC 62386-103), so a device can have
# at most 32 instances. Cap the count defensively: a device
# reporting more, or a value corrupted by a colliding event frame,
# would otherwise build an out-of-range InstanceNumber and abort the
# whole scan. Non-existent instances are skipped when they do not
# respond to QueryInstanceEnabled below.
num_inst = min(rsp.value, 32)

# For each instance, check it is enabled and then query the type
for inst_int in range(num_inst):
Expand Down
Loading
Loading