The first operator tap after a reboot was always refused, and it counted as a piece

An Operator message carried the uid of the PREVIOUS card tapped - "-" right
after a reboot - because the branch published before last_uid_str was
updated. The server checks that uid against the Card Type List, so the
first operator tap after every power-on came back "Card not found", and the
operator scan log (which the QC pages use as a fallback to turn a card into
an employee) linked each operator to whatever product card went before,
never to their own card. The message now carries the operator card's own
uid, like every other scan.

Consequence to know about: an operator card that was never registered on
the Card Type page used to be accepted by accident (the check looked at the
product card before it) and is now refused with "Card not found" until it
is registered. Every card issued through that page is already in the list.

Second fix, exposed by the first: the box added one to the product count
on ANY accepted reply, an accepted operator tap included. It was half
hidden while the first operator tap always failed. start_ack_wait() now
remembers the type of the scan being answered (pending_scan_type) and only
a Product acknowledgement counts; Operator and Reset replies just release
the wait and show the server's message. A reply arriving after the 10 s
timeout is still applied, as before.

Simulation updated: 67 checks pass, including "accepted operator tap does
not count" and "accepted RESET reply does not count".

version.txt is NOT bumped - nothing reaches a box until it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main
m.faisal 2026-09-15 16:41:49 +05:00
parent 133cfbac1e
commit e98eb11359
2 changed files with 19 additions and 15 deletions

View File

@ -34,7 +34,7 @@ Single-file, cooperative main loop: `setup()` once, then `while True: loop()`. N
- `cardtype` selects the behaviour: `Operator` sets and persists `Operator_ID`, `Product` publishes a scan, `Reset` zeroes the counter and operator and publishes a `RESET` message. Anything else is ignored. - `cardtype` selects the behaviour: `Operator` sets and persists `Operator_ID`, `Product` publishes a scan, `Reset` zeroes the counter and operator and publishes a `RESET` message. Anything else is ignored.
- `tap_handled` edge-detects a tap: set when a card is acted on (all three types, Reset included since 2026-09-15), cleared only once no card is present, so one tap publishes once however long the card rests on the reader. - `tap_handled` edge-detects a tap: set when a card is acted on (all three types, Reset included since 2026-09-15), cleared only once no card is present, so one tap publishes once however long the card rests on the reader.
- After every publish the device enters `waiting_for_ack` for 10 s: further cards are ignored ("WAITING FOR RESPONSE") until a `response_data` (or `lamp_topic`) message addressed to this serial arrives, or the deadline passes ("NO RESPONSE RECIEVED"). `tag_counter` increments only on a positive ACK from the server, never on the scan itself, so the on-screen count is the server's count. - After every publish the device enters `waiting_for_ack` for 10 s: further cards are ignored ("WAITING FOR RESPONSE") until a `response_data` (or `lamp_topic`) message addressed to this serial arrives, or the deadline passes ("NO RESPONSE RECIEVED"). `tag_counter` increments only on a positive ACK from the server, never on the scan itself, so the on-screen count is the server's count.
- Quirk: the `Operator` branch publishes before `last_uid_str` is updated, so an operator message carries the previous card's UID (`-` after boot); a `Product` message carries the current UID. - Every scan carries the tapped card's own UID. Until 2026-09-15 an `Operator` message carried the PREVIOUS card's UID (`-` after boot), so the server's "is this card registered" check looked at the wrong card - the first operator tap after a reboot was always refused with "Card not found", and the operator scan log linked operators to product cards. If an operator card is refused with "Card not found" now, it is genuinely missing from the Card Type List.
### MQTT contract ### MQTT contract
@ -47,7 +47,7 @@ Publishes:
Subscribes (each handler first checks the serial in the payload; messages for other devices are ignored): Subscribes (each handler first checks the serial in the payload; messages for other devices are ignored):
- `response_data``{"device_serial", "ack": 0|1, "output"}`. `ack == 1` increments the counter and shows `output` in the MSG bar; `ack == 0` only shows `output`. - `response_data``{"device_serial", "ack": 0|1, "output"}`. `ack == 1` increments the counter only when the scan being answered was a `Product` tap (`pending_scan_type`; an accepted Operator or Reset scan is not a piece) and shows `output` in the MSG bar; `ack == 0` only shows `output`. A reply arriving after the 10 s timeout is still applied.
- `lamp_topic``{"serial", "color": "red"|"green"|"yellow"|"purple"|"blue"}` sets the RGB unit and persists the colour to NVS. - `lamp_topic``{"serial", "color": "red"|"green"|"yellow"|"purple"|"blue"}` sets the RGB unit and persists the colour to NVS.
- `reset-topic` — payload `reset` clears the counter and UIDs (keeps the operator). - `reset-topic` — payload `reset` clears the counter and UIDs (keeps the operator).

