Skip to content

Commit 7f102ee

Browse files
committed
Update mock amplifier
1 parent dbbb963 commit 7f102ee

4 files changed

Lines changed: 119 additions & 72 deletions

File tree

Impedance.md

Lines changed: 44 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -40,36 +40,36 @@ The measurement process uses three amplifier states:
4040

4141
All electrodes actively drive the calibration signal onto the scalp:
4242

43-
| Setting | Value | Description |
44-
|---------|-------|-------------|
45-
| All 10K Resistors | OFF | Electrodes not connected to reference |
46-
| All Drive Signals | ON | All electrodes driving calibration signal |
47-
| Oscillator Gate | ON | Enable 20 Hz sine wave generator |
48-
| Calibration Frequency | 20 Hz | Low frequency for skin penetration |
49-
| Calibration Amplitude | 4095 | Maximum (12-bit DAC) |
50-
| Wave Shape | Sine (0) | Clean sinusoidal signal |
51-
| Subject Ground | OFF | Not grounded during measurement |
52-
| Current Source | OFF | Voltage mode measurement |
43+
| Setting | Value | Description |
44+
|-----------------------|----------|-------------------------------------------|
45+
| All 10K Resistors | OFF | Electrodes not connected to reference |
46+
| All Drive Signals | ON | All electrodes driving calibration signal |
47+
| Oscillator Gate | ON | Enable 20 Hz sine wave generator |
48+
| Calibration Frequency | 20 Hz | Low frequency for skin penetration |
49+
| Calibration Amplitude | 4095 | Maximum (12-bit DAC) |
50+
| Wave Shape | Sine (0) | Clean sinusoidal signal |
51+
| Subject Ground | OFF | Not grounded during measurement |
52+
| Current Source | OFF | Voltage mode measurement |
5353

5454
### Measurement State (Per-Channel)
5555

5656
For the channel being measured:
5757

58-
| Setting | Value | Description |
59-
|---------|-------|-------------|
60-
| Channel Drive Signal | OFF | Stop driving this electrode |
61-
| Channel 10K Resistor | ON | Connect to reference resistor |
58+
| Setting | Value | Description |
59+
|----------------------|--------|-------------------------------|
60+
| Channel Drive Signal | OFF | Stop driving this electrode |
61+
| Channel 10K Resistor | ON | Connect to reference resistor |
6262

6363
All other channels remain in excitation state, creating the voltage divider.
6464

6565
### Reset State
6666

6767
After measurement, return channel to excitation state:
6868

69-
| Setting | Value | Description |
70-
|---------|-------|-------------|
71-
| Channel Drive Signal | ON | Resume driving |
72-
| Channel 10K Resistor | OFF | Disconnect reference |
69+
| Setting | Value | Description |
70+
|----------------------|--------|----------------------|
71+
| Channel Drive Signal | ON | Resume driving |
72+
| Channel 10K Resistor | OFF | Disconnect reference |
7373

7474
## Measurement Algorithm
7575

@@ -91,14 +91,12 @@ After measurement, return channel to excitation state:
9191

9292
## Timing Parameters
9393

94-
From EGI's Net Station implementation:
95-
96-
| Parameter | Value | Description |
97-
|-----------|-------|-------------|
98-
| Command Time | 30 ms | Time for command to reach amplifier |
99-
| Settle Time | 0 ms | Additional settling (Net Station uses 0) |
100-
| Filter Time | 1.0 s | Duration for filter/measurement |
101-
| Peak-to-Peak Samples | 51 | Samples for amplitude calculation |
94+
| Parameter | Value | Description |
95+
|----------------------|--------|-------------------------------------|
96+
| Command Time | 30 ms | Time for command to reach amplifier |
97+
| Settle Time | 0 ms | Additional settling |
98+
| Filter Time | 1.0 s | Duration for filter/measurement |
99+
| Peak-to-Peak Samples | 51 | Samples for amplitude calculation |
102100

103101
Total time per channel: ~1.03 seconds
104102

@@ -142,14 +140,14 @@ Coordinates are on a unit sphere:
142140

143141
### Key Source Files
144142

