diff --git a/README.md b/README.md
index 6c60a12..05a6e1a 100644
--- a/README.md
+++ b/README.md
@@ -37,10 +37,18 @@ To use the mock server for development:
**Terminal 1: Start mock server**
-> python3 mock/mock_ampserver.py
+> python3 mock/mock_ampserver.py [--impedance]
**Terminal 2: Run CLI or GUI against localhost**
> ./cli/EGIAmpServerCLI --address 127.0.0.1
> # or update ampserver_config.cfg to use 127.0.0.1 and run GUI
-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.
+The mock server generates synthetic sine waves (10-50 Hz) with noise for the EEG data. When launched with `--impedance`, it instead sets the TR byte to “injecting current” and fills `refMonitor`/`eegData` with deterministic counts so you can confirm the compliance-voltage math downstream.
+
+## Impedance mode toggle
+
+The CLI exposes an `--impedance` flag that takes no value: include it on the command line (e.g., `./cli/EGIAmpServerCLI --impedance`) to request impedance mode.
+
+The same behavior can be configured in `ampserver_config.cfg` via the `true` setting under the `` block. When omitted, impedance mode remains disabled by default.
+
+When impedance mode is active the single LSL outlet (type `EEG`) carries compliance voltage samples instead of microvolt EEG. Each sample contains one value per electrode representing $(channel + ref) \times 201$, converted to volts. The outlet advertises an irregular sample rate and only publishes data while the TR bit indicates that current injection is on, so downstream consumers should expect bursts of samples separated by gaps. To recover absolute impedances divide the compliance voltage by the actual drive current used on your hardware.
diff --git a/mock/mock_ampserver.py b/mock/mock_ampserver.py
index eb05487..c2d6916 100755
--- a/mock/mock_ampserver.py
+++ b/mock/mock_ampserver.py
@@ -25,11 +25,13 @@
}
class MockAmpServer:
- def __init__(self, host="0.0.0.0", cmd_port=9877, notify_port=9878, data_port=9879):
+ def __init__(self, host="0.0.0.0", cmd_port=9877, notify_port=9878, data_port=9879,
+ impedance_mode=False):
self.host = host
self.cmd_port = cmd_port
self.notify_port = notify_port
self.data_port = data_port
+ self.impedance_mode = impedance_mode
self.running = False
self.streaming = False
@@ -54,6 +56,8 @@ def start(self):
print(f" Command port: {self.cmd_port}")
print(f" Notification port: {self.notify_port}")
print(f" Data port: {self.data_port}")
+ mode = "impedance" if self.impedance_mode else "eeg"
+ print(f"Mode: {mode}")
print(f"Press Ctrl+C to stop.\n")
try:
@@ -274,14 +278,25 @@ def _stream_data(self, client):
def _create_packet_format2(self, packet_counter, t, n_channels):
"""Create a PacketFormat2 binary packet with synthetic data."""
- # Generate synthetic EEG: sine waves at different frequencies per channel
- eeg_data = []
- for ch in range(256):
- freq = 10 + (ch % 40) # 10-50 Hz sine waves
- amplitude = 100 # microvolts (will be scaled by client)
- # Add some noise
- value = int(amplitude * math.sin(2 * math.pi * freq * t) + random.gauss(0, 10))
- eeg_data.append(value)
+ ref_monitor = 0
+
+ if self.impedance_mode:
+ # Deterministic counts so compliance math can be verified.
+ base_count = 10000
+ step = 250
+ ref_monitor = base_count
+ eeg_data = [base_count + (ch % 8) * step for ch in range(256)]
+ tr_value = 0xFB # Injecting current flag (bit 2 cleared)
+ else:
+ # Generate synthetic EEG: sine waves at different frequencies per channel
+ eeg_data = []
+ for ch in range(256):
+ freq = 10 + (ch % 40) # 10-50 Hz sine waves
+ amplitude = 100 # microvolts (will be scaled by client)
+ # Add some noise
+ value = int(amplitude * math.sin(2 * math.pi * freq * t) + random.gauss(0, 10))
+ eeg_data.append(value)
+ tr_value = 0
# Determine net code based on channel count
if n_channels == 256:
@@ -300,7 +315,7 @@ def _create_packet_format2(self, packet_counter, t, n_channels):
packet += struct.pack(" Amplifier ID (default: 0)\n"
<< " --sample-rate Sample rate (default: 1000)\n"
<< " --listen-only Don't initialize amp, just listen (for multi-client)\n"
+ << " --impedance Enable impedance mode (default: disabled)\n"
<< " --help Show this help message\n";
}
@@ -60,6 +61,8 @@ int main(int argc, char* argv[]) {
config.sampleRate = std::stoi(argv[++i]);
} else if (arg == "--listen-only") {
config.listenOnly = true;
+ } else if (arg == "--impedance") {
+ config.impedance = true;
} else {
std::cerr << "Unknown option: " << arg << std::endl;
printUsage(argv[0]);
@@ -67,6 +70,11 @@ int main(int argc, char* argv[]) {
}
}
+ if (config.impedance && config.listenOnly) {
+ std::cerr << "--impedance cannot be combined with --listen-only" << std::endl;
+ return 1;
+ }
+
// Load config file if specified
if (!configFile.empty()) {
try {
@@ -109,6 +117,7 @@ int main(int argc, char* argv[]) {
<< " Amplifier ID: " << config.amplifierId << "\n"
<< " Sample Rate: " << config.sampleRate << " Hz\n"
<< " Listen Only: " << (config.listenOnly ? "yes" : "no") << "\n"
+ << " Impedance Mode: " << (config.impedance ? "enabled" : "disabled") << "\n"
<< "Press Ctrl+C to stop.\n\n";
// Connect and stream
diff --git a/src/core/include/egiamp/AmpServerConfig.h b/src/core/include/egiamp/AmpServerConfig.h
index 891108e..adc1a8f 100644
--- a/src/core/include/egiamp/AmpServerConfig.h
+++ b/src/core/include/egiamp/AmpServerConfig.h
@@ -18,6 +18,8 @@ struct AmpServerConfig {
// If true, just listen to an already-running amp without initializing it.
// This allows multiple clients to connect without disrupting each other.
bool listenOnly = false;
+ // If true, Request hardware to enter impedance-measurement mode.
+ bool impedance = false;
static AmpServerConfig loadFromFile(const std::string& filename);
void saveToFile(const std::string& filename) const;
diff --git a/src/core/include/egiamp/EGIAmpClient.h b/src/core/include/egiamp/EGIAmpClient.h
index f190f4f..6ccdec1 100644
--- a/src/core/include/egiamp/EGIAmpClient.h
+++ b/src/core/include/egiamp/EGIAmpClient.h
@@ -58,6 +58,7 @@ class EGIAmpClient {
void emitStatus(const std::string& message);
void emitError(const std::string& message);
void emitChannelCount(int count);
+ static bool commandCompleted(const std::string& response);
bool queryAmplifierDetails();
bool initAmplifier();
@@ -66,6 +67,7 @@ class EGIAmpClient {
void readPacketFormat1();
void readPacketFormat2();
void processNotifications();
+ bool cmd_ImpedanceAcquisitionState();
AmpServerConfig config_;
AmpServerConnection connection_;
diff --git a/src/core/src/AmpServerConfig.cpp b/src/core/src/AmpServerConfig.cpp
index e89f9cc..9af4093 100644
--- a/src/core/src/AmpServerConfig.cpp
+++ b/src/core/src/AmpServerConfig.cpp
@@ -43,6 +43,9 @@ AmpServerConfig AmpServerConfig::loadFromFile(const std::string& filename) {
if (auto node = settings.child("listenonly")) {
config.listenOnly = node.text().as_bool(config.listenOnly);
}
+ if (auto node = settings.child("impedance")) {
+ config.impedance = node.text().as_bool(config.impedance);
+ }
}
return config;
@@ -63,6 +66,7 @@ void AmpServerConfig::saveToFile(const std::string& filename) const {
settings.append_child("amplifierid").text().set(amplifierId);
settings.append_child("samplingrate").text().set(sampleRate);
settings.append_child("listenonly").text().set(listenOnly);
+ settings.append_child("impedance").text().set(impedance);
if (!doc.save_file(filename.c_str())) {
throw ConfigError("Could not write to config file: " + filename);
diff --git a/src/core/src/EGIAmpClient.cpp b/src/core/src/EGIAmpClient.cpp
index 3d0a571..05fa3a6 100644
--- a/src/core/src/EGIAmpClient.cpp
+++ b/src/core/src/EGIAmpClient.cpp
@@ -3,11 +3,18 @@
#include
#include
+#include
#include
#include
+#include
+#include
namespace egiamp {
+bool EGIAmpClient::commandCompleted(const std::string& response) {
+ return response.find("(status complete)") != std::string::npos;
+}
+
EGIAmpClient::EGIAmpClient() = default;
EGIAmpClient::~EGIAmpClient() {
@@ -131,11 +138,20 @@ bool EGIAmpClient::initAmplifier() {
// Power on
connection_.sendCommand("cmd_SetPower", ampId, 0, "1");
- // Start
- connection_.sendCommand("cmd_Start", ampId, 0, "0");
+ if (config_.impedance) {
+ try {
+ cmd_ImpedanceAcquisitionState();
+ } catch (const std::exception& ex) {
+ emitError(std::string("Failed to configure impedance mode: ") + ex.what());
+ return false;
+ }
+ } else {
+ // Set default acquisition state when not in impedance mode
+ connection_.sendCommand("cmd_DefaultAcquisitionState", ampId, 0, "0");
+ }
- // Set default acquisition state
- connection_.sendCommand("cmd_DefaultAcquisitionState", ampId, 0, "0");
+ // Start stream
+ connection_.sendCommand("cmd_Start", ampId, 0, "0");
return true;
}
@@ -155,6 +171,29 @@ void EGIAmpClient::haltAmplifier() {
stopFlag_ = false;
}
+bool EGIAmpClient::cmd_ImpedanceAcquisitionState() {
+ emitStatus("Enabling impedance mode...\n");
+
+ const int ampId = config_.amplifierId;
+ const std::pair commands[] = {
+ {"cmd_TurnAll10KOhms", "1"},
+ {"cmd_SetReference10KOhms", "1"},
+ {"cmd_SetSubjectGround", "1"},
+ {"cmd_SetCurrentSource", "1"},
+ {"cmd_TurnAllDriveSignals", "1"},
+ };
+
+ for (const auto& [command, value] : commands) {
+ const std::string response = connection_.sendCommand(command, ampId, 0, value);
+ if (!commandCompleted(response)) {
+ throw std::runtime_error(std::string(command) + " failed: " + response);
+ }
+ }
+
+ emitStatus("Impedance mode enabled.\n");
+ return true;
+}
+
bool EGIAmpClient::startStreaming() {
if (isStreaming()) {
return false;
@@ -298,8 +337,9 @@ void EGIAmpClient::readPacketFormat2() {
// Create LSL outlet
std::string streamName = "EGI NetAmp " + std::to_string(header.ampID);
+ int outletRate = config_.impedance ? 0 : config_.sampleRate;
streamer_.createOutlet(streamName, nChannels,
- config_.sampleRate, config_.serverAddress);
+ outletRate, config_.serverAddress);
}
// Check for dropped or duplicate packets
@@ -336,13 +376,38 @@ void EGIAmpClient::readPacketFormat2() {
lastPacketCounterWithTimeStamp_ = packet.packetCounter;
}
+
+ const bool impedanceEnabled = config_.impedance;
+ const bool injectingCurrent = (packet.tr & 0x04) == 0;
+ if (impedanceEnabled && !injectingCurrent) {
+ continue;
+ }
+
// Convert and push sample (PacketFormat2 is little endian natively)
std::vector samples;
samples.reserve(nChannels);
+
+ const float refMicroVolts = static_cast(packet.refMonitor) *
+ details_.scalingFactor;
+
for (int ch = 0; ch < nChannels; ch++) {
- samples.push_back(static_cast(packet.eegData[ch]) *
- details_.scalingFactor);
+ float channelData = static_cast(packet.eegData[ch]) *
+ details_.scalingFactor;
+
+ if (impedanceEnabled) {
+ float complianceVolts = std::numeric_limits::quiet_NaN();
+ if (injectingCurrent) {
+ // section "Compliance Voltage" specifies
+ // V_comp = (channel + ref) * 201.
+ float complianceMicroVolts = (channelData + refMicroVolts) * 201.0f;
+ complianceVolts = complianceMicroVolts * 1e-6f;
+ }
+ samples.push_back(complianceVolts);
+ } else {
+ samples.push_back(channelData);
+ }
}
+
streamer_.pushSample(samples);
}
}
@@ -393,8 +458,9 @@ void EGIAmpClient::readPacketFormat1() {
// Create LSL outlet
std::string streamName = "EGI NetAmp " + std::to_string(header.ampID);
+ int outletRate = config_.impedance ? 0 : config_.sampleRate;
streamer_.createOutlet(streamName, nChannels,
- config_.sampleRate, config_.serverAddress);
+ outletRate, config_.serverAddress);
}
// Convert endianness and push sample