Skip to content

BLE Architecture and Protocol Layers

This document explains how the Grundfos ALPHA HWR communicates over Bluetooth Low Energy (BLE) and how different protocol layers work together.

BLE Stack Overview

The ALPHA HWR uses a layered communication architecture:

Layer Responsibility Components
Application Layer Pump logic and user features Schedule Management, Control, Telemetry
GENI Protocol Layer Protocol semantics and object mapping Class 2/3/7/10, OpSpec, Objects, SubIDs
Framing Layer Low-level packet structure and integrity Start, Length, CRC, Packet splitting
BLE GATT Layer Data transport and service definition Characteristics, Notifications, MTU
BLE Link Layer Physical connection and radio management Connection, Pairing, Security

Connection Parameters

BLE Service:

  • UUID: 0000fdd0-0000-1000-8000-00805f9b34fb (GENI Service)
  • Characteristic: 859cffd1-036e-432a-aa28-1a0085b87ba9 (Read/Write/Notify)

Connection Properties: ...

Device Discovery

The pump broadcasts BLE advertisements with:

Service Data (Grundfos Company ID: 0000fe5d-0000-1000-8000-00805f9b34fb):
[flags...][product_family][product_type][product_version][...]
  Byte 3: 52 (0x34) = ALPHA family
  Byte 4: 7  (0x07) = HWR type
  Byte 5: 2  (0x02) = Version

Example:

Service Data: 00 03 00 34 07 02 ...
              ^^ ^^ ^^ ^^ ^^ ^^
              |  |  |  |  |  └─ Product Version
              |  |  |  |  └──── Product Type (HWR)
              |  |  |  └─────── Product Family (ALPHA)
              |  |  └────────── Reserved
              |  └───────────── Reserved
              └──────────────── Flags

Notifications

The characteristic supports notifications for:

  • Real-time telemetry streaming (Class 10, OpSpec 0x0E)
  • Command acknowledgments (ACK/NACK)
  • Asynchronous responses

Layer 2: BLE GATT Layer

Characteristic Operations

Write Operations:

  • Used for sending commands and requests
  • Write Without Response (faster, no confirmation)
  • Packets >20 bytes must be split across multiple writes

Notify Operations:

  • Pump sends unsolicited data (telemetry, events)
  • Client must enable notifications on characteristic
  • High-frequency telemetry stream (~1-2 Hz)

MTU Limitations

Problem: BLE ATT has a 20-byte payload limit
Solution: Multi-chunk packet splitting

# Example: 59-byte schedule write packet
packet = [59 bytes total]

# Split into 3 chunks:
chunk1 = packet[0:20]   # 20 bytes
chunk2 = packet[20:40]  # 20 bytes
chunk3 = packet[40:59]  # 19 bytes

await client.write_gatt_char(CHAR_UUID, chunk1, response=False)
await asyncio.sleep(0.01)  # 10ms delay between chunks
await client.write_gatt_char(CHAR_UUID, chunk2, response=False)
await asyncio.sleep(0.01)
await client.write_gatt_char(CHAR_UUID, chunk3, response=False)

Critical: The pump reassembles chunks in order. Delays between chunks prevent buffer overflow.


Layer 3: Framing Layer

GENI Frame Structure

Every packet follows this format:

[Start][Length][Dest][Source][APDU...][CRC_H][CRC_L]
  0x27    N     0xE7   0xF8    ...      ...    ...

Start:  0x27 (request) or 0x24 (response)
Length: Dest + Source + APDU length
Dest:   0xE7 (pump/service)
Source: 0xF8 (local master/client)
APDU:   Application Protocol Data Unit
CRC:    CRC-16-CCITT (big-endian)

Example Request:

2705e7f802c39421a6
││││││││││││││││││
│││││││││││││││└└─ CRC: 0x21A6
││││││││││││└└──── Register: 0x94 (Class 2, ID 148)
│││││││││└└─────── OpSpec: 0xC3 (INFO, 3 bytes)
││││││││└────────── Class: 0x02 (Class 2)
│││││││└─────────── Source: 0xF8 (client)
││││││└──────────── Dest: 0xE7 (pump)
│││││└───────────── Length: 0x05 (5 bytes)
││││└────────────── Start: 0x27 (request)

CRC Calculation

Two CRC variants:

  • Read/Response CRC: Standard CRC-16-CCITT with final XOR
  • Write CRC: Alternative variant (rarely used)
def calc_crc16_read(data: bytes) -> int:
    crc = 0xFFFF
    for byte in data:
        crc ^= byte << 8
        for _ in range(8):
            if crc & 0x8000:
                crc = (crc << 1) ^ 0x1021
            else:
                crc <<= 1
            crc &= 0xFFFF
    return crc ^ 0xFFFF  # Final XOR

Layer 4: GENI Protocol Layer

Class System

GENI uses classes to categorize operations:

Class Name Purpose Example
2 Register Read single-byte values Alarm code, control mode
3 Command Execute actions Start, stop, set mode
7 String Read text data Serial number, firmware (NOT supported on HWR)
10 DataObject Complex structures Telemetry, schedules, setpoints

Class 10: DataObjects

Most commonly used for:

  • Telemetry streaming (flow, pressure, power)
  • Schedule management (Object 84)
  • Setpoint configuration (Object 82, 86)
  • Statistics (Object 93)

Structure:

[Class=0x0A][OpSpec][SubID_H][SubID_L][ObjID_H][ObjID_L][Data...]

OpSpec encoding: [OpSpec(3 bits)][Length(5 bits)]
SubID: 16-bit identifier (big-endian)
ObjID: 16-bit identifier (big-endian)

OpSpec Types:

  • 0x03: INFO (read request)
  • 0x0E: Notification (unsolicited data)
  • 0x93: SET (10-byte structure write)
  • 0xB3: SET (42-byte schedule write)

Object IDs and SubIDs

Key Objects:

Object SubID Range Type Purpose
84 1 Single Schedule overview
84 1000-1004 Multiple Schedule layers 0-4
86 varies Multiple Setpoint limits and current setpoint
87 69 Single Electrical telemetry (Motor State)
93 1 Single Operation statistics
93 290 Single Flow/pressure telemetry
93 300 Single Temperature telemetry

Layer 5: Application Layer

Telemetry Update Rate

While basic notifications might arrive at ~1Hz, the high-frequency stream for Motor State and Flow/Pressure typically updates at ~10Hz when the pump is actively running.

Schedule Layers (Focus of This Guide)

The ALPHA HWR implements 5 independent schedule layers within the GENI protocol:

Data Object Mapping

Object 84: Clock Program
├─ SubID 1:    Overview (enabled/disabled)
├─ SubID 1000: Layer 0 schedule (42 bytes)
├─ SubID 1001: Layer 1 schedule (42 bytes)
├─ SubID 1002: Layer 2 schedule (42 bytes)
├─ SubID 1003: Layer 3 schedule (42 bytes)
└─ SubID 1004: Layer 4 schedule (42 bytes)

Each layer (SubID) contains a complete 7-day schedule (42 bytes):

[Mon(6)][Tue(6)][Wed(6)][Thu(6)][Fri(6)][Sat(6)][Sun(6)]

Per-day (6 bytes):
[Enabled][Action][Start_H][Start_M][End_H][End_M]

Why 5 Layers

Architectural Benefits:

  1. Storage Efficiency: Each layer is 42 bytes, total 210 bytes
  2. Independent Management: Modify one layer without affecting others
  3. Overlay Support: Multiple layers active simultaneously
  4. Hierarchical Scheduling: Base + override pattern

Practical Use Cases:

Layer 0: Base schedule (weekday mornings)
Layer 1: Additional slots (weekday evenings)
Layer 2: Weekend schedule (different pattern)
Layer 3: Seasonal adjustments (winter/summer)
Layer 4: Temporary overrides (holidays, events)

Layer Read/Write Protocol

Read Layer 0:

Request:  [0x27][0x0B][0xE7][0xF8][0x0A][0x03][0x54][0x03][0xE8][CRC]
                                    ││    ││    ││    ││    └└─ SubID: 1000 (Layer 0)
                                    ││    ││    └└────────────── ObjID: 84
                                    ││    └└───────────────────── OpSpec: 0x03 (INFO)
                                    └└─────────────────────────── Class: 10

Response: [0x24][0x31][0xF8][0xE7][0x0A][0x03][0x54][0x03][0xE8][Header(3)][Data(42)][CRC]
                                                                 └──────────┴─────────┘
                                                                    45 bytes total

Write Layer 0:

Request:  [0x27][0x37][0xE7][0xF8][0x0A][0xB3][0x54][0x03][0xE8][0x00][Type(3)][Size(2)][Data(42)][CRC]
                                    ││    ││    ││    ││    └└── ││      └└──────┴────┴─────────┘
                                    ││    ││    ││    ││    │    ││        Type 222 header + data
                                    ││    ││    ││    ││    │    └└────────── Reserved
                                    ││    ││    ││    └└────┴────────────────── SubID: 1000
                                    ││    ││    └└───────────────────────────── ObjID: 84
                                    ││    └└──────────────────────────────────── OpSpec: 0xB3 (SET)
                                    └└─────────────────────────────────────────── Class: 10

Total: 59 bytes  Split into 3 BLE writes (20+20+19)

Layer Interaction Example

Practical Scenario: Weekday + Weekend Scheduling

User Goal:

  • Weekdays: Pump on 6:30-8:30 (morning) and 18:00-22:00 (evening)
  • Weekends: Pump on 8:00-23:00 (all day)

Implementation Using Layers:

# Layer 0: Weekday mornings
weekday_morning = [
    ScheduleEntry(day="Monday", begin_hour=6, begin_minute=30, end_hour=8, end_minute=30),
    ScheduleEntry(day="Tuesday", begin_hour=6, begin_minute=30, end_hour=8, end_minute=30),
    ScheduleEntry(day="Wednesday", begin_hour=6, begin_minute=30, end_hour=8, end_minute=30),
    ScheduleEntry(day="Thursday", begin_hour=6, begin_minute=30, end_hour=8, end_minute=30),
    ScheduleEntry(day="Friday", begin_hour=6, begin_minute=30, end_hour=8, end_minute=30),
]
await client.set_weekly_schedule(weekday_morning, layer=0)

# Layer 1: Weekday evenings
weekday_evening = [
    ScheduleEntry(day="Monday", begin_hour=18, begin_minute=0, end_hour=22, end_minute=0),
    # ... Tuesday-Friday same pattern
]
await client.set_weekly_schedule(weekday_evening, layer=1)

# Layer 2: Weekend all-day
weekend = [
    ScheduleEntry(day="Saturday", begin_hour=8, begin_minute=0, end_hour=23, end_minute=0),
    ScheduleEntry(day="Sunday", begin_hour=8, begin_minute=0, end_hour=23, end_minute=0),
]
await client.set_weekly_schedule(weekend, layer=2)

Protocol Operations:

  1. 3 BLE packets (each 59 bytes split into 3 chunks)
  2. 3 separate Class 10 SET operations (OpSpec 0xB3)
  3. 3 different SubIDs (1000, 1001, 1002)
  4. Independent storage in pump memory

Result on Pump:

Layer 0 (SubID 1000): Mon-Fri 6:30-8:30, Sat-Sun empty
Layer 1 (SubID 1001): Mon-Fri 18:00-22:00, Sat-Sun empty
Layer 2 (SubID 1002): Mon-Fri empty, Sat-Sun 8:00-23:00

Pump Behavior:

Monday:    ▓▓▓▓▓▓▓▓░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓░░░░░  (Layer 0 + Layer 1)
Saturday:  ░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░  (Layer 2)

Performance Considerations

Packet Timing

Critical Delays:

  • 10ms between BLE chunks: Prevents buffer overflow
  • 200-500ms after keep-alive: Wakes GENI controller
  • 300-500ms after write: Allows pump to persist data

Transaction Management

The library uses a transaction lock to prevent concurrent operations:

async with self._transaction_lock:
    # Send request
    await self.client.write_gatt_char(...)

    # Wait for response
    response = await asyncio.wait_for(self._response_queue.get(), timeout)

Why BLE characteristics don't support request/response matching. The lock ensures one operation completes before another starts.

Keep-Alive Burst

Large data reads (schedules, statistics) require a keep-alive burst:

# Send 3 keep-alive packets
for _ in range(3):
    await self.send_keep_alive()
    await asyncio.sleep(0.1)

# Wait for GENI controller to wake
await asyncio.sleep(0.3)

# Now send read request
response = await self._read_class10_subid(84, 1000)

Purpose: Some GENI controllers sleep between operations. The burst wakes them up for complex reads.


Debugging Protocol Issues

Common Problems

1. Schedule Write Fails Silently

  • Symptom: set_schedule_entry() returns True but data unchanged
  • Cause: Packet splitting broken (only 2 chunks sent instead of 3)
  • Fix: Use multi-chunk splitting in _query() method

2. Schedule Read Timeout

  • Symptom: get_schedule() times out or returns None
  • Cause: GENI controller asleep
  • Fix: Add keep-alive burst before read

3. CRC Errors

  • Symptom: Pump sends NACK (OpSpec 0x02)
  • Cause: Wrong CRC calculation or corrupted packet
  • Fix: Verify CRC algorithm, check packet integrity

Packet Logging

Enable debug logging to see protocol traffic:

import logging
logging.basicConfig(level=logging.DEBUG)

# Output includes:
# Querying: 2737e7f80ab35403e800de0100002a01020...
# Split-wrote 59 bytes (20 + 20 + 19)

Summary

The ALPHA HWR BLE architecture uses:

  1. BLE GATT for transport (MTU 20 bytes)
  2. GENI framing for packet structure (Start, Length, CRC)
  3. GENI protocol for operations (Class 10 DataObjects)
  4. Schedule layers for application logic (5 independent SubIDs)

Key Takeaways:

  • Schedule layers are SubIDs within Object 84, not separate BLE characteristics
  • Multi-chunk splitting is critical for packets >20 bytes
  • Layers allow complex scheduling without protocol complexity
  • All operations are synchronous request/response over a single BLE characteristic

For more details:

  • Schedule Protocol: See docs/protocol/schedules.md
  • Wire Format: See docs/protocol/wire_format.md
  • Control Operations: See docs/protocol/control.md