7.2 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 to /flash/main_ota_temp.py, stores the new version in NVS and resets. A release is therefore: push the new main.py and bump version.txt. Two things to keep in mind:
- Versions compare as floats, so
1.10is older than1.9. - Nothing in this file copies
main_ota_temp.pyovermain.py; that promotion has to happen elsewhere on the device (e.g. itsboot.py). Confirm it exists before relying on OTA, because the new version is stamped in NVS before the reset either way, after which the device reports "Already latest version".
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 ofmachine.unique_id(). It is the MQTT client id, thedevice_serialin every published message, and what every incoming message is matched against. Shown on screen asSR:.- NVS namespace
rfid_data(load_from_nvs()/save_to_nvs()):count(product counter),color(last lamp colour),operator_id, anduid_count+uid_<n>(theseen_uidsset, no longer used for counting). These survive reboots; aResetcard 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). Onlycardtypeandoperator_idstill matter; the first five are read but no longer published.cardtypeselects the behaviour:Operatorsets and persistsOperator_ID,Productpublishes a scan,Resetzeroes the counter and operator and publishes aRESETmessage. Anything else is ignored.- The
cardflag 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_ackfor 10 s: further cards are ignored ("WAITING FOR RESPONSE") until aresponse_data(orlamp_topic) message addressed to this serial arrives, or the deadline passes ("NO RESPONSE RECIEVED").tag_counterincrements 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
Operatorbranch publishes beforelast_uid_stris updated, so an operator message carries the previous card's UID (-after boot); aProductmessage 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 oncard_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 == 1increments the counter and showsoutputin the MSG bar;ack == 0only showsoutput.lamp_topic—{"serial", "color": "red"|"green"|"yellow"|"purple"|"blue"}sets the RGB unit and persists the colour to NVS.reset-topic— payloadresetclears 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 fromlamp_topic. - A publish failure calls
reconnect_mqtt(), which tears the client down and re-runsinit_mqtt(), throttled to once per 5 s. Acheck_msg()error setsmqtt_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.Imageover the old one at fixed coordinates.
Screen and hardware
init_ui()draws everything withM5.Widgetsat absolute 320x240 coordinates and keeps the handles in theui_elementsdict; change text throughsafe_label_update().label3andlabel5are referenced but never created, so updates to them are no-ops. The battery bar is anm5ui.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_initmakes 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.