The box program carried 160 lines of dead code and a public repo held hotspot passwords

A tidy-up of main.py with the same MQTT, NVS and screen contract, so that
the next person can read it. Verified on the PC with a stubbed device
environment driving every branch (65 checks: boot from empty storage, each
card type, positive / negative / foreign / undecodable replies, the 10 s
timeout, lamp and reset topics, MQTT / WiFi / reader recovery, heartbeat,
OTA accept and reject paths); nothing here can run on the device itself.

Removed without changing behaviour: every commented-out line, duplicate and
unused imports, the timestamp helper and reset-UID constant nobody used,
the seen_uids set with its uid_count/uid_<n> NVS bookkeeping (never read for
anything; old keys stay in flash, ignored), labels that were "updated" but
never created, the serial recomputed on every pass, callback locals leaking
into globals, and the non-blocking WiFi reconnect that the blocking one on
the next line always overrode. Widgets now live in `ui` under plain names
(label_set / set_msg tolerate a widget not yet drawn, which is what lets
setup() read NVS before the screen exists); lamp colours are a table.

Deliberate small changes, all in the box's favour:

- Reads only card blocks 10 (type) and 12 (operator id). Blocks 4-9 held
  fields that stopped being published long ago. RFIDUnit.read() authenticates
  the block's own sector each call, so the reads do not depend on each other.
- A Reset card fires once per tap (tap_handled). Before, one left on the
  reader re-sent RESET after every server reply.
- A failed heartbeat publish drops the MQTT client so the throttled reconnect
  runs. Before, it flagged the RFID reader for a rebuild and never touched
  MQTT.
- No NVS write on every Product tap - it rewrote identical values.
- The serial is drawn at boot instead of "SR: LOADING..." until WiFi
  finished; "RECIEVED" fixed on screen.
- The personal hotspots (EHTISHAM, A16, okay., StormFiber) are gone: this is
  a public repo. One alternative network and one alternative broker remain
  commented, as the convention says.

Kept on purpose: an Operator message still carries the PREVIOUS card's uid.
That is a server-facing contract and changes separately.

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:35 +05:00
parent 0d7c5781b0
commit 133cfbac1e
2 changed files with 420 additions and 581 deletions

View File

