Meshtastic app: full LoRa transmit and receive on HackRF - #3306
Meshtastic app: full LoRa transmit and receive on HackRF#3306verhogladov wants to merge 2 commits into
Conversation
5e4d6a0 to
7b45bd3
Compare
|
Attaching the build package and screenshots up front, since I see those are asked for
The screenshots are from a PortaPack H4M, and from the Meshtastic phone app talking to Happy to answer anything, and to rework the baseband arrangement if you would rather |
There was a problem hiding this comment.
Pull request overview
This PR introduces a full Meshtastic implementation for PortaPack that performs LoRa PHY TX/RX on the HackRF (no external LoRa radio), plus a substantial host-side bench (“lora_bench”) for validation and on-air automation. It also adds supporting firmware changes to make room in flash (moving some baseband images to SD), improve out-of-memory diagnostics, and reduce optional feature RAM pressure.
Changes:
- Add a Meshtastic app + LoRa baseband TX/RX images, with new M0↔M4 messages and baseband API hooks.
- Add
tools/lora_bench/host tests and hardware-driven scripts to validate framing/crypto/regions/TX synthesis. - Add SD-card fallback loading for baseband images moved out of SPI flash, plus small UX/memory tweaks (QRCode scaling, GeoMap tile-cache lazy allocation, OOM size reporting).
Reviewed changes
Copilot reviewed 85 out of 89 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/lora_bench/watch_rx.py | Script to monitor Heltec RX output and report decoded frames. |
| tools/lora_bench/tx_heltec.py | Builds/encodes a Meshtastic packet and emits IQ for Heltec interop testing. |
| tools/lora_bench/tx_chain_test.cpp | Host-side reference for TX encode chain (chips output). |
| tools/lora_bench/test_tx_lengths.py | Regression test for firmware TX encoder across payload lengths. |
| tools/lora_bench/test_synth_staircase.py | Compares firmware chirp synthesis vs smooth reference; explores fixes. |
| tools/lora_bench/test_mesh_telemetry.cpp | Host test for Meshtastic telemetry codec. |
| tools/lora_bench/test_mesh_regions.cpp | Host test for region/preset/slot frequency model + channel hash rules. |
| tools/lora_bench/test_mesh_partial.cpp | Host test for truncated-payload decode behavior. |
| tools/lora_bench/test_mesh_crypto.cpp | Host test for AES-CTR convention and round-trip. |
| tools/lora_bench/test_lora_rssi.cpp | Host test validating RSSI approximation vs log10. |
| tools/lora_bench/test_lora_ref.cpp | Host test verifying chunked reference chirp generation matches full-pass. |
| tools/lora_bench/sync_test.py | Front-end sync/preamble grid test to validate sync word reading. |
| tools/lora_bench/sweep_one.py | Automates bidirectional on-air testing for a single preset. |
| tools/lora_bench/shot.py | Captures PortaPack screen via console to PNG (RGB888 hex rows). |
| tools/lora_bench/sf12_off.py | Offline SF12 decode prototype against recorded air captures. |
| tools/lora_bench/selftest_demod.py | Isolates/deterministically validates demod primitive. |
| tools/lora_bench/screenshot.py | Alternate screen capture via screenframeshort. |
| tools/lora_bench/README.md | Documentation for the bench tests and scripts. |
| tools/lora_bench/preset_set.py | UI-driven preset switching automation with readback verification. |
| tools/lora_bench/ports.py | Serial port discovery and env override support for hardware scripts. |
| tools/lora_bench/ocr.py | Screen-to-text using firmware font glyph tables. |
| tools/lora_bench/mc.py | Send one console command to PortaPack and print response. |
| tools/lora_bench/Makefile | Builds/runs host-side tests against firmware sources. |
| tools/lora_bench/lora_inspect.py | Capture inspection utility (bursts, occupied BW, spectrogram). |
| tools/lora_bench/lora_gold.py | Reference decoder port of gr-lora_sdr chain (“authoritative” decode). |
| tools/lora_bench/lora_encode.py | Reference encoder inverse of the verified decode chain. |
| tools/lora_bench/lora_decode.py | Golden CSS decoder/oracle for capture inspection and symbol dumping. |
| tools/lora_bench/lora_crc.py | CRC-based brute forcing/verification helper for decode-chain cracking. |
| tools/lora_bench/lora_crack.py | Brute-force chain cracking tool for offline analysis. |
| tools/lora_bench/lf_sf11_stream.py | Streaming SF11 decoder prototype matching firmware constraints. |
| tools/lora_bench/lf_sf11_stream_finealign.py | Streaming SF11 fine-alignment variant buffering bins not samples. |
| tools/lora_bench/lf_sf11_ref.py | Reference SFD-based sync recipe to port to firmware. |
| tools/lora_bench/lf_process.py | Extracts/decimates LongFast bursts and dumps iq16 slices. |
| tools/lora_bench/hreset.py | Host-side HackRF reset helper for exiting HackRF mode. |
| tools/lora_bench/hlisten.py | Uses meshtastic python to print received packets/text from Heltec. |
| tools/lora_bench/heltec_rx_monitor/heltec_rx_monitor.ino | Arduino sketch turning Heltec into raw LoRa RX monitor. |
| tools/lora_bench/hackrf_io.py | ctypes-based HackRF capture/replay using SDR++ bundled libs. |
| tools/lora_bench/flash.py | ctypes-based SPI flash tool (read/verify/write firmware). |
| tools/lora_bench/diag.py | Sync-word discriminator + sharpness calibration script. |
| tools/lora_bench/capture.sh | Captures IQ while commanding Heltec to TX known texts. |
| tools/lora_bench/.gitignore | Ignores captures/artifacts and built host tests. |
| firmware/common/spi_image.hpp | Adds LoRa baseband image tags. |
| firmware/common/message.hpp | Adds LoRa M0↔M4 message IDs and message classes. |
| firmware/common/dsp_fir_taps.hpp | Adds FIR taps for LoRa BW125 decimation stage. |
| firmware/common/chibios_cpp.cpp | Records failed allocation size in global g_oom_size. |
| firmware/baseband/proc_lora_tx.hpp | Introduces LoRa TX processor interface and TX frame structure. |
| firmware/baseband/CMakeLists.txt | Adds LoRa RX/TX basebands; emits .img chunks; moves several images to SD. |
| firmware/application/ui/ui_qrcode.cpp | Makes QR code scale/centering depend on widget size. |
| firmware/application/ui/ui_geomap.hpp | Lazy-allocates tile cache; adds marker-link drawing API. |
| firmware/application/ui/ui_geomap.cpp | Implements marker-link drawing and lazy tile-cache allocation. |
| firmware/application/ui_navigation.cpp | Registers Meshtastic (“Mesh”) app in navigation list. |
| firmware/application/event_m0.hpp | Adds getter for display sleep state. |
| firmware/application/debug.cpp | Prints OOM request size + free core bytes on panic screen. |
| firmware/application/core_control.cpp | Adds SD-card fallback baseband loader and refactors M4 init from chunks. |
| firmware/application/CMakeLists.txt | Adds Meshtastic app sources and mesh modules to build. |
| firmware/application/bitmap.hpp | Adds Meshtastic icon bitmap. |
| firmware/application/baseband_api.hpp | Adds LoRa config/packet baseband API. |
| firmware/application/baseband_api.cpp | Implements LoRa config/packet baseband API functions. |
| firmware/application/apps/mesh/mesh_pki.hpp | Declares PKI primitives for Meshtastic direct messages. |
| firmware/application/apps/mesh/mesh_nodedb.hpp | Adds node database structures and memory-bounded limits. |
| firmware/application/apps/mesh/mesh_nodedb.cpp | Implements node DB operations, eviction, and route log. |
| firmware/application/apps/mesh/mesh_crypto.hpp | Adds Meshtastic AES-CTR crypto interface + default PSK constants. |
| firmware/application/apps/mesh/docs/streaming_rx_demod.md | Design note on streaming demodulator and buffering constraints. |
| firmware/application/app_settings.hpp | Adds explicit SettingsManager::save() API. |
| firmware/application/app_settings.cpp | Implements SettingsManager::save() (persist immediately). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| void send_lora_packet(const uint8_t* data, size_t len) { | ||
| LoRaPacketMessage message{}; | ||
| message.length = static_cast<uint8_t>(std::min(len, LoRaPacketMessage::MAX_DATA)); | ||
| std::memcpy(message.data, data, message.length); | ||
| send_message(&message); | ||
| } |
| int scale = (r.width() - 12) / qrcode.size; | ||
| if (scale < 2) scale = 2; | ||
| const int quiet = scale * 2; | ||
| const int side = qrcode.size * scale + quiet * 2; | ||
|
|
||
| // Centre the code in the space it was given: the modules only come in whole pixels, | ||
| // so the drawn square is rarely the exact width of the widget and used to sit hard | ||
| // against the left edge. | ||
| const int ox = (r.width() > side) ? (r.width() - side) / 2 : 0; | ||
| const int oy = (r.height() > side) ? (r.height() - side) / 2 : 0; | ||
| display.fill_rectangle(Rect(r.left() + ox, r.top() + oy, side, side), Color::white()); |
| bool BMPFileCache::ensure_slots() { | ||
| if (slots_) return true; | ||
| constexpr size_t need = sizeof(std::array<Slot, SlotsCount>); | ||
| if (chCoreStatus() < need + 1024u) return false; | ||
| slots_ = new std::array<Slot, SlotsCount>{}; | ||
| return slots_ != nullptr; | ||
| } | ||
|
|
||
| void BMPFileCache::release() { | ||
| delete slots_; | ||
| slots_ = nullptr; | ||
| } |
| // File carries FatFs' 512-byte sector buffer. On the heap even here: this thread's | ||
| // kilobyte is meant for the FatFs call frames, not for a 560-byte local. | ||
| auto f = std::make_unique<File>(); | ||
| if (f->open(std::filesystem::path{path})) return 0; // Optional<Error> truthy = failed | ||
| const uint64_t fsize = f->size(); | ||
| if (fsize < 13 || fsize > 48u * 1024u) return 0; // header(12)+data; M4 region is 32 KiB | ||
|
|
||
| // chHeapAlloc rather than new: this firmware's operator new panics with "Out of | ||
| // Memory" when the heap is short, and a baseband that will not fit is meant to be | ||
| // a NoImg - the same thing that happens when the file is missing - not a dead | ||
| // device. Core memory is handed out and never returned, so whether ten kilobytes | ||
| // are available here depends on which apps have already run this session; a | ||
| // restart is what gets them back. | ||
| // | ||
| // chHeapAlloc is 8-byte aligned, so the uint32 chunk fields stay word-aligned. | ||
| uint8_t* const raw = static_cast<uint8_t*>(chHeapAlloc(0x0, static_cast<size_t>(fsize))); | ||
| if (!raw) return 0; | ||
| const auto rd = f->read(raw, fsize); | ||
| if (!rd || rd.value() != fsize) { | ||
| chHeapFree(raw); | ||
| return 0; | ||
| } | ||
| req->raw = raw; | ||
| req->size = static_cast<size_t>(fsize); | ||
| return 1; |
| const auto* chunk = reinterpret_cast<const spi_flash::chunk_t*>(req.raw); | ||
| if (!(chunk->tag == image_tag)) { | ||
| chHeapFree(req.raw); | ||
| return false; | ||
| } | ||
| m4_start_from_chunk(chunk, to, full_reset); | ||
| // The M4 is running from its own RAM now; the compressed copy has done its job. | ||
| chHeapFree(req.raw); | ||
| return true; |
| // LoRa CSS transmitter. | ||
| // Full encode chain: whiten -> Hamming(CR) -> diagonal interleave -> Gray encode -> CSS chirp. | ||
| // Frame: preamble (8 up) + sync word + SFD (2 down) + header (8 syms, CR4/8) + payload symbols. | ||
| // Meshtastic sync word = 0x12: upchirps at chips (0x1 << (SF-4)) and (0x2 << (SF-4)). | ||
|
|
7b45bd3 to
ec05804
Compare
|
All six review findings are fixed, and I checked each one against the code before believing it. All six were real. Three of them could take the device down rather than step aside, and all three are in paths that run for people who never open this app. Two of your suggested changes were better than what I had written, and I took them. The header size is derived with One suggestion I did not take, and it is worth saying why. The sync word comment fix changes 0x12 to 0x2B but keeps the chips at Checked on hardware: the map opens and closes four times with the free core and the process stack high-water unmoved, so the placement new and the explicit destructor round-trip cleanly; the QR draws inside its frame; Looking Glass still loads its baseband off the card; and Weather, whose image is larger than the memory left by the time you have navigated to it, reports |
…routing A Meshtastic app for the PortaPack: chat, encrypted channels, a node list, telemetry, positions, a map, and mesh routing. The physical layer is written from scratch. A HackRF has nothing in it that understands LoRa: it hands over a stream of samples and takes one back. So preamble detection, timing and frequency synchronisation, dechirping, the Fourier transform, Gray coding, interleaving, Hamming and whitening are all here, in both directions, on the M4 core, inside 54 KB of buffers and a 1.024 ms deadline per buffer. Seven of the nine modem presets are confirmed in both directions against a stock Meshtastic node used as an outside source of truth. Also confirmed on the air: encrypted custom channels both ways, telemetry and positions received, the clock taken from a neighbour, settings surviving a crash, traceroute answered and asked, and the requests a phone makes answered. SF12 receive is not offered. The algorithm is proven off the device against a real recording; on the device it does not fit the time budget, and the limit is written down in the code with the arithmetic that produces it. The app does not fit in the SPI flash beside everything else, so eleven baseband images move to the SD card and are loaded from there. The build and the release package put them on the card; what the arrangement costs the apps that own them is set out in the pull request, with the measurements behind it.
Tests that run on an ordinary computer against the firmware's own sources, and scripts that drive a PortaPack and a Meshtastic node over USB to test on the air. The end-to-end tests carry one frame recorded off the air from a stock node. A round trip only proves the two ends agree with each other: a wrong coding rate sailed through it for weeks because both ends read the same wrong table, and only an outside frame could disagree.
ec05804 to
63bd8b3
Compare
|
Two corrections, one of them mine to own. The package I attached earlier was incomplete: it carried The second: I have taken back the Checked on the exact binary that is now in the release, not on something resembling it: Looking Glass loads and sweeps, the map opens and closes with the free core unmoved, and Weather, whose image is larger than the memory left by the time you reach it, reports |
|
Thank you for the nice feature!
Thank you again! And if possible, please join discord, and pin several of the developers so we can know your discord handle and start the discussion. |
I have built this code and tested it on physical PortaPack hardware.
Testing used a stock Meshtastic node, a Heltec V4 on unmodified Meshtastic firmware,
as the outside source of truth. Seven of the nine modem presets are confirmed in both
directions: that node decoded our text, and we displayed its text.
Written with Claude as a pair-programmer, as
AGENT.mdencourages.The causes of the bugs were found by measurement on live hardware, not by reading: raw
symbol bins dumped to the console and analysed off-device, a dropped-buffer counter, a
step-by-step map of memory use. I take full responsibility for every line and can
answer detailed questions on any of it.
What this is
A Meshtastic app for the PortaPack: chat, encrypted channels, a node list, telemetry,
positions, a map, and mesh routing.
The part that matters: this is not a wrapper around a radio module. The HackRF has
nothing in it that understands LoRa: it hands over a stream of samples and takes one
back. So the physical layer is written out in full: preamble detection, time and
frequency synchronisation, dechirping against a reference, the Fourier transform, Gray
coding, interleaving, Hamming, whitening. All of it in both directions, on the M4,
inside 54 kB of buffers and a 1.024 ms deadline per buffer.
Two pieces of the interface are unusual enough to name here, since both show up in the
diff and both have a reason in the sections below: the app carries its own on-screen
keyboard, and it can read a glyph table off the SD card so the chat shows the alphabet
the person on the other end actually types in.
What works
Also confirmed on air, against the same stock node: encrypted custom channels both
ways, telemetry and position reception, clock synchronisation from a peer, settings
surviving a hard reset, traceroute asked and answered in both directions, and the
three requests a phone app makes of a node - exchange user info, request position,
request telemetry - answered. The user info reply carries our public key, which is the
only way it travels, and without it there is no encrypted direct message either way.
A stock node may not answer a telemetry request of its own accord. Measured against a
Heltec V4: the request arrives and is acknowledged and nothing comes back, while the
same node's identical request to us is answered. The request we send is byte for byte
what the official client sends, so this looks like the far end's choice rather than a
fault at either end.
VERY_LONG_SLOWis not offered. Current Meshtastic firmware no longer implements it: anode told to use it comes back reporting LongFast's parameters. Its row is kept in the
table so the other presets keep their stored indices.
What does not
SF12 receive. The algorithm is proven offline against recorded air:
tools/lora_bench/sf12_off.pydecodes a whole frame from a capture. It is switched offon the device for want of time, not correctness: at 4096 samples per symbol one
demodulation takes a full symbol, and resolving the half-symbol ambiguity needs three of
them back to back, by which point the sample window has moved 3.5 symbols past a header
that tolerates 1.75. Fixing it means resolving the ambiguity from the already
demodulated down-chirp, or an integer FFT. The limit is stated in the code with the
arithmetic beside it.
Custom-channel passphrases. The way a passphrase becomes a key here is this app's
own. The primary channel uses Meshtastic's well-known key and interoperates out of the
box; a custom channel talks to a stock node only if you enter that node's raw 32-hex key
instead of a passphrase.
Resources
next, which is 0.6 percent. This is thebinding constraint on everything below, and the section after next is about it.
(10 nodes at 216 bytes). The limit follows from what is left and is stated in the
header, with the arithmetic that produced it.
the same time, so nothing was added.
PLOR27 kB (receive) andPLOT9.5 kB (transmit), both in flash.The cost this branch imposes on other apps
This is the part I would look at first if I were reviewing it.
The two signal-processing images this app needs are 36.5 kB together, and the flash
was already full. To make room, eleven images that
nextkeeps in flash are moved tothe SD card by this branch:
afsktx,ais,aprsrx,btletx,mic_tx,rds,replay,sonde,subghzd,weather,wideband_spectrumThat covers APRS receive and transmit, AIS, BLE transmit, the microphone transmitter,
RDS, Replay, the radiosonde receiver, the SubGHz decoder, Weather and Looking Glass.
Each of them needs its image present as
/BASEBAND/<TAG>.imgor the app reportsNoImgand does not start. The build writes each one a second time under its tag,which is the name the loader looks for, and both the
.ppfw.tarand the SD cardarchive built by the release workflow now carry that directory, so a normal install
puts them where they belong. The package grows from 2.9 MB to 3.5 MB. That was not
true of the first version of this branch, which worked only because the card in front
of me had been prepared by hand.
Loading them is done by
m4_init_from_sdinfirmware/application/core_control.cpp,which reads the image from the card when the tag is not in flash. The file on disk is
byte-identical to a flash chunk, so it is the same decompress-and-run path. That
mechanism is new here, and it is the one piece of this branch that changes behaviour
for people who never open the Meshtastic app.
What that costs, stated plainly, because it is not free. An image in flash is loaded
unconditionally: the tag is found in memory that is already mapped and decompressed
straight from there. An image on the card needs two things it did not need before,
and both depend on how much memory is left rather than on anything about the app:
The stack half is handled. The read runs on its own 1 kB thread, the same size and
the same call
capture_threadandreplay_threadalready use to touch the card, sothe caller's depth no longer matters. Before that it did: measured on the device, the
4 kB process stack has around 300 bytes left at its high-water mark after boot and
under a hundred once a few screens have been opened, so reading on the caller's stack
worked for a shallow app and hard-faulted the deep ones.
The memory half is not handled, and I do not think it can be without changing the
decompressor. Core memory on the M0 is handed out and never returned, so a device
that has been used for a while has less of it; when the block cannot be found the app
reports
NoImg, which is the same thing that happens when the file is missing. It isa clear failure rather than a crash, but it is a failure that flash never had.
Measured on the device, that puts a ceiling on the mechanism. A clean boot leaves
about 24 kB of core; opening one menu to reach an app costs about 8 kB of it. So an
image has to fit in roughly 15 kB to be loadable in practice. Nine of the eleven do.
Two do not:
weathersubghzdThose two are not marginal, they are out of reach: the memory is already spent by the
time you have navigated to them.
Rearranging which images go to the card does not fix it. Bringing those two back
needs 41 kB freed elsewhere, and the only images in flash small enough to take their
place are ADS-B receive, OOK transmit, FSK transmit and Capture, which together come
to 39 kB and still fall short. That trade makes four more apps depend on free memory
in order to rescue two, and Capture is not an app I would want to make conditional.
I have not tried to hide this behind a smaller number. Nine of eleven work and two do
not, and the two that do not are a direct consequence of this branch.
So the honest summary of the trade is not "flash space for a file on the card". It is
"an unconditional load for one that depends on free memory", for eleven apps that did
not ask for it. That is the part I would want a maintainer to weigh.
I am not attached to this arrangement. It is what fits today, not what I think the
project should do. If the maintainers would rather see fewer images moved, a
different set, or a packaging step that puts them on the card, say which and I will
rework it. If the answer is that a firmware app may not cost other apps anything at
all, then this app cannot ship as an internal app in its present size, and that is
worth knowing before either of us spends more time on it.
Memory, and one change outside this app
Worth stating plainly, because it is the one thing here that can stop the app dead.
Core memory on the M0 is handed out and never returned: a freed block joins a free list
and is reused only for a request of a similar size. The app takes about 16.7 kB at
startup, and the on-screen keyboard wants 4848 bytes in one contiguous piece the
first time it opens in a session. Between those, roughly two kilobytes are left. The
failure is not "memory ran out" but "the first large contiguous request failed".
This PR therefore also touches two shared firmware files, 26 lines, more than half
of them comment.
firmware/common/chibios_cpp.cpprecords the size of a failedallocation, and
firmware/application/debug.cppprints it on the panic screen:Out of Memoryon its own says nothing, and the panic screen is all that is leftafterwards. A four-figure request means the budget is too tight; a two-figure one means
the heap is fragmented; they call for opposite fixes. Only the store happens in the
shared file, because formatting it there cost an external application 900 bytes and
broke its 32 kB ceiling twice. It earned its place immediately: it showed the problem was the
budget, not a leak.
Fixed on our side: replaying the saved chat used to read through a heap string sized
from a setting, 12.8 kB in one piece at its maximum, plus a vector holding every line
again. It now reads a fixed 1 kB tail, the same cost at any setting.
Two things that grow with use are bounded explicitly, and both default to the safe side:
The node list is fixed at ten for the same reason and is not a setting: it lives in
memory permanently rather than on demand. It was twelve until the map view needed 3736
bytes in one piece and could not get them; two entries are 432 bytes, and they turn a
map that refuses to open into one that does. A handheld that can name ten neighbours
and show them on a map beats one that can name twelve and cannot.
The margin is still thin, and neither the code nor the guide hides it.
Tests in the repository
All of these run on an ordinary computer, no hardware needed:
tools/lora_bench/test_mesh_regions.cpp: 6429 combinations of region × preset ×slot against an independently written implementation of Meshtastic's formula, plus
reference points measured on hardware.
tools/lora_bench/test_lora_framing.cpp: 192 end-to-end frame round-trips acrossevery spreading factor, bandwidth and coding rate, and a frame recorded off the air
from a stock node.
tools/lora_bench/test_lora_ref.cpp: the chunked reference-chirp generator againstone computed whole.
tools/lora_bench/gen_font.py: walks each alphabet the glyph table claims to coverand refuses to write a file that falls short.
tools/lora_bench/test_mesh_router.cpp: dedup, relay mutation, channel-hashfiltering, packet id seeding, the traceroute request on the wire, the route journal,
and the pair that stores a real telemetry reading and then sends a request over it.
tools/lora_bench/sweep_one.py: a scripted on-air run of one preset, both directions.The second one deserves a note, because it answers "aren't you just checking yourself
against yourself?". A round-trip proves only self-consistency. The coding-rate bug would
have sailed straight through it, because both ends read the same wrong table. That is why a
real frame from a stock node is in there: it is the one part that can disagree with us.
The last one deserves a note too, for the opposite reason. A telemetry request is an
empty copy of the variant it asks for, so it decodes into a perfectly valid measurement
of all zeroes, and this app used to store it: a node broadcasting 101 percent and
4.36 V read as 0 percent and 0.0 V on its own card from the moment it asked us for
ours. The test stores a real reading and then sends the request over it. Without the
fix it fails, which is the only reason to trust it.
Anticipated objections
"New apps are supposed to be external"
It does not fit, for two independent reasons.
An external app is capped at 32 kB. The largest existing ones sit right against it
(
epirb_rxis 32492 bytes), and the application layer here is 10 800 lines. Whileworking on this PR, an edit in a shared firmware file pushed unrelated external apps
over that ceiling twice. The limit is live and tight.
Beyond that, the external format has no provision for a baseband image of its own, and
two are needed. The receive image is 27 kB and cannot be loaded from the card even in
principle: loading requires holding the compressed image whole in the M0 heap, which is
43 kB with most of it already spoken for. This is recorded in
firmware/baseband/CMakeLists.txtbeside the target.Happy to discuss a middle path if the project has plans for external apps with their own
baseband.
"That is a lot of std::, and the rules ask to avoid it"
Granted. The app uses
std::string,std::vectorandstd::functionthe way the otherbuilt-in apps do. They are absent where it matters: the demodulator runs on static
buffers, and complex multiplication is written by hand, because the standard operator
compiles to a library call and cost 424 dropped buffers per second.
Happy to cut back where you point.
"Why not error-correct the payload with Hamming?"
Because it measurably makes things worse. Hamming(8,4) corrects one error; with two, the
syndrome points at an innocent data bit. From the same recorded symbols: uncorrected, the
broadcast address came out
FF FF FF FF; corrected,FF FF FF 7F. Correction is keptfor the header, where the stake is higher.
"Why does the app read a font off the SD card?"
Because the firmware font is ASCII and Latin-1, the flash is full, and the chat shows
text people send. The file is a table of codepoint ranges rather than a built-in script,
so which alphabet a device shows belongs to whoever holds the card. It is allocated only
if the file exists, capped at 256 glyphs, and taken through
chHeapAllocrather thannew, becausenewpanics the firmware when the heap is short and an optional extra hasno business doing that. With no file, the chat says so instead of pretending.
"Why is it called Mesh and not Meshtastic?"
Their trademark policy does not allow the mark in the branding of a third-party product,
so the app carries its own name and icon, and a disclaimer on the settings screen. The
name stays in the code, where it identifies the protocol being implemented rather than
this product.