This program should work with any amplifier that works with the AmpServer produced by EGI (http://www.egi.com/). All communication with the amplifier happens through the Amp Server's low-level Network Protocol. Amp Server Pro SDK is documented here. and the network API here.
-
Make sure that your AmpServer is running and can correctly record from its connected amplifier(s).
-
Start the EGIAmpServer app. You should see a window like the following.
-
Make sure that you have the correct IP address of the AmpServer assigned. The ports correspond to the default settings of the server and should not require a change.
-
If you have multiple amplifiers connected to the AmpServer and you would like to record from a specific one, you need to set the correct amplifier ID (these should be increasing from zero). Also make sure that you are using a supported number of channels and a supported sampling rate (the defaults should work).
-
To link the application to the LSL, click the "Link" button. If all goes well you should now have a new stream on the network with name "EGI NetAmp k" (k corresponding to the index of the amplifier) and type "EEG". If you get an error you might try to manually power on the desired Amp and try to link while it is either recording or stopped.
-
For subsequent uses you can save the desired settings from the GUI via File / Save Configuration. If the app is frequently used with different settings you might make a shortcut on the desktop that points to the app and appends to the Target field of the shortcut the snippet
-c name_of_config.cfgto denote the name of the config file that should be loaded at startup.
The CLI provides a lightweight alternative to the GUI:
./EGIAmpServerCLI [options]--config <file>- Load configuration from file--address <addr>- AmpServer IP address (default: 10.10.10.51)--cmd-port <port>- Command port (default: 9877)--data-port <port>- Data port (default: 9879)--amp-id <id>- Amplifier ID (default: 0)--sample-rate <hz>- Sample rate in Hz (default: 1000). Forces amplifier to this rate if already running at a different rate. Valid rates: 250, 500, 1000 (decimated) or 500, 1000, 2000, 4000, 8000 (native).--fast-recovery- Use native rate mode (no FPGA anti-alias filter) for lower latency. See Sample Rate Modes.--impedance- Enable impedance testing mode--native-format- Transmit raw int32 ADC counts instead of float microvolts--shutdown- Shutdown the Amp Server (terminates all connections)--help- Show help message
# Basic usage
./EGIAmpServerCLI --address 10.10.10.51
# With impedance testing
./EGIAmpServerCLI --address 10.10.10.51 --impedance
# With native format (int32 ADC counts)
./EGIAmpServerCLI --address 10.10.10.51 --native-format
# Low-latency mode (fast recovery / native)
./EGIAmpServerCLI --address 10.10.10.51 --sample-rate 1000 --fast-recovery
# Decimated mode — timestamp compensation is applied automatically
./EGIAmpServerCLI --address 10.10.10.51 --sample-rate 1000When another application (e.g., Net Station Acquisition) has already started the amplifier, the CLI's behavior depends on whether you pass any rate or mode flags.
With no rate flags, the CLI detects the running amplifier, measures its current sample rate from the data stream, and attaches without disrupting the existing session:
# Attaches to whatever rate/mode the amp is already using
./EGIAmpServerCLI --address 10.10.10.51This is safe to use alongside Net Station — it will not stop or reconfigure the amplifier.
The following flags cause the CLI to stop, reconfigure, and restart the amplifier — even if it was started by another application:
--sample-rate <hz>— reinitializes if the detected rate differs from the requested rate. At 500/1000 Hz, where native and decimated are indistinguishable in the data stream, requesting a decimated rate also forces reinitialization to guarantee decimated mode (so the automatic timestamp compensation is correct)--fast-recovery— reinitializes to ensure native (unfiltered) mode
Warning: Reinitialization will interrupt any active Net Station recording. If you need to coexist with Net Station, omit these flags and let the CLI match the existing configuration.
If the amplifier is stopped or restarted externally while the CLI is streaming:
- Amp powered off by another app (e.g., Net Station clicks "Off"): The CLI detects stream loss and attempts to reinitialize the amplifier with its original settings.
- Amp restarted by another app (e.g., Net Station reconnects): The CLI detects the new stream, adapts to whatever rate the other app configured, and recreates its LSL outlets if the rate changed.
- Amp power-cycled or shutdown: The CLI waits up to 120 seconds for recovery before giving up.
The NA400/NA410 amplifiers support two operating modes that affect anti-aliasing and latency:
Uses the FPGA's digital anti-aliasing filter to downsample from the ADC's native rate. This provides:
- Better frequency response (~400 Hz bandwidth at 1000 Hz sample rate)
- Higher latency due to the filter group delay (36-111 ms depending on rate), which the app compensates for automatically — see Timestamp Compensation
Available decimated rates: 250, 500, 1000 Hz
Bypasses the FPGA filter and samples directly at the requested rate. This provides:
- Lower latency (no filter group delay)
- Reduced bandwidth (~1/4 of sample rate, e.g., 250 Hz at 1000 Hz sample rate)
- Optimized for EEG-TMS and real-time BCI applications
Available native rates: 500, 1000, 2000, 4000, 8000 Hz
Use --fast-recovery to enable native mode for rates that support both modes (500, 1000 Hz).
DIN→EEG group delay of the FPGA anti-alias filter (decimated mode only), measured with scripts/delay_capture_sweep.py:
| Mode | Sample Rate | Filter delay (ms) | (samples) |
|---|---|---|---|
| Decimated | 250 Hz | 111 | ~28 |
| Decimated | 500 Hz | 61 | ~30 |
| Decimated | 1000 Hz | 36 | 36 |
| Native | 500-8000 Hz | 0 | 0 |
The application adjusts the LSL timestamp it assigns to each sample so that the timestamp reflects when the signal actually occurred, not when the bytes happened to arrive at this client. This makes EEG, Physio16, and DIN events line up with each other and with external event markers. Three corrections are applied; together they are what we call timestamp compensation.
There is no flag to toggle this — it follows the configured mode automatically. The app trusts the mode you configure: a decimated rate means the filter corrections apply; a native rate (--fast-recovery, or any rate above 1000 Hz) means they do not. At 500/1000 Hz, where native and decimated are indistinguishable in the data stream, the app assumes the mode you asked for (and, when forcing a rate, reinitializes to guarantee it).
Every pushed timestamp (EEG, Physio16, and DIN) is moved earlier by systemdelayms to account for the time between digitization and the sample becoming available here: device firmware + network transmission + our read path. Measured at ~5 ms in native mode using scripts/audio_latency_test.
The default is 4.5 ms — deliberately a little under the measured value. Under-compensating is the safe direction: it guarantees a sample is never back-dated to before the event that produced it (a response must not precede its stimulus). It is user-configurable; raise it toward the measured latency if you prefer tighter alignment and can accept that risk.
In decimated mode the anti-alias filter delays the EEG relative to the (unfiltered) DIN by the group delay in the table above (111/61/36 ms at 250/500/1000 Hz). The app subtracts this from the EEG/Physio timestamps so filtered EEG lines up with DIN. Native mode has no filter, so nothing is subtracted. DIN itself is never filter-shifted (only the system delay applies to it).
The Physio16 (PNS/PIB) acquisition path runs ahead of the FPGA-filtered EEG by a fixed ~33 ms in decimated mode, so the same event lands at different sample indices in the EEG vs physio channels of the combined stream. To keep them sample-aligned, the physio channels are buffered (delayed) by physioaligndelayms (default 33 ms, applied as floor(33 ms × rate) samples). The physio channels therefore read zeros for the first ~33 ms after streaming starts, then real data. Native mode applies no physio delay.
Both values live in the config file (and persist via File → Save Configuration):
<settings>
...
<physioaligndelayms>33</physioaligndelayms> <!-- Physio16 realignment, decimated only -->
<systemdelayms>4.5</systemdelayms> <!-- pipeline-latency compensation, always -->
</settings>The filter-offset values (111/61/36 ms) are built in. All three were measured on real hardware; see scripts/audio_latency_test/README.md (system delay) and notebooks/delay_inspection.ipynb (filter and physio delays).
The filter corrections are only correct when the operating mode is known. If another application (e.g. Net Station) already started the amplifier at an ambiguous rate (500/1000 Hz), the app applies the corrections for the mode you configured, assuming it matches. If you are unsure of the running mode, attach with a native configuration (no filter corrections) or let the app reinitialize to a known mode (which interrupts the existing session).
The application supports electrode impedance measurement using the same algorithm as Net Station Acquisition.
When impedance mode is enabled:
- The amplifier is configured for impedance measurement (20 Hz calibration signal)
- Two LSL streams are created:
- EEG Stream (
type: EEG) - Raw amplifier data containing the calibration signal - Impedance Stream (
type: Impedance) - Calculated impedance values in kilo-ohms
- EEG Stream (
- The application cycles through each channel, measuring impedance one at a time
- A complete scan of all channels takes approximately 1-5 minutes depending on channel count
The impedance measurement follows Net Station's algorithm:
-
Initial State: All channels are "driving" the 20 Hz calibration signal
- Drive signals ON, 10K resistors OFF for all channels
- Oscillator enabled at 20 Hz sine wave, maximum amplitude (4095)
-
Per-Channel Measurement: For each channel:
- Turn OFF the drive signal (isolate channel from calibration)
- Turn ON the 10K reference resistor
- Wait ~1 second for signal to settle
- Measure peak-to-peak amplitude from collected samples
- Calculate impedance using:
Z = (idealSignal - amplitude) / (amplitude / 10) - Reset channel back to driving state
-
Output: After measuring all channels, the impedance values (in kOhms) are pushed to the LSL impedance stream
./EGIAmpServerCLI --address 10.10.10.51 --impedanceAdd to your ampserver_config.cfg:
<settings>
<impedance>true</impedance>
<!-- other settings -->
</settings>- Name:
EGI NetAmp <amp_id> - Type:
EEG - Rate: Configured sample rate (e.g., 1000 Hz)
- Channels: Depends on sensor net (32-256 channels)
- Format:
float32(default) orint32(with--native-format) - Unit:
microvolts(default) orcounts(with--native-format) - Behavior: Streams continuously with raw amplifier data (contains 20 Hz calibration signal during impedance mode)
When --native-format is enabled:
- Data is transmitted as raw int32 ADC counts instead of float microvolts
- Channel metadata includes a
conversionfield with the scaling factor - Downstream consumers can convert to microvolts:
microvolts = counts × conversion - This mode is useful for applications that need maximum precision or want to handle scaling themselves (e.g., NWB export)
- Name:
EGI NetAmp <amp_id> Impedance - Type:
Impedance - Rate: 1 Hz (regular rate)
- Channels: One per electrode (E1, E2, ...) plus a trailing reference channel labeled
Cz— i.e. one more channel than the EEG stream carries for a standard net. TheCzvalue is the reference-electrode impedance, measured via the amplifier's dedicated reference monitor once per scan cycle. - Unit:
kohms(kilo-ohms) - Behavior: Publishes current known impedance values every second
- Initially, all channels (including
Cz) show the not-measured sentinel value - As each channel is measured, its value updates
- Values persist until the next measurement of that channel
- Initially, all channels (including
Each sample in the impedance stream contains one value per channel:
- Good electrodes: Typically 5-50 kOhms
- Acceptable electrodes: 50-100 kOhms
- Poor electrodes: 100-200 kOhms
- Bad/disconnected electrodes: 1000 kOhms (maximum/clipped value)
cmd_TurnAll10KOhms(0) - 10K resistors OFF (channels driving)
cmd_TurnAllDriveSignals(1) - Drive signals ON (all channels active)
cmd_SetSubjectGround(0) - Subject ground OFF
cmd_SetCurrentSource(0) - Current source OFF
cmd_SetCalibrationSignalFreq(20) - 20 Hz calibration signal
cmd_SetWaveShape(0) - Sine wave
cmd_SetBufferedReference(0) - Buffered reference OFF
cmd_SetOscillatorGate(1) - Oscillator ON
cmd_SetReference10KOhms(0) - Reference 10K OFF
cmd_SetReferenceDriveSignal(0) - Reference drive signal OFF
cmd_SetDrivenCommon(0) - Driven leg OFF
cmd_SetCalibrationSignalAmplitude(4095) - Maximum amplitude
# To measure channel N:
cmd_TurnChannelDriveSignals(N, 0) - Turn OFF drive signal
cmd_TurnChannel10KOhms(N, 1) - Turn ON 10K resistor
# After measurement:
cmd_TurnChannelDriveSignals(N, 1) - Turn ON drive signal (reset)
cmd_TurnChannel10KOhms(N, 0) - Turn OFF 10K resistor (reset)
- Tiling-based scanning: When a tiling layout is available for the detected net (32/64/128/256 HydroCel GSN nets), channels are measured in groups (~5-6 collections per full scan) rather than one at a time — no flag required, it engages automatically. A full 256-channel scan drops from ~5 minutes to roughly a minute. Nets without a tiling layout fall back to single-channel scanning.
- Ideal signal estimation: The "ideal signal" (expected amplitude with 0 impedance) is estimated from the first measurement. For more accurate results, gains calibration should be performed first
- Net Station compatibility: Running impedance mode will interfere with Net Station Acquisition if it's connected to the same amplifier
- Channel labels: The E-channels share labels with the EEG stream (E1, E2, ..., En); the impedance stream additionally carries a trailing
Czreference channel
Example Python code to process impedance data:
import pylsl
# Resolve both streams
streams = pylsl.resolve_byprop('type', 'EEG')
impedance_streams = pylsl.resolve_byprop('type', 'Impedance')
# Create inlets
eeg_inlet = pylsl.StreamInlet(streams[0])
imp_inlet = pylsl.StreamInlet(impedance_streams[0])
# Pull impedance data (arrives at 1 Hz)
imp_sample, timestamp = imp_inlet.pull_sample(timeout=2.0)
if imp_sample:
# Count how many channels have been measured
measured = sum(1 for z in imp_sample if z < 1000)
print(f"Measured: {measured}/{len(imp_sample)} channels")
for ch, z in enumerate(imp_sample):
if z >= 1000:
status = "not measured"
elif z < 50:
status = "good"
elif z < 100:
status = "ok"
elif z < 200:
status = "poor"
else:
status = "bad"
print(f"E{ch+1}: {z:.1f} kOhms ({status})")
# EEG data continues normally (contains calibration signal during impedance mode)
eeg_sample, timestamp = eeg_inlet.pull_sample()The DIN values are published on a separate LSL stream (not as a channel in the EEG stream). The stream is event-driven: a sample is only pushed when the DIN value changes.
- Name:
EGI NetAmp <amp_id>_DIN - Type:
Markers - Format:
int32 - Rate: Irregular (event-driven, only on change)
- Channels: 1 (
DIN) - Value range: 0-65535 (0x0000-0xFFFF)
- Idle value: 0 (all inputs off)
The amplifier's DIN lines are active-low with internal pull-ups. The application inverts the raw value so that 0 = idle and a set bit = active input.
import pylsl
# Resolve the DIN stream (separate from EEG)
streams = pylsl.resolve_byprop('type', 'Markers', timeout=5)
din_streams = [s for s in streams if s.name().endswith('_DIN')]
inlet = pylsl.StreamInlet(din_streams[0])
# Pull a DIN change event
sample, timestamp = inlet.pull_sample(timeout=5.0)
if sample:
din_value = int(sample[0])
# Extract individual bits (DIN1-DIN16)
din1 = (din_value >> 0) & 1 # Bit 0
din2 = (din_value >> 1) & 1 # Bit 1
din3 = (din_value >> 2) & 1 # Bit 2
# ... etc
# Or extract all 16 bits
bits = [(din_value >> i) & 1 for i in range(16)]
print(f"DIN1-16: {bits}")The amplifier's internal DIN ADC samples at a fixed 1 kHz rate, regardless of the EEG sample rate. The DIN stream only emits a sample when the value changes, so the effective timing resolution depends on the EEG sample rate (which determines how often the DIN register is read):
| EEG Sample Rate | DIN Poll Rate | Effective DIN Resolution |
|---|---|---|
| 250 Hz | Decimated (1 in 4 samples) | 4 ms |
| 500 Hz | Decimated (1 in 2 samples) | 2 ms |
| 1000 Hz | 1:1 mapping | 1 ms |
| 2000 Hz | Duplicated (2 per DIN sample) | 1 ms |
| 4000 Hz | Duplicated (4 per DIN sample) | 1 ms |
Recommendation: Use a sample rate of 1000 Hz or higher if precise DIN timing is required. At lower sample rates, fast TTL pulses (< 4ms at 250 Hz) may be missed.
The NA400/NA410 amplifiers have a 16-bit digital I/O port with active-low inputs (internal pull-ups). When no trigger device is connected, all lines are pulled high and the DIN stream reports 0 (idle). Grounding a line activates the corresponding bit.
The cmd_SetDigitalInOutDirection command can configure specific bits for output if needed (consult EGI documentation).
This application was written to behave near-identically to the BCI2000 AmpServer module that was originally created by EGI.
The configuration settings can be saved to a .cfg file (see File / Save Configuration) and subsequently loaded from such a file (via File / Load Configuration).
Importantly, the program can be started with a command-line argument of the form
EGIAmpServer.exe myconfig.cfg, which allows to load the config automatically at start-up.
The recommended procedure to use the app in production experiments is to make a shortcut on
the experimenter's desktop which points to a previously saved configuration customized to the
study being recorded to minimize the chance of operator error.
To use the mock server for development:
Terminal 1: Start mock server
python3 mock/mock_ampserver.py
Terminal 2: Run CLI or GUI against localhost
./cli/EGIAmpServerCLI --address 127.0.0.1
The mock server generates synthetic sine waves (10-50 Hz) with noise for the EEG data, so you can also verify the LSL stream in downstream applications.
See also Connecting to an Already-Running Amplifier for details on how the CLI behaves when attaching to an amp started by Net Station.
The following table documents how EGIAmpServer behaves when it is already connected and streaming, and Net Station Acquisition (NAS) subsequently interacts with the amplifier:
| NAS Action | EGIAmpServer Behavior | Stream Status |
|---|---|---|
| NAS launches and scans for devices | No change detected | Continues |
| NAS clicks "On" (connect) | No change (amp already running) | Continues |
| NAS starts recording | No change detected | Continues |
| NAS stops recording | No change detected | Continues |
| NAS clicks "Off" (disconnect) | ERROR: The stream was lost - stream terminates |
Stopped |
| NAS closes | No additional change | Already stopped |
Key finding: NAS powering off the amplifier terminates the EGIAmpServer data stream. The CLI process remains running but stops streaming data.
NAS has an "Amplifier Shutdown" button that is more severe than simply clicking "Off". When triggered while EGIAmpServer is streaming:
| Test | Result |
|---|---|
| EGIAmpServer stream | ERROR: The stream was lost - terminates |
| Ping amplifier (10.10.10.51) | FAILED - 100% packet loss |
| TCP connect to AmpServer | Succeeded (AmpServer software still running) |
| Query amplifier details | FAILED - Failed to get amplifier details |
| Start new stream | FAILED |
Recovery: The amplifier requires a power cycle to recover from this state. The AmpServer software remains accessible but cannot communicate with the shutdown amplifier hardware.
When using this application alongside Net Station Acquisition:
-
Start Net Station first: If you plan to use Net Station Acquisition, start it and initialize the amplifier (click "On") BEFORE connecting this app. Our app will detect the running amplifier and automatically use its sample rate.
-
Do not start Net Station after: If this app is already streaming and Net Station subsequently initializes the amplifier at a different sample rate, our app cannot detect this change. AmpServer only sends notifications to one subscriber, and Net Station consumes them when it's running.
-
Recommended workflow:
- Start Net Station Acquisition
- Initialize the amplifier at your desired sample rate (click "On")
- Start EGIAmpServer (GUI: click "Link", or CLI: run with no rate flags) — it will detect the running amp and match its sample rate
- Both applications will now receive data at the correct rate
After Net Station shuts down the amplifier (via "Shutdown" command), immediately starting this app may result in excessive dropped packets and eventual stream loss. This appears to be related to stale data in the connection. Workaround: Wait a moment and restart the app, or power cycle the amplifier.
The app automatically detects the sample rate when connecting to an already-running amplifier by measuring packet timing. This detection snaps to standard rates (250, 500, or 1000 Hz). If the amplifier is idle when connecting, the app uses the sample rate configured in the UI/config file.
