Skip to content

Calibrator board v1 proposal (hardware, firmware, signal contract) - #69

Merged
LeeO86 merged 11 commits into
mainfrom
feat/calibrator-hw
Nov 9, 2026
Merged

LeeO86 merged 11 commits into
mainfrom
feat/calibrator-hw

Conversation

@LeeO86

@LeeO86 LeeO86 commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Draft, do not merge. Proposal for a low-cost AV Sync calibrator board, v0.3 (CH32X035, USB-C firmware update). Nothing is ordered, built or tested on hardware. Adrian checks schematic and renders before any order.

What it is

A 66 × 22 mm board that the phone powers over a USB-C to USB-C cable. No cable is supplied: the user brings their own. No battery, no mode button.

  • Camera mode (sensors see dark for 1 s): LED flash and buzzer click start with one register write. 3 start pulses (0/125/250 ms), then 20 pulses every 500 ms, then 2 s pause, repeat.
  • Display mode (sensors see a black/white swap): the two light sensors feed comparator 3 (with hysteresis). It flips where the two halves cross, and the board clicks at once. 200 ms lock-out.
  • Contract for the app: docs/calibrator.md (app side not built yet).

USB-C: power + update

  • 16-pin USB-C. 5.1 kΩ on CC1/CC2 asks the phone for 5 V. D+/D− go to the MCU but the firmware never enables USB, so a phone sees no device (no pop-ups, no audio route change).
  • Update: hold the "update" button while plugging into a PC; the CH32X035 ROM bootloader starts (only at power-on with D+ high). Flash with open tools: wchisp or chprog.py. Blank chips from JLC start the bootloader without the button. No programmer needed.

Look (it is merch)

  • AVsync logo on both sides: white silkscreen strokes, gold (ENIG) waveform + ring on a black mask; the white dashed line and white centre dot sit on top like in the app icon.
  • QR code on the bottom to https://leeo86.github.io/mxl-avsync/ (decodes from the render).
  • Alignment split line + notch on both sides, small "hold to update" under the button, "not a toy" at JLC's minimum size.

Mechanics

All parts on top (one-side Economic PCBA). The white LED is reverse-mount and the two sensors are reverse-gullwing: they look through holes, so the bottom is flat and touches the lens or the screen. 1.2 mm board.

Cost (JLC parts live 2026-11-08; PCB/ENIG/shipping estimated)

10 boards 100 boards
All-in incl. DHL, Swiss VAT + clearance, black + ENIG, no cables ≈ CHF 86 ≈ CHF 210

Both orders ≈ CHF 296 (budget CHF 300). Stock covers 110 boards (MCU 800, sensors 782).

Schematic

Blocks (USB-C power + update, light sensors, MCU, flash + click), power at the top, GND at the bottom, real wires, no net labels. Generated from scripts/design.py; scripts/check_netlist.py checks the drawing.

Checks

  • CPL rotations include JLC offsets for the stock footprints (QSOP-28 +270°, SOT-23 −90°, HRO USB-C +180°); check them in JLC's placement preview. Renders use simple stand-in models for U1, the button and the USB-C (render only).
  • KiCad 9.0.9: ERC 0, DRC 0, 0 unconnected; netlist check OK.
  • Firmware (CH32X035, riscv GCC): builds (~2 KB), 13 host tests pass. CI job calibrator runs all of this.

Licence

Board files and the logo artwork on the board: CERN-OHL-P-2.0. Alternative: CERN-OHL-S-2.0 (share-alike). Firmware stays MIT.

Open points (phone behaviour, bootloader entry on real hardware, comparator interrupt path, buzzer delay, sensor signal, LED fit, stock) are in hardware/calibrator/README.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

LeeO86 and others added 4 commits November 8, 2026 10:13
…nal contract

A small USB-C powered board (no battery) for the AVsync app with two automatic
modes: camera mode (LED flash and buzzer click from one register write) and
display mode (two light sensors on the comparator click at the crossing of a
black/white swap). All parts on top; the white reverse-mount LED and two
reverse-gullwing phototransistors look through holes, so the bottom stays flat.

- hardware/calibrator: KiCad 9 schematic and board generated from
  scripts/design.py, autorouted, ERC/DRC clean (one explained ERC warning),
  gerbers/BOM/CPL for JLCPCB Economic PCBA, renders, BOM and cost notes.
- hardware/calibrator/firmware: PY32F002A firmware, mode logic in plain C
  with host unit tests, Makefile. Not tested on hardware.