30
main.py
View File

@ -110,6 +110,7 @@ tap_handled = False # set once the card on the reader has been acted on, cle
rfid_needs_init = False rfid_needs_init = False
waiting_for_ack = False waiting_for_ack = False
ack_deadline = 0 ack_deadline = 0
pending_scan_type = None # card type of the scan the server has not answered yet
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# OTA update (runs once at boot, when WiFi is up) # OTA update (runs once at boot, when WiFi is up)
@ -377,10 +378,12 @@ def publish_scan(uid, cardtype, operator_id):
print("MQTT Publish Error:", e) print("MQTT Publish Error:", e)
reconnect_mqtt() reconnect_mqtt()
def start_ack_wait(): def start_ack_wait(cardtype):
global waiting_for_ack, ack_deadline """Block the reader until the server answers this tap (or ACK_TIMEOUT_MS passes)."""
global waiting_for_ack, ack_deadline, pending_scan_type
set_msg("WAITING FOR RESPONSE") set_msg("WAITING FOR RESPONSE")
waiting_for_ack = True waiting_for_ack = True
pending_scan_type = cardtype
ack_deadline = utime.ticks_add(utime.ticks_ms(), ACK_TIMEOUT_MS) ack_deadline = utime.ticks_add(utime.ticks_ms(), ACK_TIMEOUT_MS)
def reset_counter(): def reset_counter():
@ -401,10 +404,12 @@ def handle_response(msg):
output = data.get("output") output = data.get("output")
if ack == 1: if ack == 1:
waiting_for_ack = False waiting_for_ack = False
tag_counter += 1 # Only a Product scan is production: an accepted Operator or Reset scan is not a piece.
label_set('count', tag_counter) if pending_scan_type == "Product":
tag_counter += 1
label_set('count', tag_counter)
save_to_nvs()
set_msg(output) set_msg(output)
save_to_nvs()
elif ack == 0: elif ack == 0:
waiting_for_ack = False waiting_for_ack = False
set_msg(output) set_msg(output)
@ -545,26 +550,25 @@ def handle_card():
label_set('operator', "-") label_set('operator', "-")
save_to_nvs() save_to_nvs()
publish_scan("RESET", "RESET", 0) publish_scan("RESET", "RESET", 0)
start_ack_wait() start_ack_wait("Reset")
return return
last_uid_str = uid_str
if cardtype == "Operator" and card_operator_id and card_operator_id != '0': if cardtype == "Operator" and card_operator_id and card_operator_id != '0':
tap_handled = True tap_handled = True
Operator_ID = card_operator_id Operator_ID = card_operator_id
label_set('operator', Operator_ID) label_set('operator', Operator_ID)
save_to_nvs() save_to_nvs()
# Long-standing quirk kept as is: an operator message carries the PREVIOUS card's # The operator card's OWN uid. Until 2026-09-15 this sent the previous card's uid
# uid ("-" right after boot), because last_uid_str is only updated below. # ("-" after a reboot), so the server checked and logged the wrong card.
publish_scan(last_uid_str, cardtype, Operator_ID) publish_scan(uid_str, cardtype, Operator_ID)
start_ack_wait() start_ack_wait(cardtype)
last_uid_str = uid_str
return return
last_uid_str = uid_str
if cardtype == "Product": if cardtype == "Product":
tap_handled = True tap_handled = True
publish_scan(uid_str, cardtype, Operator_ID) publish_scan(uid_str, cardtype, Operator_ID)
start_ack_wait() start_ack_wait(cardtype)
# Any other type (or an unwritten card) is shown on screen and not sent. # Any other type (or an unwritten card) is shown on screen and not sent.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------