How We Reduced Overhead in Communication and Saved Bandwidth & Battery (realtime transmission of 100Hz Sensor Data over BLE)
Share
How We Reduced Overhead in Communication and Saved Bandwidth & Battery (realtime transmission of 100Hz Sensor Data over BLE) The Core Challenge Building a dog health collar that streams 100Hz IMU and piezo sensor data over BLE sounds simple until you face the real problems: • How do you timestamp 100 samples per second accurately? • How do you handle BLE callbacks without blocking sensor readings? • What happens when BLE connection and sensor callbacks try to access the same buffer? • How do you keep the watchdog happy during slow BLE operations? This post focuses on the data structures and BLE architecture we built to solve these problems. Timestamp Strategy: Finding the Right Balance The Problem with Timestamps At 100Hz, we're taking a reading every 10ms. Sending a full timestamp with every sample waste’s bandwidth: • 32-bit timestamp: 4 bytes per sample • 100 samples/second × 4 bytes = 400 bytes/second just for timestamps • Over two sensors, that's 800 bytes/second of overhead Our Solution: Testing Different Timestamp Approaches We tested three different approaches: Approach 1 - Every sample with absolute timestamp: • 4 bytes per sample × 15 samples = 60 bytes just for timestamps • Accurate but wastes bandwidth Approach 2 - Every sample with relative timestamp (What we used): • 2 bytes per sample × 15 samples = 30 bytes for timestamps • We needed proof that each individual reading was captured at exactly 100Hz • Having a timestamp on every sample gave us this verification • Better bandwidth than approach 1 Approach 3 - First and Last timestamps only (Better for bandwidth): • First Sample: Absolute timestamp (4 bytes) • Last Sample: Absolute timestamp (4 bytes) • Total: Only 8 bytes for entire packet • Proof that 100Hz timing was maintained overall (verify duration between first and last) • Phone can calculate middle timestamps by interpolating • Saves 22 bytes per packet compared to approach 2 For our case, we used approach 2 because we needed proof of each reading being at 100Hz. Overall, approach 3 is better for bandwidth if you can accept interpolated timestamps for middle samples as we are estimating the time for each reading which may not be true. Actual Packet Structure IMU Packet (228 bytes): Bytes 0-1: Packet Type (0x1001) Bytes 2-17: Device ID (16 bytes) Each of 15 samples (14 bytes per sample): - Relative timestamp: 2 bytes (milliseconds offset) - ax, ay, az: 6 bytes - gx, gy, gz: 6 bytes Piezo Packet (218 bytes): Bytes 0-1: Packet Type (0x3001) Bytes 2-17: Device ID (16 bytes) Byte 18: Strip ID Each of 20 samples (10 bytes per sample): - Relative timestamp: 2 bytes (milliseconds offset) - Raw ADC_1: 2 bytes - Voltage_1: 2 bytes (mV) - Raw ADC_2: 2 bytes - Voltage_2: 2 bytes (mV) The Callback Problem: BLE vs Sensor Interrupts Initial Naive Approach (Failed) Our first attempt was simple: sensor callback writes to buffer, BLE callback sends the buffer. This failed immediately because: 1. BLE callback could fire while sensor callback was writing 2. Sensor callback could overflow buffer if BLE was slow 3. No way to stop sensor reading during BLE connection process The BLE Connection Process Problem During BLE connection setup, the process can take 200-500ms. During this time: • Sensor callbacks keep firing at 100Hz • BLE stack is busy, can't accept data • Buffer fills up quickly (50 samples in 500ms) • We needed to pause sensor callbacks but couldn't lose timing accuracy Our solution: Stop sensor callbacks during BLE connection to avoid buffer overflow and race conditions during the connection process. We also had to disable the watchdog timer during connection because it can take more than our normal 5-second timeout in poor signal conditions. Stopping callbacks during connection means we lose a few hundred milliseconds of data, but we gain: • Clean connection without buffer overflow • Synchronized starting point with phone • No race conditions during setup Dual Buffer Solution To handle sensor and BLE callbacks running at the same time, we used dual buffering: How it works: • Two buffers per sensor (Buffer A and Buffer B) • Sensor callback always writes to "write buffer" • BLE callback always reads from "read buffer" • When write buffer fills up, we swap the pointers • Swap happens in main loop, not in interrupts The key rules: • Sensor callback: Always writes to write_buffer, never blocks • BLE callback: Always reads from read_buffer, never interferes with sensors • Buffer swap: Only happens when BLE is ready, outside of interrupt context • No locks needed: Just one flag and atomic pointer swap Why Dual Buffer Matters Without dual buffering, we had these issues: Problem 1: BLE callback blocks for 20-50ms while transmitting • During this time, 2-5 sensor readings happen • If sensor writes to same buffer, data gets corrupted Problem 2: Sensor callback can't wait • It fires every 10ms exactly • If it has to wait for BLE to finish, we lose timing accuracy • Missing even one callback means 100Hz becomes 99Hz Problem 3: Race conditions • Sensor writes sample #47 • BLE reads sample #47 at same moment • BLE gets half old data, half new data • Corrupt samples ruin heart rate analysis Dual buffering solved all of this. Sensor callback and BLE callback never touch the same memory. Watchdog Timer Management The watchdog timer was critical for field reliability (catching firmware crashes), but it caused problems during BLE operations. The Watchdog Problem We set a 5-second watchdog timeout. But: • BLE connection can take 8-10 seconds in bad signal conditions • Sending large buffered data after reconnect takes 6-8 seconds • OTA firmware updates take 30+ seconds All of these would trigger the watchdog and reset the device mid-operation. Selective Watchdog Strategy We managed the watchdog based on what operation was happening: Normal operation: 5-second timeout (catch real crashes) BLE connecting: Disabled (connection time is unpredictable) Large data sends: 15-second timeout (allow time but still protect against hangs) OTA updates: Disabled (updates can take minutes) This approach: • Keeps watchdog active during normal operation • Disables it during unpredictable operations • Extends timeout for slow but predictable operations • Always re-enables after the operation completes Final Architecture: How It All Works Together Key Lessons on BLE Data Structures 1. Timestamp Strategy Matters More Than You Think • We tested: absolute every sample, relative every sample, first+last only • We used relative timestamps (2 bytes per sample) for proof of each reading at 100Hz • First+last timestamp method is better for bandwidth (saves 22 bytes per packet) • Choose based on your needs: individual proof vs bandwidth savings 2. Callbacks Need Careful Isolation • Never let BLE callbacks block sensor callbacks • Never let sensor callbacks wait for BLE • Dual buffers completely separate the two processes • Buffer swap happens outside interrupts 3. BLE Connection is a Critical State • Must pause sensor callbacks during connection • Must disable or extend watchdog timeout • Must sync timestamps with phone immediately after connection • Better to lose 500ms of data than corrupt the whole buffer 4. Watchdog Management is Not Optional • Field devices will crash (EMI, bugs) • But watchdog can't interfere with legitimate slow operations • Use state-based watchdog timeouts • Always re-enable after special operations 5. Design for Real BLE Behaviour • Connection can take 10+ seconds in bad conditions • MTU can downgrade randomly (247 → 23 bytes) • Packets can be delayed or lost • Structure must handle all this. Results This architecture is stable and has been running on a few collars for a few weeks now as we are still in a development phase. Questions about BLE buffer management or high-frequency sensor data? Reach out to Oxeltech.de