- docs/calibrator.md: v1 signal contract (app side not built yet).
- CI job: firmware build and tests, KiCad ERC/DRC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Board (60 x 22 mm now):
- AVsync logo from assets/app-icon.svg on both sides: cyan/white strokes as
  silkscreen, the orange waveform and centre ring as bare copper with a
  mask opening (no paste, no net, kept clear by keep-outs). Black mask and
  ENIG in the stackup, so the copper looks gold.
- QR code to https://leeo86.github.io/mxl-avsync/ on the bottom: version
  3-M, 0.45 mm modules, 4-module quiet zone, light silkscreen background,
  mirrored to read from below. zxing-cpp decodes the bottom render.
- Split line now also on the top (next to the LED and behind the buzzer).
- Text redone: one font, clear sizes, JLC minimum 1.0 mm for "not a toy",
  licence and pad labels.
- GND is routed as tracks before the fills are added, so no fill islands;
  ERC 0, DRC 0, 0 unconnected.

Schematic redrawn with real wires and functional blocks (power in, light
sensors, MCU, flash + click, programming), power symbols top, GND bottom,
labels only for the programming pads, custom PY32 symbol with pins grouped
by use. scripts/check_netlist.py checks the drawing against design.py
(also in CI).

Licence: board files and logo artwork CERN-OHL-P-2.0 (text added), the
firmware stays MIT. Cost table updated for ENIG (estimate).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
The board logo now follows assets/app-icon.svg more closely: the dashed
centre line runs from y 74 to 181 (dasharray 4 7), hidden only under the
centre dot, and the inside of the gold ring is a white silkscreen dot.
Because JLC strips silkscreen from bare copper, the gold copper and its
mask opening are cut away under the dashes and inside the ring (polygon
booleans in gen_pcb.py). ERC 0, DRC 0. The app icon file is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Adrian's decision (2026-11-08): switch from PY32F002A to CH32X035G8R6
(QSOP-28) so the board can be updated over its USB-C socket.

Board and schematic:
- 16-pin USB-C (C393939, HRO TYPE-C-31-M-12 footprint), 5.1k Rd on CC1/CC2
  kept, D+/D- (A6/B6, A7/B7) to PC17/PC16.
- Update button SW1 (TS-1187A, Basic) from PC17/D+ to VDD via 4.7k: the ROM
  bootloader starts only at power-on with PC17 high. No pull-down on D+ and
  no ESD part (reasons in the README).
- Sensor pair on comparator 3 with hysteresis (PA13+, PA2-), ADC on PA0/PA2.
  PB3 (comparator output, read back by the firmware) left unconnected.
- Programming pads removed (USB update, ROM bootloader). Board 66 x 22 mm,
  same mechanics and look (gold logo with white dot and dashes, QR, split
  line both sides, "hold to update" under the button).
- Schematic redrawn with a USB-C power + update block; ERC 0, netlist check
  OK, DRC 0. Router stubs/duplicates are cleaned up in a separate step.

Firmware: new hardware layer for CH32X035 (riscv-none-elf GCC, register
definitions from ch32fun, MIT); USB, its pull-up and USB PD are never
enabled. cal_logic.c and the host tests are unchanged. CI builds with
Ubuntu's gcc-riscv64-unknown-elf.

Docs: update-over-USB guide (wchisp / chprog), BOM and CHF cost table for
10 + 100 boards without cables (the user brings a USB-C cable).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
LeeO86 and others added 5 commits November 8, 2026 16:40
- The QSOP-28 3D stand-in had its long side along x, so the render showed
  U1 turned by 90°. The footprint itself was always right (pins along y,
  pin 1 top left, as in the CH32X035 datasheet); only the render changed.
  The stand-in now matches the footprint (leads, pitch, pin-1 dot); the
  button stand-in got its legs at the pads.
- CPL: add JLC rotation offsets for the stock footprints (QSOP-28 +270,
  SOT-23 -90, HRO USB-C +180), as in the community list of JLC
  corrections. To be checked in JLC's placement preview.
- Top: logo and "AVsync calibrator" centred in the free area between the
  button and the USB-C; licence text small below the MCU.
- Bottom: 11 mm logo in the middle of the board beside the QR code, with
  "AVsync calibrator v0.3"; CAMERA / SCREEN hints and a tiny "not a toy"
  at the LED/sensor end.