@ -21,18 +21,18 @@ Devices update themselves at boot from this repo's `main` branch on Gitea, over
## How the program works
Single-file, cooperative main loop: `setup()` once, then `while True: loop()`. No threads or asyncio; all state is module-level globals (functions declare `global`). Anything added to the loop must stay non-blocking: timers use `utime.ticks_ms()` deadlines (`ack_deadline`, the 5 s throttles in `check_wifi()` / `reconnect_mqtt()`), not sleeps. The exception is `connect_wifi()`, which blocks and retries recursively up to `MAX_RETRIES`; `loop()` falls back to it whenever WiFi is down.
Single-file, cooperative main loop: `setup()` once, then `while True: loop()`. No threads or asyncio; all state is module-level globals (functions declare `global`). Anything added to the loop must stay non-blocking: timers use `utime.ticks_ms()` deadlines (`ack_deadline`, the 5 s throttle in `reconnect_mqtt()`), not sleeps. The exception is `connect_wifi()`, which blocks and retries recursively up to `MAX_RETRIES`; `loop()` falls back to it whenever WiFi is down.
### Identity and persistence
- `serial` = hex of `machine.unique_id()`. It is the MQTT client id, the `device_serial` in every published message, and what every incoming message is matched against. Shown on screen as `SR:`.
- NVS namespace `rfid_data` (`load_from_nvs()` / `save_to_nvs()`): `count` (product counter), `color` (last lamp colour), `operator_id`, and `uid_count` + `uid_<n>` (the `seen_uids` set, no longer used for counting). These survive reboots; a `Reset` card or the reset topic clears them.
- NVS namespace `rfid_data` (`load_from_nvs()` / `save_to_nvs()`): `count` (product counter), `color` (last lamp colour) and `operator_id`. Older builds also wrote `uid_count` + `uid_<n>` (a set of seen UIDs that never affected anything); those keys are left in place and ignored. These survive reboots; a `Reset` card or the reset topic clears them.
### Card handling (`loop()`)
- `read_all_fields()` reads the card's text blocks positionally with lengths `[4, 5, 6, 8, 9, 10, 12]` -> `(sku, color, size, article, remarks, cardtype, operator_id)`. Only `cardtype` and `operator_id` still matter; the first five are read but no longer published.
- `read_card_fields()` reads two MIFARE Classic blocks from the card: block 10 (`CARD_TYPE_BLOCK`, the type as text) and block 12 (`OPERATOR_ID_BLOCK`). Blocks 4-9 once held sku / colour / size / article / remarks and are no longer read; each `RFIDUnit.read(block)` authenticates its own sector, so the reads are independent. The values are written into the card by the desk writer that the UiMetrix "Register Card Type" page drives - the box never consults the database.
- `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.
- The `card` flag edge-detects a tap: set when a card is handled, cleared only once no card is present, so one tap publishes once.
- `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.
- 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.
@ -56,16 +56,16 @@ Subscribes (each handler first checks the serial in the payload; messages for ot
### Connectivity and the light bar
- Blue on the RGB unit means "not connected" (boot, WiFi down, MQTT down); once MQTT connects the persisted `lamp_color` (default green) is restored. After that, colours come only from `lamp_topic`.
- A publish failure calls `reconnect_mqtt()`, which tears the client down and re-runs `init_mqtt()`, throttled to once per 5 s. A `check_msg()` error sets `mqtt_client = None`, which takes the same path on the next loop.
- A publish failure calls `reconnect_mqtt()`, which tears the client down and re-runs `init_mqtt()`, throttled to once per 5 s. A `check_msg()` or heartbeat-publish error sets `mqtt_client = None`, which takes the same path on the next loop.
- Status icons (WiFi ok/error, MQTT cloud/error, charging) are "updated" by drawing a new `Widgets.Image` over the old one at fixed coordinates.
### Screen and hardware
- `init_ui()` draws everything with `M5.Widgets` at absolute 320x240 coordinates and keeps the handles in the `ui_elements` dict; change text through `safe_label_update()`. `label3` and `label5` are referenced but never created, so updates to them are no-ops. The battery bar is an `m5ui.M5Bar`.
- `init_ui()` draws everything with `M5.Widgets` at absolute 320x240 coordinates and keeps the handles in the `ui` dict under plain names (`operator`, `count`, `msg`, `uid`, `serial`, `ip`, `battery`, `version`, the icons); change text through `label_set(name, text)` / `set_msg(text)`, which tolerate a widget that has not been created yet (that is what lets `setup()` load NVS before drawing). The battery bar is an `m5ui.M5Bar`.
- Images are loaded from `/flash/res/img/` on the device (`Logo`, `emp`, `mach`, `rfid-tag-log`, `bar_c`, `wifi`, `wifi_error`, `MQTT_Cloud`, `mqtt_error`, `charging`, `not_charging`, all `.jpg`); they are not in this directory.
- RFID Unit on I2C bus 0 (SCL pin 1, SDA pin 2, 100 kHz); if any RFID call throws, `rfid_re_init` makes the next loop pass rebuild the bus and reader. RGB Unit on pins 8/9 with 10 LEDs.
- RFID Unit on I2C bus 0 (SCL pin 1, SDA pin 2, 100 kHz); if any RFID call throws, `rfid_needs_init` makes the next loop pass rebuild the bus and reader (`init_rfid()`). RGB Unit on pins 8/9 with 10 LEDs.
## Conventions
- WiFi SSID/password and the MQTT broker/user/password are hardcoded near the top of the file, with previously used networks left commented out; switching networks means commenting/uncommenting those lines. The file already holds live credentials, so don't echo them into other files or chat.
- WiFi SSID/password and the MQTT broker/user/password are hardcoded near the top of the file, with one alternative each left commented out (`UI-Matrix`, `scada.utopia.pk`); switching means commenting/uncommenting those lines. Personal hotspots that used to be listed were removed on 2026-09-15 - this is a public repo, do not add them back. The file already holds live credentials, so don't echo them into other files or chat.
- The serial console never prints the card UID (only MQTT traffic, NVS saves/loads, connection state and errors), which is why the enrolment tool reads UIDs from MQTT instead of the USB port.

985
main.py

File diff suppressed because it is too large Load Diff