The Core Challenge

Building a dog health collar that streams 100Hz IMU and piezo sensor data over BLE sounds simple until you face the real problems:

  • How do you timestamp 100 samples per second accurately?
  • How do you handle BLE callbacks without blocking sensor readings?
  • What happens when BLE connection and sensor callbacks try to access the same buffer?
  • How do you keep the watchdog happy during slow BLE operations?

This post focuses on the data structures and BLE architecture we built to solve these problems.

Timestamp Strategy: Finding the Right Balance

The Problem with Timestamps

At 100Hz, we’re taking a reading every 10ms. Sending a full timestamp with every sample waste’s bandwidth:

  • 32-bit timestamp: 4 bytes per sample
  • 100 samples/second × 4 bytes = 400 bytes/second just for timestamps
  • Over two sensors, that’s 800 bytes/second of overhead

Our Solution: Testing Different Timestamp Approaches

We tested three different approaches:

Approach 1 – Every sample with absolute timestamp:
  • 4 bytes per sample × 15 samples = 60 bytes just for timestamps
  • Accurate but wastes bandwidth
Approach 2 – Every sample with relative timestamp (What we used):
  • 2 bytes per sample × 15 samples = 30 bytes for timestamps
  • We needed proof that each individual reading was captured at exactly 100Hz
  • Having a timestamp on every sample gave us this verification
  • Better bandwidth than approach 1
Approach 3 – First and Last timestamps only (Better for bandwidth):
  • First Sample: Absolute timestamp (4 bytes)
  • Last Sample: Absolute timestamp (4 bytes)
  • Total: Only 8 bytes for entire packet
  • Proof that 100Hz timing was maintained overall (verify duration between first and last)
  • Phone can calculate middle timestamps by interpolating
  • Saves 22 bytes per packet compared to approach 2

For our case, we used approach 2 because we needed proof of each reading being at 100Hz. Overall, approach 3 is better for bandwidth if you can accept interpolated timestamps for middle samples as we are estimating the time for each reading which may not be true.

Actual Packet Structure

IMU Packet (228 bytes):

Bytes 0-1:   Packet Type (0x1001)

Bytes 2-17:  Device ID (16 bytes)

Each of 15 samples (14 bytes per sample):

– Relative timestamp: 2 bytes (milliseconds offset)

– ax, ay, az: 6 bytes

– gx, gy, gz: 6 bytes

Piezo Packet (218 bytes):

Bytes 0-1:   Packet Type (0x3001)

Bytes 2-17:  Device ID (16 bytes)

Byte 18:     Strip ID

Each of 20 samples (10 bytes per sample):

– Relative timestamp: 2 bytes (milliseconds offset)

– Raw ADC_1: 2 bytes