ERC 0, DRC 0, netlist check OK; QR still decodes from the render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Top: logo block 1.2 mm lower, licence text 1.3 mm to the right.
Bottom: logo block and the CAMERA / SCREEN / "not a toy" text 1.2 mm
towards the LED end, "not a toy" 1.5 mm closer to the SCREEN block.
Via keep-outs over both logo blocks so no via holes land in the artwork.
ERC 0, DRC 0, netlist check OK; the QR code still decodes from the render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
One short "BOOT" (1.2 mm bold) centred under SW1 replaces the two-line
"hold to update". Docs now say "hold the BOOT button while plugging into
a PC". ERC 0, DRC 0, netlist check OK; QR still decodes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
22 kOhm x 10 nF delayed the comparator crossing by ~0.15 ms; with 1 nF it is
~15 us. Same 0402 footprint, JLC basic part C1523. Copper and gerbers unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Same file on both branches now (BOOT button, 1 nF filter row, App side section,
changes from the v1 proposal, open points).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
LeeO86 added a commit that referenced this pull request Nov 8, 2026
Same file on both branches now, so the merge has no conflict here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
LeeO86 added a commit that referenced this pull request Nov 9, 2026
…board (#70)

* Core: calibrator analysis and calibration pattern

Shared C++ for the AV Sync calibrator board (docs/calibrator.md, contract v1):
- find_sound_onsets: clicks and beeps in a recording (trailing 0.25 ms RMS,
  25 % of peak), told apart by 2.7 kHz vs 1.5 kHz energy.
- find_flash_onsets: LED flash start from row-band means, on the
  rolling-shutter row the analyzer uses; readout from the band height or,
  with full-frame exposure, fitted over the pulses in camera_calibration.
- camera_calibration: pair flashes with clicks, robust median, uncertainty.
- display_calibration: click then beep D = 100 ms later, offset = gap - D,
  both swap directions weighted equally.
- calibration pattern for all generators: halves swap every 500 ms, beep.
Unit tests on synthetic signals (noise, missing pulses, stray sound, rolling
shutter with known/unknown readout, slow screen, sensor mismatch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Phone analyzer: calibrator take (camera and external display)

PhoneAnalyzer::begin_calibrator keeps 32 row-band means per frame and the
microphone, and poll_calibrator reports progress about once a second, then
the result (20 pairs or 30 s) or an error. No live report or QR scan while
it runs. iOS: beginCalibrator(mode) on the camera view; the camera take
first re-locks exposure (frame interval) at the lowest ISO, because the LED
floods the lens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* iPhone generator: calibration pattern mode and sender calibration in the QR

generateStyled(..., {mode: 'calibration'}) shows the calibration pattern in
the generator's own screen: the same display link, layers, audio engine and
player node as the test signal. Portrait halves left and right near the
bottom microphone, a red outline of the board, halves swap every 500 ms, a
1.5 kHz beep 100 ms after each swap (scheduled on the host clock minus the
reported output latency). The session is play-and-record on the built-in
speaker; the microphone take is analysed with the shared core
(display_calibration) and the result comes back when the screen closes.

The test signal sends a saved sender calibration in the AVS4 QR
(senderCalibration* options), only on the audio output it was measured on.
Core pattern helpers are now frame based (also 59.94 fps).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Container generator: AVSYNC_PATTERN=calibration

Same feature as the phone: the generator can send the calibration pattern
instead of the test signal, so a phone with the calibrator board can
measure a TV/monitor chain. Left/right halves swap every 500 ms on the
grain grid (also interlaced and 59.94), a 1.5 kHz beep 100 ms after each
swap at the chirp level, a grey tick at the ends of the pattern line.
Live key, also in the Signal API (pattern) and the web form (Picture).

tests/tools/calibration_flow + tests/integration/calibration_pattern.py
read both flows, check the timing and run a simulated calibrator through
display_calibration (1080p50, 1080i25, 1080p59.94: 0.03 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* App: calibrator wizard (Settings > With calibrator)

Three steps with small animations: plug in, camera (the calibrator on the
rear camera; progress ring of 20 flashes; 'Camera vs mic' result saved as
this phone's analyser calibration, method calibrator-pulse, same record and
validity as the clap), screen (the generator's calibration pattern;
'Screen vs speaker' saved as this phone's sender calibration, method
transponder). The test signal then sends it in the AVS4 QR. A TV/monitor
showing the container's calibration pattern can be measured too (result
only, not saved). The clap calibration is unchanged. iOS only for now
(Android has no generator and no calibrator take yet).

Pure logic in src/calibrator.mjs with node tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Docs: calibrator wizard, calibration pattern, contract app side

docs/calibrator.md (copy of PR #69's contract) gets an 'App side' section
and the list of changes from the v1 proposal (none needs a firmware change).
calibration.md: user guide 'With the calibrator'. phone-generator.md:
calibration mode and the sender calibration in the QR. SPECIFICATION:
AVSYNC_PATTERN and the calibrator in 12.3. IMPLEMENTATION_PLAN: entry with
the deviations. CHANGELOG, mobile-app README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Docs: native contract for the calibrator take and calibration mode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Calibrator take: Cancel during the exposure lock no longer starts it; clap gap fill unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* iOS: type displayCalibration as [String: Any] for Swift

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* iPhone generator: Stop clears the whole raster and shows "Stopped"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Analysers subtract the sender calibration from the AVS4 QR

Done once in core (Pairer::set_sender_calibration) for the phone session,
the container analyzer and the file analyzer. Events keep raw_av_offset_ms
and sender_calibration_ms; the phone reading has rawOffsetMs and
senderCalibrationMs; /api/v1/measurement has raw_offset_ms and
sender_calibration_ms; the web UI Input card shows AV sync raw. Not
calibrated: no change. The phone fixture sends a sender calibration at 2 Hz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* App: raw value and sender calibration, clear both, Title as default nickname

Measure and Results show the raw value and the sender calibration under a
corrected result; saved results keep them. The analyser calibration stays
subtracted in the app only, so nothing counts twice. Clear calibration asks
first and clears the analyser and the sender calibration. A sender's default
name is the newest Title from its QR; a set nickname wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Docs: analysers subtract the sender calibration, clear both, Title as nickname

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* docs/calibrator.md: BOOT button and 1 nF filter row from #69

Same file on both branches now, so the merge has no conflict here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
LeeO86 added a commit that referenced this pull request Nov 9, 2026
* Core: calibrator analysis and calibration pattern

Shared C++ for the AV Sync calibrator board (docs/calibrator.md, contract v1):
- find_sound_onsets: clicks and beeps in a recording (trailing 0.25 ms RMS,
  25 % of peak), told apart by 2.7 kHz vs 1.5 kHz energy.
- find_flash_onsets: LED flash start from row-band means, on the
  rolling-shutter row the analyzer uses; readout from the band height or,
  with full-frame exposure, fitted over the pulses in camera_calibration.
- camera_calibration: pair flashes with clicks, robust median, uncertainty.
- display_calibration: click then beep D = 100 ms later, offset = gap - D,
  both swap directions weighted equally.
- calibration pattern for all generators: halves swap every 500 ms, beep.
Unit tests on synthetic signals (noise, missing pulses, stray sound, rolling
shutter with known/unknown readout, slow screen, sensor mismatch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Phone analyzer: calibrator take (camera and external display)

PhoneAnalyzer::begin_calibrator keeps 32 row-band means per frame and the
microphone, and poll_calibrator reports progress about once a second, then
the result (20 pairs or 30 s) or an error. No live report or QR scan while
it runs. iOS: beginCalibrator(mode) on the camera view; the camera take
first re-locks exposure (frame interval) at the lowest ISO, because the LED
floods the lens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* iPhone generator: calibration pattern mode and sender calibration in the QR

generateStyled(..., {mode: 'calibration'}) shows the calibration pattern in
the generator's own screen: the same display link, layers, audio engine and
player node as the test signal. Portrait halves left and right near the
bottom microphone, a red outline of the board, halves swap every 500 ms, a
1.5 kHz beep 100 ms after each swap (scheduled on the host clock minus the
reported output latency). The session is play-and-record on the built-in
speaker; the microphone take is analysed with the shared core
(display_calibration) and the result comes back when the screen closes.

The test signal sends a saved sender calibration in the AVS4 QR
(senderCalibration* options), only on the audio output it was measured on.
Core pattern helpers are now frame based (also 59.94 fps).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Container generator: AVSYNC_PATTERN=calibration

Same feature as the phone: the generator can send the calibration pattern
instead of the test signal, so a phone with the calibrator board can
measure a TV/monitor chain. Left/right halves swap every 500 ms on the
grain grid (also interlaced and 59.94), a 1.5 kHz beep 100 ms after each
swap at the chirp level, a grey tick at the ends of the pattern line.
Live key, also in the Signal API (pattern) and the web form (Picture).

tests/tools/calibration_flow + tests/integration/calibration_pattern.py
read both flows, check the timing and run a simulated calibrator through
display_calibration (1080p50, 1080i25, 1080p59.94: 0.03 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* App: calibrator wizard (Settings > With calibrator)

Three steps with small animations: plug in, camera (the calibrator on the
rear camera; progress ring of 20 flashes; 'Camera vs mic' result saved as
this phone's analyser calibration, method calibrator-pulse, same record and
validity as the clap), screen (the generator's calibration pattern;
'Screen vs speaker' saved as this phone's sender calibration, method
transponder). The test signal then sends it in the AVS4 QR. A TV/monitor
showing the container's calibration pattern can be measured too (result
only, not saved). The clap calibration is unchanged. iOS only for now
(Android has no generator and no calibrator take yet).

Pure logic in src/calibrator.mjs with node tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Docs: calibrator wizard, calibration pattern, contract app side

docs/calibrator.md (copy of PR #69's contract) gets an 'App side' section
and the list of changes from the v1 proposal (none needs a firmware change).
calibration.md: user guide 'With the calibrator'. phone-generator.md:
calibration mode and the sender calibration in the QR. SPECIFICATION:
AVSYNC_PATTERN and the calibrator in 12.3. IMPLEMENTATION_PLAN: entry with
the deviations. CHANGELOG, mobile-app README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Docs: native contract for the calibrator take and calibration mode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Calibrator take: Cancel during the exposure lock no longer starts it; clap gap fill unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* iOS: type displayCalibration as [String: Any] for Swift

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* iPhone generator: Stop clears the whole raster and shows "Stopped"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Analysers subtract the sender calibration from the AVS4 QR

Done once in core (Pairer::set_sender_calibration) for the phone session,
the container analyzer and the file analyzer. Events keep raw_av_offset_ms
and sender_calibration_ms; the phone reading has rawOffsetMs and
senderCalibrationMs; /api/v1/measurement has raw_offset_ms and
sender_calibration_ms; the web UI Input card shows AV sync raw. Not
calibrated: no change. The phone fixture sends a sender calibration at 2 Hz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* App: raw value and sender calibration, clear both, Title as default nickname

Measure and Results show the raw value and the sender calibration under a
corrected result; saved results keep them. The analyser calibration stays
subtracted in the app only, so nothing counts twice. Clear calibration asks
first and clears the analyser and the sender calibration. A sender's default
name is the newest Title from its QR; a set nickname wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Docs: analysers subtract the sender calibration, clear both, Title as nickname

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Android: generator bridge to the shared core and picture renderer

PhoneGeneratorPlan, the AVS4 QR, render_picture/render_sync_foreground and the
calibration pattern functions compile into libavsync-jni as on iOS
(generator_jni.cpp mirrors AVSyncGenerator.mm). The renderer's text goes through
JNI to Android's text drawing with the bundled DejaVu Sans (GeneratorText.kt),
as iOS uses CoreText; the font and icon come from the iOS resource folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Android: fullscreen generator, calibration mode and image picker

Same behaviour and options as the iPhone generator: test picture with the AVS3
fields, sync square and scale, AVS4 QR with test ID, title and sender calibration,
stable sender ID, 'Stopped' screen, calibration pattern with the microphone.
Frames: Choreographer's expected presentation time (API 33+, else frame start
plus two periods). Sound: one AudioTrack stream written in 5 ms blocks; packets
are placed to the sample by AudioTrack.getTimestamp on System.nanoTime, the
clock Choreographer uses. Module: supportsGenerator, generate, generateStyled,
pickGeneratorImage, generatorImageInfo, like iOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Android: calibrator take in the camera view

beginCalibrator('camera' | 'display') as on iOS: the camera take re-runs the
auto step and locks at the lowest ISO (or the lowest exposure compensation when
the camera has no manual exposure), then PhoneAnalyzer::begin_calibrator starts;
display mode listens only. Progress and result reports come from
poll_calibrator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* App: calibrator wizard on Android, generator title placeholder

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Android: generator tests (audio placement, layout, bridge on a device)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Docs: Android generator and calibrator take

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* docs/calibrator.md: BOOT button and 1 nF filter row from #69

Same file on both branches now, so the merge has no conflict here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

* Android camera view: live poll goes on between calibrator reports, as on iOS

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
LeeO86 and others added 2 commits November 9, 2026 11:24
# Conflicts:
#	docs/calibration.md
#	docs/calibrator.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
@LeeO86
LeeO86 marked this pull request as ready for review November 9, 2026 10:44
@LeeO86
LeeO86 merged commit a0d6220 into main Nov 9, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant