explore-light #3

Open
solene wants to merge 10 commits from explore-light into explore-mosaic
Owner
No description provided.
The light channel (next commits) carries a few bits per second, so
128-byte blocks would take minutes each. Encoder, Decoder and Collector
now take a block size; `new` keeps the standard 128 bytes.

The coefficient rows only depend on (file_id, esi, K), so sound and
screen packets are unchanged: `standard_symbols_unchanged` pins a repair
symbol computed before this change, and `send --wav` output is
bit-identical (both profiles). No protocol change.

Decoder::add now ignores a block of the wrong size instead of
corrupting the rebuild, and Collector takes a maximum object size so
that a small block size cannot turn a header into a huge allocation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A third channel, as a teaching example of a very poor link where the
data fountain is what makes a transfer possible at all. The sender's
screen slowly changes color; the receiver only measures the mean color
of each camera picture, so the camera may film the screen or a wall lit
by it.

Never a flash: a camera at 30 pictures/s can only follow changes slower
than about 10 per second, right in the range where blinking triggers
photosensitive seizures. So the screen never changes luminance: four
hues of relative luminance 0.30 (OKLCH 60/150/240/330, chroma 0.12)
fade into each other in linear light. light::tests::no_flash paints
every pace in 8-bit colors at 240 Hz and checks the luminance stays
within 1 % of the maximum (WCAG counts a flash from 10 %). Invariant 7
in AGENTS.md now covers this channel.

New protocol (light version 1, documented in AGENTS.md section 3 and
docs/light-channel.md): 16-byte fountain blocks in 28-byte packets,
rate 1/2 convolutional code, 2 bits per hue, a 16-symbol preamble,
paces of 2, 3, 5 or 8 symbols per second (0.13 to 0.52 bytes/s). The
sound and screen protocols are unchanged.

The receiver works on log-chromaticity, where exposure changes cancel
and white balance becomes an offset. It finds frames with a Fisher
score of the preamble (how well 16 averaged colors fall into the four
expected groups), which needs no calibration and teaches it how the
camera sees each hue.

`airgap light sim` runs it through a simulated camera (src/lightsim.rs:
rolling shutter, auto exposure and white balance, lamp flicker, noise,
lost pictures, jitter), and `airgap light decode` replays camera samples
saved by the web page (next commit). Simulation only: nothing has been
tried with a real screen and camera, and the simulator ignores video
compression and phone image processing.

Untrusted input: timestamps are bounded (a huge one would have made the
frame search loop forever; the hostile-input test hangs without the
bound), samples kept are capped, the decoder size is bounded by the
4 KiB object limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reported by the maintainer: once the camera runs, scrolling the page on
a phone tends to press buttons, and the picture cannot be seen whole.
On a phone the receiver's video, progress, stats and buttons were
stacked taller than the screen, and status lines changing length moved
the buttons under the thumb. On iPhones the sender's pattern could not
go full screen (no Fullscreen API outside videos), so it stayed in the
page below the settings.

- Full views (ui.js): the receiver's camera view and the sender's
  picture can cover the whole window, fixed, with page scrolling and
  touch gestures blocked; real full screen is added where the browser
  allows it. On phones the receiver opens it by itself: video on top,
  progress, stats and buttons below, nothing to scroll. It closes when
  a file arrives, to show it. The sender's full view has one small
  "Leave full view" button; Stop is only on the page.
- Stop asks for confirmation when blocks would be lost (both pages).
- Bigger touch targets and no double-tap delay on touch screens.
- Helpers shared by the pages moved to ui.js (formatting, tabs).

Not tried in a browser (none in the dev container). The Node test now
checks that every element the page scripts look up exists in the page,
and that every page file is in the Containerfile copy list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
light.html sends and receives the light channel in a browser:

- Send: the whole screen becomes the lamp (full view). The color of
  each moment comes from the wasm module (WebLtx), which mixes the hues
  in linear light, so the page cannot get the no-flash rule wrong; it
  only paints. The Rust test checks that the page colors match the
  reference sender exactly.
