bus¶
ElliptecBus owns the serial port for one multidrop connection: a single
background worker thread services every request()/send()/read_reply()
call through a priority queue (see Polling position without starving other commands), so replies are
never interleaved and explicitly issued commands always jump ahead of
background polling.
Serial communication with an Elliptec ELLx multidrop bus (a “hub”).
A single ELLx interface adapter (USB or TTL RS-232) exposes one serial port on which up to 16 devices (addresses 0-F) can be daisy-chained. This module owns the serial port, frames outgoing commands, reads and dispatches replies, and understands the two-phase reply pattern used by long-running commands (an interim “GS” busy status followed by a final data reply).
Device-level command objects (see tl_elliptec.devices) are built on top of
an ElliptecBus instance and should not talk to serial directly.
Only one thread ever touches the serial port: a single background worker
owned by ElliptecBus. Every public entry point (request,
send, read_reply) submits a job to that worker through a priority
queue (see RequestPriority) and blocks the calling thread until its
own job completes. This is what lets several devices on a shared bus – and
a background position poller per device – all issue commands concurrently
from different threads without corrupting each other’s replies, while
letting explicitly issued commands (moves, reads, …) always jump ahead of
low-priority background polling that’s still waiting for its turn.
- class tl_elliptec.bus.RequestPriority[source]¶
Bases:
objectOrdering used by
ElliptecBus’s broker (lower value = serviced first).COMMANDis the default for every explicitly issued call (moves, status reads, addressing, …).POLLis for background/opportunistic traffic – e.g.poll_position()– so it never delays a command a caller is actively waiting on, even when several devices’ pollers share one bus.- COMMAND = 0¶
- POLL = 10¶
- class tl_elliptec.bus.Reply(address, command, data, raw)[source]¶
Bases:
objectA parsed incoming frame:
address(single hex digit the reply came from),command(2-character reply mnemonic, e.g."GS"or"PO"),data(hex-ASCII data payload, may be empty), andraw(the raw frame bytes as received, terminator stripped).- address¶
- command¶
- data¶
- raw¶
- as_status_code()[source]¶
Interpret this reply’s data as a GS/BS status byte.
- Returns:
The numeric status code (see
StatusCode).- Return type:
- raise_for_status()[source]¶
Raise
ElliptecStatusErrorif this is a non-OK GS/BS reply.- Raises:
ElliptecStatusError – If
commandis"GS"/"BS"and the status code isn’tOK.- Return type:
None
- class tl_elliptec.bus.ElliptecBus(port, baudrate=9600, timeout=2.0, serial_kwargs=None)[source]¶
Bases:
objectOwns the serial port for one Elliptec multidrop (hub) connection.
Thread-safe: a single background worker owns the serial port and services requests from a priority queue (see
RequestPriority), so replies from different addresses on the shared bus are never interleaved or misattributed, and commands a caller is actively waiting on always preempt queued background polling.- close()[source]¶
Stop the request broker’s worker thread and close the serial port.
- Return type:
None
- read_reply(timeout=None, priority=0)[source]¶
Read and parse a single incoming frame (blocking).
Useful for passively listening for spontaneous BS/BO button-status messages, which aren’t solicited by a host command.
- Parameters:
timeout (float | None) – Maximum time to wait, in seconds. Defaults to the bus’s own
timeout.priority (int) – Scheduling priority against other pending requests (see
RequestPriority).
- Returns:
The next parsed frame.
- Raises:
ElliptecTimeoutError – If no frame arrives within
timeout.- Return type:
- send(address, command, data='', priority=0)[source]¶
Send a command with no expectation of a reply being awaited here.
- Parameters:
address (str) – Single hex-digit device address.
command (str) – 2-character command mnemonic.
data (str) – Optional hex-ASCII data payload.
priority (int) – Scheduling priority against other pending requests (see
RequestPriority).
- Return type:
None
- send_urgent(address, command, data='')[source]¶
Write directly to the wire, bypassing the request broker’s queue entirely.
Ordinary commands should go through
request()/send()so they’re properly serialized and priority-ordered. This exists for the case where that’s the wrong tool: sending a command meant to affect a device while some other command for it is already executing – e.g. HOST_MOTIONSTOP “st” while continuous jog motion is running. That other job’s worker thread is blocked inside its own read loop; a normalrequest("st")would just sit in the queue until that job finishes on its own. Writing and reading are independent operations on a serial port, so this can safely interleave with an in-progress read on the broker’s worker thread –_write’s own lock only guards against two writes landing on the wire at once, not against a concurrent read.Note: confirmed on real hardware that HOST_MOTIONSTOP “st” does not interrupt a bounded move_absolute/move_relative/home once issued – only continuous jog motion (jog step size 0) or an optimize/clean cycle. Sending “st” while a bounded move is in flight is a no-op; that move’s job keeps running until the physical move completes on its own, exactly as if “st” was never sent (see
MotionMixin.stop).No reply is read here – whatever job is already in flight for this address will see any response on its own next read. Call
get_position()/get_status()afterward if you need to confirm the outcome.
- request(address, command, data='', expect=None, timeout=None, poll_timeout=None, raise_on_error=True, priority=0, reply_address=None)[source]¶
Send a command and wait for its reply.
Several commands (moves, homing, frequency search, cleaning…) reply with one or more interim “GS busy” frames before the final reply arrives. This loops, discarding interim busy frames, until either the expected reply mnemonic is seen, a non-busy status/error frame is seen, or the overall timeout elapses.
expectis the reply mnemonic to wait for (e.g. “PO” for a move). If omitted, the first non-busy reply is returned regardless of its mnemonic.reply_addressoverrides which address incoming replies must carry to be accepted as the answer to this request; defaults toaddress. Needed for HOSTREQ_CHANGEADDRESS (“ca”), the one command where the device replies from its new address rather than the one the command was sent to.prioritycontrols scheduling against other pending requests on this bus (seeRequestPriority); leave it at the default unless this call is opportunistic background polling.- Parameters:
address (str) – Single hex-digit device address to send the command to.
command (str) – 2-character command mnemonic, e.g.
"in".data (str) – Optional hex-ASCII data payload.
expect (str | None) – Reply mnemonic to wait for, e.g.
"PO". If omitted, the first non-busy reply is accepted regardless of mnemonic.timeout (float | None) – Overall deadline for this request, in seconds. Defaults to the bus’s own
timeout.poll_timeout (float | None) – Maximum time to wait for each individual interim frame (e.g. a busy status) before re-checking the overall deadline. Defaults to the remaining overall timeout.
raise_on_error (bool) – If
True(default), a non-OK GS/BS status reply raisesElliptecStatusError.priority (int) – Scheduling priority against other pending requests (see
RequestPriority).reply_address (str | None) – Address incoming replies must carry to be accepted; defaults to
address.
- Returns:
The reply that satisfied
expect(or the first non-busy reply, ifexpectwas omitted).- Raises:
ElliptecTimeoutError – If no satisfying reply arrives within
timeout.ElliptecStatusError – If
raise_on_errorisTrueand a non-OK GS/BS status reply is received.
- Return type:
- request_status(address, command, data='', **kwargs)[source]¶
Convenience wrapper for commands whose only reply is GS/BS.
- scan(addresses='0123456789ABCDEF', timeout=0.3)[source]¶
Probe each address with an “in” (get info) request; return the ones that answer.
- refresh_devices(addresses='0123456789ABCDEF', timeout=0.3)[source]¶
Re-scan the bus and rebuild this bus’s known-device registry.
Returns a plain, JSON-safe dict:
{address: {ell_type, serial_number, travel, pulses_per_unit}}.pulses_per_unitis the already-corrected value (handles the rotary pulses-per-revolution correction internally – seeElliptecDevice.PULSES_FIELD_IS_PER_REVOLUTION), so callers never need to reimplement that calibration themselves.The registry is mutated in place (
clear()``+``update(), never rebound to a new dict), so an already-runningstream_positions()generator picks up newly discovered devices on its next tick without needing to be restarted.- Parameters:
- Returns:
A dict keyed by address, each value a dict with keys
ell_type(int),serial_number(str),travel(int), andpulses_per_unit(float) – one entry per device found.- Return type:
- stream_positions(interval=0.2, tolerance=0.0)[source]¶
Yield
{address: {"pulses": int, "units": float}}for whichever devices in this bus’s registry (seerefresh_devices()) changed by more thantolerancesince the last tick.One thread, one loop: every device is read sequentially through the same request broker each tick, at
RequestPriority.POLL, so this never adds any concurrency on the serial link beyond whatrequest()/send()already handle safely, and never delays an explicitly issued command. Meant to be driven by a caller that wants continuous position updates (e.g. a WebSocket “subscribe”) rather than polled directly.Reads the registry fresh (via
list(...)) each outer-loop pass, so devices added by a concurrentrefresh_devices()call show up automatically on the next tick – no need to restart this generator.- Parameters:
- Yields:
{address: {"pulses": int, "units": float}}for whichever devices changed by more thantolerancesince the last tick. Never yields an empty dict – if nothing changed, the tick is skipped entirely.