145-
| File | Description |
146-
|------|-------------|
147-
| [ImpedanceMeasurement.h](src/core/include/egiamp/ImpedanceMeasurement.h) | Class declaration and timing structures |
148-
| [ImpedanceMeasurement.cpp](src/core/src/ImpedanceMeasurement.cpp) | Measurement algorithm implementation |
149-
| [LSLStreamer.cpp](src/core/src/LSLStreamer.cpp) | LSL outlet creation with electrode positions |
150-
| [ElectrodePositions.h](src/core/include/egiamp/ElectrodePositions.h) | Electrode coordinate definitions |
151-
| [ElectrodePositions.cpp](src/core/src/ElectrodePositions.cpp) | Geodesic position generation |
152-
| [EGIAmpClient.cpp](src/core/src/EGIAmpClient.cpp) | Integration with streaming client |
143+
| File | Description |
144+
|--------------------------------------------------------------------------|----------------------------------------------|
145+
| [ImpedanceMeasurement.h](src/core/include/egiamp/ImpedanceMeasurement.h) | Class declaration and timing structures |
146+
| [ImpedanceMeasurement.cpp](src/core/src/ImpedanceMeasurement.cpp) | Measurement algorithm implementation |
147+
| [LSLStreamer.cpp](src/core/src/LSLStreamer.cpp) | LSL outlet creation with electrode positions |
148+
| [ElectrodePositions.h](src/core/include/egiamp/ElectrodePositions.h) | Electrode coordinate definitions |
149+
| [ElectrodePositions.cpp](src/core/src/ElectrodePositions.cpp) | Geodesic position generation |
150+
| [EGIAmpClient.cpp](src/core/src/EGIAmpClient.cpp) | Integration with streaming client |
153151

154152
### Key Classes
155153

@@ -166,17 +164,17 @@ Coordinates are on a unit sphere:
166164

167165
### AmpServer Commands
168166

169-
| Command | Description |
170-
|---------|-------------|
171-
| `cmd_TurnAll10KOhms` | Set all 10K resistors (0=off, 1=on) |
172-
| `cmd_TurnAllDriveSignals` | Set all drive signals (0=off, 1=on) |
173-
| `cmd_TurnChannel10KOhms` | Set single channel 10K resistor |
174-
| `cmd_TurnChannelDriveSignals` | Set single channel drive signal |
175-
| `cmd_SetOscillatorGate` | Enable/disable calibration oscillator |
176-
| `cmd_SetCalibrationSignalFreq` | Set frequency (20 Hz for impedance) |
177-
| `cmd_SetCalibrationSignalAmplitude` | Set amplitude (0-4095) |
178-
| `cmd_SetWaveShape` | Set waveform (0=sine, 1=square) |
179-
| `cmd_DefaultAcquisitionState` | Reset to normal EEG acquisition |
167+
| Command | Description |
168+
|-------------------------------------|---------------------------------------|
169+
| `cmd_TurnAll10KOhms` | Set all 10K resistors (0=off, 1=on) |
170+
| `cmd_TurnAllDriveSignals` | Set all drive signals (0=off, 1=on) |
171+
| `cmd_TurnChannel10KOhms` | Set single channel 10K resistor |
172+
| `cmd_TurnChannelDriveSignals` | Set single channel drive signal |
173+
| `cmd_SetOscillatorGate` | Enable/disable calibration oscillator |
174+
| `cmd_SetCalibrationSignalFreq` | Set frequency (20 Hz for impedance) |
175+
| `cmd_SetCalibrationSignalAmplitude` | Set amplitude (0-4095) |
176+
| `cmd_SetWaveShape` | Set waveform (0=sine, 1=square) |
177+
| `cmd_DefaultAcquisitionState` | Reset to normal EEG acquisition |
180178

181179
## Usage
182180

