UiMetrix-M5-Firmware/CLAUDE.md

7.7 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this repository is

Firmware for the UiMetrix shop-floor RFID tag-reader stations: an M5Stack CoreS3 with an RFID Unit and an RGB light bar, running MicroPython on the UIFlow 2.0 firmware (M5, m5ui, unit, hardware, umqtt.simple, esp32.NVS APIs). The whole program is the single file M5-Stack Code for Uimetrix.txt; on the device it is /flash/main.py (the .txt extension is just how it was exported; CRLF line endings).

The repository is hosted on the company Gitea at https://git.utopiadeals.com/UIND/UiMetrix-M5-Firmware (branch main); pushing there does not reach any device, since devices pull from the GitHub repo described under "Releasing to devices". There is no PC-side toolchain: nothing to build, lint or run here, and there are no tests. Changes can only be verified on a device; debug output is print() over the USB serial port.

The server that talks to this device is the UiMetrix ASP.NET Core app in D:\Projects\UiMetrix (Services/MqttService.cs; its own CLAUDE.md documents the server side). D:\Projects\MqttWorkerService is an older standalone worker that speaks the same protocol, and its tools/rfid_enrol.py numbers cards from this device's MQTT messages.

Releasing to devices (OTA)

Devices update themselves at boot from the GitHub repo husbananjum/m5core-s3-UIMETRIX-Z87 (main branch): check_for_update() fetches version.txt, compares it as a float against the fw_version string in NVS namespace storage (default "1.0"), and if newer downloads that repo's main.py and hands it to install_update(). The download must be HTTP 200, at least 1000 bytes, contain def setup and def loop, and compile; otherwise it is rejected. Installation writes it to /flash/main_ota_temp.py, renames the running program to /flash/main_prev.py (a rollback copy for manual recovery over USB), renames the download to /flash/main.py, and only then stores the new version in NVS and resets. A rejected download or a failed swap leaves the old program and the old version in place, so the next boot retries. A release is therefore: push the new main.py and bump version.txt. Things to keep in mind:

  • Versions compare as floats, so 1.10 is older than 1.9.
  • Builds before 2026-09-11 downloaded the file but never installed it, while still stamping the new version into NVS. Devices running such a build never pick up an OTA release: they need one manual USB copy of the fixed main.py first, and because their NVS may already claim a version they never ran, the next version.txt must be higher than any number published so far.

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.

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.

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.
  • 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.
  • 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.

MQTT contract

Broker, port and credentials are constants at the top of the file and must match the broker the UiMetrix server uses. All messages are JSON, QoS 0.

Publishes:

  • send_receive_data{"card_type", "operator_id", "device_type": "tag_reader", "uid", "device_serial"}. The server routes on card_type / device_type.
  • M5devices/<serial> — an empty object on every idle loop pass (several times a second), a liveness heartbeat.

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.
  • 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).

mqtt_callback accepts both the 2-argument and 4-argument umqtt callback signatures.

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.
  • 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.
  • 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.

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.
  • 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.