Skip to content

Commit 26b2a8d

Browse files
committed
Add --native-format argument to transmit data in original (unscaled) int32 for better precision.
1 parent 40c4bb5 commit 26b2a8d

7 files changed

Lines changed: 125 additions & 46 deletions

File tree

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ The CLI provides a lightweight alternative to the GUI:
3535
- `--amp-id <id>` - Amplifier ID (default: 0)
3636
- `--sample-rate <hz>` - Sample rate in Hz (default: 1000)
3737
- `--impedance` - Enable impedance testing mode
38+
- `--native-format` - Transmit raw int32 ADC counts instead of float microvolts
3839
- `--shutdown` - Shutdown the Amp Server (terminates all connections)
3940
- `--help` - Show help message
4041

@@ -45,6 +46,9 @@ The CLI provides a lightweight alternative to the GUI:
4546

4647
# With impedance testing
4748
./EGIAmpServerCLI --address 10.10.10.51 --impedance
49+
50+
# With native format (int32 ADC counts)
51+
./EGIAmpServerCLI --address 10.10.10.51 --native-format
4852
```
4953

5054
## Impedance Testing
@@ -87,9 +91,17 @@ Currently not implemented in GUI. Use CLI or config file.
8791
- **Type**: `EEG`
8892
- **Rate**: Configured sample rate (e.g., 1000 Hz)
8993
- **Channels**: Depends on sensor net (32-256 channels)
90-
- **Unit**: microvolts
94+
- **Format**: `float32` (default) or `int32` (with `--native-format`)
95+
- **Unit**: `microvolts` (default) or `counts` (with `--native-format`)
9196
- **Behavior**: Streams continuously with raw amplifier data (includes test signals during impedance mode)
9297

98+
##### Native Format Mode
99+
When `--native-format` is enabled:
100+
- Data is transmitted as raw int32 ADC counts instead of float microvolts
101+
- Channel metadata includes a `conversion` field with the scaling factor
102+
- Downstream consumers can convert to microvolts: `microvolts = counts × conversion`
103+
- This mode is useful for applications that need maximum precision or want to handle scaling themselves (e.g., NWB export)
104+
93105
#### 2. Impedance Stream
94106
- **Name**: `EGI NetAmp <amp_id> Impedance`
95107
- **Type**: `Impedance`

src/cli/main.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ void printUsage(const char* programName) {
3232
<< " --amp-id <id> Amplifier ID (default: 0)\n"
3333
<< " --sample-rate <hz> Sample rate (default: 1000)\n"
3434
<< " --impedance Enable impedance testing mode (default: disabled)\n"
35+
<< " --native-format Transmit raw int32 ADC counts instead of float microvolts\n"
3536
<< " --shutdown Shutdown the Amp Server (terminates all connections)\n"
3637
<< " --help Show this help message\n";
3738
}
@@ -62,6 +63,8 @@ int main(int argc, char* argv[]) {
6263
config.sampleRate = std::stoi(argv[++i]);
6364
} else if (arg == "--impedance") {
6465
config.impedance = true;
66+
} else if (arg == "--native-format") {
67+
config.nativeFormat = true;
6568
} else if (arg == "--shutdown") {
6669
shutdownMode = true;
6770
} else {
@@ -141,6 +144,7 @@ int main(int argc, char* argv[]) {
141144
<< " Amplifier ID: " << config.amplifierId << "\n"
142145
<< " Sample Rate: " << config.sampleRate << " Hz\n"
143146
<< " Impedance Mode: " << (config.impedance ? "enabled" : "disabled") << "\n"
147+
<< " Data Format: " << (config.nativeFormat ? "native (int32 counts)" : "microvolts (float32)") << "\n"
144148
<< "Press Ctrl+C to stop.\n\n";
145149

146150
// Connect and stream

src/core/include/egiamp/AmpServerConfig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct AmpServerConfig {
1515
int amplifierId = 0;
1616
int sampleRate = 1000;
1717
bool impedance = false;
18+
bool nativeFormat = false; // When true, transmit raw int32 ADC counts instead of float microvolts
1819

1920
static AmpServerConfig loadFromFile(const std::string& filename);
2021
void saveToFile(const std::string& filename) const;

src/core/include/egiamp/LSLStreamer.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,25 @@ class LSLStreamer {
2525
void createOutlet(const std::string& streamName, int eegChannelCount,
2626
int physioChannelCount, int sampleRate,
2727
const std::string& hostname,
28-
const AmplifierDetails& details);
28+
const AmplifierDetails& details,
29+
bool nativeFormat = false);
2930

3031
void createImpedanceOutlet(const std::string& streamName, int channelCount,
3132
const std::string& hostname,
3233
const AmplifierDetails& details);
3334

3435
void pushSample(const std::vector<float>& sample);
36+
void pushSampleInt32(const std::vector<int32_t>& sample);
37+
38+
bool isNativeFormat() const { return nativeFormat_; }
3539

3640
bool hasOutlet() const { return outlet_ != nullptr; }
3741

3842
void closeOutlet();
3943

4044
private:
4145
std::unique_ptr<lsl::stream_outlet> outlet_;
46+
bool nativeFormat_ = false;
4247
};
4348

4449
} // namespace egiamp

src/core/src/AmpServerConfig.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ AmpServerConfig AmpServerConfig::loadFromFile(const std::string& filename) {
4343
if (auto node = settings.child("impedance")) {
4444
config.impedance = node.text().as_bool(config.impedance);
4545
}
46+
if (auto node = settings.child("nativeformat")) {
47+
config.nativeFormat = node.text().as_bool(config.nativeFormat);
48+
}
4649
}
4750

4851
return config;
@@ -63,6 +66,7 @@ void AmpServerConfig::saveToFile(const std::string& filename) const {
6366
settings.append_child("amplifierid").text().set(amplifierId);
6467
settings.append_child("samplingrate").text().set(sampleRate);
6568
settings.append_child("impedance").text().set(impedance);
69+
settings.append_child("nativeformat").text().set(nativeFormat);
6670

6771
if (!doc.save_file(filename.c_str())) {
6872
throw ConfigError("Could not write to config file: " + filename);

src/core/src/EGIAmpClient.cpp

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,8 @@ void EGIAmpClient::readPacketFormat2() {
562562
// Create LSL outlet for EEG (+ Physio if connected)
563563
std::string streamName = "EGI NetAmp " + std::to_string(header.ampID);
564564
streamer_.createOutlet(streamName, nChannels, physioChannelCount,
565-
config_.sampleRate, config_.serverAddress, details_);
565+
config_.sampleRate, config_.serverAddress, details_,
566+
config_.nativeFormat);
566567

567568
// Create LSL outlet for impedance if enabled
568569
if (config_.impedance) {
@@ -633,7 +634,8 @@ void EGIAmpClient::readPacketFormat2() {
633634
int physioChCount = (physioConnectionStatus_ == 3) ? 32 :
634635
(physioConnectionStatus_ > 0) ? 16 : 0;
635636
streamer_.createOutlet(streamName, nChannels, physioChCount,
636-
config_.sampleRate, config_.serverAddress, details_);
637+
config_.sampleRate, config_.serverAddress, details_,
638+
config_.nativeFormat);
637639

638640
// Recreate impedance outlet if enabled
639641
if (config_.impedance) {
@@ -663,43 +665,72 @@ void EGIAmpClient::readPacketFormat2() {
663665
lastPacketCounterWithTimeStamp_ = packet.packetCounter;
664666
}
665667

666-
// Convert and push sample to EEG stream (PacketFormat2 is little endian natively)
667-
std::vector<float> eegSamples;
668+
// Push sample to EEG stream (PacketFormat2 is little endian natively)
668669
int physioChannels = (physioConnectionStatus_ == 3) ? 32 :
669670
(physioConnectionStatus_ > 0) ? 16 : 0;
670-
eegSamples.reserve(nChannels + physioChannels);
671-
for (int ch = 0; ch < nChannels; ch++) {
672-
eegSamples.push_back(static_cast<float>(packet.eegData[ch]) *
673-
details_.scalingFactor);
674-
}
675671

676-
// Add PIB1 channels (if port 1 connected: status 1 or 3)
677-
// Channels 1-8 use negative scaling, 9-16 use positive scaling
678-
if (physioConnectionStatus_ & 0x01) {
679-
for (int ch = 0; ch < 8; ch++) {
680-
eegSamples.push_back(static_cast<float>(packet.pib1_Data[ch]) *
681-
PHYSIO_SCALING_1_8);
672+
if (config_.nativeFormat) {
673+
// Native format: push raw int32 ADC counts
674+
std::vector<int32_t> rawSamples;
675+
rawSamples.reserve(nChannels + physioChannels);
676+
677+
for (int ch = 0; ch < nChannels; ch++) {
678+
rawSamples.push_back(packet.eegData[ch]);
682679
}
683-
for (int ch = 8; ch < 16; ch++) {
684-
eegSamples.push_back(static_cast<float>(packet.pib1_Data[ch]) *
685-
PHYSIO_SCALING_9_16);
680+
681+
// Add PIB1 channels (if port 1 connected: status 1 or 3)
682+
if (physioConnectionStatus_ & 0x01) {
683+
for (int ch = 0; ch < 16; ch++) {
684+
rawSamples.push_back(packet.pib1_Data[ch]);
685+
}
686686
}
687-
}
688687

689-
// Add PIB2 channels (if port 2 connected: status 2 or 3)
690-
// Channels 1-8 use negative scaling, 9-16 use positive scaling
691-
if (physioConnectionStatus_ & 0x02) {
692-
for (int ch = 0; ch < 8; ch++) {
693-
eegSamples.push_back(static_cast<float>(packet.pib2_Data[ch]) *
694-
PHYSIO_SCALING_1_8);
688+
// Add PIB2 channels (if port 2 connected: status 2 or 3)
689+
if (physioConnectionStatus_ & 0x02) {
690+
for (int ch = 0; ch < 16; ch++) {
691+
rawSamples.push_back(packet.pib2_Data[ch]);
692+
}
695693
}
696-
for (int ch = 8; ch < 16; ch++) {
697-
eegSamples.push_back(static_cast<float>(packet.pib2_Data[ch]) *
698-
PHYSIO_SCALING_9_16);
694+
695+
streamer_.pushSampleInt32(rawSamples);
696+
} else {
697+
// Default: convert to float microvolts
698+
std::vector<float> eegSamples;
699+
eegSamples.reserve(nChannels + physioChannels);
700+
701+
for (int ch = 0; ch < nChannels; ch++) {
702+
eegSamples.push_back(static_cast<float>(packet.eegData[ch]) *
703+
details_.scalingFactor);
699704
}
700-
}
701705

702-
streamer_.pushSample(eegSamples);
706+
// Add PIB1 channels (if port 1 connected: status 1 or 3)
707+
// Channels 1-8 use negative scaling, 9-16 use positive scaling
708+
if (physioConnectionStatus_ & 0x01) {
709+
for (int ch = 0; ch < 8; ch++) {
710+
eegSamples.push_back(static_cast<float>(packet.pib1_Data[ch]) *
711+
PHYSIO_SCALING_1_8);
712+
}
713+
for (int ch = 8; ch < 16; ch++) {
714+
eegSamples.push_back(static_cast<float>(packet.pib1_Data[ch]) *
715+
PHYSIO_SCALING_9_16);
716+
}
717+
}
718+
719+
// Add PIB2 channels (if port 2 connected: status 2 or 3)
720+
// Channels 1-8 use negative scaling, 9-16 use positive scaling
721+
if (physioConnectionStatus_ & 0x02) {
722+
for (int ch = 0; ch < 8; ch++) {
723+
eegSamples.push_back(static_cast<float>(packet.pib2_Data[ch]) *
724+
PHYSIO_SCALING_1_8);
725+
}
726+
for (int ch = 8; ch < 16; ch++) {
727+
eegSamples.push_back(static_cast<float>(packet.pib2_Data[ch]) *
728+
PHYSIO_SCALING_9_16);
729+
}
730+
}
731+
732+
streamer_.pushSample(eegSamples);
733+
}
703734

704735
// Push impedance data if in impedance mode and current is injecting
705736
if (config_.impedance && impedanceStreamer_.hasOutlet()) {
@@ -777,9 +808,12 @@ void EGIAmpClient::readPacketFormat1() {
777808
emitChannelCount(nChannels);
778809

779810
// Create LSL outlet (no Physio16 support for PacketFormat1)
811+
// Note: PacketFormat1 (NA300) already provides float data, so nativeFormat
812+
// doesn't apply - we always use float for Format1
780813
std::string streamName = "EGI NetAmp " + std::to_string(header.ampID);
781814
streamer_.createOutlet(streamName, nChannels, 0,
782-
config_.sampleRate, config_.serverAddress, details_);
815+
config_.sampleRate, config_.serverAddress, details_,
816+
false); // Format1 is always float
783817
}
784818

785819
// Convert endianness and push sample

src/core/src/LSLStreamer.cpp

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,22 +86,26 @@ std::string getCapName(NetCode netCode) {
8686
void LSLStreamer::createOutlet(const std::string& streamName, int eegChannelCount,
8787
int physioChannelCount, int sampleRate,
8888
const std::string& hostname,
89-
const AmplifierDetails& details) {
89+
const AmplifierDetails& details,
90+
bool nativeFormat) {
9091
// Close existing outlet if any
9192
closeOutlet();
9293

94+
nativeFormat_ = nativeFormat;
9395
int totalChannelCount = eegChannelCount + physioChannelCount;
9496

9597
// Create stream info with unique source ID
9698
// Include all parameters that make streams incompatible so clients
9799
// won't auto-reconnect when these change
100+
std::string formatSuffix = nativeFormat ? "_i32" : "_f32";
98101
std::string sourceId = "EGI_" + hostname +
99102
"_ch" + std::to_string(totalChannelCount) +
100103
"_sr" + std::to_string(sampleRate) +
101-
"_f32";
104+
formatSuffix;
105+
lsl::channel_format_t channelFormat = nativeFormat ? lsl::cf_int32 : lsl::cf_float32;
102106
lsl::stream_info info(streamName, "EEG", totalChannelCount,
103107
static_cast<double>(sampleRate),
104-
lsl::cf_float32, sourceId);
108+
channelFormat, sourceId);
105109

106110
// Get the description root
107111
lsl::xml_element desc = info.desc();
@@ -178,12 +182,16 @@ void LSLStreamer::createOutlet(const std::string& streamName, int eegChannelCoun
178182
}
179183
ch.append_child_value("label", label.c_str());
180184
ch.append_child_value("type", "EEG");
181-
ch.append_child_value("unit", "microvolts");
182185

183-
// Scaling factor (raw to microvolts)
184-
if (details.scalingFactor > 0) {
185-
ch.append_child_value("scaling_factor",
186-
std::to_string(details.scalingFactor).c_str());
186+
if (nativeFormat) {
187+
ch.append_child_value("unit", "counts");
188+
// Conversion factor: multiply by this to get microvolts
189+
if (details.scalingFactor != 0) {
190+
ch.append_child_value("conversion",
191+
std::to_string(details.scalingFactor).c_str());
192+
}
193+
} else {
194+
ch.append_child_value("unit", "microvolts");
187195
}
188196
}
189197

@@ -195,12 +203,17 @@ void LSLStreamer::createOutlet(const std::string& streamName, int eegChannelCoun
195203
std::string label = "PIB" + std::to_string(i + 1);
196204
ch.append_child_value("label", label.c_str());
197205
ch.append_child_value("type", "AUX");
198-
ch.append_child_value("unit", "microvolts");
199206

200-
// Scaling factor (raw to microvolts) - same as EEG
201-
if (details.scalingFactor > 0) {
202-
ch.append_child_value("scaling_factor",
203-
std::to_string(details.scalingFactor).c_str());
207+
if (nativeFormat) {
208+
ch.append_child_value("unit", "counts");
209+
// PIB channels 1-8 use negative scaling, 9-16 use positive scaling
210+
// Channel index within each PIB port: 0-7 negative, 8-15 positive
211+
int portChannel = i % 16;
212+
float conversion = (portChannel < 8) ? PHYSIO_SCALING_1_8 : PHYSIO_SCALING_9_16;
213+
ch.append_child_value("conversion",
214+
std::to_string(conversion).c_str());
215+
} else {
216+
ch.append_child_value("unit", "microvolts");
204217
}
205218
}
206219

@@ -288,6 +301,12 @@ void LSLStreamer::pushSample(const std::vector<float>& sample) {
288301
}
289302
}
290303

304+
void LSLStreamer::pushSampleInt32(const std::vector<int32_t>& sample) {
305+
if (outlet_) {
306+
outlet_->push_sample(sample);
307+
}
308+
}
309+
291310
void LSLStreamer::closeOutlet() {
292311
outlet_.reset();
293312
}

0 commit comments

Comments
 (0)