Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<impedance>true</impedance>` setting under the `<settings>` 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.
40 changes: 29 additions & 11 deletions mock/mock_ampserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -300,7 +315,7 @@ def _create_packet_format2(self, packet_counter, t, n_channels):
packet += struct.pack("<H", 0)

# tr (uint8)
packet += struct.pack("<B", 0)
packet += struct.pack("<B", tr_value)

# pib1_aux (11 bytes)
packet += b'\x00' * 11
Expand Down Expand Up @@ -329,7 +344,7 @@ def _create_packet_format2(self, packet_counter, t, n_channels):
packet += struct.pack("<3i", 0, 0, 0)

# refMonitor, comMonitor, driveMonitor, diagnosticsChannel, currentSense (5 x int32)
packet += struct.pack("<5i", 0, 0, 0, 0, 0)
packet += struct.pack("<5i", ref_monitor, 0, 0, 0, 0)

# pib1_Data, pib2_Data (32 x int32)
packet += struct.pack("<32i", *([0] * 32))
Expand All @@ -343,13 +358,16 @@ def main():
parser.add_argument("--cmd-port", type=int, default=9877, help="Command port")
parser.add_argument("--notify-port", type=int, default=9878, help="Notification port")
parser.add_argument("--data-port", type=int, default=9879, help="Data port")
parser.add_argument("--impedance", action="store_true",
help="Emit deterministic impedance packets")
args = parser.parse_args()

server = MockAmpServer(
host=args.host,
cmd_port=args.cmd_port,
notify_port=args.notify_port,
data_port=args.data_port,
impedance_mode=args.impedance,
)
server.start()

Expand Down
9 changes: 9 additions & 0 deletions src/cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ void printUsage(const char* programName) {
<< " --amp-id <id> Amplifier ID (default: 0)\n"
<< " --sample-rate <hz> 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";
}

Expand Down Expand Up @@ -60,13 +61,20 @@ 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]);
return 1;
}
}

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 {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/core/include/egiamp/AmpServerConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/core/include/egiamp/EGIAmpClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -66,6 +67,7 @@ class EGIAmpClient {
void readPacketFormat1();
void readPacketFormat2();
void processNotifications();
bool cmd_ImpedanceAcquisitionState();

AmpServerConfig config_;
AmpServerConnection connection_;
Expand Down
4 changes: 4 additions & 0 deletions src/core/src/AmpServerConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
82 changes: 74 additions & 8 deletions src/core/src/EGIAmpClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@

#include <chrono>
#include <iostream>
#include <limits>
#include <regex>
#include <sstream>
#include <stdexcept>
#include <utility>

namespace egiamp {

bool EGIAmpClient::commandCompleted(const std::string& response) {
return response.find("(status complete)") != std::string::npos;
}

EGIAmpClient::EGIAmpClient() = default;

EGIAmpClient::~EGIAmpClient() {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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<const char*, const char*> 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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<float> samples;
samples.reserve(nChannels);

const float refMicroVolts = static_cast<float>(packet.refMonitor) *
details_.scalingFactor;

for (int ch = 0; ch < nChannels; ch++) {
samples.push_back(static_cast<float>(packet.eegData[ch]) *
details_.scalingFactor);
float channelData = static_cast<float>(packet.eegData[ch]) *
details_.scalingFactor;

if (impedanceEnabled) {
float complianceVolts = std::numeric_limits<float>::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);
}
}
Expand Down Expand Up @@ -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
Expand Down