feat(status): report the printer's own state, and the real print outcome - #3
Merged
Conversation
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.
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
And
print_labelssetPRINTINGand returned.JobResultwas published by the agent on<prefix>/<printer_id>/resultsand consumed by nobody, so InvenTree never learned whether a job succeeded.Status mapping
Uses the codes InvenTree already defines (
CONNECTED100,UNKNOWN101,PRINTING110,NO_MEDIA301,DISCONNECTED400,ERROR500):UNKNOWN— agent has never publishedidle, media okCONNECTED— with fw, battery, voltage, serial0x06)NO_MEDIAERROR, carrying the reasonDISCONNECTEDTwo deliberate distinctions:
NO_MEDIAandERRORrender differently and mean different things to whoever is looking: "go load tape" vs "go investigate".media_okis a tri-state; a printer that has said nothing showsmedia 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_Swaits for the job'sJobResult, so the UI reports what happened rather than that something was sent.job_id. One printer serves every InvenTree user; another click's result must not be mistaken for yours.PRINTING, and the next refresh reads the truth off the retained topic.partial_tape_consumedis 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.pywith 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_memberpins every name against the realLabelPrinterStatusmembers, because a typo would otherwise surface as anAttributeErrorinside a Django worker rather than at test time.34 tests pass, ruff clean.
PAPER_JAM(302) is deliberately unreachable: labelfab's contractstateis onlyidle/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.