@@ -201,18 +199,6 @@ pip install -r requirements.txt
201199
python impedance_viewer.py --threshold 50
202200
```
203201

204-
## Impedance Thresholds
205-
206-
Recommended thresholds for EGI saline-based nets:
207-
208-
| Quality | Impedance (kΩ) | Color |
209-
|---------|----------------|-------|
210-
| Excellent | < 20 | Green |
211-
| Good | 20 - 50 | Yellow-Green |
212-
| Acceptable | 50 - 100 | Yellow |
213-
| Poor | 100 - 200 | Orange |
214-
| Bad | > 200 | Red |
215-
216202
Values of 1000 kΩ indicate:
217203
- Electrode not yet measured
218204
- No contact with scalp
@@ -221,4 +207,5 @@ Values of 1000 kΩ indicate:
221207
## References
222208

223209
- EGI AmpServer SDK Documentation
210+
- Net Station Acquisition source code (NSAAmpSettings.m, ImpedancesController.m)
224211
- EGI Geodesic Sensor Net Technical Manual

mock/include/MockAmplifier.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ class MockAmplifier {
165165
// For synthetic data generation
166166
double phase_ = 0.0;
167167

168+
// DIN counter - cycles 0x0000 to 0xFFFF at 1kHz base rate
169+
// At higher sample rates, values are duplicated:
170+
// 2kHz = 2x duplicates, 4kHz = 4x, 8kHz = 8x
171+
uint32_t dinCounter_ = 0;
172+
168173
int64_t getCurrentTimeMicros() const;
169174
};
170175

mock/src/DataStreamGenerator.cpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,9 @@ void DataStreamGenerator::stopListening(int64_t ampId) {
119119
}
120120

121121
void DataStreamGenerator::streamingThread() {
122-
const int samplesPerSecond = 1000; // Base rate
123-
const int samplesPerPacket = 5; // Send 5 samples per network packet
124-
const auto packetInterval = std::chrono::milliseconds(samplesPerPacket);
122+
// We send packets at ~200 Hz (every 5ms) and adjust samples per packet
123+
// based on sample rate to achieve the correct rate
124+
const auto packetInterval = std::chrono::milliseconds(5);
125125

126126
while (running_) {
127127
if (amplifier_->isStreaming() && listening_) {
@@ -153,9 +153,15 @@ void DataStreamGenerator::sendDataToClients() {
153153
}
154154

155155
void DataStreamGenerator::generateSyntheticData(std::vector<uint8_t>& buffer) {
156-
const int samplesPerPacket = 5;
157156
const auto& state = amplifier_->state();
158157

158+
// Calculate samples per packet based on sample rate
159+
// We send packets every 5ms, so samples = rate * 0.005
160+
// 1000 Hz = 5 samples, 2000 Hz = 10 samples, 4000 Hz = 20 samples, 8000 Hz = 40 samples
161+
int sampleRate = state.decimatedRate > 0 ? state.decimatedRate : 1000;
162+
int samplesPerPacket = (sampleRate * 5) / 1000;
163+
if (samplesPerPacket < 1) samplesPerPacket = 1;
164+
159165
// Determine packet format
160166
if (state.packetFormat == PacketFormat::Format2) {
161167
// Packet Format 2

mock/src/MockAmplifier.cpp

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,16 @@ void MockAmplifier::generatePacketFormat2(PacketFormat2_SamplePacket& packet) {
369369
std::lock_guard<std::mutex> lock(mutex_);
370370
std::memset(&packet, 0, sizeof(packet));
371371

372-
// Digital inputs (mock value)
373-
packet.digitalInputs = 0;
372+
// Digital inputs - cycle 0x0000 to 0xFFFF at 1kHz base rate
373+
// At higher sample rates, duplicate the values:
374+
// 1kHz: each value once, 2kHz: twice, 4kHz: 4x, 8kHz: 8x
375+
int sampleRate = state_.decimatedRate > 0 ? state_.decimatedRate : 1000;
376+
int duplicateFactor = sampleRate / 1000;
377+
if (duplicateFactor < 1) duplicateFactor = 1;
378+
379+
// Calculate which 1kHz "tick" we're on
380+
uint32_t din1kHzTick = static_cast<uint32_t>(state_.packetCounter / duplicateFactor);
381+
packet.digitalInputs = static_cast<uint16_t>(din1kHzTick & 0xFFFF);
374382

375383
// TR byte (255 = no GTEN activity)
376384
packet.tr = state_.gtenTrainRunning ? 251 : 255;
@@ -383,21 +391,62 @@ void MockAmplifier::generatePacketFormat2(PacketFormat2_SamplePacket& packet) {
383391
packet.netCode = static_cast<uint8_t>(state_.netCode);
384392

385393
// Generate synthetic EEG data
386-
double sampleRate = state_.decimatedRate;
387394
double dt = 1.0 / sampleRate;
388395
phase_ += dt;
389396

390397
float scaleFactor = getScalingFactor(state_.amplifierType);
391398

392-
for (int ch = 0; ch < 256; ++ch) {
393-
double freq = 10.0 + (ch % 40);
394-
double amplitude = 50.0; // ~50 uV
395-
double noise = (rand() % 1000 - 500) / 500.0 * 5.0;
399+
// Check if we're in impedance mode (oscillator on, calibration signal configured)
400+
bool impedanceMode = state_.oscillatorGate &&
401+
state_.calibrationSignalFreq > 0 &&
402+
state_.calibrationSignalAmplitude > 0;
396403

397-
if (state_.channelDriveSignals[ch] || state_.allDriveSignals) {
398-
freq = state_.calibrationSignalFreq > 0 ? state_.calibrationSignalFreq : 10.0;
399-
amplitude = state_.calibrationSignalAmplitude > 0 ?
400-
state_.calibrationSignalAmplitude : 100.0;
404+
// Ideal signal amplitude in microvolts for impedance mode
405+
// This matches what a 0-ohm electrode would produce
406+
constexpr double IDEAL_SIGNAL_UV = 100.0;
407+
constexpr double REFERENCE_RESISTOR_KOHMS = 10.0;
408+
409+
for (int ch = 0; ch < 256; ++ch) {
410+
double freq;
411+
double amplitude;
412+
double noise = (rand() % 1000 - 500) / 500.0 * 2.0; // ~2 uV noise
413+
414+
// Check channel state for impedance measurement
415+
bool channelDriving = state_.channelDriveSignals[ch] || state_.allDriveSignals;
416+
bool channel10K = state_.channel10KOhms[ch] || state_.all10KOhms;
417+
418+
if (impedanceMode) {
419+
// Use calibration signal frequency
420+
freq = static_cast<double>(state_.calibrationSignalFreq);
421+
422+
if (!channelDriving && channel10K) {
423+
// Channel is in MEASUREMENT mode (drive off, 10K on)
424+
// Simulate voltage divider: amplitude reduced based on "electrode impedance"
425+
// Generate a random but stable impedance per channel (5-100 kOhms)
426+
double simulatedImpedance = 5.0 + (ch * 17 % 95); // Deterministic pseudo-random
427+
428+
// Voltage divider: Vout = Vin * R_ref / (R_ref + Z_electrode)
429+
// amplitude = ideal * R_ref / (R_ref + Z)
430+
amplitude = IDEAL_SIGNAL_UV * REFERENCE_RESISTOR_KOHMS /
431+
(REFERENCE_RESISTOR_KOHMS + simulatedImpedance);
432+
} else if (channelDriving) {
433+
// Channel is DRIVING - full calibration signal
434+
amplitude = IDEAL_SIGNAL_UV;
435+
} else {
436+
// Neither driving nor measuring - minimal signal
437+
amplitude = 5.0;
438+
}
439+
} else {
440+
// Normal EEG mode - generate synthetic brain signals
441+
freq = 10.0 + (ch % 40); // Different frequencies per channel
442+
amplitude = 50.0; // ~50 uV
443+
444+
// Add calibration signal if enabled
445+
if (channelDriving && state_.calibrationSignalFreq > 0) {
446+
freq = static_cast<double>(state_.calibrationSignalFreq);
447+
amplitude = state_.calibrationSignalAmplitude > 0 ?
448+
static_cast<double>(state_.calibrationSignalAmplitude) : 100.0;
449+
}
401450
}
402451

403452
double value = amplitude * std::sin(2.0 * M_PI * freq * phase_) + noise;

0 commit comments

Comments
 (0)