Skip to content

feat(status): report the printer's own state, and the real print outcome - #3

Merged
impuls42 merged 3 commits into
mainfrom
feat/device-status-feedback
Jul 30, 2026
Merged

feat(status): report the printer's own state, and the real print outcome#3
impuls42 merged 3 commits into
mainfrom
feat/device-status-feedback

Conversation

@impuls42

@impuls42 impuls42 commented Jul 29, 2026

Copy link
Copy Markdown
Member

Pairs with sengine-cloud/labelfab#9, which publishes the device truth this consumes.

The problem

The Machines page could say connected about a printer with no tape in it. Status collapsed to three cases, so every fault the agent published looked healthy:

if status is None:                          -> UNKNOWN
elif status.get("state") == "disconnected": -> DISCONNECTED
else:                                       -> CONNECTED     # including state == "error"

And print_labels set PRINTING and returned. JobResult was published by the agent on <prefix>/<printer_id>/results and consumed by nobody, so InvenTree never learned whether a job succeeded.

Status mapping

Uses the codes InvenTree already defines (CONNECTED 100, UNKNOWN 101, PRINTING 110, NO_MEDIA 301, DISCONNECTED 400, ERROR 500):

Printer reports Machines status
nothing retained UNKNOWN — agent has never published
idle, media ok CONNECTED — with fw, battery, voltage, serial
media bit clear (0x06) NO_MEDIA
any other fault ERROR, carrying the reason
agent will / shutdown DISCONNECTED

Two deliberate distinctions:

  • A media fault is not a generic error. NO_MEDIA and ERROR render differently and mean different things to whoever is looking: "go load tape" vs "go investigate".
  • Unreported media is not healthy media. media_ok is a tri-state; a printer that has said nothing shows media unreported. Treating silence as health is exactly how the old mapping produced a confident, wrong answer.

Battery percentage and voltage are both shown because the percentage pins at 100% whenever the unit is on charge — measured on the bench, 100% for the entire time voltage was climbing 4.16→4.17 V. On its own it cannot answer whether the printer will survive a long strip.

Print outcomes

AWAIT_RESULT_S waits for the job's JobResult, so the UI reports what happened rather than that something was sent.

  • Subscribes before publishing. The results topic is not retained, so subscribing afterwards races the agent and loses a fast print's result entirely.
  • Filters on job_id. One printer serves every InvenTree user; another click's result must not be mistaken for yours.
  • A timeout is an ordinary outcome. Strip mode holds labels until the coalescer flushes (30 s by default), which is longer than any wait worth blocking a Django-Q worker on. It stays PRINTING, and the next refresh reads the truth off the retained topic.
  • partial_tape_consumed is surfaced in the text, since a blind reprint after one double-prints.

It defaults to 0 (off). The wait holds a Django-Q worker, and in strip mode the agent buffers until its coalescer flushes (30 s), so it would usually time out and leave the status to the retained topic regardless. Worth enabling on die-cut/discrete media, where a result lands in a few seconds. The help text states the worker-blocking cost explicitly.

Structure

The mapping lives in a new status.py with no InvenTree imports, so it is unit-testable here — InvenTree is intentionally not a dependency of this package. The driver stays the thin adapter it claims to be, translating a returned member name to the enum.

test_every_mapping_names_a_real_status_member pins every name against the real LabelPrinterStatus members, because a typo would otherwise surface as an AttributeError inside a Django worker rather than at test time.

34 tests pass, ruff clean.

PAPER_JAM (302) is deliberately unreachable: labelfab's contract state is only idle/printing/disconnected/error, and HARDWARE-NOTES records that a jam has no known encoding on this printer, so there is nothing to map it from.

The Machines page could say "connected" about a printer with no tape in it. Status
collapsed to three cases -- unknown, disconnected, or connected -- so every fault
the agent published looked healthy, and print_labels set PRINTING and never learned
whether the job succeeded.

Status mapping now uses the codes InvenTree already has. A media fault becomes
NO_MEDIA (301) rather than a generic ERROR (500), because the two render
differently and mean different things to whoever is looking: "go load tape" versus
"go investigate". The status text carries firmware, battery *and* voltage, and the
media state.

Battery and voltage are both shown deliberately: the percentage reads 100% for as
long as the unit is on charge, so on its own it cannot answer whether the printer
will survive a long strip.

Unreported media is not shown as OK. media_ok is a tri-state and a printer that has
said nothing about its media reads as "media unreported" -- treating silence as
health is exactly how the old mapping produced a confident, wrong answer.

AWAIT_RESULT_S (default 15s, 0 disables) waits for the job's JobResult so the UI
reports the actual outcome. It subscribes before publishing, because the results
topic is not retained and subscribing afterwards races the agent and loses fast
prints entirely; and it filters on job_id, because one printer serves every
InvenTree user. A timeout leaves the status PRINTING rather than inventing an
outcome -- strip mode legitimately holds labels until the coalescer flushes, which
is longer than any wait worth blocking a Django-Q worker on. partial_tape_consumed
is surfaced in the text, since a blind reprint after one double-prints.

The mapping lives in status.py with no InvenTree imports, so it is unit-tested
here; the driver stays the thin adapter it claims to be. The tests also pin every
returned name against the real LabelPrinterStatus members, because a typo would
otherwise surface as an AttributeError inside a Django worker.
impuls42 added 2 commits July 30, 2026 01:52
Waiting for the result holds a Django-Q worker, and in strip mode the agent buffers
labels until its coalescer flushes -- 30s by default -- so the wait would usually
time out and leave the status to the retained topic anyway. Paying a worker for an
outcome that mostly does not arrive in time is the wrong default; opt in on
die-cut/discrete media, where a result comes back in a few seconds.
…ailure

Review asked what 'partial' means. It is terminal: the agent sets it from
`any_printed and any_failed` when it finalises a job, so some tape came out and
some labels did not. It was falling through to a bare 'job partial', which tells an
operator nothing about what to reprint -- now it reports printed-of-total and the
failed count.

Writing that branch exposed a second bug in it: returning early meant a partial job
never got the partial_tape_consumed warning, which is exactly the outcome where
moved tape matters most. The warning is now appended after the branches, so every
non-success state carries it.

Also renamed test_every_mapping_names_a_real_status_member. It only catches typos in
the strings classify_* returns; it cannot catch drift from InvenTree's enum, because
VALID_MEMBERS is a local copy (InvenTree is deliberately not a test dependency). The
old name claimed more than the test delivers.
@impuls42
impuls42 merged commit 14a2b86 into main Jul 30, 2026
1 check passed
impuls42 added a commit that referenced this pull request Jul 30, 2026
The printer's own state now reaches the Machines page -- NO_MEDIA vs ERROR, firmware,
battery and voltage, and real job outcomes (#3). Needs a release because the InvenTree
image pins this plugin by git tag.
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