diff --git a/tools/flash_device.py b/tools/flash_device.py new file mode 100755 index 0000000..223c744 --- /dev/null +++ b/tools/flash_device.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +flash_device.py — Write NVS config to TimerCamera-F over serial. + +Requires: pip install esptool nvs-partition-gen +Usage: + python flash_device.py \\ + --port /dev/ttyUSB0 \\ + --device-id dc-0042 \\ + --location-id retailer-123 \\ + --hmac-secret <32-byte-hex> # omit to auto-generate \\ + [--wifi-ssid "StoreWiFi"] \\ + [--wifi-password "secret"] \\ + [--line-offset 50] +""" +import argparse +import os +import secrets +import subprocess +import sys +import tempfile + + +NVS_NAMESPACE = "doorcounter" +NVS_PARTITION_OFFSET = "0x9000" +NVS_PARTITION_SIZE = "0x5000" # matches firmware partition table (20KB) + + +def build_nvs_csv(device_id, location_id, hmac_secret, + wifi_ssid=None, wifi_pass=None, line_offset=50): + rows = [ + "key,type,encoding,value", + f"{NVS_NAMESPACE},namespace,,", + f"device_id,data,string,{device_id}", + f"location_id,data,string,{location_id}", + f"hmac_secret,data,string,{hmac_secret}", + f"line_offset,data,u8,{line_offset}", + ] + if wifi_ssid is not None: + rows.append(f"wifi_ssid,data,string,{wifi_ssid}") + if wifi_pass is not None: + rows.append(f"wifi_pass,data,string,{wifi_pass}") + return "\n".join(rows) + "\n" + + +def main(): + parser = argparse.ArgumentParser( + description="Provision TimerCamera-F NVS config over serial") + parser.add_argument("--port", required=True, + help="Serial port, e.g. /dev/ttyUSB0 or COM3") + parser.add_argument("--device-id", required=True, + help="Unique device ID, e.g. dc-0042") + parser.add_argument("--location-id", required=True, + help="Retailer location ID, e.g. retailer-123") + parser.add_argument("--hmac-secret", default=None, + help="32-byte hex HMAC secret (auto-generated if omitted)") + parser.add_argument("--wifi-ssid", default=None, + help="WiFi SSID (optional — user can set via captive portal)") + parser.add_argument("--wifi-password", default=None, + help="WiFi password (optional)") + parser.add_argument("--line-offset", type=int, default=50, + help="Virtual line position %% of frame height (default 50)") + args = parser.parse_args() + + hmac_secret = args.hmac_secret or secrets.token_hex(32) + if args.hmac_secret is None: + print(f"Generated HMAC secret: {hmac_secret}") + print(" *** SAVE THIS — you need it to register the device on the server ***") + + if args.line_offset < 0 or args.line_offset > 100: + print("Error: --line-offset must be 0-100", file=sys.stderr) + sys.exit(1) + + with tempfile.TemporaryDirectory() as tmp: + csv_path = os.path.join(tmp, "nvs.csv") + bin_path = os.path.join(tmp, "nvs.bin") + + csv_content = build_nvs_csv( + args.device_id, args.location_id, hmac_secret, + args.wifi_ssid, args.wifi_password, args.line_offset + ) + with open(csv_path, "w") as f: + f.write(csv_content) + + # Generate NVS binary + ret = subprocess.run( + [sys.executable, "-m", "nvs_partition_gen", "generate", + csv_path, bin_path, NVS_PARTITION_SIZE], + capture_output=True, text=True + ) + if ret.returncode != 0: + print(f"nvs_partition_gen error:\n{ret.stderr}", file=sys.stderr) + sys.exit(1) + + # Flash NVS partition + ret = subprocess.run( + ["esptool.py", "--port", args.port, "--chip", "esp32", + "write_flash", NVS_PARTITION_OFFSET, bin_path] + ) + sys.exit(ret.returncode) + + +if __name__ == "__main__": + main()