NomadLink

NomadLink One · NomadLink OS · NomadLink Console · NomadLink Cloud — v0.1.0 — generated 2026-10-01 12:10 UTC

Activity Log

Append-only. Historical evidence is never rewritten. Rule 24.
WhenTypeSummaryLinks
2026-10-01T18:05:00ZfixFix the IllegalInstruction reboot loop: a FreeRTOS task that returned, plus an AT sync that gave up before the modem finished booting.
position.c: position_task no longer returns on any path (prvTaskExitError -> IllegalInstruction). · position.c: AT sync is now 30 attempts over ~90 s with every raw reply logged, instead of 6 over ~24 s. · position.c: enable rail settle 500 ms -> 800 ms, matching the measured baseline. · position.c: gpio_config failure is handled, not ESP_ERROR_CHECK'd. · Makefile: two new gates in `make check` - no bare `return;` in a FreeRTOS task, no ESP_ERROR_CHECK in position.c code.
TASK-302
2026-10-01T17:35:00ZfixFix the Location layout regression, restore the UART0 console, and fix NMEA/AT sharing one UART.
page_index.h: the Location block no longer closes #container, so buttons and layout are untouched. · sdkconfig.defaults.esp32s3: console back on UART0 so app logs are visible on the CH343. · position.c: AT+CGNSSTST=1 is now sent before the NMEA pump starts, and both hold s_uart_lock.
TASK-302
2026-10-01T17:15:00ZfeaturePosition service: cellular LBS with GNSS upgrade, shown in a new Location section on the web page.
New components/position: AT over UART1 GPIO17/18, GPIO33 enable rail, no PWRKEY pulse. · AT+CLBS=1,1 for LBS (lon comes first; retries transient ret_codes 8/9/10/11/12). · AT+CGNSSPWR=1 / AT+CGNSSPORTSWITCH=0,1 / AT+CGNSSTST=1 for GNSS; engine powered before the LBS wait so acquisition overlaps. · New Location section on the index page showing lat, lon, source (GPS vs Cellular LBS), accuracy, satellites and sample age. · GGA parsing extracted to nmea.c with no ESP dependency, plus 28 host unit tests wired into `make check`.
TASK-302
2026-10-01T16:35:00ZrevertRemove all A7670E 4G work from the product tree and restore the verified base.
Deleted components/modem_4g, main/modem_fallback.c/.h, main/led_strip_status.c, the /modem page and all sdkconfig PPP/console overrides. · firmware/nat_router source file set is now identical to verified base commit 28d137a; rebuild is 1374544 bytes. · Recorded 0.4.0-4gfb's event-loop mutex panic as an OPEN defect with cause UNKNOWN rather than a fixed one.
TASK-103 TASK-107
2026-10-01T15:10:00ZchangeWithdraw 4G as a router uplink. 4G is now an ESP32-only fallback link, dialled only when WiFi is absent.
Removed the /modem uplink-selector page, its HTTP handlers, the nav link, and main/uplink_manager.c. · components/modem_4g/modem_4g.c: dropped ppp_set_default() so PPP can never become the lwIP default route. · Added main/modem_fallback.c: polls the STA address every 5 s; dials 4G only after 3 consecutive misses (15 s); drops the link when WiFi returns. · Corrected the stale 'PPP uplink / NAPT' claims in components/modem_4g/include/modem_4g.h.
TASK-103
2026-10-01T14:40:00ZfixFix swapped handler/arg in esp_event_handler_instance_register -- the cause of both the 4G no-AP loop and the reconnect loop.
main/uplink_manager.c: pass uplink_esp_event_cb as the handler and NULL as handler_arg; log registration failures instead of asserting. · App console on UART0 is what made this diagnosable; keep it.
—
2026-10-01T14:05:00ZfixRe-add 4G uplink as 0.3.0-4g.2 with the boot-order risk removed, after 0.2.0-4g.1 bricked the SoftAP.
RGB LED left DISABLED (led_strip_gpio default back to -1). It was the prime suspect: the GPIO38 default turned a dead code path into a live RMT allocation on every boot. Re-enable via NVS ls_gpio=38. · uplink_init() moved out of app_main into uplink_boot_task, spawned at the end of app_main and delayed 2 s, so the SoftAP and web server are already up before any modem code runs. · App console moved to UART0 (CONFIG_ESP_CONSOLE_UART_DEFAULT=y, JTAG deselected). The default native USB-Serial-JTAG console is not physically exposed on this board, which is why the 0.2.0-4g.1 panic was silent. · Port contention diagnosed: serial-monitor held the CH343 exclusively, which is what caused the browser's 'Failed to open serial port'. Not a firmware fault. · firmware.json -> 0.3.0-4g.2, app 1433360 B.
—
2026-10-01T13:30:00ZdefectWithdraw 0.2.0-4g.1 and restore the TEST-810 verified base. The 4G-uplink build did not bring up the SoftAP.
Reverted firmware/nat_router to commit 28d137a (base only) and removed components/modem_4g, main/uplink_manager.*, pages/page_modem.h. Work preserved in history at 8b09974. · Deleted the generated sdkconfig: reverting sdkconfig.defaults alone is NOT enough, because the generated sdkconfig kept CONFIG_LWIP_PPP_SUPPORT=y and produced a 1403024 B binary instead of the verified 1374544 B. This is a recurrence of the stale-sdkconfig trap and must be recorded. · Restored staged nomadlink.bin to 1374544 B, bit-identical to the TEST-810 image the owner confirmed working. · docs/project/firmware.json: rollback_note added.
—
2026-10-01T12:00:00ZfeatureAdd A7670E 4G as a selectable uplink alongside WiFi, with a /modem web page and RGB status signalling. Compile-gated only; NOT hardware-verified.
firmware/nat_router/components/modem_4g/: A7670E UART (GPIO17/18) + raw-lwIP PPPoS driver. Raw lwIP chosen because esp_modem is not in ESP-IDF 5.5.4 (it lives in esp-iot-solution); path ported from the hardware-proven baseline ppp_client.ino. · firmware/nat_router/main/uplink_manager.c/.h: uplink_mode {wifi,4g,eth}, NVS-persisted, route priority, 2s poll task (raw PPP emits no IP_EVENT_PPP_GOT_IP), auto-dial on boot when mode=4g, plus uplink_ui_get/set as the web bridge. · firmware/nat_router/sdkconfig.defaults: CONFIG_LWIP_PPP_SUPPORT=y and friends; VJ header compression forced OFF because it conflicts with NAPT. · firmware/nat_router/components/http_server: new /modem page + handler + nav link; GET-query form matching the base's existing form idiom. · firmware/nat_router/main/led_strip_status.c: led_uplink_state_t - fast red = 4G no network, fast green = 4G up, slow green = WiFi uplink. WS2812B defaulted to GPIO38 (was disabled). · docs/product/led-signals.md: running, extensible LED signal list. · docs/project/firmware.json: version 0.2.0-4g.1; staged nomadlink size 1428944; base hardware PASS scoped to 0.1.0-base.1; 4G image marked NOT hardware-verified.
—
2026-10-01T02:40:00ZVERIFICATIONBASE FIRMWARE VERIFIED ON HARDWARE; tagged NAT_ROUTER_WORKING. The project owner flashed the vendored esp32_nat_router base from the published web installer and confirmed the device broadcasts 'NomadLink'. This is the first successful flash of product firmware from this repository and it closes the open question of whether the served payload actually reaches the device -- the thing two earlier defects (a prebuilt payload no source edit could reach, then 20 un-pushed commits) had left unproven. Evidence is an independent observer's report, TEST-810, not a marker printed by our own code. Repository-side corroboration at the same time: published page HTTP 200 with 'Connect & Install' (20479 B), manifest 200, and all four parts 200 at their declared offsets (17664 / 3072 / 8192 / 1374544 B), with esp32_nat_router.bin correctly 404ing. SCOPE OF THE CLAIM, kept deliberately narrow: what is proven is that the base boots, the SSID is correct, and the delivery path works. Forward throughput, DHCP lease and client internet access are NOT measured. No 4G, GNSS, camera or SD capability is in this image and none is claimed. Process: L-17 recorded after handing the user a URL while 20 commits sat un-pushed; AGENTS.md section 0 now carries the six-step delivery sequence.REQ-001 REQ-020 REQ-022 TASK-102
2026-10-01T02:10:00ZFIXRebrand the product and its artifact; installer now serves our own build. REPORTED SYMPTOM: the flashed SSID still read ESP32_NAT_Router and the flashed file was still called esp32_nat_router.bin after both had been changed in the source. ROOT CAUSE, and it is a process failure rather than a string failure: the web installer was serving upstream's PREBUILT binaries. The page rendered, the manifest resolved, every part returned HTTP 200 and every check passed -- while the served bytes were not built from this repository at all, so no source edit could ever reach a user. A correct page serving a stale artifact is exactly the failure this project already recorded for *.bin in .gitignore, and it is the same class as 'a compile gate that reported ok having built nothing' (L-16). FIX, in four parts. (1) New scripts/project-tracker/stage-firmware.sh, exposed as 'make stage-firmware', builds the product and stages it into docs/project/firmware/, so one command owns the path from source to the bytes a user flashes and the two cannot drift silently. It asserts the four flash offsets against build_esp32s3/flash_args before copying (0x0, 0x8000, 0xf000, 0x20000): a wrong offset produces an image that flashes cleanly and then does not boot, so the offsets are checked rather than assumed. Written for bash 3.2 -- an initial mapfile version failed on this machine and the failure was the check working, not the check being optional. (2) 'make product' now WARNs when the staged payload is older than the sources it came from, naming the file, so the next drift is loud instead of invisible. (3) Artifact renamed: the IDF project is now 'nomadlink' in CMakeLists.txt, and since idf.py derives the output filename from the project name this is what turns esp32_nat_router.bin into nomadlink.bin; the manifest, staging script and both build scripts were updated in the same commit. (4) Branding: AP SSID ESP32_NAT_Router -> NomadLink, mDNS hostname esp32-nat-router -> nomadlink, web UI titles, remote-console banner, hostname field placeholder, and the upstream MCP bridge script. The MQTT topic prefix stays esp32_nat_router on purpose -- it is a functional broker identifier, not branding, and renaming it would break existing subscriptions. VERIFIED BY SCANNING THE STAGED PAYLOAD, NOT THE SOURCE, because that distinction is the whole bug: in docs/project/firmware/nomadlink.bin 'NomadLink' occurs 4 times and 'ESP32 NAT Router', 'ESP32_NAT_Router' and 'esp32-nat-router' occur 0 times. Page check in Chrome: element defined, manifest resolves, all four parts 200 (17664 / 3072 / 8192 / 1374544 bytes), old esp32_nat_router.bin now 404s, and the rendered page contains no occurrence of the old brand. The one remaining 'esp32_nat_router' string on the page is deliberate attribution in the provenance line ('Built from the vendored esp32_nat_router 2.4.17 source'). NOT TESTED: no device was flashed; Web Serial needs a real user click and a connected board. Flashing remains unverified.REQ-001 REQ-020 REQ-022 TASK-102
2026-10-01T01:20:00ZDECISIONProduct firmware base replaced with vendored upstream esp32_nat_router 2.4.17 (commit 2fe7a4c), built with ESP-IDF 5.5.x (ADR-009, supersedes ADR-005/007/008). WHY: a prototype was written that reproduced feature for feature a system that already exists and was already running on this board. Hand-rolling it re-implemented ip_napt_enable() routing, DHCP, the packet hooks and the console -- and walked back into the exact defect ADR-008 records, because the Arduino port's ESPAsyncWebServer drives lwIP from the wrong thread while the reference uses esp_http_server. The base inherits proven forwarding, NAT, DHCP, ACL, WireGuard, port mapping and per-client statistics. WHERE: firmware/nat_router, vendored verbatim with provenance in firmware/nat_router/README.md. TOOLCHAIN CONSTRAINT: the base does NOT build on IDF 6.x -- main requires component 'json', which moved into the component manager, giving "Failed to resolve component 'json' required by component 'main': unknown name". That is a toolchain mismatch, not a code fault; IDF 5.5.4 was installed at ~/esp/esp-idf-5.5.4 and the base builds clean for esp32s3 at 0x14f970 / 1374576 bytes. Installing it also required SSL_CERT_FILE pointing at certifi, because this machine's system python cannot verify TLS unaided. GATE: make product now builds this tree and skips cleanly when no 5.5.x install is present, so a missing toolchain cannot pass as success. The first version of that gate was wrong twice -- it grepped the log instead of checking the exit code, and it ran idf.py from the repo root, so it reported "ok" having built nothing; both are fixed and the gate is now proven to reject a broken tree (invalid C appended to main/portmap.c -> exit 1 naming file and line, reverted, clean rebuild). .gitignore had *.bin, which would have silently excluded the web installer's four firmware parts from git, leaving a published manifest whose payloads all 404; an explicit exception was added for docs/project/firmware/*.bin. WEB INSTALLER: added to docs/project/index.html, generated by render.py from the new docs/project/firmware.json (JSON authoritative, HTML generated, ADR-003) using esp-web-tools and a manifest for ESP32-S3 at the upstream offsets. Verified in Chrome: the custom element is defined, the manifest attribute is set, the button renders, and manifest plus all four parts return HTTP 200 at the declared offsets. The page is explicitly labelled base-only, with A7670E 4G, GNSS, camera and SD absent. The actual flash is NOT TESTED -- Web Serial needs a real user click and a connected board, and no device was touched. LICENCE (RISK-011, S2): upstream ships no LICENSE file and GitHub reports license=null, so redistribution rights are unstated; only the bundled WireGuard submodule is explicitly BSD-licensed. An earlier draft of firmware.json wrongly asserted GPL-3.0 and was corrected. No release may be called a production release until this is settled (Rule 37). TWO ARTIFACTS NOW EXIST and are not interchangeable: the served binary is upstream's prebuilt (1370000 B), while the local rebuild from vendored source is 1374576 B -- toolchain drift, functionally the same base; the local build replaces the served one when features land. NOT DONE: no NomadLink feature has been added to the base yet. The Arduino sketch at firmware/nomadlink is retained as historical evidence only. ADDITIONAL LESSON L-16.REQ-001 REQ-020 REQ-022 TASK-102 TASK-801 TASK-802
2026-09-30T23:55:00ZFEATURE_IMPLEMENTEDSelectable uplink + admin console on the core WebServer (TASK-102, TASK-801). AP moved from WIFI_AP to WIFI_AP_STA so a WiFi uplink can exist alongside PPP. Uplink choice (4g|wifi) and WiFi credentials persist in the gateway's own NVS and are reapplied on boot. New endpoints: GET /api/scan, POST /api/uplink/mode, POST /api/uplink/wifi, POST /api/uplink/wifi/disconnect, each surfaced as controls in the Network tab. Switching is netif_set_default() only -- lwIP routes and NAPT translates in both cases, and no route or NAT rule is hand-rolled. Two guards: a WiFi uplink with no address is refused rather than black-holing clients, and a dropped STA cannot remain the default. CRITICAL REVERSAL: ESPAsyncWebServer was replaced with the core WebServer. AsyncTCP calls raw lwIP and this core builds lwIP with CONFIG_LWIP_CHECK_THREAD_SAFETY=y and CONFIG_LWIP_TCPIP_CORE_LOCKING_INPUT unset, so three arrangements all aborted (setup task, LOCK_TCPIP_CORE() -- which deadlocks because AsyncTCP uses a blocking tcpip_api_call() -- and a dedicated FreeRTOS task). The restriction is in the core's lwIP build, not scheduling. The esp32_nat_router reference is immune because it uses esp_http_server, not ESPAsyncWebServer, so its immunity was never evidence for AsyncTCP. Cost accepted and documented: the core WebServer is poll-driven and handles one connection at a time, so handleClient() must be called in loop() and a WiFi scan stalls the console for seconds. CORRECTIONS TO THE RECORD: (a) IDF's LWIP_TCPIP_CORE_LOCKING_INPUT default is n, so the core's value was never a deviation from a safer default; (b) CONFIG_LWIP_L2_TO_L3_COPY is not a NAT or forwarding enable switch -- it copies WiFi-driver L2 buffers into fresh L3 buffers to release the fixed L2 RX pool and may affect capacity only; (c) AGENTS.md said GPIO33 for modem control was a misreading and that pins 21/42 were correct -- the opposite is now measured: firmware drives GPIO33 as a module-enable rail, the PWRKEY net does not reach this board, and PPP works on that wiring; AGENTS.md, LESSONS_LEARNED.md S5 and the PWRKEY lesson are corrected in this change; (d) the console 'default route' field was reading the PPP gateway inside the PPP-up branch and so kept advertising the PPP peer after a switch to WiFi -- it is now read from netif_default unconditionally. Build gate: make product ok, 997,971 B (32% of the 3 MB app partition) and make check green. Browser pre-flight against a MOCK /api/status verified rendering, both mode buttons, scan, join, the refusal when no address, and the default-route label tracking the active uplink -- this is a pre-flight on the HTML only and is NOT hardware evidence. Nothing is recorded as hardware-verified here: no forwarding, no DHCP lease, no client internet and no mode switch have been measured on hardware in this increment. ADR-008 supersedes ADR-007 decision 1; L-15 added.REQ-001 REQ-020 REQ-022 TASK-101 TASK-102 TASK-404 TASK-801 TASK-802
2026-09-30T00:00:00ZFEATURE_IMPLEMENTEDAdmin console first increment (TASK-801, TASK-802). Async web server via ESPAsyncWebServer 1.2.4-fork + AsyncTCP 1.1.4, served at http://192.168.4.1 and http://nomadlink.local, with the seven-section navigation (Dashboard, Network, Camera, Location, Cloud, SMS, Telephony). Only measured values are served: SoftAP/DHCP state, PPP addresses, CSQ + operator + SAMPLE AGE, NAPT state, byte and forwarded-packet counters, heap/PSRAM/flash, and real MAC/IP client pairs from esp_wifi_ap_get_sta_list_with_ip(). A client with no lease shows 'no address yet'. Six sections are explicitly NOT INTEGRATED and name their owning TASK. The web task never touches the UART: modem telemetry is sampled once before the PPP dial and cached, and the UI shows the sample age. Process fix: the product sketch had NO compile gate at all (build-baseline.sh loops firmware/baseline/* only), so added scripts/project-tracker/build-product.sh and 'make product', now part of 'make check'. Product sketch compiles at 989,467 B (32% of the 3 MB app partition). Browser pre-flight against a MOCK /api/status confirmed rendering, tab isolation, tab persistence across reload, zero external assets, and correct degraded paths -- this is a pre-flight on the HTML and is NOT hardware evidence; TEST-801 and TEST-101 remain NOT_RUN. ADR-007 records the decision and, explicitly, that the console has no authentication and is a test surface only (TASK-404 deferred). Two spec deviations recorded rather than absorbed per ADR-002: the captive portal is impossible (no lwIP DNS server, so no probe interception point) and the DHCP pool is .2-12 not .2-254 (core ships CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8).REQ-001 REQ-020 REQ-022 TASK-101 TASK-404 TASK-801 TASK-802
2026-09-29T00:00:00ZSECURITY_RISK_ACCEPTEDRISK-008: a live GitHub token with repo scope was shared in plaintext. The user elected to keep it valid. It is not written to any repository file; gh keyring auth is used instead.REQ-022
2026-09-29T00:00:00ZCAPABILITY_VERIFIEDREQ-008 cellular LBS fallback VERIFIED: AT+CLBS=1,1 returned +CLBS: 0,18.481703,73.897415,550, independently corroborated by the GNSS fix at 18.480776, 73.897998. Requires the 4G patch antenna (CSQ 16 -> 26) and AT+SIMEI populated.REQ-008 TASK-303
2026-09-29T00:00:00ZPRD_DEFECT_FOUNDBUG-004 / ADR-002: PRD FR-3.1 names AT+CGNSINF, which does not exist on this module. AT+CGPSINFO is valid. The PRD also assumes PPP, asserts unmeasured NFRs, and omits the working LBS fallback capability entirely.REQ-007 REQ-008 REQ-003 REQ-004 REQ-017 REQ-018 TASK-301
2026-09-29T00:00:00ZDEFECT_FOUNDBUG-003: the tracker's GNSS wait loop increments an integer by 1000 per iteration instead of using a millis() deadline, so it exits after seconds rather than the intended 15 minutes.REQ-007 TASK-301
2026-09-29T00:00:00ZDEFECT_FOUNDBUG-002: device MQTT CONNECT never confirmed. The AT+CIPSEND '>' prompt and the broker CONNACK bytes are not newline-terminated, so the line-based reader cannot consume either.REQ-012 TASK-405
2026-09-29T00:00:00ZPROJECT_CREATEDNomadLink project created. Product Requirement Document v2.0 extracted into 23 traceable requirements across 9 epics and 22 features.
Created docs/product/requirements.json with 23 requirements, each carrying hardware_verified and verification_note · Created docs/product/epics.json · Created docs/project/backlog.json with 14 tasks · Created docs/project/risks.json with 9 risks, 3 of them S0 · Created docs/project/bugs.json with 4 defects · Created docs/project/test-results.json with 13 tests, 5 passing · Created docs/project/sprint-board.json with Sprint 1 scoped to de-risking · Created docs/testing/test-strategy.md · Created docs/decisions/ ADR-001 through ADR-006 · Created scripts/project-tracker/validate.py enforcing the Rule 44 invariants
TASK-001
2026-09-29T00:00:00ZmilestoneRepository made public and GitHub Pages enabled for the generated dashboard.
repo ragavellur/nomadlink created (public) · Pages serving /docs · docs/index.html added as the Pages entry point · first two commits pushed; working tree clean
TASK-001 TASK-002 TASK-004
2026-09-30T00:00:00Zfixtracker byte-level MQTT reader implemented and flashed; CIP socket path found unusable on current module firmware
tracker.ino: waitForByte('>') replaces line reader for AT+CIPSEND prompt · tracker.ino: 4-byte CONNACK state machine (20 02 <sp> <rc>) with real rc reporting · tracker.ino: MARK-CONNECTED now conditional on CONNACK rc==0; MARK-CONNECT-FAILED on failure · compiled (317460 B / 10%) and flashed to app partition · runtime: physical unit reports firmware A7670M7_V1.11.1; UART floods with raw PPP LCP frames after AT+CGACT=1,1, so AT+CIPOPEN never answers (CIPOPEN FAILED) · conclusion: AT-CIP socket stack is not usable on this unit; the working LTE data path is PPP (per NAT router work). BUG-002 reader fix retained for any future CIP path; TEST-403 remains FAIL on current firmware
REQ-012 TASK-405
2026-09-30T00:00:00ZverifiedMQTT over PPP verified end to end: CONNACK rc=0, publish observed by independent subscriber
firmware/baseline/mqtt_ppp/mqtt_ppp.ino: raw lwIP TCP socket MQTT transport over the PPP data channel · MQTT_HOST_IP override to skip UDP DNS (carrier drops non-TCP traffic) · UART feeder (feedPpp) interleaved into every blocking socket wait; blocking mode removed - starvation caused 18.3s ENETUNREACH · frame builders fixed: body placed immediately after the 1-byte remaining-length field (2-byte reservation left a garbage byte the broker dropped) · CONNACK wait loops in 500ms slices for the full 10s window instead of aborting on first slice timeout · verified: CONNACK rc=0, PUBLISH 71 bytes to nomadlink/one/gps, subscriber received the JSON payload
REQ-012 TASK-405
2026-09-30T12:17:38ZverifiedGNSS service (TASK-301) verified: BUG-003 deadline fix + ADR-006 sats gate; real cold-start lock on hardware
tracker/tracker.ino: BUG-003 GNSS wait loop rewritten from counter loop (exited after ~9s) to real millis() deadline (FIX_WINDOW_MS) with ADR-006 gate requiring gps.location.isValid() && gps.satellites.value() > 0 · gnss_hold/gnss_hold.ino: AT+CGNSSPORTSWITCH=1,1 -> 0,1 (1,1 errors on rev A7670M7_V1.11.1; 0,1 streams NMEA to UART per Waveshare board doc) · gnss_hold: wait for +CGNSSPWR: READY! before other GNSS ops; sends echo + response logging; +++ escape from PPP data mode · verified: LOCK t+130s, sats=4, hdop=7.3, fix 18.480816, 73.898640 ALT 550.7m with unit outside window · docs: TEST-301 evidence, TASK-301 DONE/VERIFIED, BUG-003 FIXED, PORTSWITCH references corrected in product-spec.md/ADR-006/requirements/traceability/LESSONS_LEARNED
REQ-007 TASK-301
2026-09-30T12:26:12ZsecurityTASK-004 closed: hardcoded HiveMQ credentials fully removed, scan passes tree + history, rotation confirmed
secrets_scan.py --history PASS: no credential patterns in committable tree nor full git history (66 files) · tracker.ino loads MQTT creds from gitignored secrets.h; secrets.h.example tracked as template · TEST-003 marked PASS with scan evidence · TASK-004 DONE/VERIFIED; user confirmed broker credential rotated out-of-band
REQ-021 REQ-022 TASK-004
2026-09-30T20:35:00ZregressionTASK-302 re-verify: BOTH location sources regressed on 2026-09-30 - AT+CLBS returns ret_code 10 (proven server-side) and GNSS dropped to 0 satellites (RF/antenna). BUG-009 and BUG-010 opened, RE-VERIFY set 2026-10-01.
LBS symptom: the UNMODIFIED committed lbsmatrix control, byte-for-byte the 09-29 flow, returned +CLBS: 10 today (was +CLBS: 0,18.481703,73.897415,550 on 09-29); same device, SIM, location · LBS isolation: lbs_debug with a TRUE cold modem power-cycle (PWRKEY hold off, then on) replayed the identical 09-29 sequence -> still +CLBS: 10 for mode1/9/2; rules out state-wedge from prior CFUN/context churn · LBS isolation: modem data plane proven healthy at failure time (NETOPEN OK, +IPADDR valid, CIPOPEN to mqtt.easyiothings.com:80 OK, CIPOPEN to lbs-simcom.com:3002 OK); only the CLBS application transaction closes · LBS root cause: official A76XX LBS ret_code table - code 10 = close network error at the LBS service, not the data plane; A76XX has no AT+CLBSCFG server override, so the failing boundary is the carrier/SIMCom LBS service for this unit's IMEI (redacted here per Rule 27; read it from the local test logs) · GNSS symptom: after a confirmed fix at 17:46 local (LOCK t+130s, 4 sats, hdop 7.3), the unmodified gnss_hold control returned satsUsed=0 / fix=no for the full 500 s across four runs from 18:37 local, while NMEA kept streaming (249->6009 sentences) and the engine stayed powered · GNSS root cause: powered engine + streaming NMEA + zero C/N0 is an RF/sky-view signature, not firmware; suspect the patch antenna was dislodged when the unit was moved during the session · docs: TEST-302 PASS -> FAIL and TEST-301 evidence extended with the 0-sat regression; REQ-007 and REQ-008 keep their VERIFIED status with dated REGRESSION NOTEs; BUG-009 (LBS) and BUG-010 (GNSS) OPEN with a combined 2026-10-01 re-verify plan · firmware landed (uncommitted until now): position_service.ino (ADR-006 LBS-first, GNSS upgrade), lbs_debug.ino (LBS isolation harness with cold power-cycle), tracker.ino (TASK-405 MQTT frame-builder fix, TASK-301 CGNSSPORTSWITCH 0,1)
REQ-007 REQ-008 TASK-301 TASK-302 TASK-303
2026-09-30T15:06:00ZprocessAdded AGENTS.md — the operating manual that captures the full project process (SDLC state machine, Definition of Done, evidence discipline, hardware truth, gate commands, secrets handling, lessons-learned discipline) so the process is followed consistently and past mistakes are not repeated. Fixed a stale FQBN in LESSONS_LEARNED.md that it exposed.
docs: new AGENTS.md at repo root — single entry point for the process, the anti-false-confidence evidence rules, the numbered Rules (13/20/23-24/27/37/42/44) the code and JSON actually cite, the hardware regression baseline and build/flash loop, and the consolidated do-not-repeat traps · docs: LESSONS_LEARNED.md Board section FQBN corrected from the stale PartitionScheme=default_8MB to app3M_fat9M_16MB (a default_8MB build compiles but will not boot; build-baseline.sh / flash-sketch.sh were already correct and are the FQBN source of truth), plus the app-partition-only (0x10000) flash note · process: no task/bug IDs claimed — this is process documentation, not product scope; make check (validate + secrets + render + baseline) passes before commit
—