Buffering
SiFi Bridge holds acquired data in in-memory buffers before it is exported to disk. Every command that reads acquired data — querying, extracting, and exporting — operates on these buffers rather than on the device or on files. Understanding how buffers are filled, kept, and cleared explains how data flows from a device to your analysis.
Buffers vs. the DataPacket stream
SiFi Bridge exposes acquired data two ways, and they are not mutually exclusive — when streaming, data flows out as a live stream and is captured into a buffer at the same time.
- The live DataPacket stream delivers each packet the moment it arrives over BLE. Consume it when you need to act on data in real time and can keep up with it as it comes.
- Buffers retain the full acquisition in RAM so you can query, slice, and export it at any point — during or after the recording — without catching every packet yourself.
Reach for the live stream when minimal latency is the primary constraint. The trade-off: you have to keep up with packets in real time. In practice that means dedicating a thread (or async task) to the acquisition loop so it drains the stream continuously, instead of simply polling SiFi Bridge when it suits your program. Furthermore, if you are using the same transport for input and output (i.e., the default stdin/stdout), there is also an added complexity of properly multiplexing command responses and live DataPacket.
Use buffers for everything else, including windowed data polling, post-hoc data analysis, data export and even agentic workflows.
Prioritize the live stream in workflows where latency matters more than convenience. ## What a buffer is
What a buffer is
Each acquisition is held in its own buffer, identified by an acquisition ID. A buffer stores the full set of samples received for that acquisition, keeping the per-sensor channels and their timestamps so they can be queried or exported at any time while the buffer exists. Buffers reside in RAM, and every value — both timestamps and data samples — is stored as a 64-bit float (f64, 8 bytes). Lost samples are held as NaN, preserving the 1-to-1 alignment with their timestamps.
Because data lives in the buffer the moment it arrives, you do not need to catch packets and export them continuously during a recording, unless you want the absolute lowest latency possible.
Memory footprint
Because buffers live in RAM and are retained until you clear them (see Buffer lifecycle), a long recording can consume a substantial amount of memory. Each enabled sensor consumes, per second of recording:
bytes/s = sample_rate × (channels + 1) × 8
The + 1 accounts for the per-sample timestamp, and 8 is the size of an f64 in bytes.
The per-sensor cost depends on the device:
- BioPoint
- SiFi Band
| Sensor | Rate | Channels | RAM / hour |
|---|---|---|---|
| ECG | 500 Hz | 1 | ~29 MB |
| EMG | 2000 Hz | 1 | ~115 MB |
| EDA | 50 Hz | 1 | ~3 MB |
| IMU | 100 Hz | 7 | ~23 MB |
| PPG | 50 Hz | 4 | ~7 MB |
| Temperature | 1 Hz | 1 | ~0.06 MB |
A BioPoint streaming all sensors uses roughly 230 MB per hour (~3.8 MB/min).
| Sensor | Rate | Channels | RAM / hour |
|---|---|---|---|
| ECG | 500 Hz | 1 | ~29 MB |
| EMG (8-channel) | 1600 Hz | 8 | ~415 MB |
| EDA | 50 Hz | 1 | ~3 MB |
| IMU | 100 Hz | 7 | ~23 MB |
| PPG | 50 Hz | 4 | ~7 MB |
| Temperature | 1 Hz | 1 | ~0.06 MB |
A SiFi Band streaming all sensors uses roughly 485 MB per hour (~7.6 MB/min), dominated by the 8-channel EMG. Streaming EMG only is about 390 MB per hour.
How data enters a buffer
There are two ways data reaches a buffer, mirroring the memory mode you acquire with:
- Streaming (automatic). When data is streamed over BLE (memory mode
streamingorboth), SiFi Bridge buffers each packet automatically as it arrives. No extra command is needed. - On-board download (explicit). Data stored in the device's on-board memory (memory mode
deviceorboth) is not in a buffer until you fetch it. Rundownload-memoryto pull an on-board recording into a buffer, after which it behaves like any other buffered acquisition.
Streamed data can have lost samples if the BLE link can't keep up. If you acquired with memory mode both, the on-board copy is intact — download-memory brings the complete recording into a buffer even when the streamed copy had gaps.
Buffer lifecycle
- A buffer is created when an acquisition starts (
start) or whendownload-memoryimports an on-board recording. - Buffers are retained across disconnect — disconnecting from a device does not discard its buffered acquisitions, so you can reconnect (or just export) afterwards.
- Buffers are freed with
buffer clear.
Working with buffers
All buffer operations are subcommands of buffer. See the command reference for the full option list.
| Command | Purpose |
|---|---|
buffer list | List buffered acquisitions and their status. |
buffer info | Show an acquisition's details, including the device configuration it was recorded with. |
buffer pull | Extract sensor data with an optional time range (--from + --to) or the last N seconds (--last-seconds). |
buffer clear | Clear buffered data. |
buffer export | Write buffered acquisitions to disk as CSV or HDF5. |
Most commands default to the latest acquisition when no --id is given. Use buffer list to see the available acquisition IDs.
buffer vs configure response formatThe configure submenu nests the subcommand inside a single response
variant:
> configure xyz
{
"configure": { <-- configure submenu
"xyz": { <-- subcommand
"some_value": true
}
}
}
while the buffer submenu collapses responses in the top-level:
> buffer list
{
"buffer_list": { <-- collapsed subcommand
acquisitions: [
--- snip ---
]
}
}
Listing acquisitions
buffer list is useful to list the buffered acquisitions, get their IDs and whether they are ongoing or completed:
> buffer list
{
"buffer_list": {
"acquisitions": [
{
"id": 1,
"device": "my_bp",
"start_time": "2026-06-27 17:29:29",
"completed": true,
"sensors": [
{
"name": "ecg",
"num_samples": 6266625
},
{
"name": "emg",
"num_samples": 25066833
},
{
"name": "event",
"num_samples": 6
},
{
"name": "imu",
"num_samples": 1253345
},
{
"name": "temperature",
"num_samples": 12489
}
],
"total_samples": 32599298
}
]
}
}
The acquisition ID is, in essence, a monotonically incrementing integer starting at 0. For example, the first acquisition is assigned ID 1, the second is ID 2, and so on.
Get information about acquisitions
buffer info provides a more complete overview of acquisitions, including: the status, the enabled sensors, and the full device configuration.
> buffer info
{
"buffer_info": {
"acquisition": {
"id": 1,
"device": "my_bp",
"start_time": "2026-06-27 17:29:29",
"completed": true,
"sensors": [
{
"name": "ecg",
"num_samples": 6266625
},
{
"name": "emg",
"num_samples": 25066833
},
{
"name": "event",
"num_samples": 6
},
{
"name": "imu",
"num_samples": 1253345
},
{
"name": "temperature",
"num_samples": 12489
}
],
"total_samples": 32599298
},
"device_config": {
"device": "BioPoint",
"name": "my_bp",
"firmware_version": "5.0",
"hardware_version": 0,
"filtering": true,
"memory_mode": "both",
"ble_power": "high",
"ecg": {
"enabled": true,
"fs": 500.0,
"filter": {
"enabled": true,
"fc_low": 0,
"fc_high": 30
},
"dc_notch": true,
"mains_notch": 60
},
"emg": {
"enabled": true,
"fs": 2000.0,
"filter": {
"enabled": true,
"fc_low": 20,
"fc_high": 450
},
"dc_notch": true,
"mains_notch": 60
},
"eda": {
"enabled": false,
"fs": 50.0,
"filter": {
"enabled": true,
"fc_low": 0,
"fc_high": 5
},
"dc_notch": true,
"mains_notch": 60,
"freq": 0
},
"ppg": {
"enabled": false,
"sensitivity": "medium",
"i_ir": 14,
"i_r": 0,
"i_g": 9,
"i_b": 9,
"sps": 400,
"avg": 8
},
"imu": {
"chip": "ICM42688",
"enabled": true,
"fs": 100.0,
"bw": 170.0,
"accel_range": 2,
"gyro_range": 16
},
"temperature": {
"fs": 1.0
},
"memory_size_gb": 4,
"low_latency_mode": true,
"enable_leds": true,
"motor_intensity": 5
}
}
}
Pulling data from buffers
Buffers offer a convenient way of pulling data. You can pull from one or many sensor at the same time by providing the --sensor/-s flag. You can also pull either the full acquisition:
> buffer pull --id 1 --sensor ecg --sensor emg
{
"buffer_pull": {
"sensors": [
{
"sensor": "ecg",
"timestamps": [
6.828,
-- snip --
6.832
],
"values": {
"ecg": [
-6.56791355298913e-7,
-- snip --
-2.1893045176630434e-7
]
}
},
{
"sensor": "emg",
"timestamps": [
6.846,
-- snip --
6.8505
],
"values": {
"emg": [
0.000019046949303668477,
-- snip --
-9.63293987771739e-6
]
}
}
]
}
}
or the latest $n$ seconds of data:
> buffer pull --id 1 --sensor eda --last-seconds 5.0
or a time slice:
> buffer pull --id 2 --sensor emg_armband --sensor imu --from 5.0 --to 10.0
{
"buffer_pull": {
"sensors": [
{
"sensor": "imu",
"timestamps": [
6.828,
-- snip --
6.832
],
"values": {
"qw": [
-6.56791355298913e-7,
-- snip --
-2.1893045176630434e-7
],
-- snip --
"az": [
-6.56791355298913e-7,
-- snip --
-2.1893045176630434e-7
]
}
},
{
"sensor": "emg_armband",
"timestamps": [
-- snip --
],
"values": {
"emg0": [
-- snip --
],
-- snip --
"emg7": [
-- snip --
]
}
}
]
}
}
The JSON returned from buffer pull is simpler than the DataPacket since we already filter for a given acquisition/device.
The sensor and channel keys are identical to those from the DataPacket schema.
Clearing data
Clearing data from the buffers is straightforward. You can clear a particular acquisition:
> buffer clear --id 1
{
"buffer_clear": {
"message": "Cleared acquisition 1 for my_bp"
}
}
Or of a particular device:
> buffer clear --handle BP_7A1F
{
"buffer_clear": {
"message": "Cleared all acquisitions for BP_7A1F"
}
}
Or finally, clear all data:
> buffer clear --all
{
"buffer_clear": {
"message": "Cleared all buffers (2)"
}
}
Exporting data to disk
buffer export writes one or more buffered acquisitions to disk in either hdf5:
> buffer export --dir export/directory --format hdf5
{
"buffer_export": {
"format": "Hdf5",
"files": [
"SB_EFFD_2026-06-28-08-20-51.h5"
]
}
}
Or you can also export in the CSV format, which does not contain acquisition metadata:
> buffer export --dir export/directory --format csv
{
"buffer_export": {
"format": "Csv",
"files": [
"SB_EFFD_2026-06-28-08-20-51_ecg.csv",
"SB_EFFD_2026-06-28-08-20-51_emg_armband.csv",
"SB_EFFD_2026-06-28-08-20-51_imu.csv",
"SB_EFFD_2026-06-28-08-20-51_ppg.csv",
"SB_EFFD_2026-06-28-08-20-51_temperature.csv"
]
}
}
See Export data to file for the difference between the two formats and the recommended workflow.