– Voltage_1: 2 bytes (mV)

– Raw ADC_2: 2 bytes

– Voltage_2: 2 bytes (mV)

The Callback Problem: BLE vs Sensor Interrupts

Initial Naive Approach (Failed)

Our first attempt was simple: sensor callback writes to buffer, BLE callback sends the buffer.

This failed immediately because:

  1. BLE callback could fire while sensor callback was writing
  2. Sensor callback could overflow buffer if BLE was slow
  3. No way to stop sensor reading during BLE connection process

The BLE Connection Process Problem

During BLE connection setup, the process can take 200-500ms. During this time:

  • Sensor callbacks keep firing at 100Hz
  • BLE stack is busy, can’t accept data
  • Buffer fills up quickly (50 samples in 500ms)
  • We needed to pause sensor callbacks but couldn’t lose timing accuracy

Our solution: Stop sensor callbacks during BLE connection to avoid buffer overflow and race conditions during the connection process. We also had to disable the watchdog timer during connection because it can take more than our normal 5-second timeout in poor signal conditions.

Stopping callbacks during connection means we lose a few hundred milliseconds of data, but we gain:

  • Clean connection without buffer overflow
  • Synchronized starting point with phone
  • No race conditions during setup

Dual Buffer Solution

To handle sensor and BLE callbacks running at the same time, we used dual buffering:

How it works:

  • Two buffers per sensor (Buffer A and Buffer B)
  • Sensor callback always writes to “write buffer”
  • BLE callback always reads from “read buffer”
  • When write buffer fills up, we swap the pointers
  • Swap happens in main loop, not in interrupts

The key rules:

  • Sensor callback: Always writes to write_buffer, never blocks
  • BLE callback: Always reads from read_buffer, never interferes with sensors
  • Buffer swap: Only happens when BLE is ready, outside of interrupt context
  • No locks needed: Just one flag and atomic pointer swap

Why Dual Buffer Matters

Without dual buffering, we had these issues:

Problem 1: BLE callback blocks for 20-50ms while transmitting

  • During this time, 2-5 sensor readings happen
  • If sensor writes to same buffer, data gets corrupted

Problem 2: Sensor callback can’t wait

  • It fires every 10ms exactly
  • If it has to wait for BLE to finish, we lose timing accuracy
  • Missing even one callback means 100Hz becomes 99Hz

Problem 3: Race conditions

  • Sensor writes sample #47
  • BLE reads sample #47 at same moment
  • BLE gets half old data, half new data
  • Corrupt samples ruin heart rate analysis

Dual buffering solved all of this. Sensor callback and BLE callback never touch the same memory.

Watchdog Timer Management

The watchdog timer was critical for field reliability (catching firmware crashes), but it caused problems during BLE operations.

The Watchdog Problem

We set a 5-second watchdog timeout. But:

  • BLE connection can take 8-10 seconds in bad signal conditions
  • Sending large buffered data after reconnect takes 6-8 seconds
  • OTA firmware updates take 30+ seconds

All of these would trigger the watchdog and reset the device mid-operation.

Selective Watchdog Strategy

We managed the watchdog based on what operation was happening:

Normal operation: 5-second timeout (catch real crashes) BLE connecting: Disabled (connection time is unpredictable) Large data sends: 15-second timeout (allow time but still protect against hangs) OTA updates: Disabled (updates can take minutes)

This approach:

  • Keeps watchdog active during normal operation
  • Disables it during unpredictable operations
  • Extends timeout for slow but predictable operations
  • Always re-enables after the operation completes

Final Architecture: How It All Works Together

Architecture

Key Lessons on BLE Data Structures

  1. Timestamp Strategy Matters More Than You Think
    • We tested: absolute every sample, relative every sample, first+last only
    • We used relative timestamps (2 bytes per sample) for proof of each reading at 100Hz
    • First+last timestamp method is better for bandwidth (saves 22 bytes per packet)
    • Choose based on your needs: individual proof vs bandwidth savings
  1. Callbacks Need Careful Isolation
    • Never let BLE callbacks block sensor callbacks
    • Never let sensor callbacks wait for BLE
    • Dual buffers completely separate the two processes
    • Buffer swap happens outside interrupts
  1. BLE Connection is a Critical State
    • Must pause sensor callbacks during connection
    • Must disable or extend watchdog timeout
    • Must sync timestamps with phone immediately after connection
    • Better to lose 500ms of data than corrupt the whole buffer
  1. Watchdog Management is Not Optional
    • Field devices will crash (EMI, bugs)
    • But watchdog can’t interfere with legitimate slow operations
    • Use state-based watchdog timeouts
    • Always re-enable after special operations
  1. Design for Real BLE Behaviour
    • Connection can take 10+ seconds in bad conditions
    • MTU can downgrade randomly (247 → 23 bytes)
    • Packets can be delayed or lost
    • Structure must handle all this.

Results

This architecture is stable and has been running on a few collars for a few weeks now as we are still in a development phase.

Questions about BLE buffer management or high-frequency sensor data? Reach out to Oxeltech

Subscribe Our Newsletter