commit 7050a7ef51a812fb0ca49e85734bb8c26d847264 Author: muhammad.faique Date: Fri Sep 11 16:00:51 2026 +0500 Add M5Stack RFID reader firmware and developer guide Import the UiMetrix tag-reader firmware (UIFlow 2.0 MicroPython script) as exported from the device, plus a CLAUDE.md describing the OTA release path, the MQTT contract with the UiMetrix server and the scan/ACK lifecycle. Co-Authored-By: Claude Fable 5.1 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..341697c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# 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.10` is older than `1.9`. +- Nothing in this file copies `main_ota_temp.py` over `main.py`; that promotion has to happen elsewhere on the device (e.g. its `boot.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 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_` (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/` — 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. diff --git a/M5-Stack Code for Uimetrix.txt b/M5-Stack Code for Uimetrix.txt new file mode 100644 index 0000000..47cfebf --- /dev/null +++ b/M5-Stack Code for Uimetrix.txt @@ -0,0 +1,773 @@ +import os, sys, io +import M5 +from M5 import * +import network +import m5ui +import lvgl as lv +from unit import RFIDUnit +from hardware import I2C, Pin +import time +from umqtt.simple import MQTTClient +import esp32 +from esp32 import NVS +import ujson +import utime +#from audio import Player +from machine import unique_id +import ubinascii +from unit import RGBUnit +import requests +import machine +#player = None +serial = ubinascii.hexlify(unique_id()).decode() + +mqtt_last_try = 0 +MQTT_RECONNECT_INTERVAL = 5000 # 5 seconds +wifi_last_try = 0 +WIFI_RECONNECT_INTERVAL = 5000 + +# Constants and Configuration + + +#WIFI_SSID = 'UI-Matrix' +#WIFI_PASS = 'Uimatrix01' +#WIFI_SSID = 'EHTISHAM' +#WIFI_PASS = '123456789' +WIFI_SSID = 'Utopia-WiFi' +WIFI_PASS = '@!nt3lGwn#1' +#WIFI_SSID = 'A16' +#WIFI_PASS = '12345678' +#WIFI_SSID = 'okay.' +#WIFI_PASS = '125125125' +#WIFI_SSID = 'StormFiber-2.4G' +#WIFI_PASS = '22733801' + +import requests +import machine + +nvs = esp32.NVS("storage") + +def get_local_version(): + try: + return nvs.get_str("fw_version") + except: + return "1.0" # default first install + +def set_local_version(version): + try: + nvs.set_str("fw_version", version) + nvs.commit() + print("Version saved:", version) + except Exception as e: + print("NVS save error:", e) + +local_version = get_local_version() + +def check_for_update(): + try: + print("Checking for update...") + + r = requests.get(UPDATE_VERSION_URL) + remote_version = r.text.strip() + r.close() + + print("Remote:", remote_version) + print("Local :", local_version) + + if float(remote_version) > float(local_version): + print("New version found. Updating...") + + r = requests.get(UPDATE_FILE_URL) + new_code = r.text + r.close() + + with open("/flash/main_ota_temp.py", "w") as f: + f.write(new_code) + + print("Update downloaded. Restarting...") + set_local_version(remote_version) + machine.reset() + + else: + print("Already latest version") + + except Exception as e: + print("Update check failed:", e) + + +UPDATE_VERSION_URL = "https://raw.githubusercontent.com/husbananjum/m5core-s3-UIMETRIX-Z87/refs/heads/main/version.txt" +UPDATE_FILE_URL = "https://raw.githubusercontent.com/husbananjum/m5core-s3-UIMETRIX-Z87/refs/heads/main/main.py" + + +MQTT_CLIENT_ID = serial +MQTT_BROKER = '192.168.87.11' +#MQTT_BROKER = 'scada.utopia.pk' +MQTT_PORT = 1883 +MQTT_USER = 'utopia' +MQTT_PASSWORD = 'utopia01' +MQTT_RESET_TOPIC = 'reset-topic' +RESET_UID = "D3:58:24:F8:00:00:00:00:00:00" +NVS_NAMESPACE = "rfid_data" +MAX_RETRIES = 5 +MQTT_TOPIC = "send_receive_data" +MQTT_LAMP = "lamp_topic" +DEVICE_TYPE = "tag_reader" +DEVICE_NUMBER = serial +MQTT_RESPONSE_DATA = 'response_data' +# UI Elements +ui_elements = { + 'label3': None, # Last UID display + 'label4': None, # IP address + 'label5': None, # Card present status + 'label6': None, # Current UID + 'title0': None, # Title + 'label7': None, # "PROD COUNT:" label + 'label8': None, # Product counter + 'label1': None, # "TAG UID:" label + 'line0': None, # Divider line + 'image1': None, + 'label9': None, # Operator ID + 'label10': None, #MSG + 'label2': None, + 'rect0': None, + 'rect1': None , + 'rect2': None, + 'rect3':None, + 'rect4':None, + 'image0': None, + 'image1': None, + 'image2': None, + 'image3': None, + 'image4': None, + 'image5': None, + 'image6': None, + 'label12': None, + 'bettery_per': None, + 'charging_icon':None +} +bettery_icon = None +card=0 # card insert=1 card not insert =0 +rfid_re_init=0 +# System Components +wlan = None +i2c0 = None +rfid_0 = None +rgb_0=None +mqtt_client = None +Operator_ID = "-" +lamp_color = 0 +tag_counter = 0 +seen_uids = set() +wifi_retry_count = 0 +last_uid_str= "-" +# --- NEW GLOBALS FOR ACK WAIT --- +waiting_for_ack = False +ack_deadline = 0 +# -------------------------------- +def read_all_fields(): + lengths = [4, 5, 6, 8, 9, 10, 12] + values = [] + for length in lengths: + try: + value = rfid_0.read(length).decode('utf-8').strip('\x00') + except: + value = None + values.append(value) + return tuple(values) +def get_datetime(): + """Get current datetime in ISO 8601 format (YYYY-MM-DDTHH:MM:SS)""" + now = utime.localtime() + return "{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}".format( + now[0], now[1], now[2], now[3], now[4], now[5]) +def safe_label_update(label, text): + """Safely update label text if label exists""" + if label is not None: + label.setText(str(text)) +def load_from_nvs(): + global tag_counter, seen_uids, Operator_ID,lamp_color + + try: + nvs = NVS(NVS_NAMESPACE) + # Load tag counter + try: + tag_counter = nvs.get_i32("count") + except: + tag_counter = 0 + nvs.set_i32("count", tag_counter) + nvs.commit() + + # Load last UID + try: + lamp_color = nvs.get_i32("color") + print(f" load from nvs {lamp_color}") + except: + lamp_color = 0x008000 + nvs.set_i32("color", lamp_color) + nvs.commit() + + # Load seen UIDs + try: + uid_count = nvs.get_i32("uid_count") + seen_uids = set() + for i in range(uid_count): + uid_key = f"uid_{i}" + seen_uids.add(nvs.get_str(uid_key)) + except: + seen_uids = set() + nvs.set_i32("uid_count", 0) + nvs.commit() + + # Load Operator ID + try: + Operator_ID = nvs.get_str("operator_id") + if Operator_ID == "" or Operator_ID == "NA": + Operator_ID = "-" + except: + Operator_ID = "-" + nvs.set_str("operator_id", Operator_ID) + nvs.commit() + + # Update UI with loaded values + safe_label_update(ui_elements['label8'], str(tag_counter)) + #safe_label_update(ui_elements['label3'], f"Last UID: {last_uid_str}") + safe_label_update(ui_elements['label9'], str(Operator_ID)) + + except Exception as e: + print("NVS Load Error:", e) + tag_counter = 0 + lamp_color = 0x008000 + seen_uids = set() + Operator_ID = "-" +def save_to_nvs(): + global lamp_color + try: + nvs = NVS(NVS_NAMESPACE) + nvs.set_i32("count", tag_counter) + nvs.set_i32("color", lamp_color) + print(f"save to nvs {lamp_color}") + nvs.set_str("operator_id", Operator_ID) + + # Save seen UIDs + nvs.set_i32("uid_count", len(seen_uids)) + for i, uid in enumerate(seen_uids): + nvs.set_str(f"uid_{i}", uid) + + nvs.commit() + except Exception as e: + print("NVS Save Error:", e) +def publish_rfid_data(sku, color, size, article, remarks, uid, count, cardtype, Operator_ID, serial): + """Publish RFID data as JSON message with datetime timestamp""" + global waiting_for_ack + if waiting_for_ack: + print("Publish blocked (waiting for ACK)") + return + try: + if mqtt_client: + message = { + "card_type": cardtype, + "operator_id": Operator_ID, + #"sku": sku, + #"color": color, + #"size": size, + #"article": article, + #"remarks": remarks, + "device_type": DEVICE_TYPE, + #"device_number": DEVICE_NUMBER, + "uid": uid, + #"count": count, + #"timestamp": get_datetime(), + "device_serial": serial + } + mqtt_client.publish(MQTT_TOPIC, ujson.dumps(message), qos=0) + except Exception as e: + print("MQTT Publish Error:", e) + reconnect_mqtt() +def init_ui(): + """Initialize all UI elements with null checks""" + try: + Widgets.fillScreen(0xffffff) + + #Version info + + #employee ID + ui_elements['rect0'] = Widgets.Rectangle(4, 101, 152, 55, 0x616161, 0xffffff) + ui_elements['image1'] = Widgets.Image("/flash/res/img/emp.jpg", 5, 102, scale_x=1, scale_y=1) + #Operator ID + ui_elements['label9'] = Widgets.Label(str(Operator_ID), 60, 124, 1.0, 0x000000, 0xffffff, Widgets.FONTS.DejaVu18) + #PRoduction Count / Sewing machine + ui_elements['rect1'] = Widgets.Rectangle(160, 101, 155, 55, 0x616161, 0xffffff) + ui_elements['image0'] = Widgets.Image("/flash/res/img/mach.jpg", 165, 102, scale_x=1, scale_y=1) + ui_elements['label8'] = Widgets.Label(str(tag_counter), 220, 124, 1.0, 0x000000, 0xffffff, Widgets.FONTS.DejaVu18) + #RESPONSE DATA MESSAGE + ui_elements['rect2'] = Widgets.Rectangle(4, 160, 311, 25, 0x616161, 0xffffff) + ui_elements['label10'] = Widgets.Label("MSG : ", 8, 166, 1.0, 0x000000, 0xffffff, Widgets.FONTS.DejaVu12) + #RFID PRESENT TAG UID + ui_elements['rect3'] = Widgets.Rectangle(4, 189, 311, 47, 0x616161, 0xffffff) + ui_elements['image3'] = Widgets.Image("/flash/res/img/rfid-tag-log.jpg", 5, 190, scale_x=1, scale_y=0.9) + ui_elements['label6'] = Widgets.Label("-", 60, 205, 1.0, 0x000000, 0xffffff, Widgets.FONTS.DejaVu12) + # Device Serial no + ui_elements['rect4'] = Widgets.Rectangle(4, 47, 311, 50, 0x616161, 0xffffff) + ui_elements['image4'] = Widgets.Image("/flash/res/img/bar_c.jpg", 5, 58, scale_x=1.2, scale_y=1.2) + # WIFI & MQTT LOGO + ui_elements['image2'] = Widgets.Image("/flash/res/img/wifi.jpg", 149, 3, scale_x=0.9, scale_y=1) + ui_elements['image5'] = Widgets.Image("/flash/res/img/mqtt_error.jpg", 104, 1, scale_x=1, scale_y=1) + ui_elements['label4'] = Widgets.Label("IP: Connecting...", 205, 35, 1.0, 0x000000, 0xffffff, Widgets.FONTS.DejaVu9) + # Utopia Logo + ui_elements['image6'] = Widgets.Image("/flash/res/img/Logo.jpg", 0, 0) + + # Bettery % + ui_elements['bettery_per'] =Widgets.Label("b%", 230, 15,1.0, 0x000000, 0xffffff, Widgets.FONTS.DejaVu12) + #Version info + ui_elements['label12'] = Widgets.Label(f"V{local_version}", 275, 55, 1.0, 0x000000, 0xffffff, Widgets.FONTS.DejaVu12) + + + except Exception as e: + print("UI Init Error:", e) +def connect_wifi(): + global wlan, wifi_retry_count + + if not wlan.isconnected(): + try: + safe_label_update(ui_elements['label4'], "WiFi: Connecting...") + ui_elements['image2'] = Widgets.Image("/flash/res/img/wifi_error.jpg", 150, 3, scale_x=1.1, scale_y=1) + rgb_0.fill_color(0x0000FF) + wlan.connect(WIFI_SSID, WIFI_PASS) + + # Wait for connection with timeout + timeout = 10 # 20 seconds timeout + while not wlan.isconnected() and timeout > 0: + time.sleep_ms(500) + timeout -= 0.5 + M5.update() + + if wlan.isconnected(): + ip_address = wlan.ifconfig()[0] + safe_label_update(ui_elements['label4'], "IP:" + ip_address) + ui_elements['image2'] = Widgets.Image("/flash/res/img/wifi.jpg", 149, 3, scale_x=0.9, scale_y=1) + rgb_0.fill_color(lamp_color) + else: + safe_label_update(ui_elements['label4'], "WiFi: Timeout") + ui_elements['image2'] = Widgets.Image("/flash/res/img/wifi_error.jpg", 150, 3, scale_x=1.1, scale_y=1) + rgb_0.fill_color(0x0000FF) + wifi_retry_count += 1 + if wifi_retry_count < MAX_RETRIES: + time.sleep(2) + connect_wifi() + + except Exception as e: + safe_label_update(ui_elements['label4'], "WiFi: Error") + ui_elements['image2'] = Widgets.Image("/flash/res/img/wifi_error.jpg", 150, 3, scale_x=1.1, scale_y=1) + print("WiFi Error:", e) + rgb_0.fill_color(0x0000FF) + wifi_retry_count += 1 + if wifi_retry_count < MAX_RETRIES: + time.sleep(2) + connect_wifi() +def check_wifi(): + global wifi_last_try + + if wlan.isconnected(): + ip = wlan.ifconfig()[0] + safe_label_update(ui_elements['label4'], "IP:" + ip) + return + + now = utime.ticks_ms() + + if utime.ticks_diff(now, wifi_last_try) < WIFI_RECONNECT_INTERVAL: + return + + wifi_last_try = now + + print("WiFi reconnecting...") + + try: + wlan.disconnect() + except: + pass + + wlan.connect(WIFI_SSID, WIFI_PASS) + +def init_wifi(): + global wlan + + try: + wlan = network.WLAN(network.STA_IF) + wlan.active(True) + wlan.config(reconnects=MAX_RETRIES) + connect_wifi() + except Exception as e: + print("WiFi Init Error:", e) + safe_label_update(ui_elements['label4'], "WiFi Error") + ui_elements['image2'] = Widgets.Image("/flash/res/img/wifi_error.jpg", 150, 3, scale_x=1.1, scale_y=1) + rgb_0.fill_color(0x0000FF) + +def mqtt_callback(*args): + """Universal callback that handles all MQTT library versions""" + global tag_counter, seen_uids, last_uid_str, Operator_ID, output, ack, target_serial, data, lamp_color, waiting_for_ack + + try: + # Determine callback signature based on args length + if len(args) == 2: # Standard umqtt.simple (topic, message) + topic, msg = args + elif len(args) == 4: # Some variants (topic, msg, retained, duplicate) + topic, msg, _, _ = args + else: + print("Unsupported callback format") + return + + # Ensure proper string decoding + topic = topic.decode('utf-8') if isinstance(topic, bytes) else topic + msg = msg.decode('utf-8') if isinstance(msg, bytes) else msg + + print(f"MQTT: {topic} -> {msg}") + + + if topic.endswith(MQTT_RESET_TOPIC) and msg.lower() == 'reset': + # Reset all counters and data + seen_uids.clear() + tag_counter = 0 + last_uid_str = "-" + #Operator_ID = "-" + + # Update UI + safe_label_update(ui_elements['label8'], "0") + safe_label_update(ui_elements['label3'], "Last UID: - (Reset)") + #safe_label_update(ui_elements['label9'], "-") #Label for Operator ID + safe_label_update(ui_elements['label6'], "-") + safe_label_update(ui_elements['label10'], "MSG : ") + + # Persist state + save_to_nvs() + print("System reset via MQTT") + if topic.endswith(MQTT_RESPONSE_DATA): + try: + data = ujson.loads(msg) # decode JSON payload + target_serial = data.get("device_serial") + ack = data.get("ack") + output=data.get("output") + # Only act if the message is for THIS device + print(f"target sr= {target_serial}") + if target_serial == serial and ack == 1: + # ACK received for this device + waiting_for_ack = False + print(output) + tag_counter += 1 + safe_label_update(ui_elements['label8'], tag_counter) + safe_label_update(ui_elements['label10'], f"MSG : {output}") + save_to_nvs() + elif target_serial == serial and ack == 0: + # Negative ACK but still targeted to this device + waiting_for_ack = False + safe_label_update(ui_elements['label10'], f"MSG : {output}") + print(output) + else: + print("Message ignored, condition does not match") + #safe_label_update(ui_elements['label10'], f"MSG : ") + except Exception as e: + print("response data error:", e) + if topic.endswith(MQTT_LAMP): + try: + data = ujson.loads(msg) # decode JSON payload + target_serial = data.get("serial") + color = data.get("color") + # Only act if the message is for THIS device + if target_serial == serial: + # Clear waiting flag if message is specifically for this device + waiting_for_ack = False + if color == "red": + print("Set light RED") + rgb_0.fill_color(0xff0000) + lamp_color = 0xff0000 + save_to_nvs() + elif color == "green": + print("Set light GREEN") + rgb_0.fill_color(0x008000) + lamp_color = 0x008000 + save_to_nvs() + elif color == "yellow": + print("Set light yellow") + rgb_0.fill_color(0xffff00) + lamp_color = 0xffff00 + save_to_nvs() + elif color == "purple": + print("Set light purple") + rgb_0.fill_color(0x800080) + lamp_color = 0x800080 + save_to_nvs() + elif color == "blue": + print("Set light blue") + rgb_0.fill_color(0x0000FF) + lamp_color = 0x0000FF + save_to_nvs() + else: + pass + else: + print("Message ignored, serial does not match") + except Exception as e: + print("QC-status topic parse error:", e) + except Exception as e: + print("MQTT Callback Error:", e) + +def init_mqtt(): + global mqtt_client + + try: + if not wlan.isconnected(): + print("WiFi not connected") + return + + mqtt_client = MQTTClient( + client_id=MQTT_CLIENT_ID, + server=MQTT_BROKER, + port=MQTT_PORT, + user=MQTT_USER, + password=MQTT_PASSWORD, + keepalive=60, + ssl=False + ) + + mqtt_client.set_callback(mqtt_callback) + mqtt_client.connect() + + mqtt_client.subscribe(MQTT_RESET_TOPIC) + mqtt_client.subscribe(MQTT_RESPONSE_DATA) + mqtt_client.subscribe(MQTT_LAMP) + + print("MQTT Connected") + + ui_elements['image5'] = Widgets.Image("/flash/res/img/MQTT_Cloud.jpg",100,0) + + rgb_0.fill_color(lamp_color) + + except Exception as e: + print("MQTT init error:", e) + mqtt_client = None + ui_elements['image5'] = Widgets.Image("/flash/res/img/mqtt_error.jpg", 104, 1, scale_x=1, scale_y=1) + +def reconnect_mqtt(): + global mqtt_client, mqtt_last_try + + now = utime.ticks_ms() + + # prevent continuous reconnect spam + if utime.ticks_diff(now, mqtt_last_try) < MQTT_RECONNECT_INTERVAL: + return + + mqtt_last_try = now + + print("MQTT reconnect attempt") + + try: + if mqtt_client: + try: + mqtt_client.disconnect() + except: + pass + + mqtt_client = None + + if not wlan.isconnected(): + print("WiFi not connected, skipping MQTT reconnect") + ui_elements['image2'] = Widgets.Image("/flash/res/img/wifi_error.jpg", 150, 3, scale_x=1.1, scale_y=1) + return + + init_mqtt() + + except Exception as e: + print("MQTT reconnect error:", e) + ui_elements['image5'] = Widgets.Image("/flash/res/img/mqtt_error.jpg", 104, 1, scale_x=1, scale_y=1) + mqtt_client = None +def loop(): + global tag_counter, seen_uids, last_uid_str, mqtt_client, serial, player, Operator_ID ,battery_per , bettery_icon, card,rfid_re_init,i2c0,rfid_0,lamp_color, waiting_for_ack, ack_deadline + + #M5.update() + + serial = ubinascii.hexlify(unique_id()).decode() + check_wifi() + time.sleep(0.1) + bettery_icon.set_value(Power.getBatteryLevel(), True) + #bettery_per.set_text(f"{str(Power.getBatteryLevel())}%") + safe_label_update(ui_elements['bettery_per'], str(Power.getBatteryLevel())) + if Power.isCharging(): + ui_elements['charging_icon'] = Widgets.Image("/flash/res/img/charging.jpg", 305, 11) + else: + ui_elements['charging_icon'] = Widgets.Image("/flash/res/img/not_charging.jpg", 305,11) + + if not wlan.isconnected(): + connect_wifi() + if wlan.isconnected(): + init_mqtt() + print("MQTT RE-INITIALIZE SUCCESS") + if not wlan.isconnected(): + time.sleep(1) + return + + # Check for MQTT messages + try: + if mqtt_client: + mqtt_client.check_msg() + except Exception as e: + print("MQTT error:", e) + mqtt_client = None + + # --- ACK TIMEOUT HANDLING (non-blocking) --- + if waiting_for_ack: + # utime.ticks_diff returns positive when now >= ack_deadline + if utime.ticks_diff(utime.ticks_ms(), ack_deadline) >= 0: + print("ACK TIMEOUT: No response received") + safe_label_update(ui_elements['label10'], "MSG : NO RESPONSE RECIEVED") + waiting_for_ack = False + # ------------------------------------------------ + # Ensure MQTT connection + if wlan.isconnected() and mqtt_client is None: + reconnect_mqtt() + # RFID Processing + try: + if rfid_re_init==1: + i2c0 = I2C(0, scl=Pin(1), sda=Pin(2), freq=100000) + rfid_0 = RFIDUnit(i2c0) + rfid_re_init=0 + print("RFID RE_INITIALIZED") + else: + pass + card_present = rfid_0.is_new_card_present() if rfid_0 else False + safe_label_update(ui_elements['label5'], card_present) + if waiting_for_ack: + # Show card is ignored during waiting + if card_present: + safe_label_update(ui_elements['label10'], "MSG : WAITING FOR RESPONSE") + return + + if card_present: + current_uid = rfid_0.read_card_uid() if rfid_0 else None + if current_uid: + + # Convert UID bytearray to consistent string format + uid_str = ':'.join(['%02X' % b for b in current_uid]) + safe_label_update(ui_elements['label6'], uid_str) + # Read all fields from the card + sku, color, size, article, remarks, cardtype, new_operator_id = read_all_fields() + rfid_0.close() + # Update Operator ID if we have a new valid ID + if new_operator_id and new_operator_id != '0' and cardtype=='Operator' and card==0: + card=1 + Operator_ID = new_operator_id + safe_label_update(ui_elements['label9'], str(Operator_ID)) + # Save the new Operator ID immediately + save_to_nvs() + publish_rfid_data(sku, color, size, article, remarks, last_uid_str, tag_counter, cardtype, Operator_ID, serial) + # --- START WAIT FOR ACK (seconds) --- + safe_label_update(ui_elements['label10'], "MSG : WAITING FOR RESPONSE") + waiting_for_ack = True + ack_deadline = utime.ticks_add(utime.ticks_ms(), 10000) + # --------------------------------------- + #player.play_tone(2000, 0.04, volume=100, sync=True) + #player.play("file://flash/res/audio/beep.mp3", pos=0, volume=100, sync=True) + #if uid_str == RESET_UID: + if cardtype == "Reset": + # Reset logic + seen_uids.clear() + tag_counter = 0 + last_uid_str = "-" + Operator_ID = "0" + safe_label_update(ui_elements['label8'], "0") + safe_label_update(ui_elements['label3'], "Last UID: - (Reset)") + safe_label_update(ui_elements['label9'], "-") + safe_label_update(ui_elements['label10'], "MSG : ") + save_to_nvs() + publish_rfid_data("RESET", "RESET", "RESET", "RESET", "RESET", "RESET", "RESET", "RESET", 0, serial) + # --- START WAIT FOR ACK (seconds) AFTER RESET --- + safe_label_update(ui_elements['label10'], "MSG : WAITING FOR RESPONSE") + waiting_for_ack = True + ack_deadline = utime.ticks_add(utime.ticks_ms(), 10000) + # --------------------------------------- + return + + # Update last UID shown + last_uid_str = uid_str + safe_label_update(ui_elements['label3'], f"Last UID: {last_uid_str}") + + #if uid_str not in seen_uids and cardtype == 'Product': + if cardtype == 'Product' and card==0: + card=1 + #seen_uids.add(uid_str) + #if cardtype == 'Product': + # tag_counter += 1 + safe_label_update(ui_elements['label8'], str(tag_counter)) + save_to_nvs() + publish_rfid_data(sku, color, size, article, remarks, last_uid_str, tag_counter, cardtype, Operator_ID, serial) + # --- START WAIT FOR ACK (seconds) --- + safe_label_update(ui_elements['label10'], "MSG : WAITING FOR RESPONSE") + waiting_for_ack = True + ack_deadline = utime.ticks_add(utime.ticks_ms(), 10000) + # --------------------------------------- + #player.play_tone(2000, 0.04, volume=100, sync=True) + #player.play("file://flash/res/audio/beep.mp3", pos=0, volume=100, sync=True) + else: + safe_label_update(ui_elements['label6'], "-") + safe_label_update(ui_elements['label3'], f"Last UID: {last_uid_str}") + card=0 + + #mqtt_client.publish(f"M5devices/{DEVICE_NUMBER}", ujson.dumps({"online":"True","operator_id":f"{Operator_ID}","serial":f"{serial}"}), qos=0) + if mqtt_client: + mqtt_client.publish(f"M5devices/{DEVICE_NUMBER}", ujson.dumps({}), qos=0) + + except Exception as e: + print("RFID Processing Error:", e) + safe_label_update(ui_elements['label6'], "RFID Error") + rfid_re_init=1 + + M5.update() + time.sleep(0.1) +def setup(): + global wlan, i2c0, rfid_0, mqtt_client, label2, player , battery_per , bettery_icon , rfid_re_init,rgb_0,lamp_color + #machine.WDT(timeout=60000) + M5.begin() + m5ui.init() + bettery_icon = m5ui.M5Bar(x=256, y=7, w=49, h=25, min_value=0, max_value=100, value=25, bg_c=0x616161, color=0x21f398) + #player = Player(None) + rgb_0 = RGBUnit((8, 9), 10) + + load_from_nvs() + print(lamp_color) + #if lamp_color==16776960: + #rgb_0.fill_color(0xffff00) + + #rgb_0.fill_color(lamp_color) + rgb_0.fill_color(0x0000FF) + init_ui() + label2 = Widgets.Label("SR: LOADING...", 70, 70, 1.0,0x000000, 0xffffff, Widgets.FONTS.DejaVu18) + init_wifi() + if wlan.isconnected(): + check_for_update() #check update files from git hub + + serial = ubinascii.hexlify(unique_id()).decode() + + label2.setText("SR: " + serial) + label2.setColor(0x000000, 0xffffff) + + # Initialize RFID reader + try: + i2c0 = I2C(0, scl=Pin(1), sda=Pin(2), freq=100000) + rfid_0 = RFIDUnit(i2c0) + rfid_re_init=0 + except Exception as e: + print("RFID Init Error:", e) + safe_label_update(ui_elements['label6'], "RFID Error") + rfid_re_init=1 + + # Initialize MQTT + init_mqtt() + #rgb_0.fill_color(0x33ff33) +if __name__ == '__main__': + try: + setup() + while True: + loop() + except Exception as e: + print("Main Error:", e) + try: + from utility import print_error_msg + print_error_msg(e) + except ImportError: + print("Please update firmware") \ No newline at end of file