Skip to content

NA500: match the mock to real hardware; fix NA500 streaming, timing and physio - #20

Merged
cboulay merged 8 commits into
cboulay/na500_supportfrom
cboulay/na500-conformance
Nov 2, 2026
Merged

cboulay merged 8 commits into
cboulay/na500_supportfrom
cboulay/na500-conformance

Conversation

@cboulay

@cboulay cboulay commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #18. Everything here was measured against a real Net Amps 500 / GES 500 (firmware 0.4.9). All app changes are gated on the NA500; the NA400 code path is unchanged.

Conformance recorder and NA500 mock

  • scripts/ampserver_conformance.py records how an AmpServer behaves on the wire, and diffs two recordings. The recording of the real NA500 is mock/conformance/na500_fw0.4.9.json.
  • MockAmpServer --amp-type NA500 now runs a session-based server (NA500Server). It matches the device on all 152 compared behaviors, including:
    • one client per port per session;
    • commands with no reply or a double reply;
    • leftover packets after cmd_Start;
    • front-end state;
    • the ~8000 samples/s delivery ceiling;
    • sample loss when a client stops reading.
  • docs/NA500_AmpServer.md documents the protocol as observed.

App fixes (NA500)

  • EEG pinned at full scale: EEG stayed at −375000 µV until the buffered reference is enabled. Every cmd_Stop turns it off, and switch commands sent within ~20 ms of power-on are lost. The app now waits 200 ms after power-on and enables the buffered reference before each start.
  • Stale packets: the app discards the leftover packets the amp resends after cmd_Start. They had carried stale samples into LSL and caused a false "Packet(s) Dropped".
  • Physio16 wait: the app no longer waits 5 s for a Physio16 notification the NA500 never sends. Leaving the data port unread that long made the amp drop ~4 s of samples.
  • Physio streaming: the app now streams the 16 physio channels as PIB1–16. Their gains persist on the amp across sessions, so the app sets them to 12 before each start.
  • Amp clock: the app sets the amp clock with cmd_SetAmpTime at power-on, as Net Station does.
  • Rates: 8000 and 16000 Hz are no longer offered. The amp delivers at most ~7995 samples/s and drops the rest without any gap in the packet counter.
  • System delay: systemdelayms is now optional, with a per-model default.
    • NA500: 0.8 ms + one sample period, a little under the measured ~1.0 ms + 1.1 sample periods.
    • All other models: 4.5 ms, as before.

Latency (audio tone test, relative to DAC-stamped tone markers)

Rate EEG Physio (PIB1)
1000 Hz +0.15 ms (std 0.13) +0.15 ms (std 0.13)
4000 Hz +0.29 ms (std 0.07) +0.21 ms (std 0.07)

There is no physio-vs-EEG skew on the NA500, and no FPGA filter delay. So decimated and native modes stream identically.

Also

Triggers and impedance (NA500)

  • Triggers:
    • DB9 digital inputs enabled with cmd_SetDinEnable; without it they read as all 16 active.
    • BNC in, in event mode, is reported in the _DIN stream as bit 16. Verified with a BNC out → BNC in loopback at 1 and 4 kHz.
    • The EEG trails a BNC-in event by ~1.5 samples, the ADC filter's delay. No correction is applied, as in Net Station.
  • Impedance mode:
    • DAC amplitude 1023, giving ~5000 µV p-p; uses Net Station's fixed 5000 µV ideal signal.
    • The oscillator is switched on after amplitude and frequency, because the NA500 latches them at switch-on.
    • The reference is measured in the NA500's COM/REF measurement mode.
    • The acquisition state is restored explicitly afterwards.
    • Verified on a test jig that shorts all inputs: every channel ~0 kΩ, reference ~0.5 kΩ.
  • Mock: models the above. A mock bug that ignored per-channel drive/10K switches is fixed; it also affected the NA400 mock.

Not covered

  • Impedance accuracy: not checked against real electrodes or Net Station; only a shorting jig was available.
  • DB9 pins: the physical pin-to-DIN mapping is not verified, for lack of a mating connector.
  • NA400: could not be re-tested; no hardware is available.

@cboulay
cboulay force-pushed the cboulay/na500-conformance branch from 3ce0744 to 191e29a Compare November 1, 2026 20:08
@cboulay
cboulay added this pull request to stack #21 November 2, 2026 06:59
Measured against a real NA500 (fw 0.4.9) with a new wire-level
conformance recorder, scripts/ampserver_conformance.py. Its recording of
the device is mock/conformance/na500_fw0.4.9.json, and the observed
protocol is written up in docs/NA500_AmpServer.md.

Mock: --amp-type NA500 now runs a dedicated session-based server
(NA500Server) that matches the device on every compared behavior: one
client per port per session, session ends with the command connection,
one command per TCP segment, unknown commands re-run the previous one,
missing/double replies, unbalanced replies, notification timing,
8-sample blocks, counter restart and leftover packets after cmd_Start,
the ~8000 samples/s ceiling at 16 kHz, and sample loss when a client
stops reading. --net-code None emulates a unit with no net. NA400 and
other models are unchanged.

App (all NA500-gated; the NA400 path is untouched):
- Discard the leftover packets the NA500 sends after cmd_Start; they
  carried stale samples into LSL and a false "Packet(s) Dropped".
- Skip the Physio16 status wait: the NA500 never sends the notification,
  and leaving its data port unread for 5 s lost ~4 s of samples.
- Set the amp clock with cmd_SetAmpTime at power-on, as Net Station does.
- Stop offering 16000 Hz: the NA500 delivers only ~8000 samples/s there.

Also bumps liblsl to v1.18.0.b6 and documents finding the NA500 by mDNS.
On the NA500 the EEG reads negative full scale until cmd_SetBufferedReference 1,
every cmd_Stop switches it off again, and switch commands sent within ~20 ms
of the cmd_SetPower 1 reply are silently lost. The app sent neither, so its
NA500 EEG was pinned at -375000 uV. It now waits 200 ms after power-on and
enables the buffered reference before each cmd_Start (NA500 only; skipped in
impedance mode).

The mock and the conformance recorder model and check all three behaviors;
the recorder also waits briefly after connecting so the NA500 has accepted
the notification port before the first command.
…loss

Measured with scripts/audio_latency_test against DAC-stamped tone markers,
the NA500's digitization-to-client latency is ~1.0 ms + 1.1 sample periods
(5.4 ms at 250 Hz, 2.1 ms at 1 kHz, 1.3 ms at 4 kHz), well under the NA400's
~5 ms, so the 4.5 ms system delay back-dated NA500 samples by 1-3 ms.

systemdelayms is now optional. Unset, it defaults to 0.8 ms + one sample
period on the NA500 (a little under the measured latency at every rate;
verified +0.33 ms at 1 kHz) and to 4.5 ms on every other model, so NA400
behavior is unchanged. The shipped config no longer pins it.

The NA500 also delivers at most ~7995 packets/s: 8 kHz silently loses ~4
samples/s (not only 16 kHz), so its timeline drifts. Documented, and the
mock's delivery ceiling now matches.
The NA500 delivers at most ~7995 samples/s and silently drops the rest, so at
8000 Hz it loses ~4 samples/s and the sample timeline drifts by milliseconds
over a run (measured with the audio latency test). 4000 Hz is now the NA500's
highest rate; the NA400's rates are unchanged.
The NA500 sends no Physio16 status notification, so the app streamed no physio
at all. Its 16 physio inputs are always in the data stream (pib1_Data[0-7],
pib2_Data[0-7]), so the app now always streams them as PIB1-16 on the NA500.

Physio gains persist on the amplifier across sessions (a gain set by another
client stays), so before each cmd_Start the app pins all 16 to 12, the
power-on default, and scales them like the EEG. Measured with the audio
latency test: PIB1 timing matches EEG within 0.1 ms at 250-4000 Hz, so no
physio realignment is needed (filter delays are 0 on the NA500).

The NA500 mock zeroes the unused physio slots, as the device does. All
changes are NA500-only; the NA400 physio path is unchanged.
The NA500's DB9 inputs read 0 until cmd_SetDinEnable, and the app never sent
it, so its _DIN stream reported all 16 (active-low) inputs as active. The app
now enables them, and puts the BNC input in event mode, before each start.

Each rising edge on BNC in arrives as a 1 ms event in bit 2 of the packet's
first reserved byte (verified with a BNC out -> BNC in loopback at 1 and
4 kHz); the app reports it in the _DIN stream as bit 16 (0x10000). On the
NA400 that bit is never set, so its DIN events are unchanged.

The NA500 mock emulates the BNC input from its test-jig pulse, and resets the
BNC mode at the end of a session as the device does.
The NA400 impedance sequence did not work on the NA500. Changes, all
NA500-only unless noted:

- Drive amplitude 1023 (its DAC is 10-bit), which gives ~5000 uV p-p on every
  channel; use Net Station's fixed 5000 uV ideal signal, since the app's
  self-calibration caught a switching transient (5300-5900 uV).
- Switch the oscillator on after setting amplitude and frequency: the NA500
  latches them at switch-on, so an app run that followed a reset drove nothing.
- Measure the reference in the NA500's COM/REF measurement mode
  (cmd_SetCOMAndRefForMeasurement, ~1.2 s settles, then re-enable the DINs), as
  Net Station does.
- Restore the acquisition state explicitly when impedance mode ends:
  cmd_DefaultAcquisitionState does nothing on the NA500.
- Apply the NA500 front-end/trigger/physio setup in impedance mode too.

Verified on a GES 500 with a test jig that shorts all inputs: 256/256 channels
~0 kOhm, reference ~0.5 kOhm, and normal acquisition afterwards.

Mock: the NA500 mock models the 5000 uV drive, the oscillator latch and the
COM/REF mode. A long-standing mock bug ignored per-channel drive/10K switches
while "all" was set, so per-channel impedance never registered (this also
affects the NA400 mock, which now measures per channel).
@cboulay
cboulay force-pushed the cboulay/na500-conformance branch from 6ea5fb0 to a37067f Compare November 2, 2026 07:01
@cboulay
cboulay merged commit 1b0182e into master Nov 2, 2026
6 checks passed
@cboulay
cboulay deleted the cboulay/na500-conformance branch November 2, 2026 07:05
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