Skip to main content

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.
Rule of thumb

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:

SensorRateChannelsRAM / hour
ECG500 Hz1~29 MB
EMG2000 Hz1~115 MB
EDA50 Hz1~3 MB
IMU100 Hz7~23 MB
PPG50 Hz4~7 MB
Temperature1 Hz1~0.06 MB

A BioPoint streaming all sensors uses roughly 230 MB per hour (~3.8 MB/min).

Managing memory

For long or back-to-back recordings, export acquisitions you no longer need and clear them from the buffers to reclaim RAM.

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 streaming or both), 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 device or both) is not in a buffer until you fetch it. Run download-memory to pull an on-board recording into a buffer, after which it behaves like any other buffered acquisition.
Recovering lost samples

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 when download-memory imports 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.

CommandPurpose
buffer listList buffered acquisitions and their status.
buffer infoShow an acquisition's details, including the device configuration it was recorded with.
buffer pullExtract sensor data with an optional time range (--from + --to) or the last N seconds (--last-seconds).
buffer clearClear buffered data.
buffer exportWrite 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 format

The 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
}
]
}
}
note

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 --
]
}
}
]
}
}
JSON schema

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.