Phone-to-phone text over light. One phone's screen flashes a grid of colours; the other phone's camera reads it. No Wi‑Fi, Bluetooth, cell signal or pairing: if you can see the other phone, you can text it.
It is an experiment, and it works, slowly. Messages decode reliably at a few metres indoors. Longer range is the open problem, and the reason this repo is public.
iPhone app (app/, Flutter) |
Working: texting-style chats, encrypted chats per 3-character code, an Open channel, delivery confirmation by flashlight. Tested on an iPhone 12 Pro and 14 Pro. |
| Android | Builds from the same code but untested. The camera exposure, brightness and torch controls are native iOS code with no Android version yet. |
Web app (src/, TypeScript) |
The original prototype, kept as the reference protocol implementation and an in-browser simulator. It can send and receive, with no delivery confirmation and no encryption. |
Measured so far:
- Near (2×4 grid, four colours): reliable at roughly 1.5–3 m.
- Mid (two cells, three colours): works, at shorter range than expected.
- Far (whole screen, two colours): decoded at arm's length. It is slow by
design: about 7.5 bits/s, so "Oh" takes about 30 s. A simulation built from that
run (
app/test/range_sim_test.dart) predicts several metres with the phone held still. Hand movement is the limit it points to; that is still to be checked in the field.
The sender loops the message as a fountain code: there is no "resend", the receiver just keeps watching until it has enough pieces. A walking black cell (or, in Far mode, a black frame between symbols) acts as the clock. The receiver finds the screen by what makes it unusual (its colour keeps changing, unlike lamps and windows), reads the cell colours, and reassembles the message. When it succeeds it blinks its flashlight, and the sender's front camera watches for that to stop sending.
PROTOCOL.md: the wire format and receiver, as implemented.DESIGN.md: the original design reasoning, with prior art (some of it has since been superseded;PROTOCOL.mdwins).SECURITY.mdandPRIVACY.md: what the chat codes protect against (and what they don't), and what the app stores.
iPhone: there's no App Store or TestFlight build yet. To build it yourself you need a Mac with Xcode, Flutter 3.32+, and an Apple developer account (a free one works for your own phones):
cd app
flutter pub get
open ios/Runner.xcworkspace # set your signing team and a unique bundle id
flutter run --release # with the phone connected
Using it: both people create a chat with the same 3-character code (agree it in person). One types and taps send, then holds the screen toward the other phone; the other taps Listen and points the camera at it. Turn the sender's brightness up. It works best with the phones steady and the screen the brightest thing in view.
Photosensitivity warning: the sending screen flashes rapidly, which can trigger seizures in people with photosensitive epilepsy.
Browser: the web prototype runs on any phone with a camera over HTTPS. See Web prototype below.
app/ Flutter app (iOS; Android untested)
lib/core/ protocol and vision, pure Dart (a port of src/core)
lib/chat/ chat list, history and settings storage
lib/ui/ screens: chats, chat, send, listen
ios/Runner/ native camera/brightness/torch channels (AppDelegate.swift)
test/ unit tests, golden vectors, real-frame fixtures, range simulation
src/core/ reference protocol in TypeScript (fountain code, framing, receiver, vision)
src/app/ web prototype: sender, receiver, simulator
test-data/ a real receive log used as a regression test
scripts/ Mac helpers: app.sh (test/build/install), smoke.mjs (headless web check)
npm install && npm test # TypeScript protocol tests
cd app && flutter test # Dart tests (protocol, crypto, vision on real frames)
The Dart core is checked against vectors generated by the TypeScript core
(app/test/golden.json). After any protocol change, regenerate them:
GEN_GOLDEN=1 npx vitest run src/core/golden.gen.test.ts.
scripts/app.sh test|build|install runs the Flutter steps from a copy outside
~/Documents, because iCloud Drive adds file attributes that break code signing.
install puts a release build on every iPhone connected by cable.
The range simulation prints a table of screen size × hand movement vs. decode success (about 5 minutes):
cd app && flutter test test/range_sim_test.dart --dart-define=RANGE_TABLE=1
Turn on Settings › Save field-test diagnostics on the receiving phone. Each
Listen session then saves a per-capture CSV log, periodic snapshots and short frame
bursts to the app's folder (Files › On My iPhone › Distance Bits, or
xcrun devicectl device copy from … --domain-type appDataContainer). Most fixes in
the history were found by replaying these against the code. Note the snapshots are
pictures of whatever the camera was pointed at.
npm run dev # https://<your-computer-ip>:5173 (self-signed cert; accept it on each phone)
npm run build # production build in dist/
Pushes to main publish the web app to GitHub Pages
(.github/workflows/deploy.yml). The simulator page sweeps "screen pixels" to
stand in for distance. npm run smoke runs it headless in Chromium (after
npm run build && npm run preview -- --port 4173, and
npx playwright install chromium once).
Issues and pull requests are welcome, especially field logs from new distances, lighting and phones, and an Android port of the native camera controls. If you send logs, check the snapshots first: they are camera pictures.
GNU Affero General Public License v3.0. © 2026 Trevor Johnson.