v0.3.1
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
92d31a3191 |
Every file is spelled the way the formatter spells it
`ruff format .` across the repo: 153 files, no behaviour change. This is the second gate CONTRIBUTING.md promises, and until now it reported 154 files needing reformat — so a first contributor ran the command the guide told them to run and could not tell their own diff from the background. Landed on its own, with nothing else in it, because a whole-repo reformat is only safe to skip past in blame if it is provably nothing but layout. It was checked that way rather than assumed: every changed file was formatted in memory and compared by full `ast.dump()`, and 152 of 153 come out structurally identical (the one exception was a docstring opening on a quote character, fixed in the commit before this one). The string and comment token streams were diffed separately, so a quote flip or an edited comment could not hide inside a re-layout — five quote-style normalisations and sixteen implicit-concatenation joins, all value-identical, and one commented-out line gaining the space after its `#`. `meshterm specimen` is byte-identical on both platforms, and the suite is 1888 green, gallery gate and `test_theme16` included. What actually changed, in order of how much of the diff it is: wrapped calls and signatures rejoin onto one line where they fit inside the hundred columns; the magic trailing comma expands what it is there to expand; complex slices gain their spaces (`[len(noise) :]`); a few redundant parens go. The tables that are laid out to be read were fenced in the previous commit and are untouched here. The aligned trailing-comment column is gone from about ninety lanes that were not fenced — including the `heat.*` ladder in `MESH_THEME`, which cannot be fenced where it sits. Every comment stays on the entry it describes, so nothing there reads wrong; it just no longer lines up. Add this commit to `.git-blame-ignore-revs`, which follows immediately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c8c7eab128 |
A direct message's frames are its own, by the MAC over its ciphertext
The paths on a DM listed a message's own retransmissions, the other end's traffic, and both of those for every other message sent in the same minute and a half. On one real bob_caribou message that was 30 rows over 8 paths, of which 23 were the same two one-hop routes repeated. Those one-hops were the tell (JP): they were our *own* sends, overheard coming back off the two repeaters in earshot, correctly showing one hop because one hop is all they had travelled — but shown under the message as if they were its route. The channel view has always matched by decrypted content, and asked for here. A direct frame cannot be read, but it does not need to be: its body is [dest 1B][src 1B][MAC 2B][ciphertext], and that MAC is a tag over this message's plaintext. Copies of one message share it; the next message's differ. frame_addressing already knew where those two bytes were and threw them away, and the observations table already had a cipher_mac column its channel sibling used — so a direct message now groups exactly as a channel one does, with no schema change and nothing decrypted. Three filters, in order. The frame ran between the conversation's two ends; it travelled the direction this message did; it carries its group's MAC. The middle one is doing more work than it looks: both ends' hashes sit on every frame either way, so matching on the pair alone was what put our sends under a received message. The window is clamped by the conversation's own messages rather than a flat ±90 s — nine messages fell inside one such window. The clamp reads only messages going the *same* way, which the first cut of it got wrong: an inbound stamp is the sender's clock, so ordering it against our own sends compares two clocks, and a received message came back with an empty window. The directions are also bounded differently, because we know different things about them. A send is stamped by our clock as it leaves, so no frame of it predates it and all the room goes forward to the next send, where the retries are. A received message keeps a centred window, halfway to the received messages either side. Rows are paths now, not frames: identical routes fold into one line carrying its copy count, its first sighting, and the best SNR that path managed. Twelve identical lines said "twelve arrivals" where the truth was "two paths, six times each". Measured on the conversation that prompted this, whose history predates the MAC and so exercises only the direction and window fixes: outbound "Allo! Ça marche JF" 30 rows / 8 paths -> 2 rows (x3 each) inbound "J'ai pas de ack..." 30 rows / 8 paths -> 2 rows, 6 and 7 hops which is exactly what the packet log says by hand. Old history keeps the address-and-time fallback and is billed as such; frames recorded from now on match exactly, and say so in the same words the channel view uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c246fb8080 |
Persistence: a trace's link readings get a column, not a borrowed one
The readings a trace collects arrive where every other class keeps its relay hashes, so `path` was exactly the wrong place to keep them — and keeping them there fed the topology graph links nobody observed. They get `observations.trace_snrs` instead, written and read like the crypto trio and the addressing columns before them, so a replayed frame shows the same links a live one does rather than a blank lane. The 1606 rows already stored were migrated in place: 2454 readings recovered, every one of them at the 1-byte width the wire uses, and every fabricated path cleared. The pairs read as what they always were — one trace heard twice, once a hop out (+13.25 dB) and once on the way home (+13.25, -5.0, -4.25), the return legs weaker exactly as a longer walk should be. |
||
|
|
6217a7dc58 |
Packets: the frames a neighbour sends were being thrown away
Two silent drops in the RX-log mapper, and between them a device sitting beside this one could trace all day and never appear in the feed. A frame that crossed no relays was discarded as having "no topology content". It has the most there is: nothing stood between us, so we heard the transmitter itself. Only an advert survived, and only because it carries an origin key — every trace, message, request and ack from an adjacent node went into the bin. Now a frame is kept unless the library could not even give it a payload class, and the empty path says what it means: zero hops crossed. The other is a trace reading its own header wrong. Every class keeps relay hashes in the path field; a trace keeps one signed SNR byte per hop it has traversed. Read as hashes those bytes resolved to whichever nodes shared their leading digits and entered the topology graph as links that were never observed. The population says so plainly: across 1604 stored traces, all 2221 path entries land inside the LoRa SNR band when read as signed bytes, spanning -10.5 to +15.0 dB, where hashes would put five in six outside it. So a trace now yields no hops, and its readings — which are worth having — ride alongside as trace_snrs. The 1604 rows already stored keep their bad paths; a one-shot cleanup follows separately. |
||
|
|
01ca242c98 |
Live feed: the lane is about the packet, whatever the packet is about
The NODE lane suited the two classes that name a node and left every other one reading a dash. On this mesh that is half the traffic: the direct messages, requests, responses, returned paths, acks and traces that relay past us naming no origin. So the lane asks each class its own question instead, and the header says SUBJECT — the word that stays true down the whole column. An advert or a telemetry frame is still about its node. A channel text is about its channel, named by the key whose MAC confirms the frame — never by the fingerprint alone, which 256 buckets make no proof at all; a channel we hold no key for reads as that fingerprint and says so. A direct message, request, response, returned path or anonymous request is about the two nodes it travels between, drawn sender → recipient on the hop-sequence widget, so endpoints read exactly as route hops do: resolved names, key-derived hues, our own node white the moment a frame is addressed to us. Too wide, and the sender drops to its hash — the frame is addressed, so the addressee is the half worth the cells. An ack is about the message it answers, a trace about its tag. And an unsigned channel post is filed under its channel rather than the bare word "channel", which named nothing. Half of that was unreadable before because nobody had opened the frame. The companion hands every overheard packet to the library, which decodes the body for exactly two classes — advert and channel text — and leaves the rest as bytes. Those bytes are not opaque: MeshCore addresses a frame in the first few of them, and core/frames.py now reads them, class by class, against the layouts the protocol's own docs/payloads.md publishes. The trace tag is the little-endian uint32 the firmware writes, so it reads as the same number a trace reply reports; an ack's checksum keeps wire order, the form the companion reports its own delivery acks in. Two checks guard every decode, because a wrong slice yields not an error but a plausible-looking hash: the frame's payload version must be v1 (the header reserves two bits for a v2 that widens both hash and MAC), and the body must be long enough for the layout its class promises. Either failing yields nothing at all. The feed opens seeded from history, so any of this that lived only in a live event's raw payload would leave the opening screen blank — the same problem the channel-text crypto trio had. Same answer: three columns (dest/src/tag), written on capture and restored to the very keys the event used, so a replayed frame and a fresh one read identically all the way up. Older rows carry NULL until re-heard. The viewer gained the matching rows: to, from, tag, acks, and a channel for a datagram — whose body is not text, but whose MAC still names it. Verified against the MeshCore protocol documentation and firmware source, then corroborated on the air: every frame captured off the radio has a body length that decomposes exactly into its documented layout, an anonymous request's 51 bytes (1 + 32 + 2 + one AES block) included. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |