Skip to content

HamiltonTextTCP and discover_instruments: Hamilton's text protocol over TCP, and finding the machines that speak itHamilton/text tcp transport - #1556

Open
jrast wants to merge 6 commits into
PyLabRobot:mainfrom
jrast:hamilton/text-tcp-transport
Open

jrast wants to merge 6 commits into
PyLabRobot:mainfrom
jrast:hamilton/text-tcp-transport

Conversation

@jrast

@jrast jrast commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Draft, stacked on #1555. Its three commits come first; review the last three
(97acbb6, 637af0f, 7cbba60).

Adds, in pylabrobot/hamilton/transport/text_tcp/, what a v1 driver needs to talk to a Hamilton
machine that speaks the text protocol over TCP:

  • HamiltonTextTCP: an IOBase around Socket. One ASCII line per command or reply, each
    ending in CRLF, port 24652. read returns one whole line, and a connection the machine closed
    raises ConnectionError.
  • discover_instruments: a UDP broadcast that machines answer with bdc_<key> <value> pairs.
    A machine that answers more than once is listed once. Waits 0.5 s by default.
  • resolve_host: an IP address as it is, localhost and *.local through the system, and any
    other name matched against the system id and module name of the machines that answered.

@jrast
jrast marked this pull request as ready for review October 28, 2026 12:52
@jrast
jrast requested a review from a team as a code owner October 28, 2026 12:52
jrast added 6 commits November 2, 2026 08:40
A machine sends the messages it was not asked for with id 0, so a command must not carry that id.
STAR's firmware takes ids 0001 to 9999, so the only difference STAR sees is that id 0000 is never
sent.
…ts id is read

The router takes an optional `is_spontaneous` predicate. A line it accepts answers no command,
whatever id it carries: it is logged at DEBUG and dropped, and is not reported as a reply that
nothing was waiting for. Without a predicate nothing changes.

The reading loop hands each line to `_handle_reply` and expires timed-out commands in
`_expire_timed_out`, so a reader of another kind can use the same two.
…the transport

A socket's streams belong to the loop they were opened in, so the reader thread, which runs a
loop of its own, cannot await them. With `read_on_owner_loop` the replies are read by a task on
the loop that `start` is called in. It reads from `start` to `stop`, whether or not a command is
waiting, so a line the machine sends unasked is taken off the link at once.

One read stays outstanding while commands whose time is up are failed at a short interval, so an
idle link causes no read timeouts. A read that fails with anything but a timeout means the link is
down: the waiting commands are failed with that error, the reader ends, and a later `send` raises
`ConnectionError`. `stop` cancels the reading task without waiting for it, so it can be called
from the owner loop and the transport can be closed straight after.

Without the option the reader thread is used.
…ated line at a time

A Hamilton machine reached over TCP takes a command as one ASCII line ending in CRLF and answers
with one line, and it sends lines of its own that nobody asked for. There is no handshake and no
keepalive. The default port is 24652.

`write` adds the terminator. `read` returns one complete line without it, so a reply split across
TCP segments is returned once whole, and several replies in one segment are returned one by one.
A connection the machine closed raises `ConnectionError`, so a reader that drains the link cannot
spin on it. The socket's read timeout is long: an idle link is normal, because the reply router
reads continuously.
… broadcast, and connect by its name

A machine that speaks the text protocol answers a UDP broadcast with its own header followed by
`bdc_<key> <value>` pairs that say who it is. A value is a bare token, as a number is, or text in
double quotes that may contain spaces: `bdc_systemid "HAMILTON XRP 0000" bdc_modulemode 2`.

`discover_instruments` sends the broadcast to `x.y.z.255` for each IPv4 address of the computer,
assuming a /24, and to the limited broadcast address, then reads the answers for a timeout. A
machine that the broadcast reaches through more than one of those targets answers once for each,
so it is listed once, by address.

`HamiltonInstrumentInfo` holds the address, system identifier, module name, module description and
MAC address, and every key of the reply in `raw`. The address in a reply is used only if it is a
valid IPv4 address; otherwise the address the datagram came from is.

`resolve_host` returns an IPv4 address as it is, resolves `localhost` and `*.local` names through
the system, and otherwise matches the name against the system identifier and module name of the
machines that answered, ignoring case. A name that matches nothing, or more than one machine,
raises `ValueError`.

The broadcast is sent from a plain UDP socket in this module, not through a `pylabrobot.io` class.
…age exports

The package lists its public names in `__all__`, as the other Hamilton packages do. Without it the
autosummary of a package that only re-exports documents nothing, and the docs build passes all the
same.
@jrast
jrast force-pushed the hamilton/text-tcp-transport branch from acd10f5 to 1a2823a Compare November 2, 2026 07:47

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant