Skip to content

feat(agent): refresh device truth on demand and on an interval - #14

Merged
impuls42 merged 4 commits into
mainfrom
feat/probe-on-demand-and-interval
Aug 2, 2026
Merged

feat(agent): refresh device truth on demand and on an interval#14
impuls42 merged 4 commits into
mainfrom
feat/probe-on-demand-and-interval

Conversation

@impuls42

@impuls42 impuls42 commented Aug 2, 2026

Copy link
Copy Markdown
Member

0.3.0 gave the status topic a memory. This adds two ways to refresh what it remembers, both routed through the existing probe_device rather than inventing a second path to the printer.

On demand — probe on the cmd topic

The nice part is how little it needs. That topic already exists and already carries flush, the agent already subscribes to it, and the broker ACL already grants inventree topic write se/v1/print/+/cmd. So a refresh button in a consumer UI needs no new topic, no ACL change, no new credential.

Accepts probe or status, case- and whitespace-insensitive — it arrives from a human pressing a button, and being fussy about which synonym they typed buys nothing.

Like flush, it only enqueues a sentinel. The cmd handler runs on paho's network thread, and nothing there may touch the printer.

On an interval — device.probe_interval_s

Off by default. Two clocks, and conflating them is the trap:

  • staleness is measured on seen_at — what the printer last said — so a printer that is being used is never probed at all, because printing keeps that fresh on its own
  • the rate limit is measured on the last attempt, because a failed probe deliberately leaves seen_at untouched. Without a separate attempt clock the stale condition would still hold on the next tick, and a sleeping printer would be dialled once a second, each attempt blocking the print loop for the connect timeout.

That second one has a test of its own (test_a_sleeping_printer_is_not_dialled_every_tick), because it is the kind of bug that looks fine in review and eats a print loop in production.

A missed probe stays quiet

It does not publish disconnected. That is reserved for a printer that could not take a job, which is news a producer needs. A background probe finding the D30 asleep is not news — it is the normal state of an idle printer, and flipping the page between connected and disconnected as it naps would make the distinction worthless. The reading ages instead, which is exactly what device_seen_at was added for.

Verified

274 tests, ruff clean. Pairs with a consumer change that makes InvenTree's existing "Restart Machine" action send probe, turning it into a real refresh instead of a re-read of memory.

Worth noting for anyone looking for a new button: InvenTree's machine framework has no generic custom-action mechanism — restart_machine is the single hardcoded action, with its own endpoint and a fixed UI menu. Reusing it is the whole reason this lands without patching InvenTree.

0.3.0 gave the status topic a memory. Two ways to refresh what it remembers, both
reusing probe_device rather than inventing a second path to the printer.

`probe` (or `status`) on the cmd topic surveys the printer immediately. That topic
already exists and already carries `flush`, the agent already reads it, and the broker
ACL already lets InvenTree write it -- so a refresh button in a consumer UI needs no new
topic, no ACL change and no new credential. Like flush, it only enqueues a sentinel: the
cmd handler runs on paho's network thread and nothing there may touch the printer.

device.probe_interval_s re-surveys once the reading goes stale, off by default. Two
clocks here and conflating them is the trap. Staleness is measured on seen_at, what the
printer last *said*, so a printer that is being used is never probed -- printing keeps
it fresh already. The rate limit is measured on the last *attempt*, because a failed
probe deliberately leaves seen_at alone; without a separate attempt clock the stale
condition would still hold a tick later and a sleeping printer would be dialled once a
second, each attempt blocking the print loop for the connect timeout.

A missed probe stays silent rather than publishing `disconnected`. That is reserved for
a printer that could not take a job, which is news a producer needs. A background probe
finding the D30 asleep is the normal state of a printer nobody is using, and flipping
the page between connected and disconnected as it naps would make the distinction
worthless. The reading ages instead, which is what device_seen_at is for.
…Ping' controls

An operator who turns off InvenTree's Machine Ping expecting the printer to stop being
contacted would be half right: that setting stops InvenTree re-reading the retained
topic, a cheap read that never touches Bluetooth, and cannot reach this timer, which is
the one that actually dials the printer. Says so where the knob is, since neither side
is guessable from the other.

Also records the ping's 5-minute cadence as the sensible floor for this value.
…s not a shortcut

Partly answers the open question on wake behaviour: it answers BR/EDR inquiry
continuously while powered, not only for a window after wake, and disappears within
about a minute of being switched off. Measured on fw 2.1.2 with auto_power=0.

Three things worth writing down because each would otherwise be discovered the hard way.

l2ping gets no response at all -- including while the unit was demonstrably up and
RFCOMM connected 3.3s later -- so the usual liveness trick does not exist here.

Presence must be judged from an RSSI update inside a scan window, not from the cached
devices list. That list happens to self-clear today only because the printer is
unpaired and BlueZ expires temporary devices; pairing it would make the entry permanent
and a naive check would then report the printer online forever.

And presence detection is not a cheap substitute for connecting, which was the reason
to want it: a 3-8s inquiry window against a 5.2s failed connect is no saving, and the
connect returns real device state when it succeeds while a scan returns a boolean.
The docstring said 'called from the run loop at startup, and only there', which this
same branch made false by adding the idle tick and the probe command. Reworded around
the constraint that actually matters and still holds: every caller is on the print loop,
and nothing may call it from paho's network thread.

That is also why the probe command arrives as a queue sentinel rather than being
serviced in the cmd handler -- the sentinel exists to move the work onto the loop.
impuls42 added a commit to sengine-cloud/inventree-label-dispatch that referenced this pull request Aug 2, 2026
…ing memory (#9)

Pairs with sengine-cloud/labelfab#14, which adds the `probe` command
this sends.

## The problem with the button

The retained status is what the agent last *heard*. Re-reading it on a
manual refresh returned the same thing the page was already showing —
pressing the button could not tell you anything you did not already
know.

It now publishes `probe` on the `cmd` topic and waits for the agent to
actually go and look at the printer.

## Why this needed nothing new

The `cmd` topic already existed and already carries `flush`, the agent
already subscribes to it, and the broker ACL already grants `inventree`
`topic write se/v1/print/+/cmd`. So: **no new topic, no ACL change, no
new credential.**

## On "custom actions"

Worth recording, because it is the first thing anyone will ask.
InvenTree's machine framework has **no generic custom-action mechanism**
— I checked the running pod. `restart_machine` is the single hardcoded
action, with its own `MachineRestart` endpoint and a fixed place in the
Machines menu. Adding a *new* menu item means patching InvenTree.

So "Restart Machine" **is** the refresh button whether or not it is
called that, and it was being wasted on a re-read.

## Degradation

`probe_status` subscribes *before* publishing, because the answer is a
retained message and a subscription set up afterwards would race the
agent. The message already on the topic arrives immediately and is kept
as the baseline, so a probe the printer cannot answer — the normal case
for a D30 that has powered itself down — degrades to exactly what
`read_status` would have returned. The page keeps the old reading and
its `seen Xh ago`, rather than going blank.

The automatic paths (`init_machine`, `ping_machines`) deliberately keep
reading rather than probing: neither should dial a sleeping printer on a
schedule nobody asked for.

## Verified

54 tests on 3.12 and on a real 3.9.25 in Docker, ruff clean. The new
tests cover the subscribe-before-publish ordering, the
fall-back-to-remembered path, a malformed retained payload, and
nothing-retained-and-no-answer.
@impuls42
impuls42 merged commit d83a9a3 into main Aug 2, 2026
1 check passed
@impuls42
impuls42 deleted the feat/probe-on-demand-and-interval branch August 2, 2026 22:01
impuls42 added a commit that referenced this pull request Aug 3, 2026
Two ways to refresh what the status topic remembers (#14). `probe` on the cmd topic
surveys the printer on demand, which is what a refresh button in a consumer should
send; device.probe_interval_s re-surveys once the reading goes stale, off by default.

Both route through the existing probe_device, so a miss still cannot wake a sleeping
printer and still leaves the remembered reading and its timestamp alone.
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