From 76c4b010bf0961bef9d024d0655395bf289f3789 Mon Sep 17 00:00:00 2001 From: John Poole Date: Wed, 1 Apr 2026 15:41:24 -0700 Subject: [PATCH 1/6] No difference between AMY vs. BOB, going to revise and dig deeper into SD card states --- notes_Feb_18_2026.txt | 95 ++++ tools/constantTFCard/hw_debug/1st_run.log | 190 ++++++++ .../hw_debug/amy_vs_bob_identifications.txt | 44 ++ tools/constantTFCard/hw_debug/platformio.ini | 31 ++ tools/constantTFCard/hw_debug/src/main.cpp | 443 ++++++++++++++++++ tools/constantTFCard/platformio.ini | 31 ++ tools/constantTFCard/src/main.cpp | 257 ++++++++++ 7 files changed, 1091 insertions(+) create mode 100644 notes_Feb_18_2026.txt create mode 100644 tools/constantTFCard/hw_debug/1st_run.log create mode 100644 tools/constantTFCard/hw_debug/amy_vs_bob_identifications.txt create mode 100644 tools/constantTFCard/hw_debug/platformio.ini create mode 100644 tools/constantTFCard/hw_debug/src/main.cpp create mode 100644 tools/constantTFCard/platformio.ini create mode 100644 tools/constantTFCard/src/main.cpp diff --git a/notes_Feb_18_2026.txt b/notes_Feb_18_2026.txt new file mode 100644 index 0000000..dc1119c --- /dev/null +++ b/notes_Feb_18_2026.txt @@ -0,0 +1,95 @@ + + + + +A: + pio run -e node_a -t upload --upload-port /dev/ttyACM0 +then: + date; screen /dev/ttyACM0 115200 + +B: + pio run -e node_b -t upload --upload-port /dev/ttyACM1 +then: + date; screen /dev/ttyACM1 115200 + + + +tbeam: +Console 1: + cd /usr/local/src/sx1302_hal/packet_forwarder + sudo ./lora_pkt_fwd -c global_conf.reticulum_915000000_sf8_bw125.json + +Console 2: + cd /usr/local/src/sx1302_hal/util_net_downlink + ./net_downlink -P 1730 -l uplinks_$(date +%Y%m%d_%H%M%S).csv + + +Example capture: + INFO: Received pkt from mote: 65732042 (fcnt=29540) + + JSON up: {"rxpk":[{"jver":1,"tmst":413765588,"chan":8,"rfch":0,"freq":915.000000,"mid":16,"stat":1,"modu":"LORA","datr":"SF7BW125","codr":"4/5","rssis":-16,"lsnr":10.8,"foff":-45,"rssi":-15,"size":29,"data":"IEIgc2VuZHMgZ3JlZXRpbmdzLiBpdGVyPTE1MDQ="}]} + INFO: [up] PUSH_ACK received in 31 ms + + INFO: Received pkt from mote: 65732041 (fcnt=29540) + + JSON up: {"rxpk":[{"jver":1,"tmst":414742074,"chan":8,"rfch":0,"freq":915.000000,"mid":16,"stat":1,"modu":"LORA","datr":"SF7BW125","codr":"4/5","rssis":-16,"lsnr":10.2,"foff":-297,"rssi":-15,"size":29,"data":"IEEgc2VuZHMgZ3JlZXRpbmdzLiBpdGVyPTE0OTg="}]} + INFO: [up] PUSH_ACK received in 31 ms + + INFO: Received pkt from mote: 65732042 (fcnt=29540) + + JSON up: {"rxpk":[{"jver":1,"tmst":415766626,"chan":8,"rfch":0,"freq":915.000000,"mid":16,"stat":1,"modu":"LORA","datr":"SF7BW125","codr":"4/5","rssis":-16,"lsnr":11.0,"foff":-53,"rssi":-15,"size":29,"data":"IEIgc2VuZHMgZ3JlZXRpbmdzLiBpdGVyPTE1MDU="}]} + INFO: [up] PUSH_ACK received in 31 ms + + INFO: Received pkt from mote: 65732041 (fcnt=29540) + + JSON up: {"rxpk":[{"jver":1,"tmst":416744088,"chan":8,"rfch":0,"freq":915.000000,"mid":16,"stat":1,"modu":"LORA","datr":"SF7BW125","codr":"4/5","rssis":-16,"lsnr":10.2,"foff":-289,"rssi":-15,"size":29,"data":"IEEgc2VuZHMgZ3JlZXRpbmdzLiBpdGVyPTE0OTk="}]} + INFO: [up] PUSH_ACK received in 31 ms + INFO: [down] PULL_ACK received in 31 ms + +tbeam /usr/local/src/sx1302_hal/util_net_downlink # tail uplinks_20260218_155659.csv +445782393,,8,0,915.000000,16,1,LORA,7,125,4/5,-15.0,-16.0,10.8,29,20422073656e6473206772656574696e67732e20697465723d31353230 +446764659,,8,0,915.000000,16,1,LORA,7,125,4/5,-15.0,-16.0,10.5,29,20412073656e6473206772656574696e67732e20697465723d31353134 + + +==== With all 5 units ====== +Amy: +set the tab: + echo -ne "\033]30;Amy\007" + pio run -e amy -t upload --upload-port /dev/ttyACM0 + + date; screen /dev/ttyACM0 115200 + +Bob: + echo -ne "\033]30;Bob\007" + pio run -e bob -t upload --upload-port /dev/ttyACM1 + + date; screen /dev/ttyACM1 115200 + +Cy: + source ~/rnsenv/bin/activate + echo -ne "\033]30;Cy\007" + pio run -e cy -t upload --upload-port /dev/ttyACM2 + + date; screen /dev/ttyACM2 115200 + +Dan: + source ~/rnsenv/bin/activate + echo -ne "\033]30;Dan\007" + pio run -e cy -t upload --upload-port /dev/ttyACM3 + + date; screen /dev/ttyACM3 115200 + +Ed: + source ~/rnsenv/bin/activate + echo -ne "\033]30;Ed\007" + pio run -e cy -t upload --upload-port /dev/ttyACM4 + date; screen /dev/ttyACM4 115200 + + +Logging: + + cd /usr/local/src/sx1302_hal/util_net_downlink + ./net_downlink -P 1730 -l uplinks_$(date +%Y%m%d_%H%M%S).csv + + ls -lath /usr/local/src/sx1302_hal/util_net_downlink |head -n 3 + \ No newline at end of file diff --git a/tools/constantTFCard/hw_debug/1st_run.log b/tools/constantTFCard/hw_debug/1st_run.log new file mode 100644 index 0000000..6e01b1f --- /dev/null +++ b/tools/constantTFCard/hw_debug/1st_run.log @@ -0,0 +1,190 @@ +John's interaction with the events below: + + 14149 Pressed down + [ 17543] let up + [ 30325] Pressed down + 35143 let up + 39993 killed. + +Compiled and uploaded: Wed Apr 1 15:07:20 PDT 2026 + +[ 12793][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 12799][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +sample=3 state=BEGIN_FAIL rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL +[ 13265][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 13271][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 13277][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 13701][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 13707][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 13713][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 14137][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 14143][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 14149][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +sample=4 state=MOUNT_OK rail=ON vbus=5.19 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@10000000 type=SDHC size=14910MB root=OK +sample=5 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=6 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=7 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=8 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=9 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=10 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=11 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=12 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=13 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=14 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=15 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=16 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +[ 17543][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0xsample=17 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +37 +[ 17549][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 17555][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 17979][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 17985][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 17991][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 18415][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 18421][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 18427][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 18851][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 18857][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 18863][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 19287][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 19293][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 19299][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 19723][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 19729][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 19735][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 20159][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 20165][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 20171][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 20595][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 20601][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 20607][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +sample=18 state=BEGIN_FAIL rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL +[ 21073][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 21079][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 21085][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 21509][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 21515][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 21521][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 21945][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 21951][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 21957][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 22381][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 22387][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 22393][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 22817][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 22823][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 22829][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 23253][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 23259][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 23265][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 23689][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 23695][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 23701][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 24125][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 24131][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 24137][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 24603][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0xsample=19 state=BEGIN_FAIL rail=ON vbus=5.19 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL +37 +[ 24609][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 24615][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 25039][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 25045][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 25051][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 25475][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 25481][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 25487][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 25911][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 25917][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 25923][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 26347][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 26353][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 26359][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 26783][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 26789][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 26795][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 27219][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 27225][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 27231][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 27655][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 27661][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 27667][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +sample=20 state=BEGIN_FAIL rail=ON vbus=5.19 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL +[ 28133][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 28139][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 28145][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 28569][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 28575][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 28581][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 29005][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 29011][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 29017][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 29441][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 29447][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 29453][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 29877][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 29883][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 29889][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 30313][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 30319][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 30325][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +sample=21 state=MOUNT_OK rail=ON vbus=5.19 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=FSPI@4000000 type=SDHC size=14910MB root=OK +sample=22 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=23 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=24 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=25 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=26 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=27 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=28 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=29 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=30 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=31 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=32 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=33 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=34 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=35 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=36 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=37 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=38 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=39 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +sample=40 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +[ 35143][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0xsample=41 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK +37 +[ 35149][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 35155][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 35579][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 35585][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 35591][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 36015][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 36021][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 36027][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 36451][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 36457][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 36463][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 36887][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 36893][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 36899][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 37323][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 37329][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 37335][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 37759][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 37765][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 37771][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 38195][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 38201][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 38207][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +sample=42 state=BEGIN_FAIL rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL +[ 38673][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 38679][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 38685][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 39109][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 39115][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 39121][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 39545][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 39551][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 39557][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +[ 39981][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37 +[ 39987][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29 +[ 39993][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work +(rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam $ date +Wed Apr 1 15:08:35 PDT 2026 +(rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam $ \ No newline at end of file diff --git a/tools/constantTFCard/hw_debug/amy_vs_bob_identifications.txt b/tools/constantTFCard/hw_debug/amy_vs_bob_identifications.txt new file mode 100644 index 0000000..8fd2999 --- /dev/null +++ b/tools/constantTFCard/hw_debug/amy_vs_bob_identifications.txt @@ -0,0 +1,44 @@ +April 1, 2026 ~ 15:34 + +From AMY (broken SD Card): + + [ 15549][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (13) There is no valid FAT volume + + STARTUP REPLAY + uptime_ms=15438 + BOARD IDENTITY + chip_model=ESP32-S3 + chip_revision=0 + chip_cores=2 + sdk_version=v4.4.7-dirty + cpu_mhz=240 + flash_size=8388608 + flash_speed=80000000 + flash_mode=QIO + efuse_mac=68BF5B43CA48 + chip_id=5BBF68 + reset_reason=UNKNOWN + arduino_board=Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM) + sample=34 state=BEGIN_FAIL rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL + [ 15618][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (13) There is no valid FAT volume + +from BOB: + + sample=30 state=MOUNT_OK rail=ON vbus=5.03 batt=3.86 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK + + STARTUP REPLAY + uptime_ms=13167 + BOARD IDENTITY + chip_model=ESP32-S3 + chip_revision=0 + chip_cores=2 + sdk_version=v4.4.7-dirty + cpu_mhz=240 + flash_size=8388608 + flash_speed=80000000 + flash_mode=QIO + efuse_mac=DC935A43CA48 + chip_id=5A93DC + reset_reason=UNKNOWN + arduino_board=Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM) + sample=31 state=MOUNT_OK rail=ON vbus=5.03 batt=3.86 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK diff --git a/tools/constantTFCard/hw_debug/platformio.ini b/tools/constantTFCard/hw_debug/platformio.ini new file mode 100644 index 0000000..de55c86 --- /dev/null +++ b/tools/constantTFCard/hw_debug/platformio.ini @@ -0,0 +1,31 @@ +; 20260401 Codex +; constantTFCard hardware debug monitor + +[platformio] +default_envs = amy + +[env] +platform = espressif32 +framework = arduino +board = esp32-s3-devkitc-1 +monitor_speed = 115200 +lib_deps = + lewisxhe/XPowersLib@0.3.3 + Wire + olikraus/U8g2@^2.36.4 + +build_flags = + -I ../../../shared/boards + -I ../../../external/microReticulum_Firmware + -D BOARD_MODEL=BOARD_TBEAM_S_V1 + -D OLED_SDA=17 + -D OLED_SCL=18 + -D OLED_ADDR=0x3C + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + +[env:amy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"AMY\" diff --git a/tools/constantTFCard/hw_debug/src/main.cpp b/tools/constantTFCard/hw_debug/src/main.cpp new file mode 100644 index 0000000..23a7e0e --- /dev/null +++ b/tools/constantTFCard/hw_debug/src/main.cpp @@ -0,0 +1,443 @@ +// 20260401 Codex + +#include +#include +#include +#include +#include +#include +#include + +#include "tbeam_supreme_adapter.h" + +#ifndef NODE_LABEL +#define NODE_LABEL "NODE" +#endif + +#ifndef OLED_SDA +#define OLED_SDA 17 +#endif + +#ifndef OLED_SCL +#define OLED_SCL 18 +#endif + +static const uint32_t kSerialDelayMs = 1500; +static const uint32_t kPollIntervalMs = 200; +static const uint32_t kStartupQuietMs = 5000; +static const uint32_t kStartupReplayWindowMs = 20000; +static const uint32_t kStartupReplayPeriodMs = 2000; + +static const uint32_t kFreqs[] = { + 400000, + 1000000, + 4000000, + 10000000 +}; + +struct PinSnapshot { + int cs = -1; + int sck = -1; + int miso = -1; + int mosi = -1; +}; + +struct ProbeSummary { + uint8_t ffCount = 0; + uint8_t zeroCount = 0; + uint8_t otherCount = 0; + uint8_t firstBytes[8] = {0}; +}; + +enum class DebugState : uint8_t { + PMU_FAIL = 0, + RAIL_OFF, + BUS_FLOAT, + BUS_LOW, + BUS_CHATTER, + SD_BEGIN_FAIL, + CARD_NONE, + FS_FAIL, + MOUNT_OK +}; + +struct DebugSnapshot { + DebugState state = DebugState::PMU_FAIL; + bool pmuOk = false; + bool railOn = false; + float vbusV = -1.0f; + float battV = -1.0f; + PinSnapshot pins{}; + ProbeSummary probeH{}; + ProbeSummary probeF{}; + const char* mountBus = "none"; + uint32_t mountHz = 0; + uint8_t cardType = CARD_NONE; + uint64_t cardSizeMB = 0; + bool rootOk = false; +}; + +static XPowersLibInterface* g_pmu = nullptr; +static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); +static SPIClass g_spiH(HSPI); +static SPIClass g_spiF(FSPI); +static uint32_t g_sampleCount = 0; +static uint32_t g_bootMs = 0; +static uint32_t g_lastStartupReplayMs = 0; + +static const char* resetReasonToString(esp_reset_reason_t reason) { + switch (reason) { + case ESP_RST_UNKNOWN: return "UNKNOWN"; + case ESP_RST_POWERON: return "POWERON"; + case ESP_RST_EXT: return "EXT"; + case ESP_RST_SW: return "SW"; + case ESP_RST_PANIC: return "PANIC"; + case ESP_RST_INT_WDT: return "INT_WDT"; + case ESP_RST_TASK_WDT: return "TASK_WDT"; + case ESP_RST_WDT: return "WDT"; + case ESP_RST_DEEPSLEEP: return "DEEPSLEEP"; + case ESP_RST_BROWNOUT: return "BROWNOUT"; + case ESP_RST_SDIO: return "SDIO"; + default: return "OTHER"; + } +} + +static const char* flashModeToString(FlashMode_t mode) { + switch (mode) { + case FM_QIO: return "QIO"; + case FM_QOUT: return "QOUT"; + case FM_DIO: return "DIO"; + case FM_DOUT: return "DOUT"; + case FM_FAST_READ: return "FAST"; + case FM_SLOW_READ: return "SLOW"; + default: return "UNKNOWN"; + } +} + +static void printBoardIdentity() { + uint64_t mac = ESP.getEfuseMac(); + uint32_t chipId = 0; + for (int i = 0; i < 17; i += 8) { + chipId |= ((mac >> (40 - i)) & 0xFF) << i; + } + + Serial.println("BOARD IDENTITY"); + Serial.printf("chip_model=%s\r\n", ESP.getChipModel()); + Serial.printf("chip_revision=%u\r\n", (unsigned)ESP.getChipRevision()); + Serial.printf("chip_cores=%u\r\n", (unsigned)ESP.getChipCores()); + Serial.printf("sdk_version=%s\r\n", ESP.getSdkVersion()); + Serial.printf("cpu_mhz=%u\r\n", (unsigned)ESP.getCpuFreqMHz()); + Serial.printf("flash_size=%u\r\n", (unsigned)ESP.getFlashChipSize()); + Serial.printf("flash_speed=%u\r\n", (unsigned)ESP.getFlashChipSpeed()); + Serial.printf("flash_mode=%s\r\n", flashModeToString(ESP.getFlashChipMode())); + Serial.printf("efuse_mac=%012llX\r\n", mac); + Serial.printf("chip_id=%06lX\r\n", (unsigned long)chipId); + Serial.printf("reset_reason=%s\r\n", resetReasonToString(esp_reset_reason())); + Serial.printf("arduino_board=%s\r\n", ARDUINO_BOARD); +} + +static void printStartupBanner() { + Serial.println(); + Serial.println("STARTUP REPLAY"); + Serial.printf("uptime_ms=%lu\r\n", (unsigned long)(millis() - g_bootMs)); + printBoardIdentity(); +} + +static void forceSpiDeselected() { + pinMode(tbeam_supreme::sdCs(), OUTPUT); + digitalWrite(tbeam_supreme::sdCs(), HIGH); + pinMode(tbeam_supreme::imuCs(), OUTPUT); + digitalWrite(tbeam_supreme::imuCs(), HIGH); +} + +static PinSnapshot readPins() { + PinSnapshot s; + s.cs = gpio_get_level((gpio_num_t)tbeam_supreme::sdCs()); + s.sck = gpio_get_level((gpio_num_t)tbeam_supreme::sdSck()); + s.miso = gpio_get_level((gpio_num_t)tbeam_supreme::sdMiso()); + s.mosi = gpio_get_level((gpio_num_t)tbeam_supreme::sdMosi()); + return s; +} + +static void readPmu(DebugSnapshot& snap) { + snap.pmuOk = (g_pmu != nullptr); + if (!g_pmu) { + return; + } + + snap.railOn = g_pmu->isPowerChannelEnable(XPOWERS_BLDO1); + snap.vbusV = g_pmu->getVbusVoltage() / 1000.0f; + snap.battV = g_pmu->getBattVoltage() / 1000.0f; +} + +static ProbeSummary runIdleProbe(SPIClass& bus) { + ProbeSummary out; + + SD.end(); + bus.end(); + delay(2); + forceSpiDeselected(); + + bus.begin( + tbeam_supreme::sdSck(), + tbeam_supreme::sdMiso(), + tbeam_supreme::sdMosi(), + tbeam_supreme::sdCs() + ); + + digitalWrite(tbeam_supreme::sdCs(), HIGH); + delay(1); + for (int i = 0; i < 8; ++i) { + uint8_t b = bus.transfer(0xFF); + out.firstBytes[i] = b; + if (b == 0xFF) out.ffCount++; + else if (b == 0x00) out.zeroCount++; + else out.otherCount++; + } + + return out; +} + +static const char* cardTypeToString(uint8_t type) { + switch (type) { + case CARD_MMC: return "MMC"; + case CARD_SD: return "SDSC"; + case CARD_SDHC: return "SDHC"; + default: return "NONE"; + } +} + +static bool tryMount(SPIClass& bus, + const char* busName, + uint32_t hz, + DebugSnapshot& snap) { + SD.end(); + bus.end(); + delay(2); + forceSpiDeselected(); + + bus.begin( + tbeam_supreme::sdSck(), + tbeam_supreme::sdMiso(), + tbeam_supreme::sdMosi(), + tbeam_supreme::sdCs() + ); + + digitalWrite(tbeam_supreme::sdCs(), HIGH); + delay(1); + for (int i = 0; i < 10; ++i) { + bus.transfer(0xFF); + } + + if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) { + snap.state = DebugState::SD_BEGIN_FAIL; + return false; + } + + snap.cardType = SD.cardType(); + snap.mountBus = busName; + snap.mountHz = hz; + if (snap.cardType == CARD_NONE) { + SD.end(); + snap.state = DebugState::CARD_NONE; + return false; + } + + snap.cardSizeMB = SD.cardSize() / (1024ULL * 1024ULL); + + File root = SD.open("/", FILE_READ); + snap.rootOk = (bool)root; + if (root) { + root.close(); + } + SD.end(); + + snap.state = snap.rootOk ? DebugState::MOUNT_OK : DebugState::FS_FAIL; + return snap.rootOk; +} + +static DebugState classifyProbe(const ProbeSummary& probe) { + if (probe.ffCount == 8) return DebugState::BUS_FLOAT; + if (probe.zeroCount == 8) return DebugState::BUS_LOW; + return DebugState::BUS_CHATTER; +} + +static DebugSnapshot captureSnapshot() { + DebugSnapshot snap; + readPmu(snap); + snap.pins = readPins(); + + if (!snap.pmuOk) { + snap.state = DebugState::PMU_FAIL; + return snap; + } + + if (!snap.railOn) { + snap.state = DebugState::RAIL_OFF; + return snap; + } + + snap.probeH = runIdleProbe(g_spiH); + snap.probeF = runIdleProbe(g_spiF); + snap.state = classifyProbe(snap.probeH); + + for (size_t i = 0; i < (sizeof(kFreqs) / sizeof(kFreqs[0])); ++i) { + if (tryMount(g_spiH, "HSPI", kFreqs[i], snap)) { + return snap; + } + } + + for (size_t i = 0; i < (sizeof(kFreqs) / sizeof(kFreqs[0])); ++i) { + if (tryMount(g_spiF, "FSPI", kFreqs[i], snap)) { + return snap; + } + } + + return snap; +} + +static const char* stateToString(DebugState state) { + switch (state) { + case DebugState::PMU_FAIL: return "PMU_FAIL"; + case DebugState::RAIL_OFF: return "RAIL_OFF"; + case DebugState::BUS_FLOAT: return "NO_RESP"; + case DebugState::BUS_LOW: return "BUS_LOW"; + case DebugState::BUS_CHATTER: return "BUS_CHAT"; + case DebugState::SD_BEGIN_FAIL: return "BEGIN_FAIL"; + case DebugState::CARD_NONE: return "CARD_NONE"; + case DebugState::FS_FAIL: return "FS_FAIL"; + case DebugState::MOUNT_OK: return "MOUNT_OK"; + default: return "UNKNOWN"; + } +} + +static void printSnapshot(const DebugSnapshot& snap) { + Serial.printf( + "sample=%lu state=%s rail=%s vbus=%.2f batt=%.2f pins=%d/%d/%d/%d " + "probeH(ff=%u z=%u o=%u %02X %02X %02X %02X) " + "probeF(ff=%u z=%u o=%u %02X %02X %02X %02X) " + "mount=%s@%lu type=%s size=%lluMB root=%s\r\n", + (unsigned long)g_sampleCount, + stateToString(snap.state), + snap.railOn ? "ON" : "OFF", + snap.vbusV, + snap.battV, + snap.pins.cs, + snap.pins.sck, + snap.pins.miso, + snap.pins.mosi, + (unsigned)snap.probeH.ffCount, + (unsigned)snap.probeH.zeroCount, + (unsigned)snap.probeH.otherCount, + snap.probeH.firstBytes[0], + snap.probeH.firstBytes[1], + snap.probeH.firstBytes[2], + snap.probeH.firstBytes[3], + (unsigned)snap.probeF.ffCount, + (unsigned)snap.probeF.zeroCount, + (unsigned)snap.probeF.otherCount, + snap.probeF.firstBytes[0], + snap.probeF.firstBytes[1], + snap.probeF.firstBytes[2], + snap.probeF.firstBytes[3], + snap.mountBus, + (unsigned long)snap.mountHz, + cardTypeToString(snap.cardType), + snap.cardSizeMB, + snap.rootOk ? "OK" : "FAIL" + ); +} + +static void showSnapshot(const DebugSnapshot& snap) { + char line1[24]; + char line2[24]; + char line3[24]; + char line4[24]; + char line5[24]; + + snprintf(line1, sizeof(line1), "%s TF HWDBG", NODE_LABEL); + snprintf(line2, sizeof(line2), "STATE %s", stateToString(snap.state)); + snprintf(line3, sizeof(line3), "H %u/%u/%u F %u/%u/%u", + (unsigned)snap.probeH.ffCount, + (unsigned)snap.probeH.zeroCount, + (unsigned)snap.probeH.otherCount, + (unsigned)snap.probeF.ffCount, + (unsigned)snap.probeF.zeroCount, + (unsigned)snap.probeF.otherCount); + snprintf(line4, sizeof(line4), "%s %luk %s", + snap.mountBus, + (unsigned long)(snap.mountHz / 1000UL), + cardTypeToString(snap.cardType)); + snprintf(line5, sizeof(line5), "P %d%d%d%d R%s %lu", + snap.pins.cs, + snap.pins.sck, + snap.pins.miso, + snap.pins.mosi, + snap.railOn ? "1" : "0", + (unsigned long)g_sampleCount); + + g_oled.clearBuffer(); + g_oled.setFont(u8g2_font_5x8_tf); + g_oled.drawUTF8(0, 12, line1); + g_oled.drawUTF8(0, 24, line2); + g_oled.drawUTF8(0, 36, line3); + g_oled.drawUTF8(0, 48, line4); + g_oled.drawUTF8(0, 60, line5); + g_oled.sendBuffer(); +} + +void setup() { + Serial.begin(115200); + g_bootMs = millis(); + delay(kSerialDelayMs); + + Wire.begin(OLED_SDA, OLED_SCL); + g_oled.begin(); + g_oled.clearDisplay(); + + tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial); + forceSpiDeselected(); + + Serial.println(); + Serial.println("constantTFCard hardware debug"); + Serial.printf("Node: %s\r\n", NODE_LABEL); + Serial.printf("Poll interval: %lu ms\r\n", (unsigned long)kPollIntervalMs); + Serial.println("States: PMU_FAIL RAIL_OFF NO_RESP BUS_LOW BUS_CHAT BEGIN_FAIL CARD_NONE FS_FAIL MOUNT_OK"); + Serial.printf("Startup quiet delay: %lu ms\r\n", (unsigned long)kStartupQuietMs); + Serial.printf("Startup replay window: %lu ms\r\n", (unsigned long)kStartupReplayWindowMs); + + g_oled.clearBuffer(); + g_oled.setFont(u8g2_font_5x8_tf); + g_oled.drawUTF8(0, 12, "TF HWDBG"); + g_oled.drawUTF8(0, 24, "startup hold"); + g_oled.drawUTF8(0, 36, "attach monitor"); + g_oled.drawUTF8(0, 48, "waiting..."); + g_oled.sendBuffer(); + + delay(kStartupQuietMs); + printStartupBanner(); +} + +void loop() { + static uint32_t lastPollMs = 0; + + uint32_t now = millis(); + if (now - lastPollMs < kPollIntervalMs) { + delay(5); + return; + } + + lastPollMs = now; + g_sampleCount++; + + DebugSnapshot snap = captureSnapshot(); + + if (now - g_bootMs <= kStartupReplayWindowMs && + now - g_lastStartupReplayMs >= kStartupReplayPeriodMs) { + g_lastStartupReplayMs = now; + printStartupBanner(); + } + + printSnapshot(snap); + showSnapshot(snap); +} diff --git a/tools/constantTFCard/platformio.ini b/tools/constantTFCard/platformio.ini new file mode 100644 index 0000000..705281e --- /dev/null +++ b/tools/constantTFCard/platformio.ini @@ -0,0 +1,31 @@ +; 20260401 Codex +; constantTFCard + +[platformio] +default_envs = amy + +[env] +platform = espressif32 +framework = arduino +board = esp32-s3-devkitc-1 +monitor_speed = 115200 +lib_deps = + lewisxhe/XPowersLib@0.3.3 + Wire + olikraus/U8g2@^2.36.4 + +build_flags = + -I ../../shared/boards + -I ../../external/microReticulum_Firmware + -D BOARD_MODEL=BOARD_TBEAM_S_V1 + -D OLED_SDA=17 + -D OLED_SCL=18 + -D OLED_ADDR=0x3C + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + +[env:amy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"AMY\" diff --git a/tools/constantTFCard/src/main.cpp b/tools/constantTFCard/src/main.cpp new file mode 100644 index 0000000..3b172d9 --- /dev/null +++ b/tools/constantTFCard/src/main.cpp @@ -0,0 +1,257 @@ +// 20260401 Codex + +#include +#include +#include +#include +#include +#include + +#include "tbeam_supreme_adapter.h" + +#ifndef NODE_LABEL +#define NODE_LABEL "NODE" +#endif + +#ifndef OLED_SDA +#define OLED_SDA 17 +#endif + +#ifndef OLED_SCL +#define OLED_SCL 18 +#endif + +static const uint32_t kSerialDelayMs = 1500; +static const uint32_t kPollIntervalMs = 200; +static const uint32_t kSdFreqHz = 400000; +static const uint32_t kBootSettleMs = 2000; +static const uint32_t kSdRailOffMs = 300; +static const uint32_t kSdRailOnSettleMs = 1200; +static const uint8_t kOutVotesBeforeRailCycle = 10; +static const uint32_t kMinRailCycleGapMs = 5000; + +static const uint32_t kDelayedRetryOffsetsMs[] = { + 1000, + 3000, + 7000, + 15000 +}; + +static XPowersLibInterface* g_pmu = nullptr; +static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); +static SPIClass g_sdSpi(HSPI); +static uint32_t g_bootMs = 0; +static size_t g_nextDelayedRetry = 0; +static uint32_t g_lastRailCycleMs = 0; +static uint8_t g_consecutiveOut = 0; + +static void forceSpiDeselected() { + pinMode(tbeam_supreme::sdCs(), OUTPUT); + digitalWrite(tbeam_supreme::sdCs(), HIGH); + pinMode(tbeam_supreme::imuCs(), OUTPUT); + digitalWrite(tbeam_supreme::imuCs(), HIGH); +} + +static void oledShowStatus(const char* status, uint32_t sampleCount) { + char line1[24]; + char line2[24]; + char line3[24]; + + snprintf(line1, sizeof(line1), "%s TF CARD", NODE_LABEL); + snprintf(line2, sizeof(line2), "%s", status); + snprintf(line3, sizeof(line3), "sample %lu", (unsigned long)sampleCount); + + g_oled.clearBuffer(); + g_oled.setFont(u8g2_font_6x12_tf); + g_oled.drawUTF8(0, 14, line1); + g_oled.setFont(u8g2_font_logisoso20_tf); + g_oled.drawUTF8(0, 42, status); + g_oled.setFont(u8g2_font_6x12_tf); + g_oled.drawUTF8(0, 62, line3); + g_oled.sendBuffer(); +} + +static void oledShowBootPhase(const char* line2, const char* line3) { + char line1[24]; + + snprintf(line1, sizeof(line1), "%s TF CARD", NODE_LABEL); + + g_oled.clearBuffer(); + g_oled.setFont(u8g2_font_6x12_tf); + g_oled.drawUTF8(0, 14, line1); + if (line2) g_oled.drawUTF8(0, 32, line2); + if (line3) g_oled.drawUTF8(0, 50, line3); + g_oled.sendBuffer(); +} + +static bool cardReadable() { + SD.end(); + g_sdSpi.end(); + delay(2); + + forceSpiDeselected(); + g_sdSpi.begin( + tbeam_supreme::sdSck(), + tbeam_supreme::sdMiso(), + tbeam_supreme::sdMosi(), + tbeam_supreme::sdCs() + ); + + digitalWrite(tbeam_supreme::sdCs(), HIGH); + delay(1); + for (int i = 0; i < 10; ++i) { + g_sdSpi.transfer(0xFF); + } + + if (!SD.begin(tbeam_supreme::sdCs(), g_sdSpi, kSdFreqHz)) { + return false; + } + + if (SD.cardType() == CARD_NONE) { + SD.end(); + return false; + } + + File root = SD.open("/", FILE_READ); + bool ok = (bool)root; + if (root) { + root.close(); + } + + SD.end(); + return ok; +} + +static bool cycleSdRail() { + if (!g_pmu) { + Serial.println("rail cycle skipped: no PMU"); + return false; + } + + SD.end(); + g_sdSpi.end(); + forceSpiDeselected(); + + g_pmu->disablePowerOutput(XPOWERS_BLDO1); + delay(kSdRailOffMs); + g_pmu->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); + g_pmu->enablePowerOutput(XPOWERS_BLDO1); + delay(kSdRailOnSettleMs); + forceSpiDeselected(); + + g_lastRailCycleMs = millis(); + Serial.printf("rail cycle: off=%lu on_settle=%lu\r\n", + (unsigned long)kSdRailOffMs, + (unsigned long)kSdRailOnSettleMs); + return true; +} + +static void runDelayedRetry(const char* label) { + bool readable = cardReadable(); + const char* status = readable ? "card IN" : "card OUT"; + Serial.printf("delayed retry %s -> %s\r\n", label, status); + oledShowStatus(status, 0); +} + +static void handleSerialCommands() { + while (Serial.available() > 0) { + int ch = Serial.read(); + if (ch == 'r' || ch == 'R') { + Serial.println("manual command: SD rail reset"); + oledShowBootPhase("manual SD reset", "re-probing"); + if (cycleSdRail()) { + bool readable = cardReadable(); + Serial.printf("manual SD reset -> %s\r\n", readable ? "card IN" : "card OUT"); + oledShowStatus(readable ? "card IN" : "card OUT", 0); + } + } else if (ch == 'b' || ch == 'B') { + Serial.println("manual command: full reboot"); + Serial.println("restarting now..."); + oledShowBootPhase("manual reboot", "restarting"); + delay(250); + ESP.restart(); + } else if (ch == '\r' || ch == '\n') { + continue; + } else { + Serial.printf("commands: r=sd reset, b=reboot (got '%c')\r\n", (char)ch); + } + } +} + +void setup() { + Serial.begin(115200); + delay(kSerialDelayMs); + + Wire.begin(OLED_SDA, OLED_SCL); + g_oled.begin(); + g_oled.clearDisplay(); + oledShowBootPhase("BOOT", "init"); + + tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial); + forceSpiDeselected(); + g_bootMs = millis(); + + Serial.println(); + Serial.println("constantTFCard"); + Serial.printf("Node: %s\r\n", NODE_LABEL); + Serial.printf("Polling every %lu ms\r\n", (unsigned long)kPollIntervalMs); + Serial.printf("Initial settle delay %lu ms\r\n", (unsigned long)kBootSettleMs); + Serial.println("Commands: r=SD rail reset, b=full reboot"); + + oledShowBootPhase("BOOT", "settling SD rail"); + delay(kBootSettleMs); + + runDelayedRetry("after_settle"); +} + +void loop() { + static uint32_t lastPollMs = 0; + static uint32_t sampleCount = 0; + + handleSerialCommands(); + + uint32_t now = millis(); + + if (g_nextDelayedRetry < (sizeof(kDelayedRetryOffsetsMs) / sizeof(kDelayedRetryOffsetsMs[0])) && + now - g_bootMs >= kDelayedRetryOffsetsMs[g_nextDelayedRetry]) { + char label[20]; + snprintf(label, sizeof(label), "t+%lus", (unsigned long)(kDelayedRetryOffsetsMs[g_nextDelayedRetry] / 1000)); + runDelayedRetry(label); + g_nextDelayedRetry++; + } + + if (now - lastPollMs < kPollIntervalMs) { + delay(5); + return; + } + + lastPollMs = now; + sampleCount++; + + bool readable = cardReadable(); + if (readable) { + g_consecutiveOut = 0; + } else if (g_consecutiveOut < 255) { + g_consecutiveOut++; + } + + if (!readable && + g_consecutiveOut >= kOutVotesBeforeRailCycle && + now - g_lastRailCycleMs >= kMinRailCycleGapMs) { + Serial.printf("persistent OUT: %u polls, forcing SD rail cycle\r\n", + (unsigned)g_consecutiveOut); + oledShowBootPhase("OUT -> rail reset", "re-probing SD"); + + if (cycleSdRail()) { + bool retryReadable = cardReadable(); + readable = retryReadable; + g_consecutiveOut = retryReadable ? 0 : kOutVotesBeforeRailCycle; + Serial.printf("after rail cycle -> %s\r\n", retryReadable ? "card IN" : "card OUT"); + } + } + + const char* status = readable ? "card IN" : "card OUT"; + + Serial.println(status); + oledShowStatus(status, sampleCount); +} From b5ff96d6a91b449e3948fb22e4b0fb41d4b90f05 Mon Sep 17 00:00:00 2001 From: John Poole Date: Fri, 3 Apr 2026 14:35:33 -0700 Subject: [PATCH 2/6] After modifying all pio settings to allow for Ublox GPS alternative, defaults to L76k; TODO code to implement Ublox --- docs/pio.md | 134 +++++ exercises/09_GPS_Time/platformio.ini | 1 + exercises/09_GPS_Time/src/main.cpp | 24 +- exercises/10_Simple_GPS/platformio.ini | 1 + exercises/11_Set_RTC2GPS/platformio.ini | 1 + exercises/12_FiveTalk/READEME.md | 11 + exercises/12_FiveTalk/platformio.ini | 18 + exercises/12_FiveTalk/src/main.cpp | 4 +- tools/constantTFCard/raw_probe/.gitignore | 2 + tools/constantTFCard/raw_probe/platformio.ini | 55 +++ tools/constantTFCard/raw_probe/src/main.cpp | 463 ++++++++++++++++++ 11 files changed, 700 insertions(+), 14 deletions(-) create mode 100644 docs/pio.md create mode 100644 exercises/12_FiveTalk/READEME.md create mode 100644 tools/constantTFCard/raw_probe/.gitignore create mode 100644 tools/constantTFCard/raw_probe/platformio.ini create mode 100644 tools/constantTFCard/raw_probe/src/main.cpp diff --git a/docs/pio.md b/docs/pio.md new file mode 100644 index 0000000..bdcb30e --- /dev/null +++ b/docs/pio.md @@ -0,0 +1,134 @@ +# pio +pio stands for "PlatformIO". + +In your context (ESP32 / T-Beam / PlatformIO workflows), **“pio” stands for:** + +> **PlatformIO** + +More precisely, `pio` is the **command-line interface (CLI)** for the PlatformIO environment. + +--- + +## What PlatformIO is (practically speaking) + +PlatformIO is an embedded development framework that wraps: + +* toolchains (GCC for ESP32, ARM, etc.) +* build system +* library manager +* uploader (esptool, etc.) +* serial monitor + +So instead of juggling: + +```bash +xtensa-esp32-elf-gcc +esptool.py +arduino-cli +``` + +…you use: + +```bash +pio run +pio run -t upload +pio device monitor +``` + +--- + +## What `pio` actually does in your workflow + +Given your T-Beam usage (as shown in the LilyGO docs, which explicitly include PlatformIO as a supported environment ), `pio` is orchestrating: + +### Build + +```bash +pio run +``` + +* compiles your firmware using the environment in `platformio.ini` + +### Upload + +```bash +pio run -t upload +``` + +* invokes the correct uploader (ESP32 → `esptool`) +* uses the selected `/dev/tty*` device + +### Monitor + +```bash +pio device monitor +``` + +* serial console at configured baud + +--- + +## Why it exists (the “systems view”) + +PlatformIO abstracts away: + +* board definitions +* pin mappings +* compiler flags +* library dependencies + +In your `platformio.ini`, for example: + +```ini +[env:node_a] +platform = espressif32 +board = esp32-s3-devkitc-1 +``` + +That single block drives: + +* toolchain selection +* linker scripts +* upload protocol +* memory layout + +--- + +## Important nuance (since you’re working low-level) + +`pio` is **not**: + +* a compiler +* a flasher +* an IDE + +It is a **build orchestration layer** (think “make + package manager + uploader + board database”). + +--- + +## Quick mental model + +If we translate to Unix analogies: + +| Concept | Analogy | +| ------------------- | -------------------------------------------- | +| PlatformIO | `portage + make + gcc + flash tool` combined | +| `pio run` | `make` | +| `pio run -t upload` | `make install` (but to hardware) | +| `platformio.ini` | hybrid of `Makefile` + config | + +--- + +## Bottom line + +When you type: + +```bash +pio run -t upload +``` + +You are saying: + +> “Use PlatformIO to build this firmware for the specified environment and flash it to the target device.” + + diff --git a/exercises/09_GPS_Time/platformio.ini b/exercises/09_GPS_Time/platformio.ini index fff0d30..b4d48d9 100644 --- a/exercises/09_GPS_Time/platformio.ini +++ b/exercises/09_GPS_Time/platformio.ini @@ -29,6 +29,7 @@ build_flags = -D GPS_1PPS_PIN=6 -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 + -D GPS_L76K [env:node_a] build_flags = diff --git a/exercises/09_GPS_Time/src/main.cpp b/exercises/09_GPS_Time/src/main.cpp index 36b0624..22d81d7 100644 --- a/exercises/09_GPS_Time/src/main.cpp +++ b/exercises/09_GPS_Time/src/main.cpp @@ -53,7 +53,7 @@ static size_t g_gpsLineLen = 0; enum class GpsModuleKind : uint8_t { UNKNOWN = 0, L76K, - QUECTEL_TODO + UBLOX }; struct RtcDateTime { @@ -169,8 +169,8 @@ static String gpsModuleToString(GpsModuleKind kind) { if (kind == GpsModuleKind::L76K) { return "L76K"; } - if (kind == GpsModuleKind::QUECTEL_TODO) { - return "Quectel/TODO"; + if (kind == GpsModuleKind::UBLOX) { + return "UBLOX"; } return "Unknown"; } @@ -198,7 +198,7 @@ static void detectModuleFromText(const char* text) { if (t.indexOf("QUECTEL") >= 0 || t.indexOf("2021-10-20") >= 0 || t.indexOf("2021/10/20") >= 0) { if (g_gps.module != GpsModuleKind::L76K) { - g_gps.module = GpsModuleKind::QUECTEL_TODO; + g_gps.module = GpsModuleKind::UBLOX; } } } @@ -386,8 +386,8 @@ static uint8_t bestSatelliteCount() { return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView; } -static bool isUnsupportedQuectelMode() { - return g_gps.module == GpsModuleKind::QUECTEL_TODO; +static bool isUnsupportedGpsMode() { + return g_gps.module == GpsModuleKind::UBLOX; } static void reportStatusToSerial() { @@ -402,7 +402,7 @@ static void reportStatusToSerial() { } static void maybeAnnounceGpsTransitions() { - if (isUnsupportedQuectelMode()) { + if (isUnsupportedGpsMode()) { return; } @@ -432,7 +432,7 @@ static void maybeAnnounceGpsTransitions() { (unsigned)g_gps.utcMinute, (unsigned)g_gps.utcSecond); snprintf(line3, sizeof(line3), "Satellites: %u", (unsigned)sats); - oledShowLines("GPS UTC acquired", line2, line3, "Source: L76K"); + oledShowLines("GPS UTC acquired", line2, line3, ("Source: " + gpsModuleToString(g_gps.module)).c_str()); logf("Transition: GPS UTC acquired: %s", line2); g_timeAcquiredAnnounced = true; } @@ -442,9 +442,9 @@ static void maybeAnnounceGpsTransitions() { } static void drawMinuteStatus() { - if (isUnsupportedQuectelMode()) { - oledShowLines("GPS module mismatch", "Quectel detected", "L76K required", "TODO: implement", "Quectel support"); - logf("GPS module mismatch: Quectel detected but this exercise currently supports only L76K (TODO)"); + if (isUnsupportedGpsMode()) { + oledShowLines("GPS module mismatch", "UBLOX detected", "L76K required", "TODO: implement", "UBLOX support"); + logf("GPS module mismatch: UBLOX detected but this exercise currently supports only L76K (TODO)"); return; } @@ -462,7 +462,7 @@ static void drawMinuteStatus() { (unsigned)g_gps.utcMinute, (unsigned)g_gps.utcSecond); snprintf(line3, sizeof(line3), "Satellites: %u", (unsigned)sats); - oledShowLines("GPS time (UTC)", line2, line3, "Source: L76K"); + oledShowLines("GPS time (UTC)", line2, line3, ("Source: " + gpsModuleToString(g_gps.module)).c_str()); logf("GPS time (UTC): %s satellites=%u", line2, (unsigned)sats); return; } diff --git a/exercises/10_Simple_GPS/platformio.ini b/exercises/10_Simple_GPS/platformio.ini index a183c3b..02b2f71 100644 --- a/exercises/10_Simple_GPS/platformio.ini +++ b/exercises/10_Simple_GPS/platformio.ini @@ -28,6 +28,7 @@ build_flags = -D GPS_1PPS_PIN=6 -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 + -D GPS_L76K [env:node_a] build_flags = diff --git a/exercises/11_Set_RTC2GPS/platformio.ini b/exercises/11_Set_RTC2GPS/platformio.ini index a91b0bc..8c7c7f3 100644 --- a/exercises/11_Set_RTC2GPS/platformio.ini +++ b/exercises/11_Set_RTC2GPS/platformio.ini @@ -28,6 +28,7 @@ build_flags = -D GPS_TX_PIN=8 -D GPS_WAKEUP_PIN=7 -D GPS_1PPS_PIN=6 + -D GPS_L76K -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 diff --git a/exercises/12_FiveTalk/READEME.md b/exercises/12_FiveTalk/READEME.md new file mode 100644 index 0000000..36960c5 --- /dev/null +++ b/exercises/12_FiveTalk/READEME.md @@ -0,0 +1,11 @@ + + +main.cpp needs to be modified to reflect the number of units. It is a zero-based array, so for 7 possible unite, the value of 6 is used in both lines below: + + #if (NODE_SLOT_INDEX < 0) || (NODE_SLOT_INDEX > 6) + #error "NODE_SLOT_INDEX must be 0..6" + #endif + + + +INSERT SCREENSHOT HERE. \ No newline at end of file diff --git a/exercises/12_FiveTalk/platformio.ini b/exercises/12_FiveTalk/platformio.ini index 8bb9199..0eff093 100644 --- a/exercises/12_FiveTalk/platformio.ini +++ b/exercises/12_FiveTalk/platformio.ini @@ -27,6 +27,7 @@ build_flags = -D GPS_TX_PIN=8 -D GPS_WAKEUP_PIN=7 -D GPS_1PPS_PIN=6 + -D GPS_L76K -D LORA_CS=10 -D LORA_MOSI=11 -D LORA_SCK=12 @@ -77,3 +78,20 @@ build_flags = -D NODE_LABEL=\"Ed\" -D NODE_SHORT=\"E\" -D NODE_SLOT_INDEX=4 + +[env:flo] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"Flo\" + -D NODE_SHORT=\"F\" + -D NODE_SLOT_INDEX=5 + +[env:guy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"Guy\" + -D NODE_SHORT=\"G\" + -D NODE_SLOT_INDEX=6 + diff --git a/exercises/12_FiveTalk/src/main.cpp b/exercises/12_FiveTalk/src/main.cpp index 4a77047..8c612cd 100644 --- a/exercises/12_FiveTalk/src/main.cpp +++ b/exercises/12_FiveTalk/src/main.cpp @@ -63,8 +63,8 @@ #define FW_BUILD_UTC "unknown" #endif -#if (NODE_SLOT_INDEX < 0) || (NODE_SLOT_INDEX > 4) -#error "NODE_SLOT_INDEX must be 0..4" +#if (NODE_SLOT_INDEX < 0) || (NODE_SLOT_INDEX > 6) +#error "NODE_SLOT_INDEX must be 0..6" #endif static const uint32_t kSerialDelayMs = 1000; diff --git a/tools/constantTFCard/raw_probe/.gitignore b/tools/constantTFCard/raw_probe/.gitignore new file mode 100644 index 0000000..cfce1ad --- /dev/null +++ b/tools/constantTFCard/raw_probe/.gitignore @@ -0,0 +1,2 @@ +*.log + diff --git a/tools/constantTFCard/raw_probe/platformio.ini b/tools/constantTFCard/raw_probe/platformio.ini new file mode 100644 index 0000000..889b197 --- /dev/null +++ b/tools/constantTFCard/raw_probe/platformio.ini @@ -0,0 +1,55 @@ +; 20260401 Codex +; constantTFCard raw SPI/SD probe + +[platformio] +default_envs = amy + +[env] +platform = espressif32 +framework = arduino +board = esp32-s3-devkitc-1 +monitor_speed = 115200 +lib_deps = + lewisxhe/XPowersLib@0.3.3 + Wire + olikraus/U8g2@^2.36.4 + +build_flags = + -I ../../../shared/boards + -I ../../../external/microReticulum_Firmware + -D BOARD_MODEL=BOARD_TBEAM_S_V1 + -D OLED_SDA=17 + -D OLED_SCL=18 + -D OLED_ADDR=0x3C + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + +[env:amy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"AMY\" + +[env:bob] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"BOB\" + +[env:cy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"CY\" + +[env:dan] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"DAN\" + +[env:ed] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"ED\" diff --git a/tools/constantTFCard/raw_probe/src/main.cpp b/tools/constantTFCard/raw_probe/src/main.cpp new file mode 100644 index 0000000..68196c4 --- /dev/null +++ b/tools/constantTFCard/raw_probe/src/main.cpp @@ -0,0 +1,463 @@ +// 20260401 Codex + +#include +#include +#include +#include +#include +#include + +#include "tbeam_supreme_adapter.h" + +#ifndef NODE_LABEL +#define NODE_LABEL "NODE" +#endif + +#ifndef OLED_SDA +#define OLED_SDA 17 +#endif + +#ifndef OLED_SCL +#define OLED_SCL 18 +#endif + +static const uint32_t kSerialDelayMs = 1500; +static const uint32_t kPollIntervalMs = 200; +static const uint32_t kSpiHz = 400000; +static const uint32_t kReadyHeartbeatMs = 2000; + +enum class RawState : uint8_t { + PMU_FAIL = 0, + RAIL_OFF, + BUS_FLOAT, + BUS_LOW, + CMD0_TIMEOUT, + CMD0_NOT_IDLE, + CMD8_TIMEOUT, + CMD8_BAD_R1, + ACMD41_TIMEOUT, + CMD58_TIMEOUT, + READY +}; + +struct PinSnapshot { + int cs = -1; + int sck = -1; + int miso = -1; + int mosi = -1; +}; + +struct ProbeSummary { + uint8_t ffCount = 0; + uint8_t zeroCount = 0; + uint8_t otherCount = 0; + uint8_t bytes[8] = {0}; +}; + +struct RawSnapshot { + RawState state = RawState::PMU_FAIL; + bool pmuOk = false; + bool railOn = false; + PinSnapshot pins{}; + ProbeSummary idle{}; + uint8_t cmd0 = 0xFF; + uint8_t cmd8r1 = 0xFF; + uint8_t cmd8data[4] = {0xFF, 0xFF, 0xFF, 0xFF}; + uint8_t acmd41 = 0xFF; + uint8_t cmd58r1 = 0xFF; + uint8_t ocr[4] = {0xFF, 0xFF, 0xFF, 0xFF}; +}; + +static XPowersLibInterface* g_pmu = nullptr; +static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); +static SPIClass g_spi(HSPI); +static uint32_t g_sampleCount = 0; +static uint32_t g_markCount = 0; +static char g_inputLine[32] = {0}; +static uint8_t g_inputLen = 0; + +static void forceSpiDeselected() { + pinMode(tbeam_supreme::sdCs(), OUTPUT); + digitalWrite(tbeam_supreme::sdCs(), HIGH); + pinMode(tbeam_supreme::imuCs(), OUTPUT); + digitalWrite(tbeam_supreme::imuCs(), HIGH); +} + +static PinSnapshot readPins() { + PinSnapshot s; + s.cs = gpio_get_level((gpio_num_t)tbeam_supreme::sdCs()); + s.sck = gpio_get_level((gpio_num_t)tbeam_supreme::sdSck()); + s.miso = gpio_get_level((gpio_num_t)tbeam_supreme::sdMiso()); + s.mosi = gpio_get_level((gpio_num_t)tbeam_supreme::sdMosi()); + return s; +} + +static void beginBus() { + SD.end(); + g_spi.end(); + delay(2); + forceSpiDeselected(); + g_spi.begin( + tbeam_supreme::sdSck(), + tbeam_supreme::sdMiso(), + tbeam_supreme::sdMosi(), + tbeam_supreme::sdCs() + ); + digitalWrite(tbeam_supreme::sdCs(), HIGH); +} + +static ProbeSummary idleProbe() { + ProbeSummary out; + beginBus(); + delay(1); + for (int i = 0; i < 8; ++i) { + uint8_t b = g_spi.transfer(0xFF); + out.bytes[i] = b; + if (b == 0xFF) out.ffCount++; + else if (b == 0x00) out.zeroCount++; + else out.otherCount++; + } + return out; +} + +static uint8_t waitR1(uint16_t tries = 16) { + for (uint16_t i = 0; i < tries; ++i) { + uint8_t r = g_spi.transfer(0xFF); + if ((r & 0x80) == 0) { + return r; + } + } + return 0xFF; +} + +static uint8_t sendCommand(uint8_t cmd, uint32_t arg, uint8_t crc) { + g_spi.transfer(0xFF); + digitalWrite(tbeam_supreme::sdCs(), LOW); + g_spi.transfer(0x40 | cmd); + g_spi.transfer((arg >> 24) & 0xFF); + g_spi.transfer((arg >> 16) & 0xFF); + g_spi.transfer((arg >> 8) & 0xFF); + g_spi.transfer(arg & 0xFF); + g_spi.transfer(crc); + uint8_t r1 = waitR1(); + return r1; +} + +static void endCommand() { + digitalWrite(tbeam_supreme::sdCs(), HIGH); + g_spi.transfer(0xFF); +} + +static const char* stateToString(RawState state) { + switch (state) { + case RawState::PMU_FAIL: return "PMU_FAIL"; + case RawState::RAIL_OFF: return "RAIL_OFF"; + case RawState::BUS_FLOAT: return "BUS_FLOAT"; + case RawState::BUS_LOW: return "BUS_LOW"; + case RawState::CMD0_TIMEOUT: return "CMD0_TO"; + case RawState::CMD0_NOT_IDLE: return "CMD0_BAD"; + case RawState::CMD8_TIMEOUT: return "CMD8_TO"; + case RawState::CMD8_BAD_R1: return "CMD8_BAD"; + case RawState::ACMD41_TIMEOUT: return "ACMD41_TO"; + case RawState::CMD58_TIMEOUT: return "CMD58_TO"; + case RawState::READY: return "READY"; + default: return "UNKNOWN"; + } +} + +static RawSnapshot captureSnapshot() { + RawSnapshot snap; + snap.pins = readPins(); + snap.pmuOk = (g_pmu != nullptr); + if (!snap.pmuOk) { + snap.state = RawState::PMU_FAIL; + return snap; + } + + snap.railOn = g_pmu->isPowerChannelEnable(XPOWERS_BLDO1); + if (!snap.railOn) { + snap.state = RawState::RAIL_OFF; + return snap; + } + + snap.idle = idleProbe(); + if (snap.idle.ffCount == 8) { + snap.state = RawState::BUS_FLOAT; + } else if (snap.idle.zeroCount == 8) { + snap.state = RawState::BUS_LOW; + } + + beginBus(); + delay(1); + for (int i = 0; i < 10; ++i) { + g_spi.transfer(0xFF); + } + + snap.cmd0 = sendCommand(0, 0, 0x95); + endCommand(); + if (snap.cmd0 == 0xFF) { + snap.state = RawState::CMD0_TIMEOUT; + return snap; + } + if (snap.cmd0 != 0x01) { + snap.state = RawState::CMD0_NOT_IDLE; + return snap; + } + + snap.cmd8r1 = sendCommand(8, 0x000001AAUL, 0x87); + if (snap.cmd8r1 == 0xFF) { + endCommand(); + snap.state = RawState::CMD8_TIMEOUT; + return snap; + } + for (int i = 0; i < 4; ++i) { + snap.cmd8data[i] = g_spi.transfer(0xFF); + } + endCommand(); + if (!(snap.cmd8r1 == 0x01 || snap.cmd8r1 == 0x05)) { + snap.state = RawState::CMD8_BAD_R1; + return snap; + } + + uint8_t ready = 0xFF; + for (int attempt = 0; attempt < 12; ++attempt) { + uint8_t r1 = sendCommand(55, 0, 0x65); + endCommand(); + if (r1 == 0xFF) { + continue; + } + ready = sendCommand(41, 0x40000000UL, 0x77); + endCommand(); + if (ready == 0x00) { + break; + } + delay(10); + } + snap.acmd41 = ready; + if (snap.acmd41 != 0x00) { + snap.state = RawState::ACMD41_TIMEOUT; + return snap; + } + + snap.cmd58r1 = sendCommand(58, 0, 0xFD); + if (snap.cmd58r1 == 0xFF) { + endCommand(); + snap.state = RawState::CMD58_TIMEOUT; + return snap; + } + for (int i = 0; i < 4; ++i) { + snap.ocr[i] = g_spi.transfer(0xFF); + } + endCommand(); + + snap.state = RawState::READY; + return snap; +} + +static void printSnapshot(const RawSnapshot& snap) { + Serial.printf( + "sample=%lu state=%s rail=%s pins=%d/%d/%d/%d " + "idle(ff=%u z=%u o=%u %02X %02X %02X %02X) " + "cmd0=%02X cmd8=%02X [%02X %02X %02X %02X] " + "acmd41=%02X cmd58=%02X [%02X %02X %02X %02X]\r\n", + (unsigned long)g_sampleCount, + stateToString(snap.state), + snap.railOn ? "ON" : "OFF", + snap.pins.cs, + snap.pins.sck, + snap.pins.miso, + snap.pins.mosi, + (unsigned)snap.idle.ffCount, + (unsigned)snap.idle.zeroCount, + (unsigned)snap.idle.otherCount, + snap.idle.bytes[0], + snap.idle.bytes[1], + snap.idle.bytes[2], + snap.idle.bytes[3], + snap.cmd0, + snap.cmd8r1, + snap.cmd8data[0], + snap.cmd8data[1], + snap.cmd8data[2], + snap.cmd8data[3], + snap.acmd41, + snap.cmd58r1, + snap.ocr[0], + snap.ocr[1], + snap.ocr[2], + snap.ocr[3] + ); +} + +static void printReadyHeartbeat() { + Serial.printf("[%10lu] READY heartbeat\r\n", (unsigned long)millis()); +} + +static bool sameSnapshot(const RawSnapshot& a, const RawSnapshot& b) { + if (a.state != b.state) return false; + if (a.railOn != b.railOn) return false; + if (a.pins.cs != b.pins.cs || a.pins.sck != b.pins.sck || + a.pins.miso != b.pins.miso || a.pins.mosi != b.pins.mosi) return false; + if (a.idle.ffCount != b.idle.ffCount || + a.idle.zeroCount != b.idle.zeroCount || + a.idle.otherCount != b.idle.otherCount) return false; + if (a.cmd0 != b.cmd0 || a.cmd8r1 != b.cmd8r1 || + a.acmd41 != b.acmd41 || a.cmd58r1 != b.cmd58r1) return false; + + for (int i = 0; i < 4; ++i) { + if (a.cmd8data[i] != b.cmd8data[i]) return false; + if (a.ocr[i] != b.ocr[i]) return false; + } + + return true; +} + +static void showSnapshot(const RawSnapshot& snap) { + char l1[24]; + char l2[24]; + char l3[24]; + char l4[24]; + char l5[24]; + + snprintf(l1, sizeof(l1), "%s RAW SD", NODE_LABEL); + snprintf(l2, sizeof(l2), "STATE %s", stateToString(snap.state)); + snprintf(l3, sizeof(l3), "CMD0 %02X C8 %02X A41 %02X", snap.cmd0, snap.cmd8r1, snap.acmd41); + snprintf(l4, sizeof(l4), "OCR %02X%02X%02X%02X", + snap.ocr[0], snap.ocr[1], snap.ocr[2], snap.ocr[3]); + snprintf(l5, sizeof(l5), "IDL %u/%u/%u #%lu", + (unsigned)snap.idle.ffCount, + (unsigned)snap.idle.zeroCount, + (unsigned)snap.idle.otherCount, + (unsigned long)g_sampleCount); + + g_oled.clearBuffer(); + g_oled.setFont(u8g2_font_5x8_tf); + g_oled.drawUTF8(0, 12, l1); + g_oled.drawUTF8(0, 24, l2); + g_oled.drawUTF8(0, 36, l3); + g_oled.drawUTF8(0, 48, l4); + g_oled.drawUTF8(0, 60, l5); + g_oled.sendBuffer(); +} + +static void handleSerialInput() { + auto handleCommand = []() { + if (g_inputLen == 0) { + Serial.println(); + return; + } + + g_inputLine[g_inputLen] = '\0'; + if (strcmp(g_inputLine, "m") == 0 || strcmp(g_inputLine, "M") == 0) { + g_markCount++; + Serial.printf("----- MARK %lu -----\r\n", (unsigned long)g_markCount); + } else if (strcmp(g_inputLine, "ls") == 0 || strcmp(g_inputLine, "LS") == 0) { + Serial.println("----- LS / -----"); + beginBus(); + for (int i = 0; i < 10; ++i) { + g_spi.transfer(0xFF); + } + + if (!SD.begin(tbeam_supreme::sdCs(), g_spi, kSpiHz)) { + Serial.println("ls: SD.begin failed"); + g_inputLen = 0; + return; + } + + File root = SD.open("/", FILE_READ); + if (!root) { + Serial.println("ls: open / failed"); + SD.end(); + g_inputLen = 0; + return; + } + + File entry = root.openNextFile(); + if (!entry) { + Serial.println("ls: root empty or unreadable"); + } + + while (entry) { + Serial.printf("%s%s %lu\r\n", + entry.name(), + entry.isDirectory() ? "/" : "", + (unsigned long)entry.size()); + entry.close(); + entry = root.openNextFile(); + } + + root.close(); + SD.end(); + Serial.println("----- END LS -----"); + } else { + Serial.printf("unknown command: %s\r\n", g_inputLine); + } + + g_inputLen = 0; + }; + + while (Serial.available() > 0) { + int ch = Serial.read(); + if (ch == '\r' || ch == '\n') { + handleCommand(); + } else if (ch == 0x08 || ch == 0x7F) { + if (g_inputLen > 0) { + g_inputLen--; + } + } else if (g_inputLen + 1 < sizeof(g_inputLine)) { + g_inputLine[g_inputLen++] = (char)ch; + } + } +} + +void setup() { + Serial.begin(115200); + delay(kSerialDelayMs); + + Wire.begin(OLED_SDA, OLED_SCL); + g_oled.begin(); + g_oled.clearDisplay(); + + tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial); + forceSpiDeselected(); + + Serial.println(); + Serial.println("constantTFCard raw probe"); + Serial.printf("Node: %s\r\n", NODE_LABEL); + Serial.printf("Poll interval: %lu ms\r\n", (unsigned long)kPollIntervalMs); + Serial.println("States: PMU_FAIL RAIL_OFF BUS_FLOAT BUS_LOW CMD0_TO CMD0_BAD CMD8_TO CMD8_BAD ACMD41_TO CMD58_TO READY"); + Serial.println("Input: Enter=blank line, m=mark, ls=list root"); +} + +void loop() { + static uint32_t lastPollMs = 0; + static bool haveLastPrinted = false; + static RawSnapshot lastPrinted{}; + static uint32_t lastReadyHeartbeatMs = 0; + + handleSerialInput(); + + uint32_t now = millis(); + if (now - lastPollMs < kPollIntervalMs) { + delay(5); + return; + } + + lastPollMs = now; + g_sampleCount++; + + RawSnapshot snap = captureSnapshot(); + bool changed = (!haveLastPrinted || !sameSnapshot(snap, lastPrinted)); + if (changed) { + printSnapshot(snap); + lastPrinted = snap; + haveLastPrinted = true; + if (snap.state == RawState::READY) { + lastReadyHeartbeatMs = now; + } + } else if (snap.state == RawState::READY && now - lastReadyHeartbeatMs >= kReadyHeartbeatMs) { + printReadyHeartbeat(); + lastReadyHeartbeatMs = now; + } + showSnapshot(snap); +} From 0395fed90762492195e1f291dc13cba8cc95dacc Mon Sep 17 00:00:00 2001 From: John Poole Date: Fri, 3 Apr 2026 15:38:10 -0700 Subject: [PATCH 3/6] Allow for increased number of reporting units, each having 2 seconds of air time and keeping frame within 60 seconds, evenly divided. Example: was 10, now 15, could be 20 or 30. --- docs/gps.md | 0 exercises/12_FiveTalk/platformio.ini | 3 +++ exercises/12_FiveTalk/src/main.cpp | 35 ++++++++++++++++++++++++---- 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 docs/gps.md diff --git a/docs/gps.md b/docs/gps.md new file mode 100644 index 0000000..e69de29 diff --git a/exercises/12_FiveTalk/platformio.ini b/exercises/12_FiveTalk/platformio.ini index 0eff093..84b0b70 100644 --- a/exercises/12_FiveTalk/platformio.ini +++ b/exercises/12_FiveTalk/platformio.ini @@ -28,6 +28,7 @@ build_flags = -D GPS_WAKEUP_PIN=7 -D GPS_1PPS_PIN=6 -D GPS_L76K + -D NODE_SLOT_COUNT=7 -D LORA_CS=10 -D LORA_MOSI=11 -D LORA_SCK=12 @@ -94,4 +95,6 @@ build_flags = -D NODE_LABEL=\"Guy\" -D NODE_SHORT=\"G\" -D NODE_SLOT_INDEX=6 + -D GPS_UBLOX + diff --git a/exercises/12_FiveTalk/src/main.cpp b/exercises/12_FiveTalk/src/main.cpp index 8c612cd..b292aab 100644 --- a/exercises/12_FiveTalk/src/main.cpp +++ b/exercises/12_FiveTalk/src/main.cpp @@ -27,6 +27,10 @@ #define NODE_SHORT "?" #endif +#ifndef NODE_SLOT_COUNT +#define NODE_SLOT_COUNT 7 +#endif + #ifndef NODE_SLOT_INDEX #define NODE_SLOT_INDEX 0 #endif @@ -63,8 +67,8 @@ #define FW_BUILD_UTC "unknown" #endif -#if (NODE_SLOT_INDEX < 0) || (NODE_SLOT_INDEX > 6) -#error "NODE_SLOT_INDEX must be 0..6" +#if (NODE_SLOT_INDEX < 0) || (NODE_SLOT_INDEX >= NODE_SLOT_COUNT) +#error "NODE_SLOT_INDEX must be 0..NODE_SLOT_COUNT-1" #endif static const uint32_t kSerialDelayMs = 1000; @@ -156,6 +160,20 @@ static uint8_t bestSatelliteCount() return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView; } +static uint32_t computeFrameSeconds(uint32_t requiredSeconds) +{ + uint32_t frame = ((requiredSeconds + 4U) / 5U) * 5U; // round up to 5s + while (frame <= 60U && (60U % frame) != 0U) + { + frame += 5U; + } + if (frame == 0U || frame > 60U) + { + frame = 60U; // fallback + } + return frame; +} + static void logf(const char *fmt, ...) { char msg[256]; @@ -1030,7 +1048,7 @@ static bool initRadio() return false; } - logf("Radio ready for %s (%s), slot=%d sec=%d", NODE_LABEL, NODE_SHORT, NODE_SLOT_INDEX, NODE_SLOT_INDEX * 2); + logf("Radio ready for %s (%s), slot=%d/%d (2s each)", NODE_LABEL, NODE_SHORT, NODE_SLOT_INDEX, NODE_SLOT_COUNT); return true; } @@ -1051,8 +1069,15 @@ static void runTxScheduler() if (!getCurrentUtc(now, epoch)) return; - int slotSecond = NODE_SLOT_INDEX * (int)kSlotSeconds; - int secInFrame = now.second % 10; + uint32_t requiredTxSeconds = (uint32_t)NODE_SLOT_COUNT * kSlotSeconds; + uint32_t frameSeconds = computeFrameSeconds(requiredTxSeconds); + uint32_t slotSecond = (uint32_t)NODE_SLOT_INDEX * kSlotSeconds; + if (slotSecond >= frameSeconds) + return; + + uint32_t secInFrame = (uint32_t)now.second % frameSeconds; + if (secInFrame >= requiredTxSeconds) + return; // idle guard interval if (secInFrame != slotSecond) return; From ab37d32b6ddeda6b7ce48b63bed1ad35f2c89d76 Mon Sep 17 00:00:00 2001 From: John Poole Date: Fri, 3 Apr 2026 15:54:08 -0700 Subject: [PATCH 4/6] Shows available RAM --- exercises/15_RAM/README.md | 16 ++++ exercises/15_RAM/platformio.ini | 55 ++++++++++++ exercises/15_RAM/scripts/set_build_epoch.py | 12 +++ exercises/15_RAM/src/main.cpp | 93 +++++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 exercises/15_RAM/README.md create mode 100644 exercises/15_RAM/platformio.ini create mode 100644 exercises/15_RAM/scripts/set_build_epoch.py create mode 100644 exercises/15_RAM/src/main.cpp diff --git a/exercises/15_RAM/README.md b/exercises/15_RAM/README.md new file mode 100644 index 0000000..7f35cf7 --- /dev/null +++ b/exercises/15_RAM/README.md @@ -0,0 +1,16 @@ +# Exercise 15: RAM + +This exercise shows available RAM on the console and on the OLED display on a T-Beam Supreme. + +Behavior: +- Reports heap statistics every second over serial. +- Shows live heap status on the OLED display. +- Designed as the first step toward volatile /tmp RAM-backed storage. + +Build and upload: + +```bash +cd /usr/local/src/microreticulum/microReticulumTbeam/exercises/15_RAM +pio run -e amy -t upload +pio device monitor -b 115200 +``` diff --git a/exercises/15_RAM/platformio.ini b/exercises/15_RAM/platformio.ini new file mode 100644 index 0000000..f8e943d --- /dev/null +++ b/exercises/15_RAM/platformio.ini @@ -0,0 +1,55 @@ +; 20260403 ChatGPT +; Exercise 15_RAM + +[platformio] +default_envs = amy + +[env] +platform = espressif32 +framework = arduino +board = esp32-s3-devkitc-1 +monitor_speed = 115200 +extra_scripts = pre:scripts/set_build_epoch.py +lib_deps = + Wire + olikraus/U8g2@^2.36.4 + +build_flags = + -I ../../shared/boards + -I ../../external/microReticulum_Firmware + -D BOARD_MODEL=BOARD_TBEAM_S_V1 + -D OLED_SDA=17 + -D OLED_SCL=18 + -D OLED_ADDR=0x3C + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + +[env:amy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"AMY\" + +[env:bob] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"BOB\" + +[env:cy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"CY\" + +[env:dan] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"DAN\" + +[env:ed] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"ED\" diff --git a/exercises/15_RAM/scripts/set_build_epoch.py b/exercises/15_RAM/scripts/set_build_epoch.py new file mode 100644 index 0000000..3011129 --- /dev/null +++ b/exercises/15_RAM/scripts/set_build_epoch.py @@ -0,0 +1,12 @@ +import time +Import("env") + +epoch = int(time.time()) +utc_tag = time.strftime("%Y%m%d_%H%M%S_z", time.gmtime(epoch)) + +env.Append( + CPPDEFINES=[ + ("FW_BUILD_EPOCH", str(epoch)), + ("FW_BUILD_UTC", '\\"%s\\"' % utc_tag), + ] +) diff --git a/exercises/15_RAM/src/main.cpp b/exercises/15_RAM/src/main.cpp new file mode 100644 index 0000000..5f3c5a6 --- /dev/null +++ b/exercises/15_RAM/src/main.cpp @@ -0,0 +1,93 @@ +// 20260403 ChatGPT +// Exercise 15_RAM + +#include +#include +#include + +#ifndef NODE_LABEL +#define NODE_LABEL "RAM" +#endif + +#ifndef OLED_SDA +#define OLED_SDA 17 +#endif +#ifndef OLED_SCL +#define OLED_SCL 18 +#endif +#ifndef OLED_ADDR +#define OLED_ADDR 0x3C +#endif + +static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); + +static void oledShowLines(const char *l1, + const char *l2 = nullptr, + const char *l3 = nullptr, + const char *l4 = nullptr, + const char *l5 = nullptr) +{ + g_oled.clearBuffer(); + g_oled.setFont(u8g2_font_5x8_tf); + if (l1) g_oled.drawUTF8(0, 12, l1); + if (l2) g_oled.drawUTF8(0, 24, l2); + if (l3) g_oled.drawUTF8(0, 36, l3); + if (l4) g_oled.drawUTF8(0, 48, l4); + if (l5) g_oled.drawUTF8(0, 60, l5); + g_oled.sendBuffer(); +} + +static size_t getAvailableRamBytes() +{ + return ESP.getFreeHeap(); +} + +static void printRamStatus() +{ + const size_t freeBytes = getAvailableRamBytes(); + const size_t totalBytes = ESP.getHeapSize(); + const size_t maxAlloc = ESP.getMaxAllocHeap(); + + Serial.printf("RAM total=%u free=%u maxAlloc=%u\r\n", (unsigned)totalBytes, (unsigned)freeBytes, (unsigned)maxAlloc); + + char line1[32]; + char line2[32]; + char line3[32]; + + snprintf(line1, sizeof(line1), "Exercise 15 RAM"); + snprintf(line2, sizeof(line2), "Node: %s", NODE_LABEL); + snprintf(line3, sizeof(line3), "Free: %u KB", (unsigned)(freeBytes / 1024U)); + char line4[32]; + snprintf(line4, sizeof(line4), "Total: %u KB", (unsigned)(totalBytes / 1024U)); + char line5[32]; + snprintf(line5, sizeof(line5), "Max alloc: %u KB", (unsigned)(maxAlloc / 1024U)); + + oledShowLines(line1, line2, line3, line4, line5); +} + +void setup() +{ + Serial.begin(115200); + delay(800); + Serial.println("Exercise 15_RAM boot"); + + Wire.begin(OLED_SDA, OLED_SCL); + g_oled.setI2CAddress(OLED_ADDR << 1); + g_oled.begin(); + oledShowLines("Exercise 15_RAM", "Node: " NODE_LABEL, "Booting..."); + + delay(1000); +} + +void loop() +{ + static uint32_t lastMs = 0; + const uint32_t now = millis(); + if (now - lastMs < 1000) { + delay(10); + return; + } + lastMs = now; + + printRamStatus(); +} From c7646e169eee890f36998423af34d47ca584774c Mon Sep 17 00:00:00 2001 From: John Poole Date: Fri, 3 Apr 2026 16:01:58 -0700 Subject: [PATCH 5/6] creates /tmp file which can be written to through the console. --- exercises/15_RAM/src/main.cpp | 130 ++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/exercises/15_RAM/src/main.cpp b/exercises/15_RAM/src/main.cpp index 5f3c5a6..17eb519 100644 --- a/exercises/15_RAM/src/main.cpp +++ b/exercises/15_RAM/src/main.cpp @@ -21,6 +21,11 @@ static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); +static const char *kTmpPath = "/tmp/volatile.txt"; +static const size_t kTmpFileCapacity = 4096; +static char g_tmpFileBuffer[kTmpFileCapacity]; +static size_t g_tmpFileSize = 0; + static void oledShowLines(const char *l1, const char *l2 = nullptr, const char *l3 = nullptr, @@ -65,6 +70,112 @@ static void printRamStatus() oledShowLines(line1, line2, line3, line4, line5); } +static void showHelp() +{ + Serial.println("RAM command list:"); + Serial.println(" help - show this menu"); + Serial.println(" stat - show /tmp file state"); + Serial.println(" read - read /tmp contents"); + Serial.println(" clear - clear /tmp contents"); + Serial.println(" write - write text to /tmp"); + Serial.println(" append - append text to /tmp"); +} + +static void printTmpFileStat() +{ + Serial.printf("Path: %s\r\n", kTmpPath); + Serial.printf("Size: %u bytes\r\n", (unsigned)g_tmpFileSize); + Serial.printf("Capacity: %u bytes\r\n", (unsigned)kTmpFileCapacity); +} + +static void printTmpFileContents() +{ + if (g_tmpFileSize == 0) { + Serial.println("/tmp file is empty"); + return; + } + + Serial.print("/tmp contents: "); + Serial.write((const uint8_t *)g_tmpFileBuffer, g_tmpFileSize); + if (g_tmpFileBuffer[g_tmpFileSize - 1] != '\n') + Serial.println(); +} + +static void setTmpFileContent(const char *text) +{ + if (!text) { + g_tmpFileSize = 0; + return; + } + const size_t newLen = strlen(text); + if (newLen > kTmpFileCapacity - 1) { + Serial.printf("Error: content too large (%u/%u)\r\n", (unsigned)newLen, (unsigned)kTmpFileCapacity); + return; + } + memcpy(g_tmpFileBuffer, text, newLen); + g_tmpFileSize = newLen; +} + +static void appendTmpFileContent(const char *text) +{ + if (!text || text[0] == '\0') return; + const size_t textLen = strlen(text); + if (g_tmpFileSize + textLen > kTmpFileCapacity - 1) { + Serial.printf("Error: append would exceed %u bytes\r\n", (unsigned)kTmpFileCapacity); + return; + } + memcpy(g_tmpFileBuffer + g_tmpFileSize, text, textLen); + g_tmpFileSize += textLen; +} + +static void processSerialCommand(const char *line) +{ + if (!line || line[0] == '\0') return; + + char tmp[384]; + strncpy(tmp, line, sizeof(tmp) - 1); + tmp[sizeof(tmp) - 1] = '\0'; + + char *cmd = strtok(tmp, " \t\r\n"); + if (!cmd) return; + + if (strcasecmp(cmd, "help") == 0) { + showHelp(); + return; + } + + if (strcasecmp(cmd, "stat") == 0) { + printTmpFileStat(); + return; + } + + if (strcasecmp(cmd, "read") == 0) { + printTmpFileContents(); + return; + } + + if (strcasecmp(cmd, "clear") == 0) { + g_tmpFileSize = 0; + Serial.println("/tmp cleared"); + return; + } + + if (strcasecmp(cmd, "write") == 0 || strcasecmp(cmd, "append") == 0) { + const char *payload = line + strlen(cmd); + while (*payload == ' ' || *payload == '\t') payload++; + if (strcasecmp(cmd, "write") == 0) + setTmpFileContent(payload); + else + appendTmpFileContent(payload); + + Serial.printf("%s: %u bytes\r\n", cmd, + (unsigned)g_tmpFileSize); + return; + } + + Serial.println("Unknown command (help for list)"); +} + void setup() { Serial.begin(115200); @@ -83,6 +194,25 @@ void loop() { static uint32_t lastMs = 0; const uint32_t now = millis(); + + // check serial commands at all times + static char rxLine[384]; + static size_t rxLen = 0; + + while (Serial.available()) { + int c = Serial.read(); + if (c <= 0) continue; + if (c == '\r' || c == '\n') { + if (rxLen > 0) { + rxLine[rxLen] = '\0'; + processSerialCommand(rxLine); + rxLen = 0; + } + } else if (rxLen + 1 < sizeof(rxLine)) { + rxLine[rxLen++] = (char)c; + } + } + if (now - lastMs < 1000) { delay(10); return; From 41c1fe68191cfc470a15948436fb33481f6a3df3 Mon Sep 17 00:00:00 2001 From: John Poole Date: Sat, 4 Apr 2026 11:52:41 -0700 Subject: [PATCH 6/6] After various memory exercises, then back to GPS to get UBlox working. UBlox is working. TODO: start libraries, downplay SD Card as we can use memory for interim logging --- .gitignore | 1 + exercises/09_GPS_Time/README.md | 17 +- exercises/09_GPS_Time/platformio.ini | 6 + exercises/09_GPS_Time/src/main.cpp | 823 ++++++++++++++++-- exercises/10_Simple_GPS/platformio.ini | 6 + exercises/15_RAM/src/main.cpp | 55 +- exercises/16_PSRAM/README.md | 32 + exercises/16_PSRAM/platformio.ini | 54 ++ exercises/16_PSRAM/scripts/set_build_epoch.py | 12 + exercises/16_PSRAM/src/main.cpp | 342 ++++++++ exercises/17_Flash/README.md | 32 + exercises/17_Flash/platformio.ini | 50 ++ exercises/17_Flash/read_partition_bin.py | 11 + exercises/17_Flash/scripts/set_build_epoch.py | 12 + exercises/17_Flash/show_partition_table.py | 26 + exercises/17_Flash/src/main.cpp | 608 +++++++++++++ 16 files changed, 2016 insertions(+), 71 deletions(-) create mode 100644 exercises/16_PSRAM/README.md create mode 100644 exercises/16_PSRAM/platformio.ini create mode 100644 exercises/16_PSRAM/scripts/set_build_epoch.py create mode 100644 exercises/16_PSRAM/src/main.cpp create mode 100644 exercises/17_Flash/README.md create mode 100644 exercises/17_Flash/platformio.ini create mode 100644 exercises/17_Flash/read_partition_bin.py create mode 100644 exercises/17_Flash/scripts/set_build_epoch.py create mode 100644 exercises/17_Flash/show_partition_table.py create mode 100644 exercises/17_Flash/src/main.cpp diff --git a/.gitignore b/.gitignore index 94818e5..ae506da 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ /hold/ .platformio_local/ +.codex diff --git a/exercises/09_GPS_Time/README.md b/exercises/09_GPS_Time/README.md index a8a1a81..f3e2889 100644 --- a/exercises/09_GPS_Time/README.md +++ b/exercises/09_GPS_Time/README.md @@ -1,4 +1,4 @@ -## Exercise 09: GPS Time (L76K) +## Exercise 09: GPS Time (L76K + UBLOX) This exercise boots the T-Beam Supreme and verifies GPS behavior at startup. @@ -13,11 +13,10 @@ Implemented behavior: 1. Initializes PMU, OLED, and SD startup watcher (same startup SD path used in Exercise 08). 2. Probes GPS at startup for NMEA traffic, module identity, satellite count, and UTC time availability. - Uses explicit GPS UART pins and an active startup probe (multi-baud + common GPS query commands), aligned with the approach validated in Exercise 10. -3. If L76K is detected, normal GPS-time flow continues. -4. If L76K is not detected and Quectel-style module text is detected, OLED shows a hard TODO error: - - Quectel detected - - L76K required - - Quectel support is TODO +3. Supports both module profiles via `platformio.ini` build flags: + - `node_a` / `node_b`: `GPS_L76K` + - `node_c`: `GPS_UBLOX` +4. If detected module data conflicts with the selected node profile, OLED shows a `GPS module mismatch` error. 5. Every minute: - If GPS UTC is valid: shows GPS UTC time and satellites on OLED. - If satellites are seen but UTC is not valid yet: shows that condition and RTC time. @@ -31,8 +30,12 @@ Implemented behavior: Notes: - GPS time displayed is UTC from NMEA RMC with valid status. -- Satellite count uses best available from GGA/GSV. +- Satellite count uses best available from GGA/GSA/GSV. - RTC fallback reads PCF8563 via Wire1. +- For UBLOX hardware use `-e node_c`. +- The UBLOX MAX-M10S path is given a longer startup window than L76K because cold starts are slower, especially if backup power/orbit data are unavailable. +- On T-Beam Supreme, `GPS_WAKEUP_PIN=7` is relevant for the L76K variant; the UBLOX MAX-M10S does not use that wake pin in the same way. +- For fastest UBLOX reacquisition, test with the 18650 attached so the GNSS backup domain can preserve assistance state across resets/power cycles. ## Build diff --git a/exercises/09_GPS_Time/platformio.ini b/exercises/09_GPS_Time/platformio.ini index b4d48d9..96344b6 100644 --- a/exercises/09_GPS_Time/platformio.ini +++ b/exercises/09_GPS_Time/platformio.ini @@ -40,3 +40,9 @@ build_flags = build_flags = ${env.build_flags} -D NODE_LABEL=\"B\" + +[env:node_c] +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"C\" + -D GPS_UBLOX \ No newline at end of file diff --git a/exercises/09_GPS_Time/src/main.cpp b/exercises/09_GPS_Time/src/main.cpp index 22d81d7..02d3f39 100644 --- a/exercises/09_GPS_Time/src/main.cpp +++ b/exercises/09_GPS_Time/src/main.cpp @@ -3,6 +3,7 @@ // $HeadURL$ #include +#include #include #include @@ -30,8 +31,12 @@ #endif static const uint32_t kSerialDelayMs = 5000; -static const uint32_t kGpsStartupProbeMs = 20000; static const uint32_t kMinuteMs = 60000; +static const uint32_t kGpsDiagnosticLogMs = 15000; +static const char* kGpsLogDir = "/gpsdiag"; +static const char* kGpsLogPath = "/gpsdiag/current.log"; +static const char* kBuildDate = __DATE__; +static const char* kBuildTime = __TIME__; static XPowersLibInterface* g_pmu = nullptr; static StartupSdManager g_sd(Serial); @@ -40,15 +45,34 @@ static HardwareSerial g_gpsSerial(1); static uint32_t g_logSeq = 0; static uint32_t g_lastMinuteReportMs = 0; +static uint32_t g_lastGpsDiagnosticLogMs = 0; static uint32_t g_gpsBaud = GPS_BAUD; +static int g_gpsRxPin = GPS_RX_PIN; +static int g_gpsTxPin = GPS_TX_PIN; +static bool g_spiffsReady = false; +static bool g_ubloxConfigAttempted = false; +static bool g_ubloxConfigured = false; +static bool g_ubloxIsM10 = false; static bool g_prevHadSatellites = false; static bool g_prevHadValidUtc = false; static bool g_satellitesAcquiredAnnounced = false; static bool g_timeAcquiredAnnounced = false; +static uint8_t g_lastDrawnSatsUsed = 255; +static uint8_t g_lastDrawnSatsView = 255; +static bool g_lastDrawnValidUtc = false; +static bool g_haveLastDrawnState = false; +static uint32_t g_lastDisplayRefreshMs = 0; static char g_gpsLine[128]; static size_t g_gpsLineLen = 0; +static char g_serialLine[128]; +static size_t g_serialLineLen = 0; +static uint8_t g_rawLogGgaCount = 0; +static uint8_t g_rawLogGsaCount = 0; +static uint8_t g_rawLogGsvCount = 0; +static uint8_t g_rawLogRmcCount = 0; +static uint8_t g_rawLogPubxCount = 0; enum class GpsModuleKind : uint8_t { UNKNOWN = 0, @@ -56,6 +80,14 @@ enum class GpsModuleKind : uint8_t { UBLOX }; +#if defined(GPS_UBLOX) +static const GpsModuleKind kExpectedGpsModule = GpsModuleKind::UBLOX; +#elif defined(GPS_L76K) +static const GpsModuleKind kExpectedGpsModule = GpsModuleKind::L76K; +#else +static const GpsModuleKind kExpectedGpsModule = GpsModuleKind::UNKNOWN; +#endif + struct RtcDateTime { uint16_t year; uint8_t month; @@ -66,13 +98,18 @@ struct RtcDateTime { }; struct GpsState { - GpsModuleKind module = GpsModuleKind::UNKNOWN; + GpsModuleKind module = kExpectedGpsModule; bool sawAnySentence = false; uint8_t satsUsed = 0; uint8_t satsInView = 0; + uint8_t satsUsedWindowMax = 0; + uint8_t satsInViewWindowMax = 0; + uint32_t satsUsedWindowMs = 0; + uint32_t satsInViewWindowMs = 0; bool hasValidUtc = false; + uint32_t utcFixMs = 0; uint16_t utcYear = 0; uint8_t utcMonth = 0; uint8_t utcDay = 0; @@ -82,6 +119,355 @@ struct GpsState { }; static GpsState g_gps; +static const uint32_t kSatelliteWindowMs = 2000; +static const uint32_t kDisplayRefreshMinMs = 1000; +static const uint32_t kFixFreshMs = 5000; + +static String gpsModuleToString(GpsModuleKind kind); +static GpsModuleKind activeGpsModule(); +static uint8_t bestSatelliteCount(); +static uint8_t displayedSatsUsed(); +static uint8_t displayedSatsInView(); +static bool displayHasFreshUtc(); +static String formatRtcNow(); + +static bool ensureGpsLogDirectory() { + if (!g_spiffsReady) { + return false; + } + if (SPIFFS.exists(kGpsLogDir)) { + return true; + } + return SPIFFS.mkdir(kGpsLogDir); +} + +static bool gpsDiagAppendLine(const char* line) { + if (!g_spiffsReady || !line) { + return false; + } + File file = SPIFFS.open(kGpsLogPath, FILE_APPEND); + if (!file) { + return false; + } + file.print(line); + file.print("\r\n"); + file.close(); + return true; +} + +static void formatGpsSnapshot(char* out, size_t outSize, const char* event) { + if (!out || outSize == 0) { + return; + } + + const uint8_t sats = bestSatelliteCount(); + const char* ev = event ? event : "sample"; + if (g_gps.hasValidUtc) { + snprintf(out, + outSize, + "ms=%lu event=%s module=%s nmea=%s sats_used=%u sats_view=%u sats_best=%u utc=%04u-%02u-%02uT%02u:%02u:%02u rx=%d tx=%d baud=%lu", + (unsigned long)millis(), + ev, + gpsModuleToString(activeGpsModule()).c_str(), + g_gps.sawAnySentence ? "yes" : "no", + (unsigned)g_gps.satsUsed, + (unsigned)g_gps.satsInView, + (unsigned)sats, + (unsigned)g_gps.utcYear, + (unsigned)g_gps.utcMonth, + (unsigned)g_gps.utcDay, + (unsigned)g_gps.utcHour, + (unsigned)g_gps.utcMinute, + (unsigned)g_gps.utcSecond, + g_gpsRxPin, + g_gpsTxPin, + (unsigned long)g_gpsBaud); + } else { + String rtc = formatRtcNow(); + snprintf(out, + outSize, + "ms=%lu event=%s module=%s nmea=%s sats_used=%u sats_view=%u sats_best=%u utc=NO rtc=\"%s\" rx=%d tx=%d baud=%lu", + (unsigned long)millis(), + ev, + gpsModuleToString(activeGpsModule()).c_str(), + g_gps.sawAnySentence ? "yes" : "no", + (unsigned)g_gps.satsUsed, + (unsigned)g_gps.satsInView, + (unsigned)sats, + rtc.c_str(), + g_gpsRxPin, + g_gpsTxPin, + (unsigned long)g_gpsBaud); + } +} + +static void appendGpsSnapshot(const char* event) { + char line[256]; + formatGpsSnapshot(line, sizeof(line), event); + (void)gpsDiagAppendLine(line); +} + +static String buildStampShort() { + char buf[32]; + snprintf(buf, sizeof(buf), "%s %.5s", kBuildDate, kBuildTime); + return String(buf); +} + +static void maybeLogRawSentence(const char* type, const char* sentence) { + if (!type || !sentence || !g_spiffsReady) { + return; + } + + uint8_t* counter = nullptr; + if (strcmp(type, "GGA") == 0) { + counter = &g_rawLogGgaCount; + } else if (strcmp(type, "GSA") == 0) { + counter = &g_rawLogGsaCount; + } else if (strcmp(type, "GSV") == 0) { + counter = &g_rawLogGsvCount; + } else if (strcmp(type, "RMC") == 0) { + counter = &g_rawLogRmcCount; + } else if (strcmp(type, "PUBX") == 0) { + counter = &g_rawLogPubxCount; + } + + if (!counter || *counter >= 12) { + return; + } + (*counter)++; + + char line[220]; + snprintf(line, + sizeof(line), + "ms=%lu event=raw_%s idx=%u sentence=%s", + (unsigned long)millis(), + type, + (unsigned)*counter, + sentence); + (void)gpsDiagAppendLine(line); +} + +static void clearGpsSerialInput() { + g_gpsLineLen = 0; + while (g_gpsSerial.available() > 0) { + (void)g_gpsSerial.read(); + } +} + +static void ubxChecksum(uint8_t* message, size_t length) { + uint8_t ckA = 0; + uint8_t ckB = 0; + for (size_t i = 2; i < length - 2; ++i) { + ckA = (uint8_t)((ckA + message[i]) & 0xFF); + ckB = (uint8_t)((ckB + ckA) & 0xFF); + } + message[length - 2] = ckA; + message[length - 1] = ckB; +} + +static size_t makeUbxPacket(uint8_t* out, + size_t outSize, + uint8_t classId, + uint8_t msgId, + const uint8_t* payload, + uint16_t payloadSize) { + if (!out || outSize < (size_t)payloadSize + 8U) { + return 0; + } + out[0] = 0xB5; + out[1] = 0x62; + out[2] = classId; + out[3] = msgId; + out[4] = (uint8_t)(payloadSize & 0xFF); + out[5] = (uint8_t)((payloadSize >> 8) & 0xFF); + for (uint16_t i = 0; i < payloadSize; ++i) { + out[6 + i] = payload ? payload[i] : 0; + } + out[6 + payloadSize] = 0; + out[7 + payloadSize] = 0; + ubxChecksum(out, payloadSize + 8U); + return (size_t)payloadSize + 8U; +} + +static bool waitForUbxAck(uint8_t classId, uint8_t msgId, uint32_t waitMs) { + uint8_t ack[10] = {0xB5, 0x62, 0x05, 0x01, 0x02, 0x00, classId, msgId, 0x00, 0x00}; + ubxChecksum(ack, sizeof(ack)); + uint8_t ackPos = 0; + uint32_t deadline = millis() + waitMs; + + while ((int32_t)(deadline - millis()) > 0) { + if (g_gpsSerial.available() <= 0) { + delay(2); + continue; + } + uint8_t b = (uint8_t)g_gpsSerial.read(); + if (b == ack[ackPos]) { + ackPos++; + if (ackPos == sizeof(ack)) { + return true; + } + } else { + ackPos = (b == ack[0]) ? 1 : 0; + } + } + return false; +} + +static int waitForUbxPayload(uint8_t* buffer, + uint16_t bufferSize, + uint8_t classId, + uint8_t msgId, + uint32_t waitMs) { + uint16_t framePos = 0; + uint16_t needRead = 0; + uint32_t deadline = millis() + waitMs; + + while ((int32_t)(deadline - millis()) > 0) { + if (g_gpsSerial.available() <= 0) { + delay(2); + continue; + } + + int c = g_gpsSerial.read(); + switch (framePos) { + case 0: + framePos = (c == 0xB5) ? 1 : 0; + break; + case 1: + framePos = (c == 0x62) ? 2 : 0; + break; + case 2: + framePos = (c == classId) ? 3 : 0; + break; + case 3: + framePos = (c == msgId) ? 4 : 0; + break; + case 4: + needRead = (uint16_t)c; + framePos = 5; + break; + case 5: + needRead |= (uint16_t)(c << 8); + if (needRead == 0 || needRead >= bufferSize) { + framePos = 0; + break; + } + if (g_gpsSerial.readBytes(buffer, needRead) != needRead) { + framePos = 0; + break; + } + if (g_gpsSerial.available() >= 2) { + (void)g_gpsSerial.read(); + (void)g_gpsSerial.read(); + } + return (int)needRead; + default: + framePos = 0; + break; + } + } + + return 0; +} + +static bool detectUbloxM10() { + uint8_t packet[8]; + uint8_t payload[256] = {0}; + size_t len = makeUbxPacket(packet, sizeof(packet), 0x0A, 0x04, nullptr, 0); + if (len == 0) { + return false; + } + + clearGpsSerialInput(); + g_gpsSerial.write(packet, len); + int payloadLen = waitForUbxPayload(payload, sizeof(payload), 0x0A, 0x04, 1200); + if (payloadLen < 40) { + appendGpsSnapshot("ubx_monver_timeout"); + return false; + } + + char hwVersion[11] = {0}; + memcpy(hwVersion, payload + 30, 10); + char line[160]; + snprintf(line, sizeof(line), "ms=%lu event=ubx_monver hw=%s", (unsigned long)millis(), hwVersion); + (void)gpsDiagAppendLine(line); + + if (strncmp(hwVersion, "000A0000", 8) == 0) { + return true; + } + + for (int pos = 40; pos + 30 <= payloadLen; pos += 30) { + if (strncmp((const char*)(payload + pos), "PROTVER=", 8) == 0) { + int prot = atoi((const char*)(payload + pos + 8)); + snprintf(line, sizeof(line), "ms=%lu event=ubx_monver prot=%d", (unsigned long)millis(), prot); + (void)gpsDiagAppendLine(line); + if (prot >= 27) { + return true; + } + } + } + + return false; +} + +static bool sendUbxValset(uint8_t classId, + uint8_t msgId, + const uint8_t* payload, + uint16_t payloadLen, + uint32_t ackMs, + const char* eventName) { + uint8_t packet[96]; + size_t len = makeUbxPacket(packet, sizeof(packet), classId, msgId, payload, payloadLen); + if (len == 0) { + return false; + } + clearGpsSerialInput(); + g_gpsSerial.write(packet, len); + bool ok = waitForUbxAck(classId, msgId, ackMs); + char line[160]; + snprintf(line, + sizeof(line), + "ms=%lu event=%s ack=%s", + (unsigned long)millis(), + eventName ? eventName : "ubx_cfg", + ok ? "yes" : "no"); + (void)gpsDiagAppendLine(line); + delay(150); + return ok; +} + +static bool configureUbloxM10() { + static const uint8_t kValsetDisableTxtRam[] = {0x00, 0x01, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; + static const uint8_t kValsetDisableTxtBbr[] = {0x00, 0x02, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03}; + static const uint8_t kValsetEnableNmeaRam[] = {0x00, 0x01, 0x00, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; + static const uint8_t kValsetEnableNmeaBbr[] = {0x00, 0x02, 0x00, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01}; + static const uint8_t kSave10[] = {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}; + + bool ok = true; + ok &= sendUbxValset(0x06, 0x8A, kValsetDisableTxtRam, sizeof(kValsetDisableTxtRam), 300, "ubx_m10_disable_txt_ram"); + ok &= sendUbxValset(0x06, 0x8A, kValsetDisableTxtBbr, sizeof(kValsetDisableTxtBbr), 300, "ubx_m10_disable_txt_bbr"); + ok &= sendUbxValset(0x06, 0x8A, kValsetEnableNmeaBbr, sizeof(kValsetEnableNmeaBbr), 400, "ubx_m10_enable_nmea_bbr"); + ok &= sendUbxValset(0x06, 0x8A, kValsetEnableNmeaRam, sizeof(kValsetEnableNmeaRam), 400, "ubx_m10_enable_nmea_ram"); + ok &= sendUbxValset(0x06, 0x09, kSave10, sizeof(kSave10), 800, "ubx_m10_save"); + appendGpsSnapshot(ok ? "ubx_m10_configured" : "ubx_m10_config_failed"); + return ok; +} + +static void maybeConfigureUblox() { + if (g_ubloxConfigAttempted || kExpectedGpsModule != GpsModuleKind::UBLOX) { + return; + } + g_ubloxConfigAttempted = true; + appendGpsSnapshot("ubx_config_attempt"); + + g_ubloxIsM10 = detectUbloxM10(); + if (!g_ubloxIsM10) { + appendGpsSnapshot("ubx_non_m10_or_unknown"); + return; + } + + g_ubloxConfigured = configureUbloxM10(); +} static void logf(const char* fmt, ...) { char msg[220]; @@ -197,9 +583,7 @@ static void detectModuleFromText(const char* text) { } if (t.indexOf("QUECTEL") >= 0 || t.indexOf("2021-10-20") >= 0 || t.indexOf("2021/10/20") >= 0) { - if (g_gps.module != GpsModuleKind::L76K) { - g_gps.module = GpsModuleKind::UBLOX; - } + g_gps.module = GpsModuleKind::L76K; } } @@ -210,6 +594,10 @@ static void parseGga(char* fields[], int count) { int sats = atoi(fields[7]); if (sats >= 0 && sats <= 255) { g_gps.satsUsed = (uint8_t)sats; + if ((uint8_t)sats > g_gps.satsUsedWindowMax) { + g_gps.satsUsedWindowMax = (uint8_t)sats; + } + g_gps.satsUsedWindowMs = millis(); } } @@ -220,6 +608,16 @@ static void parseGsv(char* fields[], int count) { int sats = atoi(fields[3]); if (sats >= 0 && sats <= 255) { g_gps.satsInView = (uint8_t)sats; + if ((uint8_t)sats > g_gps.satsInViewWindowMax) { + g_gps.satsInViewWindowMax = (uint8_t)sats; + } + g_gps.satsInViewWindowMs = millis(); + } +} + +static void parseGsa(char* fields[], int count) { + if (count <= 3) { + return; } } @@ -256,6 +654,7 @@ static void parseRmc(char* fields[], int count) { g_gps.utcMonth = mo; g_gps.utcYear = (uint16_t)(2000U + yy); g_gps.hasValidUtc = true; + g_gps.utcFixMs = millis(); } static void parseTxt(char* fields[], int count) { @@ -265,11 +664,34 @@ static void parseTxt(char* fields[], int count) { detectModuleFromText(fields[4]); } +static int splitCsvPreserveEmpty(char* line, char* fields[], int maxFields) { + if (!line || !fields || maxFields <= 0) { + return 0; + } + + int count = 0; + char* p = line; + fields[count++] = p; + + while (*p && count < maxFields) { + if (*p == ',') { + *p = '\0'; + fields[count++] = p + 1; + } + ++p; + } + + return count; +} + static void processNmeaLine(char* line) { if (!line || line[0] != '$') { return; } g_gps.sawAnySentence = true; + char rawLine[128]; + strncpy(rawLine, line, sizeof(rawLine) - 1); + rawLine[sizeof(rawLine) - 1] = '\0'; char* star = strchr(line, '*'); if (star) { @@ -277,26 +699,28 @@ static void processNmeaLine(char* line) { } char* fields[24] = {0}; - int count = 0; - char* saveptr = nullptr; - char* tok = strtok_r(line, ",", &saveptr); - while (tok && count < 24) { - fields[count++] = tok; - tok = strtok_r(nullptr, ",", &saveptr); - } + int count = splitCsvPreserveEmpty(line, fields, 24); if (count <= 0 || !fields[0]) { return; } const char* header = fields[0]; + if (strcmp(header, "$PUBX") == 0) { + g_gps.module = GpsModuleKind::UBLOX; + maybeLogRawSentence("PUBX", rawLine); + return; + } size_t n = strlen(header); if (n < 6) { return; } const char* type = header + (n - 3); + maybeLogRawSentence(type, rawLine); if (strcmp(type, "GGA") == 0) { parseGga(fields, count); + } else if (strcmp(type, "GSA") == 0) { + parseGsa(fields, count); } else if (strcmp(type, "GSV") == 0) { parseGsv(fields, count); } else if (strcmp(type, "RMC") == 0) { @@ -329,21 +753,145 @@ static void pollGpsSerial() { } } -static void startGpsUart(uint32_t baud) { +static void showGpsLogHelp() { + Serial.println("Command list:"); + Serial.println(" help - show command menu"); + Serial.println(" stat - show current GPS log file info"); + Serial.println(" list - list files in /gpsdiag"); + Serial.println(" read - dump current GPS log"); + Serial.println(" clear - erase current GPS log"); +} + +static void gpsLogStat() { + Serial.printf("SPIFFS: %s\r\n", g_spiffsReady ? "ready" : "not ready"); + Serial.printf("Path: %s\r\n", kGpsLogPath); + if (!g_spiffsReady || !SPIFFS.exists(kGpsLogPath)) { + Serial.println("Current GPS log does not exist"); + return; + } + File file = SPIFFS.open(kGpsLogPath, FILE_READ); + if (!file) { + Serial.println("Unable to open current GPS log"); + return; + } + Serial.printf("Size: %u bytes\r\n", (unsigned)file.size()); + file.close(); + Serial.printf("Flash total=%u used=%u free=%u\r\n", + (unsigned)SPIFFS.totalBytes(), + (unsigned)SPIFFS.usedBytes(), + (unsigned)(SPIFFS.totalBytes() - SPIFFS.usedBytes())); +} + +static void gpsLogList() { + if (!g_spiffsReady) { + Serial.println("SPIFFS not ready"); + return; + } + File dir = SPIFFS.open(kGpsLogDir); + if (!dir || !dir.isDirectory()) { + Serial.printf("Unable to open %s\r\n", kGpsLogDir); + return; + } + Serial.printf("Files in %s:\r\n", kGpsLogDir); + File file = dir.openNextFile(); + while (file) { + Serial.printf(" %s (%u bytes)\r\n", file.name(), (unsigned)file.size()); + file = dir.openNextFile(); + } +} + +static void gpsLogRead() { + if (!g_spiffsReady || !SPIFFS.exists(kGpsLogPath)) { + Serial.println("Current GPS log is not available"); + return; + } + File file = SPIFFS.open(kGpsLogPath, FILE_READ); + if (!file) { + Serial.println("Unable to read current GPS log"); + return; + } + Serial.printf("Reading %s:\r\n", kGpsLogPath); + while (file.available()) { + Serial.write(file.read()); + } + if (file.size() > 0) { + Serial.println(); + } + file.close(); +} + +static void gpsLogClear() { + if (!g_spiffsReady) { + Serial.println("SPIFFS not ready"); + return; + } + File file = SPIFFS.open(kGpsLogPath, FILE_WRITE); + if (!file) { + Serial.println("Unable to clear current GPS log"); + return; + } + file.close(); + Serial.printf("Cleared %s\r\n", kGpsLogPath); +} + +static void processSerialCommand(const char* line) { + if (!line || line[0] == '\0') { + return; + } + Serial.printf("-->%s\r\n", line); + if (strcasecmp(line, "help") == 0) { + showGpsLogHelp(); + } else if (strcasecmp(line, "stat") == 0) { + gpsLogStat(); + } else if (strcasecmp(line, "list") == 0) { + gpsLogList(); + } else if (strcasecmp(line, "read") == 0) { + gpsLogRead(); + } else if (strcasecmp(line, "clear") == 0) { + gpsLogClear(); + } else { + Serial.println("Unknown command (help for list)"); + } +} + +static void pollSerialConsole() { + while (Serial.available() > 0) { + int c = Serial.read(); + if (c < 0) { + continue; + } + if (c == '\r' || c == '\n') { + if (g_serialLineLen > 0) { + g_serialLine[g_serialLineLen] = '\0'; + processSerialCommand(g_serialLine); + g_serialLineLen = 0; + } + continue; + } + if (g_serialLineLen + 1 < sizeof(g_serialLine)) { + g_serialLine[g_serialLineLen++] = (char)c; + } else { + g_serialLineLen = 0; + } + } +} + +static void startGpsUart(uint32_t baud, int rxPin, int txPin) { g_gpsSerial.end(); delay(20); g_gpsSerial.setRxBufferSize(1024); - g_gpsSerial.begin(baud, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); + g_gpsSerial.begin(baud, SERIAL_8N1, rxPin, txPin); g_gpsBaud = baud; + g_gpsRxPin = rxPin; + g_gpsTxPin = txPin; } static bool collectGpsTraffic(uint32_t windowMs, bool updateSd) { uint32_t start = millis(); - size_t bytesSeen = 0; + bool sawBytes = false; while ((uint32_t)(millis() - start) < windowMs) { - while (g_gpsSerial.available() > 0) { - (void)g_gpsSerial.read(); - bytesSeen++; + if (g_gpsSerial.available() > 0) { + sawBytes = true; } pollGpsSerial(); if (updateSd) { @@ -351,12 +899,12 @@ static bool collectGpsTraffic(uint32_t windowMs, bool updateSd) { } delay(2); } - return bytesSeen > 0 || g_gps.sawAnySentence; + return sawBytes || g_gps.sawAnySentence; } -static bool probeGpsAtBaud(uint32_t baud) { - startGpsUart(baud); - logf("Probing GPS at %lu baud...", (unsigned long)baud); +static bool probeGpsAtBaud(uint32_t baud, int rxPin, int txPin) { + startGpsUart(baud, rxPin, txPin); + logf("Probing GPS at %lu baud on RX=%d TX=%d...", (unsigned long)baud, rxPin, txPin); if (collectGpsTraffic(700, true)) { return true; } @@ -365,6 +913,7 @@ static bool probeGpsAtBaud(uint32_t baud) { g_gpsSerial.write("$PCAS06,0*1B\r\n"); g_gpsSerial.write("$PMTK605*31\r\n"); g_gpsSerial.write("$PQTMVERNO*58\r\n"); + g_gpsSerial.write("$PUBX,00*33\r\n"); g_gpsSerial.write("$PMTK353,1,1,1,1,1*2A\r\n"); g_gpsSerial.write("$PMTK314,0,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29\r\n"); @@ -373,32 +922,94 @@ static bool probeGpsAtBaud(uint32_t baud) { static void initialGpsProbe() { const uint32_t bauds[] = {GPS_BAUD, 115200, 38400, 57600, 19200}; - for (size_t i = 0; i < sizeof(bauds) / sizeof(bauds[0]); ++i) { - if (probeGpsAtBaud(bauds[i])) { - logf("GPS traffic detected at %lu baud", (unsigned long)g_gpsBaud); - return; + int pinCandidates[2][2] = { + {GPS_RX_PIN, GPS_TX_PIN}, + {34, 12}, // Legacy T-Beam UBLOX mapping. + }; + size_t pinCount = 1; + if (kExpectedGpsModule == GpsModuleKind::UBLOX && + !(GPS_RX_PIN == 34 && GPS_TX_PIN == 12)) { + pinCount = 2; + } + + for (size_t p = 0; p < pinCount; ++p) { + int rxPin = pinCandidates[p][0]; + int txPin = pinCandidates[p][1]; + for (size_t i = 0; i < sizeof(bauds) / sizeof(bauds[0]); ++i) { + if (probeGpsAtBaud(bauds[i], rxPin, txPin)) { + logf("GPS traffic detected at %lu baud on RX=%d TX=%d", + (unsigned long)g_gpsBaud, g_gpsRxPin, g_gpsTxPin); + return; + } } } logf("No GPS traffic detected during startup probe"); } +static uint32_t startupProbeWindowMs() { + return (kExpectedGpsModule == GpsModuleKind::UBLOX) ? 45000U : 20000U; +} + +static GpsModuleKind activeGpsModule() { + if (g_gps.module != GpsModuleKind::UNKNOWN) { + return g_gps.module; + } + return kExpectedGpsModule; +} + static uint8_t bestSatelliteCount() { - return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView; + uint32_t now = millis(); + + if ((uint32_t)(now - g_gps.satsUsedWindowMs) > kSatelliteWindowMs) { + g_gps.satsUsedWindowMax = g_gps.satsUsed; + } + if ((uint32_t)(now - g_gps.satsInViewWindowMs) > kSatelliteWindowMs) { + g_gps.satsInViewWindowMax = g_gps.satsInView; + } + + uint8_t used = (g_gps.satsUsedWindowMax > g_gps.satsUsed) ? g_gps.satsUsedWindowMax : g_gps.satsUsed; + uint8_t inView = (g_gps.satsInViewWindowMax > g_gps.satsInView) ? g_gps.satsInViewWindowMax : g_gps.satsInView; + return (used > inView) ? used : inView; +} + +static uint8_t displayedSatsUsed() { + if ((uint32_t)(millis() - g_gps.satsUsedWindowMs) > kFixFreshMs) { + return 0; + } + return g_gps.satsUsed; +} + +static uint8_t displayedSatsInView() { + if ((uint32_t)(millis() - g_gps.satsInViewWindowMs) > kFixFreshMs) { + return 0; + } + return g_gps.satsInView; +} + +static bool displayHasFreshUtc() { + return g_gps.hasValidUtc && (uint32_t)(millis() - g_gps.utcFixMs) <= kFixFreshMs; } static bool isUnsupportedGpsMode() { - return g_gps.module == GpsModuleKind::UBLOX; + GpsModuleKind active = activeGpsModule(); + if (kExpectedGpsModule == GpsModuleKind::UNKNOWN || active == GpsModuleKind::UNKNOWN) { + return false; + } + return active != kExpectedGpsModule; } static void reportStatusToSerial() { - uint8_t sats = bestSatelliteCount(); - logf("GPS module: %s", gpsModuleToString(g_gps.module).c_str()); + uint8_t satsUsed = displayedSatsUsed(); + uint8_t satsView = displayedSatsInView(); + logf("GPS module active: %s", gpsModuleToString(activeGpsModule()).c_str()); + logf("GPS module expected: %s", gpsModuleToString(kExpectedGpsModule).c_str()); logf("GPS sentences seen: %s", g_gps.sawAnySentence ? "yes" : "no"); - logf("GPS satellites: used=%u in-view=%u best=%u", - (unsigned)g_gps.satsUsed, - (unsigned)g_gps.satsInView, - (unsigned)sats); - logf("GPS can provide time from satellites: %s", g_gps.hasValidUtc ? "YES" : "NO"); + logf("GPS satellites: used=%u in-view=%u recent-best=%u", + (unsigned)satsUsed, + (unsigned)satsView, + (unsigned)bestSatelliteCount()); + logf("GPS can provide time from satellites: %s", displayHasFreshUtc() ? "YES" : "NO"); + appendGpsSnapshot("status"); } static void maybeAnnounceGpsTransitions() { @@ -406,22 +1017,28 @@ static void maybeAnnounceGpsTransitions() { return; } - uint8_t sats = bestSatelliteCount(); + uint8_t satsUsed = displayedSatsUsed(); + uint8_t satsView = displayedSatsInView(); + uint8_t sats = satsUsed > 0 ? satsUsed : satsView; bool hasSats = sats > 0; - bool hasUtc = g_gps.hasValidUtc; + bool hasUtc = displayHasFreshUtc(); if (!g_satellitesAcquiredAnnounced && !g_prevHadSatellites && hasSats) { String rtc = formatRtcNow(); char l2[28]; - snprintf(l2, sizeof(l2), "Satellites: %u", (unsigned)sats); - oledShowLines("GPS acquired", l2, "Satellite lock found", "Waiting for UTC...", rtc.c_str()); + char l3[28]; + snprintf(l2, sizeof(l2), "Used: %u", (unsigned)satsUsed); + snprintf(l3, sizeof(l3), "View: %u", (unsigned)satsView); + oledShowLines("GPS acquired", l2, l3, "Waiting for UTC...", rtc.c_str()); logf("Transition: satellites acquired (%u)", (unsigned)sats); + appendGpsSnapshot("satellites_acquired"); g_satellitesAcquiredAnnounced = true; } if (!g_timeAcquiredAnnounced && !g_prevHadValidUtc && hasUtc) { char line2[40]; char line3[28]; + char line4[28]; snprintf(line2, sizeof(line2), "%04u-%02u-%02u %02u:%02u:%02u", @@ -431,9 +1048,11 @@ static void maybeAnnounceGpsTransitions() { (unsigned)g_gps.utcHour, (unsigned)g_gps.utcMinute, (unsigned)g_gps.utcSecond); - snprintf(line3, sizeof(line3), "Satellites: %u", (unsigned)sats); - oledShowLines("GPS UTC acquired", line2, line3, ("Source: " + gpsModuleToString(g_gps.module)).c_str()); + snprintf(line3, sizeof(line3), "Used:%u View:%u", (unsigned)satsUsed, (unsigned)satsView); + snprintf(line4, sizeof(line4), "Source: %s", gpsModuleToString(activeGpsModule()).c_str()); + oledShowLines("GPS UTC acquired", line2, line3, line4); logf("Transition: GPS UTC acquired: %s", line2); + appendGpsSnapshot("utc_acquired"); g_timeAcquiredAnnounced = true; } @@ -443,15 +1062,22 @@ static void maybeAnnounceGpsTransitions() { static void drawMinuteStatus() { if (isUnsupportedGpsMode()) { - oledShowLines("GPS module mismatch", "UBLOX detected", "L76K required", "TODO: implement", "UBLOX support"); - logf("GPS module mismatch: UBLOX detected but this exercise currently supports only L76K (TODO)"); + oledShowLines("GPS module mismatch", + ("Expected: " + gpsModuleToString(kExpectedGpsModule)).c_str(), + ("Detected: " + gpsModuleToString(activeGpsModule())).c_str(), + "Check node profile"); + logf("GPS module mismatch: expected=%s detected=%s", + gpsModuleToString(kExpectedGpsModule).c_str(), + gpsModuleToString(activeGpsModule()).c_str()); return; } - uint8_t sats = bestSatelliteCount(); - if (g_gps.hasValidUtc) { + uint8_t satsUsed = displayedSatsUsed(); + uint8_t satsView = displayedSatsInView(); + if (displayHasFreshUtc()) { char line2[40]; char line3[28]; + char line4[28]; snprintf(line2, sizeof(line2), "%04u-%02u-%02u %02u:%02u:%02u", @@ -461,24 +1087,54 @@ static void drawMinuteStatus() { (unsigned)g_gps.utcHour, (unsigned)g_gps.utcMinute, (unsigned)g_gps.utcSecond); - snprintf(line3, sizeof(line3), "Satellites: %u", (unsigned)sats); - oledShowLines("GPS time (UTC)", line2, line3, ("Source: " + gpsModuleToString(g_gps.module)).c_str()); - logf("GPS time (UTC): %s satellites=%u", line2, (unsigned)sats); + snprintf(line3, sizeof(line3), "Used:%u View:%u", (unsigned)satsUsed, (unsigned)satsView); + snprintf(line4, sizeof(line4), "Source: %s", gpsModuleToString(activeGpsModule()).c_str()); + oledShowLines("GPS time (UTC)", line2, line3, line4); + logf("GPS time (UTC): %s used=%u view=%u", line2, (unsigned)satsUsed, (unsigned)satsView); return; } String rtc = formatRtcNow(); - if (sats > 0) { + if (satsUsed > 0 || satsView > 0) { char line2[28]; - snprintf(line2, sizeof(line2), "Satellites: %u", (unsigned)sats); - oledShowLines("GPS signal detected", line2, "GPS UTC not ready", "yet, using RTC", rtc.c_str()); - logf("Satellites detected (%u) but GPS UTC not ready. %s", (unsigned)sats, rtc.c_str()); + char line3[28]; + snprintf(line2, sizeof(line2), "Used: %u", (unsigned)satsUsed); + snprintf(line3, sizeof(line3), "View: %u", (unsigned)satsView); + oledShowLines("GPS signal detected", line2, line3, "GPS UTC not ready", rtc.c_str()); + logf("Satellites detected (used=%u view=%u) but GPS UTC not ready. %s", + (unsigned)satsUsed, + (unsigned)satsView, + rtc.c_str()); } else { oledShowLines("Unable to acquire", "satellites", "Take me outside so I", "can see satellites", rtc.c_str()); logf("Unable to acquire satellites. %s", rtc.c_str()); } } +static bool shouldRefreshDisplay() { + uint32_t now = millis(); + if (g_lastDisplayRefreshMs != 0 && (uint32_t)(now - g_lastDisplayRefreshMs) < kDisplayRefreshMinMs) { + return false; + } + uint8_t satsUsed = displayedSatsUsed(); + uint8_t satsView = displayedSatsInView(); + bool hasUtc = displayHasFreshUtc(); + if (!g_haveLastDrawnState) { + return true; + } + return satsUsed != g_lastDrawnSatsUsed || + satsView != g_lastDrawnSatsView || + hasUtc != g_lastDrawnValidUtc; +} + +static void markDisplayStateDrawn() { + g_lastDrawnSatsUsed = displayedSatsUsed(); + g_lastDrawnSatsView = displayedSatsInView(); + g_lastDrawnValidUtc = displayHasFreshUtc(); + g_haveLastDrawnState = true; + g_lastDisplayRefreshMs = millis(); +} + void setup() { Serial.begin(115200); delay(kSerialDelayMs); @@ -486,15 +1142,33 @@ void setup() { Serial.println("\r\n=================================================="); Serial.println("Exercise 09: GPS Time"); Serial.println("=================================================="); + Serial.printf("Build: %s %s\r\n", kBuildDate, kBuildTime); if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) { logf("PMU init failed"); } + g_spiffsReady = SPIFFS.begin(true); + if (!g_spiffsReady) { + logf("SPIFFS mount failed"); + } else if (!ensureGpsLogDirectory()) { + logf("GPS log directory create/open failed"); + } else { + File file = SPIFFS.open(kGpsLogPath, FILE_WRITE); + if (file) { + file.println("Exercise 09 GPS diagnostics"); + file.printf("Build: %s %s\r\n", kBuildDate, kBuildTime); + file.close(); + } else { + logf("GPS log file open failed: %s", kGpsLogPath); + } + } + Wire.begin(OLED_SDA, OLED_SCL); g_oled.setI2CAddress(OLED_ADDR << 1); g_oled.begin(); - oledShowLines("GPS Time exercise", "Booting..."); + String buildStamp = buildStampShort(); + oledShowLines("09_GPS_Time", buildStamp.c_str(), "Booting..."); SdWatcherConfig sdCfg{}; if (!g_sd.begin(sdCfg, nullptr)) { @@ -510,47 +1184,74 @@ void setup() { pinMode(GPS_1PPS_PIN, INPUT); #endif - startGpsUart(GPS_BAUD); - logf("GPS UART started: RX=%d TX=%d baud=%lu", GPS_RX_PIN, GPS_TX_PIN, (unsigned long)g_gpsBaud); + startGpsUart(GPS_BAUD, GPS_RX_PIN, GPS_TX_PIN); + logf("GPS UART started: RX=%d TX=%d baud=%lu", g_gpsRxPin, g_gpsTxPin, (unsigned long)g_gpsBaud); + appendGpsSnapshot("uart_started"); initialGpsProbe(); + appendGpsSnapshot("startup_probe_complete"); + maybeConfigureUblox(); oledShowLines("GPS startup probe", "Checking satellites", "and GPS time..."); + uint32_t probeWindowMs = startupProbeWindowMs(); + if (kExpectedGpsModule == GpsModuleKind::UBLOX) { + logf("UBLOX startup window: %lu ms (allowing cold start acquisition)", + (unsigned long)probeWindowMs); + } + uint32_t probeStart = millis(); uint32_t lastProbeUiMs = 0; - while ((uint32_t)(millis() - probeStart) < kGpsStartupProbeMs) { + while ((uint32_t)(millis() - probeStart) < probeWindowMs) { + pollSerialConsole(); pollGpsSerial(); g_sd.update(); uint32_t now = millis(); + if ((uint32_t)(now - g_lastGpsDiagnosticLogMs) >= kGpsDiagnosticLogMs) { + g_lastGpsDiagnosticLogMs = now; + appendGpsSnapshot("startup_wait"); + } if ((uint32_t)(now - lastProbeUiMs) >= 1000) { lastProbeUiMs = now; char l3[28]; char l4[30]; char l5[24]; - snprintf(l3, sizeof(l3), "Sats: %u", (unsigned)bestSatelliteCount()); - snprintf(l4, sizeof(l4), "Module: %s", gpsModuleToString(g_gps.module).c_str()); - snprintf(l5, sizeof(l5), "NMEA:%s %lu", g_gps.sawAnySentence ? "yes" : "no", (unsigned long)g_gpsBaud); + snprintf(l3, sizeof(l3), "Used:%u View:%u", (unsigned)displayedSatsUsed(), (unsigned)displayedSatsInView()); + snprintf(l4, sizeof(l4), "Module: %s", gpsModuleToString(activeGpsModule()).c_str()); + snprintf(l5, sizeof(l5), "NMEA:%s %d/%d", g_gps.sawAnySentence ? "yes" : "no", g_gpsRxPin, g_gpsTxPin); oledShowLines("GPS startup probe", "Checking satellites", l3, l4, l5); } delay(10); } reportStatusToSerial(); - g_prevHadSatellites = (bestSatelliteCount() > 0); - g_prevHadValidUtc = g_gps.hasValidUtc; + g_prevHadSatellites = (displayedSatsUsed() > 0 || displayedSatsInView() > 0); + g_prevHadValidUtc = displayHasFreshUtc(); drawMinuteStatus(); + markDisplayStateDrawn(); g_lastMinuteReportMs = millis(); + g_lastGpsDiagnosticLogMs = millis(); } void loop() { + pollSerialConsole(); pollGpsSerial(); g_sd.update(); maybeAnnounceGpsTransitions(); uint32_t now = millis(); + if (shouldRefreshDisplay()) { + drawMinuteStatus(); + markDisplayStateDrawn(); + } + if ((uint32_t)(now - g_lastGpsDiagnosticLogMs) >= kGpsDiagnosticLogMs) { + g_lastGpsDiagnosticLogMs = now; + appendGpsSnapshot("periodic"); + } if ((uint32_t)(now - g_lastMinuteReportMs) >= kMinuteMs) { g_lastMinuteReportMs = now; drawMinuteStatus(); + markDisplayStateDrawn(); + appendGpsSnapshot("minute_status"); } } diff --git a/exercises/10_Simple_GPS/platformio.ini b/exercises/10_Simple_GPS/platformio.ini index 02b2f71..3422f89 100644 --- a/exercises/10_Simple_GPS/platformio.ini +++ b/exercises/10_Simple_GPS/platformio.ini @@ -39,3 +39,9 @@ build_flags = build_flags = ${env.build_flags} -D NODE_LABEL=\"B\" + +[env:node_c] +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"C\" + -D GPS_UBLOX \ No newline at end of file diff --git a/exercises/15_RAM/src/main.cpp b/exercises/15_RAM/src/main.cpp index 17eb519..7d535c1 100644 --- a/exercises/15_RAM/src/main.cpp +++ b/exercises/15_RAM/src/main.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #ifndef NODE_LABEL #define NODE_LABEL "RAM" @@ -21,10 +22,11 @@ static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); -static const char *kTmpPath = "/tmp/volatile.txt"; -static const size_t kTmpFileCapacity = 4096; +static const char *kTmpPath = "/tmp/AMY_output.log"; +static const size_t kTmpFileCapacity = 32768; static char g_tmpFileBuffer[kTmpFileCapacity]; static size_t g_tmpFileSize = 0; +static unsigned g_tmpLineNumber = 0; static void oledShowLines(const char *l1, const char *l2 = nullptr, @@ -47,6 +49,51 @@ static size_t getAvailableRamBytes() return ESP.getFreeHeap(); } +static void getTimestamp(char *out, size_t outSize) +{ + const time_t now = time(nullptr); + if (now > 1700000000) { + struct tm tmNow; + localtime_r(&now, &tmNow); + snprintf(out, outSize, "%04d-%02d-%02d %02d:%02d:%02d", + tmNow.tm_year + 1900, + tmNow.tm_mon + 1, + tmNow.tm_mday, + tmNow.tm_hour, + tmNow.tm_min, + tmNow.tm_sec); + return; + } + + const uint32_t sec = millis() / 1000; + const uint32_t hh = sec / 3600; + const uint32_t mm = (sec % 3600) / 60; + const uint32_t ss = sec % 60; + snprintf(out, outSize, "uptime %02u:%02u:%02u", (unsigned)hh, (unsigned)mm, (unsigned)ss); +} + +static void appendTimestampLine() +{ + char timestamp[32]; + getTimestamp(timestamp, sizeof(timestamp)); + + char line[96]; + const int written = snprintf(line, sizeof(line), "%u, %s\r\n", g_tmpLineNumber + 1, timestamp); + if (written <= 0) { + return; + } + + const size_t lineLen = (size_t)written; + if (g_tmpFileSize + lineLen > kTmpFileCapacity - 1) { + Serial.println("Warning: /tmp log full, stopping writes"); + return; + } + + memcpy(g_tmpFileBuffer + g_tmpFileSize, line, lineLen); + g_tmpFileSize += lineLen; + g_tmpLineNumber++; +} + static void printRamStatus() { const size_t freeBytes = getAvailableRamBytes(); @@ -65,7 +112,7 @@ static void printRamStatus() char line4[32]; snprintf(line4, sizeof(line4), "Total: %u KB", (unsigned)(totalBytes / 1024U)); char line5[32]; - snprintf(line5, sizeof(line5), "Max alloc: %u KB", (unsigned)(maxAlloc / 1024U)); + snprintf(line5, sizeof(line5), "Lines: %u", (unsigned)g_tmpLineNumber); oledShowLines(line1, line2, line3, line4, line5); } @@ -85,6 +132,7 @@ static void printTmpFileStat() { Serial.printf("Path: %s\r\n", kTmpPath); Serial.printf("Size: %u bytes\r\n", (unsigned)g_tmpFileSize); + Serial.printf("Lines: %u\r\n", (unsigned)g_tmpLineNumber); Serial.printf("Capacity: %u bytes\r\n", (unsigned)kTmpFileCapacity); } @@ -219,5 +267,6 @@ void loop() } lastMs = now; + appendTimestampLine(); printRamStatus(); } diff --git a/exercises/16_PSRAM/README.md b/exercises/16_PSRAM/README.md new file mode 100644 index 0000000..06d8f39 --- /dev/null +++ b/exercises/16_PSRAM/README.md @@ -0,0 +1,32 @@ +# Exercise 16: PSRAM + +This exercise demonstrates usage of PSRAM (Pseudo SRAM) on an ESP32-S3 board, alongside regular RAM metrics. + +Behavior: +- Reports heap and PSRAM statistics every second over serial. +- Shows live heap and PSRAM status on the OLED display (both on same line). +- Allows you to write/append/read/clear data in a PSRAM-backed buffer (up to ~2MB). +- Designed as an extension of Exercise 15_RAM to explore larger volatile storage. + +Note: the exercise now targets a PSRAM-enabled ESP32-S3 board definition (`freenove_esp32_s3_wroom`). This board profile has 8MB flash + 8MB PSRAM, matching the T-Beam Supreme specifications. If your hardware differs, adjust accordingly. + +Sources: +- LilyGo T-Beam SUPREME datasheet/wiki: https://wiki.lilygo.cc/get_started/en/LoRa_GPS/T-Beam-SUPREME/T-Beam-SUPREME.html +- PlatformIO board definition: https://docs.platformio.org/page/boards/espressif32/freenove_esp32_s3_wroom.html +- Local PlatformIO board metadata: ~/.platformio/platforms/espressif32/boards/freenove_esp32_s3_wroom.json + +Build and upload: + +```bash +cd /usr/local/src/microreticulum/microReticulumTbeam/exercises/16_PSRAM +pio run -e amy -t upload +pio device monitor -b 115200 +``` + +Commands: +- `help` - show command menu +- `stat` - show PSRAM buffer state +- `read` - read PSRAM buffer contents +- `clear` - clear PSRAM buffer +- `write ` - write text to PSRAM buffer +- `append ` - append text to PSRAM buffer diff --git a/exercises/16_PSRAM/platformio.ini b/exercises/16_PSRAM/platformio.ini new file mode 100644 index 0000000..785b26c --- /dev/null +++ b/exercises/16_PSRAM/platformio.ini @@ -0,0 +1,54 @@ +; 20260403 ChatGPT +; Exercise 16_PSRAM + +[platformio] +default_envs = amy + +[env] +platform = espressif32 +framework = arduino +board = freenove_esp32_s3_wroom +monitor_speed = 115200 +extra_scripts = pre:scripts/set_build_epoch.py +lib_deps = + Wire + olikraus/U8g2@^2.36.4 + +build_flags = + -I ../../shared/boards + -I ../../external/microReticulum_Firmware + -D BOARD_MODEL=BOARD_TBEAM_S_V1 + -D OLED_SDA=17 + -D OLED_SCL=18 + -D OLED_ADDR=0x3C + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + +board_build.flash_mode = qio +board_build.psram = 1 +board_build.psram_type = spi +board_build.arduino.memory_type = qio_qspi + +[env:amy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"AMY\" + +[env:bob] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"BOB\" + +[env:cy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"CY\" + +[env:dan] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"DAN\" diff --git a/exercises/16_PSRAM/scripts/set_build_epoch.py b/exercises/16_PSRAM/scripts/set_build_epoch.py new file mode 100644 index 0000000..3011129 --- /dev/null +++ b/exercises/16_PSRAM/scripts/set_build_epoch.py @@ -0,0 +1,12 @@ +import time +Import("env") + +epoch = int(time.time()) +utc_tag = time.strftime("%Y%m%d_%H%M%S_z", time.gmtime(epoch)) + +env.Append( + CPPDEFINES=[ + ("FW_BUILD_EPOCH", str(epoch)), + ("FW_BUILD_UTC", '\\"%s\\"' % utc_tag), + ] +) diff --git a/exercises/16_PSRAM/src/main.cpp b/exercises/16_PSRAM/src/main.cpp new file mode 100644 index 0000000..ce3afa2 --- /dev/null +++ b/exercises/16_PSRAM/src/main.cpp @@ -0,0 +1,342 @@ +// 20260403 ChatGPT +// Exercise 16_PSRAM - Extended Exercise 15_RAM with PSRAM support + +#include +#include +#include +#include +#include + +#ifndef NODE_LABEL +#define NODE_LABEL "PSRAM" +#endif + +#ifndef OLED_SDA +#define OLED_SDA 17 +#endif +#ifndef OLED_SCL +#define OLED_SCL 18 +#endif +#ifndef OLED_ADDR +#define OLED_ADDR 0x3C +#endif + +static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); + +static const size_t kTmpFileCapacity = 2097152; // 2MB in PSRAM +static char *g_tmpFileBuffer = nullptr; +static size_t g_tmpFileSize = 0; +static unsigned g_tmpLineNumber = 0; + +static void oledShowLines(const char *l1, + const char *l2 = nullptr, + const char *l3 = nullptr, + const char *l4 = nullptr, + const char *l5 = nullptr) +{ + g_oled.clearBuffer(); + g_oled.setFont(u8g2_font_5x8_tf); + if (l1) g_oled.drawUTF8(0, 12, l1); + if (l2) g_oled.drawUTF8(0, 24, l2); + if (l3) g_oled.drawUTF8(0, 36, l3); + if (l4) g_oled.drawUTF8(0, 48, l4); + if (l5) g_oled.drawUTF8(0, 60, l5); + g_oled.sendBuffer(); +} + +static size_t getAvailableRamBytes() +{ + return ESP.getFreeHeap(); +} + +static size_t getTotalRamBytes() +{ + return ESP.getHeapSize(); +} + +static size_t getPSRAMFreeBytes() +{ + size_t freeBytes = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + if (freeBytes == 0 && ESP.getFreePsram() > 0) { + freeBytes = ESP.getFreePsram(); + } + return freeBytes; +} + +static size_t getPSRAMTotalBytes() +{ + size_t totalBytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); + if (totalBytes == 0 && ESP.getPsramSize() > 0) { + totalBytes = ESP.getPsramSize(); + } + return totalBytes; +} + +static void getTimestamp(char *out, size_t outSize) +{ + const time_t now = time(nullptr); + if (now > 1700000000) { + struct tm tmNow; + localtime_r(&now, &tmNow); + snprintf(out, outSize, "%04d-%02d-%02d %02d:%02d:%02d", + tmNow.tm_year + 1900, + tmNow.tm_mon + 1, + tmNow.tm_mday, + tmNow.tm_hour, + tmNow.tm_min, + tmNow.tm_sec); + return; + } + + const uint32_t sec = millis() / 1000; + const uint32_t hh = sec / 3600; + const uint32_t mm = (sec % 3600) / 60; + const uint32_t ss = sec % 60; + snprintf(out, outSize, "uptime %02u:%02u:%02u", (unsigned)hh, (unsigned)mm, (unsigned)ss); +} + +static void appendTimestampLine() +{ + if (!g_tmpFileBuffer) return; + + char timestamp[32]; + getTimestamp(timestamp, sizeof(timestamp)); + + char line[96]; + const int written = snprintf(line, sizeof(line), "%u, %s\r\n", g_tmpLineNumber + 1, timestamp); + if (written <= 0) { + return; + } + + const size_t lineLen = (size_t)written; + if (g_tmpFileSize + lineLen > kTmpFileCapacity - 1) { + Serial.println("Warning: PSRAM log full, stopping writes"); + return; + } + + memcpy(g_tmpFileBuffer + g_tmpFileSize, line, lineLen); + g_tmpFileSize += lineLen; + g_tmpLineNumber++; +} + +static void printRamStatus() +{ + const size_t freeRam = getAvailableRamBytes(); + const size_t totalRam = getTotalRamBytes(); + const size_t maxAllocRam = ESP.getMaxAllocHeap(); + + const size_t freePSRAM = getPSRAMFreeBytes(); + const size_t totalPSRAM = getPSRAMTotalBytes(); + + Serial.printf("RAM total=%u free=%u maxAlloc=%u | PSRAM total=%u free=%u\r\n", + (unsigned)totalRam, (unsigned)freeRam, (unsigned)maxAllocRam, + (unsigned)totalPSRAM, (unsigned)freePSRAM); + + char line1[32]; + char line2[32]; + char line3[32]; + char line4[32]; + char line5[32]; + + snprintf(line1, sizeof(line1), "Exercise 16 PSRAM"); + snprintf(line2, sizeof(line2), "Node: %s", NODE_LABEL); + + // Display format: "Free XXXKb/8.0Mbs" + const float psramMb = totalPSRAM / (1024.0f * 1024.0f); + const size_t ramKb = freeRam / 1024U; + snprintf(line3, sizeof(line3), "Free %uKb/%.1fMbs", (unsigned)ramKb, psramMb); + + snprintf(line4, sizeof(line4), "PSRAM: %u KB", (unsigned)(freePSRAM / 1024U)); + snprintf(line5, sizeof(line5), "Lines: %u", (unsigned)g_tmpLineNumber); + + oledShowLines(line1, line2, line3, line4, line5); +} + +static void showHelp() +{ + Serial.println("PSRAM command list:"); + Serial.println(" help - show this menu"); + Serial.println(" stat - show PSRAM buffer state"); + Serial.println(" read - read PSRAM buffer contents"); + Serial.println(" clear - clear PSRAM buffer contents"); + Serial.println(" write - write text to PSRAM buffer"); + Serial.println(" append - append text to PSRAM buffer"); +} + +static void printPSRAMFileStat() +{ + Serial.printf("PSRAM Buffer Capacity: %u bytes\r\n", (unsigned)kTmpFileCapacity); + Serial.printf("Current Size: %u bytes\r\n", (unsigned)g_tmpFileSize); + Serial.printf("Lines: %u\r\n", (unsigned)g_tmpLineNumber); + Serial.printf("PSRAM Total: %u bytes (%.2f MB)\r\n", (unsigned)getPSRAMTotalBytes(), + getPSRAMTotalBytes() / (1024.0f * 1024.0f)); + Serial.printf("PSRAM Free: %u bytes\r\n", (unsigned)getPSRAMFreeBytes()); +} + +static void printPSRAMFileContents() +{ + if (!g_tmpFileBuffer) { + Serial.println("PSRAM buffer not allocated"); + return; + } + + if (g_tmpFileSize == 0) { + Serial.println("PSRAM buffer is empty"); + return; + } + + Serial.print("PSRAM contents: "); + Serial.write((const uint8_t *)g_tmpFileBuffer, g_tmpFileSize); + if (g_tmpFileBuffer[g_tmpFileSize - 1] != '\n') + Serial.println(); +} + +static void setPSRAMFileContent(const char *text) +{ + if (!g_tmpFileBuffer) { + Serial.println("Error: PSRAM buffer not allocated"); + return; + } + + if (!text) { + g_tmpFileSize = 0; + g_tmpLineNumber = 0; + return; + } + const size_t newLen = strlen(text); + if (newLen > kTmpFileCapacity - 1) { + Serial.printf("Error: content too large (%u/%u)\r\n", (unsigned)newLen, (unsigned)kTmpFileCapacity); + return; + } + memcpy(g_tmpFileBuffer, text, newLen); + g_tmpFileSize = newLen; + g_tmpLineNumber = 0; +} + +static void appendPSRAMFileContent(const char *text) +{ + if (!g_tmpFileBuffer) { + Serial.println("Error: PSRAM buffer not allocated"); + return; + } + + if (!text || text[0] == '\0') return; + const size_t textLen = strlen(text); + if (g_tmpFileSize + textLen > kTmpFileCapacity - 1) { + Serial.printf("Error: append would exceed %u bytes\r\n", (unsigned)kTmpFileCapacity); + return; + } + memcpy(g_tmpFileBuffer + g_tmpFileSize, text, textLen); + g_tmpFileSize += textLen; +} + +static void processSerialCommand(const char *line) +{ + if (!line || line[0] == '\0') return; + + char tmp[384]; + strncpy(tmp, line, sizeof(tmp) - 1); + tmp[sizeof(tmp) - 1] = '\0'; + + char *cmd = strtok(tmp, " \t\r\n"); + if (!cmd) return; + + if (strcasecmp(cmd, "help") == 0) { + showHelp(); + return; + } + + if (strcasecmp(cmd, "stat") == 0) { + printPSRAMFileStat(); + return; + } + + if (strcasecmp(cmd, "read") == 0) { + printPSRAMFileContents(); + return; + } + + if (strcasecmp(cmd, "clear") == 0) { + g_tmpFileSize = 0; + g_tmpLineNumber = 0; + Serial.println("PSRAM buffer cleared"); + return; + } + + if (strcasecmp(cmd, "write") == 0 || strcasecmp(cmd, "append") == 0) { + const char *payload = line + strlen(cmd); + while (*payload == ' ' || *payload == '\t') payload++; + if (strcasecmp(cmd, "write") == 0) + setPSRAMFileContent(payload); + else + appendPSRAMFileContent(payload); + + Serial.printf("%s: %u bytes\r\n", cmd, + (unsigned)g_tmpFileSize); + return; + } + + Serial.println("Unknown command (help for list)"); +} + +void setup() +{ + Serial.begin(115200); + delay(800); + Serial.println("Exercise 16_PSRAM boot"); + + // Boot-time PSRAM diagnostics + Serial.printf("Boot PSRAM size: %u bytes\r\n", (unsigned)ESP.getPsramSize()); + Serial.printf("Boot PSRAM free: %u bytes\r\n", (unsigned)ESP.getFreePsram()); + + // Allocate PSRAM buffer + g_tmpFileBuffer = (char *)heap_caps_malloc(kTmpFileCapacity, MALLOC_CAP_SPIRAM); + if (!g_tmpFileBuffer) { + Serial.println("ERROR: Failed to allocate PSRAM buffer!"); + oledShowLines("Exercise 16_PSRAM", "Node: " NODE_LABEL, "PSRAM alloc FAILED"); + } else { + Serial.printf("PSRAM buffer allocated: %u bytes\r\n", (unsigned)kTmpFileCapacity); + } + + Wire.begin(OLED_SDA, OLED_SCL); + g_oled.setI2CAddress(OLED_ADDR << 1); + g_oled.begin(); + oledShowLines("Exercise 16_PSRAM", "Node: " NODE_LABEL, "Booting..."); + + delay(1000); +} + +void loop() +{ + static uint32_t lastMs = 0; + const uint32_t now = millis(); + + // check serial commands at all times + static char rxLine[384]; + static size_t rxLen = 0; + + while (Serial.available()) { + int c = Serial.read(); + if (c <= 0) continue; + if (c == '\r' || c == '\n') { + if (rxLen > 0) { + rxLine[rxLen] = '\0'; + processSerialCommand(rxLine); + rxLen = 0; + } + } else if (rxLen + 1 < sizeof(rxLine)) { + rxLine[rxLen++] = (char)c; + } + } + + if (now - lastMs < 1000) { + delay(10); + return; + } + lastMs = now; + + if (g_tmpFileBuffer) { + appendTimestampLine(); + } + printRamStatus(); +} diff --git a/exercises/17_Flash/README.md b/exercises/17_Flash/README.md new file mode 100644 index 0000000..cf123c6 --- /dev/null +++ b/exercises/17_Flash/README.md @@ -0,0 +1,32 @@ +# Exercise 17_Flash + +This exercise demonstrates using Flash storage as a persistent directory-like file system on an ESP32-S3 board. + +Behavior: +- Mounts SPIFFS at boot and reports total / used / free flash space. +- Ensures a flash directory at `/flash_logs` exists. +- Creates a new log file when the device boots, based on the current timestamp: `YYYYMMDD_HHMM.log`. +- Writes a timestamped line into the new log file once per second. +- Supports console commands to inspect the current file, read it, clear it, append or rewrite it, and list stored files. +- Files persist across reboots and are stored in flash. + +Build and upload: + +```bash +cd /usr/local/src/microreticulum/microReticulumTbeam/exercises/17_Flash +pio run -e amy -t upload +pio device monitor -b 115200 +``` + +Commands: +- `help` - show command menu +- `stat` - show flash / current file status +- `list` - list files under `/flash_logs` +- `read` - read the current flash file contents +- `clear` - clear the current flash file contents +- `write ` - overwrite the current flash file with text +- `append ` - append text to the current flash file + +Notes: +- If the current timestamp file name already exists, the exercise will append a numeric suffix to keep the file unique. +- On each reboot a new file is created so persistent flash logs accumulate. diff --git a/exercises/17_Flash/platformio.ini b/exercises/17_Flash/platformio.ini new file mode 100644 index 0000000..605b6c9 --- /dev/null +++ b/exercises/17_Flash/platformio.ini @@ -0,0 +1,50 @@ +; 20260403 ChatGPT +; Exercise 17_Flash + +[platformio] +default_envs = amy + +[env] +platform = espressif32 +framework = arduino +board = esp32-s3-devkitc-1 +monitor_speed = 115200 +extra_scripts = pre:scripts/set_build_epoch.py +lib_deps = + Wire + olikraus/U8g2@^2.36.4 + lewisxhe/XPowersLib@0.3.3 + +build_flags = + -I ../../shared/boards + -I ../../external/microReticulum_Firmware + -D BOARD_MODEL=BOARD_TBEAM_S_V1 + -D OLED_SDA=17 + -D OLED_SCL=18 + -D OLED_ADDR=0x3C + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + +[env:amy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"AMY\" + +[env:bob] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"BOB\" + +[env:cy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"CY\" + +[env:dan] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"DAN\" diff --git a/exercises/17_Flash/read_partition_bin.py b/exercises/17_Flash/read_partition_bin.py new file mode 100644 index 0000000..3dc21d7 --- /dev/null +++ b/exercises/17_Flash/read_partition_bin.py @@ -0,0 +1,11 @@ +import struct +with open('AMY_test_partitions_read.bin', 'rb') as f: + data = f.read() + seq0 = struct.unpack(' seq1: + print("→ app0 is active, new uploads go to app1") + else: + print("→ app1 is active, new uploads go to app0") diff --git a/exercises/17_Flash/scripts/set_build_epoch.py b/exercises/17_Flash/scripts/set_build_epoch.py new file mode 100644 index 0000000..44b46a0 --- /dev/null +++ b/exercises/17_Flash/scripts/set_build_epoch.py @@ -0,0 +1,12 @@ +import time +Import("env") + +epoch = int(time.time()) +utc_tag = time.strftime("%Y%m%d_%H%M%S_z", time.gmtime(epoch)) + +env.Append( + CPPDEFINES=[ + ("FW_BUILD_EPOCH", str(epoch)), + ("FW_BUILD_UTC", '\"%s\"' % utc_tag), + ] +) diff --git a/exercises/17_Flash/show_partition_table.py b/exercises/17_Flash/show_partition_table.py new file mode 100644 index 0000000..3bf3a7e --- /dev/null +++ b/exercises/17_Flash/show_partition_table.py @@ -0,0 +1,26 @@ +import struct + +with open('partitions_backup.bin', 'rb') as f: + data = f.read() + +print("Name | Type | SubType | Offset | Size | Flags") +print("-" * 75) + +for i in range(0, len(data), 32): + entry = data[i:i+32] + if len(entry) < 32: + break + + magic = struct.unpack(' +#include +#include +#include +#include + +#include "tbeam_supreme_adapter.h" + +#ifndef NODE_LABEL +#define NODE_LABEL "FLASH" +#endif + +#ifndef RTC_I2C_ADDR +#define RTC_I2C_ADDR 0x51 +#endif + +#ifndef OLED_SDA +#define OLED_SDA 17 +#endif +#ifndef OLED_SCL +#define OLED_SCL 18 +#endif +#ifndef OLED_ADDR +#define OLED_ADDR 0x3C +#endif + +static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); +static const char *kFlashDir = "/flash_logs"; +static char g_currentFilePath[64] = {0}; +static File g_flashFile; +static unsigned g_flashLineNumber = 0; +static XPowersLibInterface* g_pmu = nullptr; +static bool g_hasRtc = false; +static bool g_rtcLowVoltage = false; + +struct RtcDateTime { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + uint8_t weekday; +}; + +static void oledShowLines(const char *l1, + const char *l2 = nullptr, + const char *l3 = nullptr, + const char *l4 = nullptr, + const char *l5 = nullptr) +{ + g_oled.clearBuffer(); + g_oled.setFont(u8g2_font_5x8_tf); + if (l1) g_oled.drawUTF8(0, 12, l1); + if (l2) g_oled.drawUTF8(0, 24, l2); + if (l3) g_oled.drawUTF8(0, 36, l3); + if (l4) g_oled.drawUTF8(0, 48, l4); + if (l5) g_oled.drawUTF8(0, 60, l5); + g_oled.sendBuffer(); +} + +static size_t getFlashTotalBytes() +{ + return SPIFFS.totalBytes(); +} + +static size_t getFlashUsedBytes() +{ + return SPIFFS.usedBytes(); +} + +static size_t getFlashFreeBytes() +{ + const size_t total = getFlashTotalBytes(); + const size_t used = getFlashUsedBytes(); + return total > used ? total - used : 0; +} + +static uint8_t toBcd(uint8_t v) { + return ((v / 10U) << 4U) | (v % 10U); +} + +static uint8_t fromBcd(uint8_t b) { + return ((b >> 4U) * 10U) + (b & 0x0FU); +} + +static bool isRtcDateTimeValid(const RtcDateTime& dt) { + if (dt.year < 2020 || dt.year > 2099) return false; + if (dt.month < 1 || dt.month > 12) return false; + if (dt.day < 1 || dt.day > 31) return false; + if (dt.hour > 23 || dt.minute > 59 || dt.second > 59) return false; + return true; +} + +static bool rtcRead(RtcDateTime& out, bool& lowVoltageFlag) { + Wire1.beginTransmission(RTC_I2C_ADDR); + Wire1.write(0x02); + if (Wire1.endTransmission(false) != 0) { + return false; + } + + const uint8_t need = 7; + uint8_t got = Wire1.requestFrom((int)RTC_I2C_ADDR, (int)need); + if (got != need) { + return false; + } + + uint8_t sec = Wire1.read(); + uint8_t min = Wire1.read(); + uint8_t hour = Wire1.read(); + uint8_t day = Wire1.read(); + uint8_t weekday = Wire1.read(); + uint8_t month = Wire1.read(); + uint8_t year = Wire1.read(); + + lowVoltageFlag = (sec & 0x80U) != 0; + out.second = fromBcd(sec & 0x7FU); + out.minute = fromBcd(min & 0x7FU); + out.hour = fromBcd(hour & 0x3FU); + out.day = fromBcd(day & 0x3FU); + out.weekday = fromBcd(weekday & 0x07U); + out.month = fromBcd(month & 0x1FU); + uint8_t yy = fromBcd(year); + bool century = (month & 0x80U) != 0; + out.year = century ? (1900U + yy) : (2000U + yy); + + return true; +} + +static bool initRtc() { + if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) { + Serial.println("RTC init: PMU/i2c init failed"); + return false; + } + RtcDateTime now{}; + if (!rtcRead(now, g_rtcLowVoltage) || !isRtcDateTimeValid(now)) { + Serial.println("RTC init: no valid time available"); + return false; + } + g_hasRtc = true; + Serial.printf("RTC init: %04u-%02u-%02u %02u:%02u:%02u%s\r\n", + (unsigned)now.year, (unsigned)now.month, (unsigned)now.day, + (unsigned)now.hour, (unsigned)now.minute, (unsigned)now.second, + g_rtcLowVoltage ? " [LOW_BATT]" : ""); + return true; +} + +static bool getRtcTimestamp(char *out, size_t outSize) { + if (!g_hasRtc) { + return false; + } + RtcDateTime now{}; + bool low = false; + if (!rtcRead(now, low) || !isRtcDateTimeValid(now)) { + return false; + } + g_rtcLowVoltage = low; + snprintf(out, outSize, "%04u-%02u-%02u %02u:%02u:%02u", + now.year, + now.month, + now.day, + now.hour, + now.minute, + now.second); + return true; +} + +static void getTimestamp(char *out, size_t outSize) +{ + if (getRtcTimestamp(out, outSize)) { + return; + } + + const time_t now = time(nullptr); + if (now > 1700000000) { + struct tm tmNow; + localtime_r(&now, &tmNow); + snprintf(out, outSize, "%04d-%02d-%02d %02d:%02d:%02d", + tmNow.tm_year + 1900, + tmNow.tm_mon + 1, + tmNow.tm_mday, + tmNow.tm_hour, + tmNow.tm_min, + tmNow.tm_sec); + return; + } + + const uint32_t sec = millis() / 1000; + const uint32_t hh = sec / 3600; + const uint32_t mm = (sec % 3600) / 60; + const uint32_t ss = sec % 60; + snprintf(out, outSize, "uptime %02u:%02u:%02u", (unsigned)hh, (unsigned)mm, (unsigned)ss); +} + +static void getFilenameTimestamp(char *out, size_t outSize) +{ + if (g_hasRtc) { + RtcDateTime now{}; + bool low = false; + if (rtcRead(now, low) && isRtcDateTimeValid(now)) { + snprintf(out, outSize, "%04u%02u%02u_%02u%02u", + now.year, + now.month, + now.day, + now.hour, + now.minute); + return; + } + } + + const time_t now = time(nullptr); + if (now > 1700000000) { + struct tm tmNow; + localtime_r(&now, &tmNow); + snprintf(out, outSize, "%04d%02d%02d_%02d%02d", + tmNow.tm_year + 1900, + tmNow.tm_mon + 1, + tmNow.tm_mday, + tmNow.tm_hour, + tmNow.tm_min); + return; + } + + const uint32_t sec = millis() / 1000; + const uint32_t hh = sec / 3600; + const uint32_t mm = (sec % 3600) / 60; + const uint32_t ss = sec % 60; + snprintf(out, outSize, "uptime_%02u%02u%02u", (unsigned)hh, (unsigned)mm, (unsigned)ss); +} + +static String getNewFlashFilePath() +{ + char baseName[64]; + getFilenameTimestamp(baseName, sizeof(baseName)); + + char candidate[96]; + snprintf(candidate, sizeof(candidate), "%s/%s.log", kFlashDir, baseName); + if (!SPIFFS.exists(candidate)) { + return String(candidate); + } + + int suffix = 1; + do { + snprintf(candidate, sizeof(candidate), "%s/%s-%d.log", kFlashDir, baseName, suffix); + suffix += 1; + } while (SPIFFS.exists(candidate)); + + return String(candidate); +} + +static bool ensureFlashDirectory() +{ + if (SPIFFS.exists(kFlashDir)) { + return true; + } + if (!SPIFFS.mkdir(kFlashDir)) { + Serial.printf("Warning: failed to create %s\r\n", kFlashDir); + return false; + } + return true; +} + +static bool openCurrentFlashFile(bool truncate = false) +{ + if (g_flashFile) { + g_flashFile.close(); + } + + if (truncate) { + g_flashFile = SPIFFS.open(g_currentFilePath, FILE_WRITE); + } else { + g_flashFile = SPIFFS.open(g_currentFilePath, FILE_APPEND); + } + + if (!g_flashFile) { + Serial.printf("ERROR: cannot open %s\r\n", g_currentFilePath); + return false; + } + return true; +} + +static bool createFlashLogFile() +{ + if (!ensureFlashDirectory()) { + return false; + } + + String path = getNewFlashFilePath(); + path.toCharArray(g_currentFilePath, sizeof(g_currentFilePath)); + + g_flashFile = SPIFFS.open(g_currentFilePath, FILE_WRITE); + if (!g_flashFile) { + Serial.printf("ERROR: could not create %s\r\n", g_currentFilePath); + return false; + } + + const char *header = "FLASH log file created\r\n"; + g_flashFile.print(header); + g_flashFile.flush(); + g_flashLineNumber = 0; + return true; +} + +static void appendFlashTimestampLine() +{ + if (!g_flashFile) { + return; + } + + char timestamp[32]; + getTimestamp(timestamp, sizeof(timestamp)); + + char line[96]; + const int written = snprintf(line, sizeof(line), "%u, %s\r\n", g_flashLineNumber + 1, timestamp); + if (written <= 0) { + return; + } + + const size_t lineLen = (size_t)written; + if (g_flashFile.write(reinterpret_cast(line), lineLen) != lineLen) { + Serial.println("Warning: flash write failed"); + return; + } + g_flashFile.flush(); + g_flashLineNumber += 1; +} + +static void printFlashStatus() +{ + const size_t total = getFlashTotalBytes(); + const size_t used = getFlashUsedBytes(); + const size_t freeBytes = getFlashFreeBytes(); + + Serial.printf("FLASH total=%u used=%u free=%u\r\n", + (unsigned)total, (unsigned)used, (unsigned)freeBytes); + + char line1[32]; + char line2[32]; + char line3[32]; + char line4[32]; + char line5[32]; + + snprintf(line1, sizeof(line1), "Exercise 17 Flash"); + snprintf(line2, sizeof(line2), "Node: %s", NODE_LABEL); + snprintf(line3, sizeof(line3), "Free: %u KB", (unsigned)(freeBytes / 1024U)); + snprintf(line4, sizeof(line4), "Used: %u KB", (unsigned)(used / 1024U)); + snprintf(line5, sizeof(line5), "Lines: %u", (unsigned)g_flashLineNumber); + + oledShowLines(line1, line2, line3, line4, line5); +} + +static void showHelp() +{ + Serial.println("Flash command list:"); + Serial.println(" help - show this menu"); + Serial.println(" stat - show flash/file state"); + Serial.println(" rtc - show RTC time status"); + Serial.println(" list - list files in /flash_logs"); + Serial.println(" read - read current flash file"); + Serial.println(" clear - clear current flash file"); + Serial.println(" write - overwrite current flash file"); + Serial.println(" append - append text to current flash file"); +} + +static void printFlashFileStat() +{ + Serial.printf("Current file: %s\r\n", g_currentFilePath); + if (!SPIFFS.exists(g_currentFilePath)) { + Serial.println("Current file missing"); + return; + } + + File file = SPIFFS.open(g_currentFilePath, FILE_READ); + if (!file) { + Serial.println("Unable to open current file for stats"); + return; + } + + Serial.printf("Size: %u bytes\r\n", (unsigned)file.size()); + Serial.printf("Lines written: %u\r\n", (unsigned)g_flashLineNumber); + file.close(); +} + +static void printFlashFileContents() +{ + if (!SPIFFS.exists(g_currentFilePath)) { + Serial.println("Current flash file does not exist"); + return; + } + + File file = SPIFFS.open(g_currentFilePath, FILE_READ); + if (!file) { + Serial.println("Unable to open current flash file"); + return; + } + + if (file.size() == 0) { + Serial.println("Current flash file is empty"); + file.close(); + return; + } + + Serial.print("Flash file contents: "); + while (file.available()) { + Serial.write(file.read()); + } + if (file.size() > 0) { + Serial.println(); + } + file.close(); +} + +static void clearFlashFileContents() +{ + if (!SPIFFS.exists(g_currentFilePath)) { + Serial.println("No current file to clear"); + return; + } + + if (!openCurrentFlashFile(true)) { + return; + } + g_flashFile.close(); + g_flashLineNumber = 0; + openCurrentFlashFile(false); + Serial.println("Current flash file cleared"); +} + +static void setFlashFileContent(const char *text) +{ + if (!text) { + clearFlashFileContents(); + return; + } + + File file = SPIFFS.open(g_currentFilePath, FILE_WRITE); + if (!file) { + Serial.println("Unable to overwrite current flash file"); + return; + } + + file.print(text); + file.close(); + openCurrentFlashFile(false); + g_flashLineNumber = 0; +} + +static void appendFlashFileContent(const char *text) +{ + if (!text || text[0] == '\0') { + return; + } + + if (!openCurrentFlashFile(false)) { + return; + } + + g_flashFile.print(text); + g_flashFile.flush(); +} + +static void listFlashFiles() +{ + File dir = SPIFFS.open(kFlashDir); + if (!dir || !dir.isDirectory()) { + Serial.printf("Unable to list files in %s\r\n", kFlashDir); + return; + } + + Serial.printf("Files in %s:\r\n", kFlashDir); + File file = dir.openNextFile(); + while (file) { + Serial.printf(" %s (%u bytes)\r\n", file.name(), (unsigned)file.size()); + file = dir.openNextFile(); + } + dir.close(); +} + +static void processSerialCommand(const char *line) +{ + if (!line || line[0] == '\0') return; + + char tmp[384]; + strncpy(tmp, line, sizeof(tmp) - 1); + tmp[sizeof(tmp) - 1] = '\0'; + + char *cmd = strtok(tmp, " \t\r\n"); + if (!cmd) return; + + if (strcasecmp(cmd, "help") == 0) { + showHelp(); + return; + } + + if (strcasecmp(cmd, "stat") == 0) { + printFlashFileStat(); + return; + } + + if (strcasecmp(cmd, "rtc") == 0) { + if (g_hasRtc) { + char ts[32]; + if (getRtcTimestamp(ts, sizeof(ts))) { + Serial.printf("RTC now: %s\r\n", ts); + if (g_rtcLowVoltage) { + Serial.println("RTC low-voltage flag is set"); + } + } else { + Serial.println("RTC present but time read failed"); + } + } else { + Serial.println("RTC unavailable"); + } + return; + } + + if (strcasecmp(cmd, "list") == 0) { + listFlashFiles(); + return; + } + + if (strcasecmp(cmd, "read") == 0) { + printFlashFileContents(); + return; + } + + if (strcasecmp(cmd, "clear") == 0) { + clearFlashFileContents(); + return; + } + + if (strcasecmp(cmd, "write") == 0 || strcasecmp(cmd, "append") == 0) { + const char *payload = line + strlen(cmd); + while (*payload == ' ' || *payload == '\t') payload++; + if (strcasecmp(cmd, "write") == 0) + setFlashFileContent(payload); + else + appendFlashFileContent(payload); + + Serial.printf("%s: %s\r\n", cmd, payload); + return; + } + + Serial.println("Unknown command (help for list)"); +} + +void setup() +{ + Serial.begin(115200); + delay(800); + Serial.println("Exercise 17_Flash boot"); + + initRtc(); + + if (!SPIFFS.begin(true)) { + Serial.println("ERROR: SPIFFS mount failed"); + oledShowLines("Exercise 17_Flash", "Node: " NODE_LABEL, "SPIFFS mount FAILED"); + } else { + Serial.println("SPIFFS mounted successfully"); + if (createFlashLogFile()) { + Serial.printf("Current flash file: %s\r\n", g_currentFilePath); + } + } + + Wire.begin(OLED_SDA, OLED_SCL); + g_oled.setI2CAddress(OLED_ADDR << 1); + g_oled.begin(); + oledShowLines("Exercise 17_Flash", "Node: " NODE_LABEL, "Booting..."); + + delay(1000); +} + +void loop() +{ + static uint32_t lastMs = 0; + const uint32_t now = millis(); + + static char rxLine[384]; + static size_t rxLen = 0; + + while (Serial.available()) { + int c = Serial.read(); + if (c <= 0) continue; + if (c == '\r' || c == '\n') { + if (rxLen > 0) { + rxLine[rxLen] = '\0'; + processSerialCommand(rxLine); + rxLen = 0; + } + } else if (rxLen + 1 < sizeof(rxLine)) { + rxLine[rxLen++] = (char)c; + } + } + + if (now - lastMs < 1000) { + delay(10); + return; + } + lastMs = now; + + if (g_flashFile) { + appendFlashTimestampLine(); + } + printFlashStatus(); +}