- Receive: for each camera picture (requestVideoFrameCallback where
  available, with the capture time when the browser gives it), the
  picture is shrunk to 80x60 and averaged, and the time and mean color
  go to WebLrx. The page shows the recent colors as a strip, a signal
  meter, the packet being read and the blocks gathered. It offers to
  lock exposure and white balance when the camera allows it (optional),
  and saves the measured colors as CSV for `airgap light decode`.

Both use the full views of the previous commit. The pages link to each
other; the Containerfile, stamp.sh and nginx.conf know the new page.

Tested under Node (the page's colors, blurred, noisy and with lost
pictures, into the page's receiver; page wiring) and in Rust through
the simulated camera. Not tried in a browser or with a real camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`airgap recv` printed the received name as is. The name comes from
the air: a sender could put terminal escape sequences in it (clear
the screen, change the window title, fake the output that follows).
The saved file name was cleaned of control characters, but not of the
invisible marks that reorder text: with U+202E, a file saved as
"a<U+202E>txt.exe" shows as "aexe.txt" in a file manager.

Received names are now printed through `printable` (hidden characters
become U+FFFD), and `safe_name` also drops the bidirectional marks
(U+200E, U+200F, U+202A-U+202E, U+2066-U+2069). The pages do the same
through a shared `safeName` in ui.js (the page text was already safe,
set with textContent, but the download name and the shown name could
be reordered).

Regression tests: `received_name_is_never_printed_raw` sends a name
with an OSC title sequence, a clear-screen sequence and U+202E through
`send --wav` and checks `recv` prints none of them (it fails before
this change); the Node test checks `safeName`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Asked by the maintainer to make the channels easier to try: typing a
message beats picking a file, and on the light channel a message is
the only thing that makes sense.

A text message is an object with an empty name (no protocol change:
older receivers save it as "received_file"). It also saves the bytes
of a name, which matters at a third of a byte per second.

- CLI: `--text MSG` instead of a file, for send, sim, truchet and
  light. `recv` and `light decode` print a received message on
  standard output through `printable` (escape sequences neutralized,
  newlines kept) and save it as message.txt.
- Pages: every Send tab offers "A file" or "A text message" (the light
  page starts with the message; it shows the expected time while
  typing). Receivers show a message as text (textContent, never HTML)
  with Copy and Save buttons. The received-file box moved to ui.js
  (showReceived), shared by the three pages.

Tests: `text_message_round_trip` (send --text -> WAV -> recv: stdout,
message.txt, an escape sequence neutralized); the Node test sends a
message through the wasm modem, and now also checks that every name a
page script imports is exported by its module (a missing one would
stop the page from loading). Not tried in a browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
style.css had no [hidden] rule, and an author `display: flex` beats
the browser's own `[hidden] { display: none }`. So `.row` and `.stage`
elements stayed visible when hidden: on visual.html the "Colors" row
did not go away with the mosaic style and the empty sender stage
showed before starting; with the previous commit, the file input and
the message box would both show.

A global `[hidden] { display: none !important }`, the usual fix. The
Node test checks the rule is there (it fails without it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reported by the maintainer on 2026-11-03: a laptop sending with
light.html, a phone receiving with light.html, the phone filming the
laptop's keyboard, so seeing only the light the screen casts on it.
Pace, message size, browsers and transfer time were not given; the
figures in the docs remain simulation measurements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The maintainer added that the 2026-11-03 transfer (laptop to phone,
filming the keyboard) ran at 8 colors per second with no packet lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin 'explore-light:explore-light'
git switch 'explore-light'

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch 'explore-mosaic'
git merge --no-ff 'explore-light'
git switch 'explore-light'
git rebase 'explore-mosaic'
git switch 'explore-mosaic'
git merge --ff-only 'explore-light'
git switch 'explore-light'
git rebase 'explore-mosaic'
git switch 'explore-mosaic'
git merge --no-ff 'explore-light'
git switch 'explore-mosaic'
git merge --squash 'explore-light'
git switch 'explore-mosaic'
git merge --ff-only 'explore-light'
git switch 'explore-mosaic'
git merge 'explore-light'
git push origin 'explore-mosaic'
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
public/data-fountain-over-audio!3
No description provided.