Commit Graph
36 Commits
Author SHA1 Message Date
torlando-agent[bot] 9d5cc32b82 fix: avoid duplicate delivery-state persistence 2026-07-27 04:24:58 +00:00
torlando-agent[bot] 0506452f77 docs: record message-store stack contract 2026-07-26 15:43:54 +00:00
torlando-agent[bot] e885f27c77 fix: prefer opportunistic delivery for short UI messages 2026-07-26 15:30:45 +00:00
torlando-agent[bot] 12d25b2acc fix: preserve retryable messages on storage failure 2026-07-24 19:02:06 +00:00
torlando-agent[bot] 4e2e74296b fix: fail closed when message persistence fails 2026-07-24 18:22:35 +00:00
torlando-agent[bot] 0f3b39bc19 fix: serialize LXST audio teardown on loopTask 2026-07-23 02:11:39 +00:00
torlando-agent[bot] efd3b0bb8f fix: address Greptile safety findings 2026-07-22 19:28:32 -04:00
torlando-agent[bot] 31631b0afd fix: make full-duplex voice fit live device memory 2026-07-22 13:28:23 -04:00
torlando-agent[bot] 9e151877cc fix(voice): stabilize LXST call lifecycle and add E2E harness 2026-07-22 10:25:45 -04:00
torlando-agent[bot]andClaude Opus 4.8 bd7b357b96 fix(audio): ES7210 mic gain saturation + heap-starvation reboots + raw-mic diagnostic harness
Confirmed fixes:
- LVGL hybrid-allocator threshold 1024->256 (lib/lv_mem_hybrid.h): moves ~45KB of
  small LVGL objects to PSRAM, fixing internal-heap starvation that made the
  call/loopback pipeline intermittently fail to allocate and reboot mid-call
  (internal free 71->116KB, largest contiguous block 61->106KB).
- ES7210 mic PGA gain 21dB->12dB (lxst_audio.cpp): 21dB saturated the ADC -- an
  rms-7003 noise floor in silence + 0x8000 negative-rail spikes + a spectral-peak
  shift that masqueraded as a "+17% pitch warp". At 12dB idle silence is clean (rms 48).

Diagnostic harness (PYXIS_TEST_HOOKS, test-only): T:RAWMIC[stage], T:REG, and
T:RECORD/T:DUMPREC add a raw-mic UDP tap plus a reliable PSRAM recorder with a
checksummed serial transfer, and T:LOOPBACK wiring, for objective mic-capture
analysis (offboard Whisper STT scoring).

Exploratory / still unresolved: captured speech remains garbled (oscillating static,
likely an ES7210 sigma-delta capture instability -- under investigation). Includes a
16kHz/main-PLL clock path + MICBIAS exploration; the "warp" was the gain artifact, not
a clock issue, so the 16kHz path can later be simplified back to 8kHz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-24 17:21:25 -04:00
torlando-agent[bot]andClaude Opus 4.8 36d93f965f fix: announce/sync interval units+defaults, live Settings clock, OTA repair
Settings/clock (user-reported):
- Announce interval: default 1h (was 60s); the field is now MINUTES (label
  "Announce Interval (min):", populate /60, read *60).
- Prop sync interval: default 4h (was 1h); the field is now HOURS (label
  "Prop Sync Interval (hrs):", populate /3600, read *3600); log prints hours.
- Settings clock now ticks live: SettingsScreen::tick() (throttled ~1s) is hooked
  into UIManager::update() like the announce-list tick, so the Time/GPS/system
  readouts refresh while the screen is open instead of being a static snapshot.

OTA repair (was broken two ways; diagnosed via an ultracode workflow):
- patch_nimble.py hardcoded the `tdeck` libdeps path, so building any OTHER env
  (tdeck-ota gets its own libdeps tree) never received the NimBLE patch that
  DEFINES nimble_host_reset_reason -> undefined-reference link error. Made it
  env-aware (env.get("PIOENV",...)), matching the four sibling pre-scripts. Also
  restores 3 NimBLE stability patches that were silently missing from OTA builds.
- ArduinoOTA onStart synchronously called TCPClientInterface::stop() (blocks up to
  30s), overrunning espota's ~10s connect-back window -> "No response from device"
  (phase 2), and its un-fed watchdog tripped reboots. Made onStart non-blocking.
- [env:tdeck-ota] upload_command invoked a non-existent tools/espota.py -> switched
  to upload_protocol=espota.
- Do NOT WiFi.setSleep(false): the ESP32 requires WiFi modem-sleep when WiFi+BT
  coexist (aborts at boot otherwise) -- documented inline.

Verified on device: tdeck-ota links; OTA over WiFi completes 100% (Result: OK) and
the device reboots clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-22 23:28:48 -04:00
torlando-agent[bot]andClaude Opus 4.8 142ff464e8 fix: populate announces end-to-end (ratchet name, blob leak, path-table, safe refresh)
Pins microReticulum to the fork's pyxis-fixes-on-0.4.1 @ b582986 (three 0.4.1-graft
regression fixes) and adds the pyxis-side pieces.

microReticulum fixes (via the pin):
- Identity::recall now accounts for the announce ratchet (matches validate_announce)
  — it read app_data 32 bytes early for ratchet-bearing announces (Sideband/Columba),
  garbling peer display names.
- DestinationEntry caps persisted random_blobs to PERSIST_RANDOM_BLOBS (16); the set
  was never trimmed, growing path entries ~12B/re-announce past microStore's 1024B
  ceiling so put() rejected them (empty path table) — and under a raised ceiling
  bloated the FS until compaction crashed.
- Transport mirrors learned paths into _path_table so path_table() (the UI announce
  list + T:PATHS) reflects them; the microStore migration stopped populating it.

pyxis pieces:
- -DUSTORE_DEFAULT_MAX_RECS=400 caps the live path-store record count (defense-in-depth).
- AnnounceListScreen::refresh() defers to a main-loop tick(): the gather (path-table
  iteration + per-entry recall/app_data) runs off the LVGL lock, serialized with the
  main-loop path-table writes, instead of racing them on the LVGL task and blocking
  past the 5s lock timeout — which hung/crashed on open once the table was non-empty.

Verified on device: display names correct, T:PATHS 0 -> 11, no rejections, no
crashes, announce list opens cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-21 02:58:45 -04:00
torlando-agent[bot]andClaude Opus 4.8 f8bbfba3e3 feat(chat): render newest messages first, stream the rest in on the main loop
Open a conversation by rendering only the 3 newest messages synchronously (fast),
then stream the rest of the first page in BG_FILL_BATCH (2) at a time from
UIManager::update() via tick_background_fill(). Each step holds the LVGL lock
only briefly, so a large conversation no longer freezes the UI or trips
LVGLLock's 5s timeout (which previously asserted/crashed). Runs on the main loop
rather than a task because MessageStore shares one _json_doc between save and
load and is not safe for concurrent access.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-20 00:27:54 -04:00
torlando-agent[bot]andClaude Opus 4.8 70d4aa6be9 feat: graft pyxis onto upstream microReticulum 0.4.1
Repins microReticulum + microLXMF onto the upstream-0.4.1 graft and adapts
pyxis to the new src/microReticulum/ layout and 0.4.x APIs. The far-diverged
0.3.0 fork's Resource/Transport/Identity work is subsumed by upstream's
reimplementation; only the still-needed fixes ride on the pinned branches
(PKCS7/HMAC/X25519 crypto -- proven byte-identical to python RNS 1.3.1 --
Packet link-proof callback, Identity short-sig guard, and the bz2 layer +
decompress-on-receive in Resource::assemble()).

Consumer-side changes:
- platformio.ini: pin microReticulum @2f21fee (pyxis-fixes-on-0.4.1) and
  microLXMF @33760d0 (chore/microreticulum-0.4.1-layout); bump microStore
  ceea8f5 -> c5fb69d (0.4.x requires the new BasicFileStore::init API);
  -std=gnu++11 -> gnu++17 (upstream requires C++17).
- Namespace all microReticulum includes (angle + quote) to <microReticulum/...>
  for the relocated layout; shim-local Utilities/Stream.h|Print.h preserved.
- Interface::send_outgoing now returns bool: update TCP/BLE/SX1262/Auto
  overrides with correct success/failure returns.
- SDArchiveFileSystem::init(bool reformatOnFail=true) to match new microStore.
- Static Transport::get_path_table() -> path_table(); instance getter unchanged.
- Remove duplicate shim Cryptography/BZ2 (microReticulum provides it now; keep
  lib/libbz2 as the ESP32 bzlib provider).
- patch_littlefs_paths.py: normalize microStore's LittleFS adapter paths to a
  leading "/" -- ESP32 Arduino LittleFS rejects "./"-prefixed paths, which
  silently broke the path store (no peer paths learned, all messaging blocked).

Validated on T-Deck Plus: builds (RAM 27.5% / Flash 77.7%), boots stable
(no WDT/panic), and a full on-device LXMF e2e (DIRECT + OPPORTUNISTIC +
bz2-compressed-Resource receive) passes 5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 15:49:44 -04:00
torlando-agent[bot]andClaude Opus 4.8 ed3ec541fd fix: defer ConversationListScreen display-name writes out of the LVGL lock
refresh() called set_display_name() (a microStore/LittleFS write) while the
LVGL lock was held by UIManager::update(). On a cold-boot announce burst,
refresh() writes a name per newly-seen peer, serially stalling the LVGL render
task for the combined I/O time. Mirror the on_message_received fix: accumulate
the write-throughs in _pending_name_writes during refresh(), and flush them at
the top of UIManager::update() before it takes the LVGL lock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 13:14:05 -04:00
torlando-techandClaude Opus 4.7 9699a15a2d fix(ui): coalesce ConversationListScreen refresh on inbound flood
on_message_received() called \`_conversation_list_screen->refresh()\`
unconditionally per message. Under propagation-sync flood (50+ queued
messages delivered back-to-back) that's 50 full LVGL list redraws,
each holding LVGL_LOCK across:
  - lv_obj_clean(_list) + reload conversations from MessageStore
  - per-conversation container construction
  - per-peer Identity::recall_app_data calls for display names
  - SPI display flush

The refreshes serialize behind LVGL_LOCK, the SPI bus stays saturated
flushing dirty regions, and pyxis's USB CDC TX buffer overflows
because the main loop is too busy with display work to drain the
serial-output FIFO. Harness commands time out as a side effect.

Replace the per-message refresh with a coalescing flag drained from
update():
  - on_message_received only sets _pending_conversation_refresh
  - update() refreshes at most once per 750ms
  - update() also skips the refresh entirely when the user isn't on
    the conversation list (show_conversation_list refreshes when they
    navigate back, so nothing's lost — a chat-screen user gets quiet
    background ingestion)

Validated under a 2-round LXMF soak: direct + opportunistic short and
medium messages all PASS round-trip with the coalescing in effect.
Propagation flake is unchanged (known timing issue between pyxis
upload and bot's 8s sync poll, not a UI regression).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:59:40 -04:00
torlando-techandClaude Opus 4.7 f5d9e6a480 feat(lxst): T:LXSTDEST + T:ANNLXST hooks for callee interop tests
Adds two harness hooks for testing pyxis as the LXST callee:

- T:LXSTDEST: returns pyxis's lxst.telephony destination hash. The
  caller-side bot (real LXST.Telephony.Telephone in this case) needs
  this to dial pyxis. Backed by a new test_lxst_dest_hex() accessor on
  UIManager that reads _lxst_destination and returns hex (or empty if
  the destination isn't registered yet).

- T:ANNLXST: forces a fresh announce of the lxst.telephony destination.
  The TCP-reconnect handler in main.cpp:963 only announces LXMF, so on
  a fresh boot the lxst.telephony destination is missing from rnsd's
  cache and link requests addressed to it get dropped with "no known
  path to final destination" (rnsd debug log). The harness pings this
  before each callee test to ensure rnsd has a fresh path.

Also adds INFO logging to announce_lxst() mirroring the LXMF announce
log (Announcing destination: <hash> ... announce sent), so it's
visible in tdeck-side traces when an announce actually went out vs.
silently no-oped.

Validated: bot dials, pyxis transitions IDLE -> INCOMING_RINGING (UI
shows incoming-call screen), harness sends T:CALL_ANSWER, state
becomes ACTIVE. (Audio path crashes shortly after via the same
Ed25519 announce-validation bug — fix landed in microReticulum
f4bad06, but PIO's libdeps cache had been holding a stale copy; a
manual rm -rf .pio/libdeps/tdeck/microReticulum was needed to pick it
up. Tracked in #73.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:29:05 -04:00
torlando-techandClaude Opus 4.7 612f44e274 feat(lxst): Codec2-700C (ULBW) default profile + T:CALL_ANSWER hook
Adds two profile constants beyond the existing LXST_PROFILE_LBW (0x30,
Codec2-3200): LXST_PROFILE_VLBW (0x20, Codec2-1600) and
LXST_PROFILE_ULBW (0x10, Codec2-700C). Default is now ULBW — a 700C
frame fits comfortably inside an SF7-9 LoRa packet, which is the
target medium for pyxis voice. The previous 3200bps default was 4.5x
larger and unsuitable for the radio path.

Profile is selectable at runtime via T:CALL_PROFILE [hex]. Replaces
five hardcoded LXST_PROFILE_LBW sites: three audio-init paths in
call_process_signal and two profile-negotiation send_signal calls.

Adds T:CALL_ANSWER for harness pyxis-as-callee testing — sets the
same _call_answer_pending flag the UI button does so call_answer()
runs on the main loop in its proper context. Validates against
real LXST.Telephony.Telephone callers.

Validated: pyxis dialed real LXST upstream Telephone bot, negotiated
ULBW end-to-end, reached STATUS_ESTABLISHED, decoded frames cleanly
both directions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:07:41 -04:00
torlando-techandClaude Opus 4.7 12fd28b67c test(lxst): bidirectional audio content-fidelity validation
Extends the LXST harness from frame-flow only (#62) and decoder QoS
only (#63) to a full content-fidelity test: a known 1kHz sine wave
flows in BOTH directions through the Codec2-3200 round-trip, and
the harness asserts the decoded RMS at each end matches expected
energy within tolerance (after Codec2's lossy speech-codec behavior).

Firmware additions (under PYXIS_TEST_HOOKS):

I2SCapture
  setInjectSine(enabled, freq=1000, amp=0.5)
    Replaces mic input with a phase-continuous synthesized sine.
    Bypasses ES7210 capture and the voice filter chain so the
    encoder sees pure samples — peer's decoded RMS validates that
    pyxis's TX path delivers content.

I2SPlayback
  pcmSampleCount(), pcmSumSquares()
    Decoded-PCM energy accumulators, fed from each successful
    Codec2 decode. uint64 sumsq holds ~2³⁴ frames before overflow,
    far longer than any test call.

LXSTAudio + UIManager (test-only)
  captureSetInjectSine, playbackPcmSampleCount, playbackPcmSumSquares
  test_call_set_inject_sine, test_call_pcm_sample_count,
  test_call_pcm_sum_squares

Serial T: hooks (main.cpp)
  T:CALL_INJECT <on|off> [freq] [amp_pct]
    Drive the capture-side injection from the harness.
  T:CALL_QOS now also returns pcm_n + pcm_ss
    Harness divides + sqrts to RMS for content validation.

Validated with /tmp/lxst_call_harness.py + /tmp/lxst_call_bot.py
(scripts vault-local per the no-PII rule):

  pyxis_rms = 6363, bot_rms = 1930, decode_fail = 0
  PASS: bidirectional audio + content-fidelity validated

The empirical RMS floor is 800 (pycodec2 self-test on 1kHz amp 0.5
yields ~1400; pyxis decoder hits ~6300; bot decoder hits ~1900 —
all far above the ~5-50 silence floor). Codec2 is a speech codec
so pure-tone round-trip is naturally lossy; the test gates on
"audio bytes carry actual content energy", not lossless round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 03:11:09 -04:00
torlando-techandClaude Opus 4.7 8b608ec258 test(lxst): T:CALL/T:CALL_QOS hooks + Codec2 decode counters
Adds the on-device test surface the LXST voice-call harness needs to
drive an end-to-end audio flow + QoS validation against a Mac-side
softphone bot. Both hooked behind PYXIS_TEST_HOOKS so production
firmware is unaffected.

T: serial commands (main.cpp):
  T:CALL <hex>      — initiate call (hex = peer's lxst.telephony dest)
  T:CALL_STATE      — current call FSM state name
  T:CALL_HANGUP     — tear down the active call
  T:CALL_STATS      — audio frame counters: tx, rx, state
  T:CALL_QOS        — Codec2 decoder QoS: decode_ok, decode_fail, state
  T:HASIDENTITY <h> — bool, distinct from T:HASPATH because path_store
                     and known_destinations are populated by separate
                     code paths (announce can land in one before the
                     other; harness has to wait for both).

UIManager additions (under PYXIS_TEST_HOOKS):
  test_call_initiate, test_call_hangup, test_call_state_name,
  test_call_audio_tx_count, test_call_audio_rx_count,
  test_call_decode_ok, test_call_decode_fail.

I2SPlayback / LXSTAudio additions (always on — counters are tiny):
  decodeOkCount(), decodeFailCount(), resetCounters() on I2SPlayback
  surface the Codec2 decode success/fail rate. LXSTAudio re-exports.
  Each writeEncodedPacket call increments exactly one counter so the
  ratio is "wire-level audio fidelity" of the peer's encoder.

Validated:
  Frame-flow soak (12s call): pyxis tx=73 rx=72, bot tx=230 rx=72.
  QoS soak (12s call): pyxis tx=73 rx=76, decode_ok=81 decode_fail=0,
    bot tx=227 rx=73.

Harness scripts that drive these hooks live in the local Obsidian
vault (under 80 Assistant/Memory/pyxis/soak_scripts/) — they encode
LAN-specific state and aren't checked in. See the vault's
automated_soak_testing.md for the full procedure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 02:45:11 -04:00
torlando-techandClaude Opus 4.7 3194af0639 feat(pyxis): wire SD card as MessageStore archive tier
Adds Hardware::TDeck::SDArchiveFileSystem — a microStore::FileSystem
adapter that piggybacks on the SD card already mounted by SDAccess::init
and serializes every operation through the shared SPI bus mutex (so it
cooperates with display + LoRa traffic on HSPI).

In setup_lxmf, after MessageStore is constructed, attach the SD as the
archive tier at "/lxmf-archive". When the SD is missing the message
store falls back to delete-on-cull (still bounded, just no historical
scrollback). Without this the LittleFS partition (1.875MB) fills in
~30 min of sustained receive and trips lfs_alloc divide-by-zero.

Also moves the LVGL_LOCK in UIManager::on_message_received to AFTER
the save_message call. The 5s LVGL lock timeout was tripping when
LittleFS compaction stalled the save for several seconds, panicking
the loop watchdog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:51:04 -04:00
torlando-techandClaude Opus 4.7 ba18c32c04 feat(test-hooks): T:SEND/SENDOPP/SENDPROP + T:SETPROP/SYNCPROP harness API
Adds a USB-CDC serial command interface gated behind PYXIS_TEST_HOOKS
that lets a host-side harness drive pyxis end-to-end without UI taps.
Built specifically to run /tmp/tdeck_harness.py and prove LXMF DIRECT,
OPPORTUNISTIC, and PROPAGATED delivery against a Mac-side echo bot
over the Mac's rnsd + lxmd.

Commands (all newline-terminated, replies T:OK or T:ERR):

  T:DEST                          — pyxis's delivery dest hash (hex)
  T:ID                            — pyxis's identity hash (hex)
  T:ANN                           — force an announce
  T:PATHS                         — count + dump in-memory path table
  T:HASPATH <hex>                 — Transport::has_path + in-memory check
  T:RECALL <hex>                  — Identity::recall_app_data hex
  T:SEND <hex> <text>             — outbound DIRECT LXMessage
  T:SENDOPP <hex> <text>          — outbound OPPORTUNISTIC LXMessage
  T:SENDPROP <hex> <text>         — outbound PROPAGATED LXMessage
  T:SETPROP <hex> <stamp_cost>    — set outbound propagation node
  T:SYNCPROP                      — request_messages_from_propagation_node
  T:SYNCSTATE                     — current PR_* sync state
  T:STATE <msg_hash>              — LXMessage state for a tracked send
  T:RX                            — drain inbound RX ring
  T:RXCLR                         — clear RX ring

Build-flag side:

  -DPYXIS_TEST_HOOKS               — gates all of the above
  -DPYXIS_TEST_TCP_HOST="..."      — hard-overrides NVS tcp_host so the
                                     harness's rnsd is the only target
  -DPYXIS_TEST_TCP_PORT=...        — same for tcp_port

Also: replaces `lib_extra_dirs = deps/microReticulum` with an explicit
`file://~/repos/microReticulum` lib_dep. lib_extra_dirs
caused PIO to compile microReticulum twice (once through the extra
dir, once through microLXMF's transitive auto-fetch), producing two
copies of `Transport::_path_store` in BSS. Different translation
units linked against different statics, so put() and exists() landed
in different in-memory indexes. Symptom: `T:HASPATH` returned 0 even
when the previous announce's `[ustore] put: wrote key` log line was
visible. Single source path → single static → consistent reads.

`patch_filestore.py` is committed but commented out in extra_scripts
— used during diagnostic when the dual-static issue was being
triaged. Easy to re-arm if FileStore put/exists drift recurs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:03:42 -04:00
torlando-tech 02ceeda75b Track A.8: re-vendor shim from ca355e5; UniversalFileSystem→microStore;
relocate shim to lib/; pyxis lib API renames

Compile-tier graft progress on top of 40e561f. Key changes:

- Re-vendor lib/microreticulum-shim/ (was src-shim/) from the *actual*
  ca355e5 commit content rather than the stale "feat/t-deck HEAD"
  /tmp clone the previous commit pulled from. Recovers process_sync()
  on LXMRouter and MEMORY_MONITOR_POLL macro that were missing.
- Move src-shim/ → lib/microreticulum-shim/ + add library.json so
  PlatformIO discovers the .cpp files and links them. Was previously
  only on the include path; the .cpps weren't in the build.
  (This unblocks the 30+ undefined-reference linker errors for LXMF
  and Instrumentation symbols.)
- Drop -Isrc-shim/Utilities (/Cryptography/Instrumentation) from
  build_flags — they were over-broad and put our Stream.h on the
  GLOBAL header path, breaking Arduino's Wire.cpp which has
  `class TwoWire: public Stream`. -Ilib/microreticulum-shim alone
  resolves subdir lookups via <Cryptography/X.h>, <Utilities/Y.h>.

- UniversalFileSystem migrated to microStore::Adapters::SPIFFSFileSystem
  (activated by -DUSTORE_USE_SPIFFS). Vanilla upstream microReticulum
  @ 0.3.0 deleted RNS::FileSystem in favor of microStore. Pyxis's
  lib/universal_filesystem/ is now dead code on this build path.

- pyxis lib API renames for the post-graft world:
    SDLogger.cpp:        RNS::setLogCallback -> RNS::set_log_callback
    AnnounceListScreen:  Transport::get_destination_table ->
                         Transport::get_path_table
                         Transport::DestinationEntry ->
                         RNS::Persistence::DestinationEntry
    UIManager.cpp:       _lxst_destination ctor explicit RNS::Type::NONE
                         (vanilla Destination has no default ctor)
                         Identity::mark_persistent calls disabled w/
                         restoration TODO
    ConversationListScreen: Interface::get_rssi/get_stats calls
                            disabled (the methods are non-virtual
                            on BLEInterface/SX1262Interface post
                            de-virtualization in a0ff631)

Compile is clean against the fixed-cryptography submodule pin; current
failure layer is fork-only Type::Channel constants referenced by the
vendored shim's Buffer/ChannelData files. That's the next session's
problem — see pyxis_microReticulum_graft_spike_findings.md for the
plan options (most likely: remove Channel/Buffer/ChannelData/Ratchet
from the shim, since LXMF doesn't use Channel anyway per the 2026-05-04
investigation).
2026-05-05 01:39:44 -04:00
davidcranorandClaude Sonnet 4.6 827ff2eb42 Fix cross-platform build: replace ${PROJECT_DIR} with relative paths
platformio.ini:
- Replace -I${PROJECT_DIR}/lib, -I${PROJECT_DIR}/deps/... with relative
  paths (-Ilib, -Ideps/...) in both tdeck-bluedroid and tdeck environments;
  ${PROJECT_DIR} is mangled on Windows inside build_flags, causing include
  paths to resolve inside the PlatformIO builder directory instead of the
  project root
- Remove hardcoded -I.pio/libdeps/tdeck/TinyGPSPlus/src and
  -I.pio/libdeps/tdeck/NimBLE-Arduino/src; these paths reference generated
  cache, break on fresh clones, and are redundant with lib_ldf_mode = deep+
- Fix OTA upload_command: replace python3 with $PYTHONEXE so it resolves
  to PlatformIO's bundled Python on Windows, macOS, and Linux

src/main.cpp, lib/tdeck_ui/UI/LXMF/UIManager.cpp:
- Change #include "tone/Tone.h" to #include "Tone.h"; PlatformIO
  automatically adds -Ilib/tone for local libraries, making the
  subdirectory prefix unnecessary and broken when -Ilib is not effective

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:19:32 -05:00
torlando-techandClaude Opus 4.6 609a3bc62b LXMF propagation sync, manual node entry, and status improvements
Propagation sync (microReticulum submodule):
- Fix msgpack interop: send nil (not 0) for per_transfer_limit so
  Python server doesn't reject all messages as exceeding "0 KB limit"
- Fix Resource response routing: extract request_id from packed data
  when not present in Resource advertisement, route to pending request
  callback instead of generic concluded handler
- Fix Link::request() to manually build packed arrays, avoiding
  Bytes::to_msgpack() BIN-wrapping that breaks protocol interop

UI enhancements:
- PropagationNodesScreen: manual node entry via 32-char hex hash in
  search field, with paste support and radio button selection
- StatusScreen: display stamp cost from propagation node
- UIManager: NVS persistence for selected propagation node, proactive
  path request on node selection, sync state machine with timeout
  handling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:03:32 -05:00
torlando-techandClaude Opus 4.6 6744eb136d LXST voice call stability: fix hangup crash, signal queue, TX pump, mic tuning
- Fix use-after-free crash on hangup: set _call_state=IDLE before deleting
  _lxst_audio, preventing pump_call_tx() (runs without LVGL lock) from
  accessing freed memory
- Replace single-slot _call_signal_pending with 8-element ring buffer queue
  to prevent signal loss when CONNECTING+ESTABLISHED arrive in rapid succession
- Extract TX pump into pump_call_tx() called right after reticulum->loop()
  for low-latency audio TX without LVGL lock dependency (was buried at step 10)
- Tune ES7210 mic gain to 21dB (was 15dB) to improve Codec2 input level
  without ADC clipping that occurred at 24dB
- I2S capture: use APLL for accurate 8kHz clock, direct 8kHz sampling
  (no more 16→8kHz decimation), DMA 16x64 for encode burst headroom
- Reduce Reticulum log verbosity to LOG_INFO (was LOG_TRACE)
- BLE: add ble_hs_sched_reset() tiered recovery before reboot on desync,
  widen supervision timeout to 4.0s for WiFi coexistence
- Add UDP multicast log broadcasting and OTA flash support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 10:57:14 -05:00
torlando-techandClaude Opus 4.6 ddd19a04db Fix LXST TX audio wire format to match Columba's expected batch size
Columba's native OboePlaybackEngine ring buffer expects exactly
frameSamples (1600 for Codec2 3200 mode) decoded samples per
writeEncodedPacket call = 10 sub-frames of 160 samples each.

Changes:
- Batch exactly 10 sub-frames per fixarray element (82 bytes each:
  codec_type + mode_header + 10*8 raw bytes)
- Up to 2 batches per msgpack packet, matching Columba C2C format
- Proper fixarray wrapping for multi-batch, bare bin8 for single
- Add codec_type byte (0x02) prefix per batch element
- Respond to PREFERRED_PROFILE negotiation with LBW (Codec2 3200)
- Add capture diagnostics (raw PCM peaks, I2S dump, rate logging)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 10:57:14 -05:00
torlando-techandClaude Opus 4.6 4e1f379d94 Persist only contacts to flash, mark on send/receive
Only destinations that have exchanged messages are written to SPIFFS.
UIManager marks destinations as persistent on send_message() and
on_message_received(). Reduces persist time from 40-50s to <1s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:17:28 -05:00
torlando-techandClaude Opus 4.6 a499a2b30a Persistence reliability: NimBLE crash fix, atomic save, fast persist
NimBLE crash fix:
- Patch ble_hs.c assert(0) in BLE_HS_SYNC_STATE_BRINGUP timer handler
  via pre-build script (patch_nimble.py). The assert fires when a timer
  callback races with host re-sync — harmless, but kills the ESP32 and
  corrupts any file writes in progress.

Persistence fixes (in microReticulum submodule):
- Atomic save: write to temp file then rename, protecting existing data
- Fast persist: 5s after dirty flag instead of waiting 60s interval
- Corrupt file recovery: delete invalid files, recover from temp files
- INFO-level logging for load/save visibility

Other:
- Wrap LXMF announce in try/catch for crash safety
- Call Identity::should_persist_data() from main loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 01:44:38 -05:00
torlando-techandClaude Opus 4.6 d58ac9573f Switch to Codec2 3200 (LBW profile) for better voice quality
- Change codec from 1600bps to 3200bps (2x bitrate, 20ms frames)
- Signal LBW profile (0x30) instead of VLBW (0x20) to Columba
- TX batch size 10 frames (10×160=1600 samples matches Columba LBW)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 19:28:50 -05:00
torlando-techandClaude Opus 4.6 3655a8a9eb LXST audio fixes: split codecs, TDM stride-4, anti-alias, TX batching
- Split Codec2 into separate encode/decode instances to eliminate mutex
  contention between capture task (core 0) and main thread decode
- Fix TDM deinterleave: stride-4 (was stride-2) for [CH0,CH1] at 16kHz
  to produce 8kHz mono output matching Codec2's expected sample rate
- Add 2-tap anti-aliasing average before decimation to reduce >4kHz alias
- Add hard limiter at ±16000 to prevent ADC clipping artifacts
- TX batching: send exactly 8 Codec2 frames per packet (2560 decoded
  samples) to match Columba's PacketRingBuffer.frameSamples requirement
- Add capture diagnostics: sample rate, raw/downsampled peaks, hex dumps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:25:38 -05:00
torlando-techandClaude Opus 4.6 6dbdd4854b LXST voice TX: link routing fix, frame batching, TX diagnostics
- Fix link packet routing in microReticulum to only transmit on the
  link's attached interface (was flooding all interfaces including LoRa)
- Batch up to 8 Codec2 frames per Reticulum packet to reduce per-packet
  encryption/transport overhead (~3 packets/sec instead of 25)
- Add TX diagnostics: hex dump of first 2 packets, per-interval stats
  with link status, idle/drop warnings
- Audio now reaches remote end (TX=325 frames in 12s test call)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 02:11:13 -05:00
torlando-techandClaude Opus 4.6 06743635eb Fix ES7210 mic capture: use LilyGO library with slave mode
Replace custom ES7210 register-level driver with the verbatim LilyGO
T-Deck Plus ES7210 library. The custom driver was writing MODE_CONFIG
and clock doubler registers (master mode path) which broke the ES7210's
clock chain, causing all-zero PCM output. The LilyGO library in slave
mode leaves those registers at power-on defaults, which is correct since
the ESP32 I2S master provides MCLK/BCLK/LRCK.

Also includes LXST voice call protocol improvements:
- LXST IN destination for receiving calls
- Announce handler for tracking voice-capable peers
- Path request before outgoing calls
- Throttled SPIFFS saves in microReticulum (dirty flag)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 01:42:47 -05:00
torlando-techandClaude Opus 4.6 f73fbb2568 Fix LXST voice call interop with Python LXST/Columba
- Fix thread safety: defer Reticulum link callbacks (packet, link_closed)
  to call_update() which runs under LVGL lock, preventing crashes from
  concurrent LVGL access across cores
- Fix outgoing call signal handling: store link reference and re-register
  packet/link_closed callbacks in on_call_link_established so signals
  are actually received
- Fix call answer screen freeze: update UI before blocking audio init
  (I2S/ES7210/Codec2 setup) so screen renders immediately
- Fix audio direction: use startPlayback() (speaker RX) instead of
  startCapture() (mic TX) so received audio is actually heard
- Add msgpack wire format for LXST signalling and audio frames
- Add LXST IN destination for receiving calls + announce support
- Add incoming call UI (Answer/Reject buttons) on CallScreen
- Add path request before outgoing call link establishment
- Add LXST announce handler registration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:00:07 -05:00
torlando-techandClaude Opus 4.6 8f265da0bb LXST voice call UI and state machine
- Add CallScreen with ring/active/ended states and call controls
- Add call state machine to UIManager (link establish, identify, ring, answer)
- Add call button to ChatScreen header
- Add call initiate/hangup with Reticulum Link management
- Add StatusScreen call status display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:41:40 -05:00
torlando-techandClaude Opus 4.6 ac6ceca9f8 Initial commit: standalone Pyxis T-Deck firmware
Split T-Deck firmware from microReticulum examples/lxmf_tdeck/ into its
own repo. microReticulum is consumed as a git submodule dependency pinned
to feat/t-deck. All include paths updated from relative symlinks to bare
includes resolved via library build flags.

Both tdeck (NimBLE) and tdeck-bluedroid environments compile successfully.
Licensed under AGPLv3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 19:48:33 -05:00