SRAM Eagle Powertrain
SRAM Eagle Powertrain support is in progress. The bonding handshake has been implemented and now work has begun to reverse engineer the underlying payloads.
Bonding
Values sent to the phone from the bike are mostly encrypted. This means BikeBridge cannot parse what the bike is sending without a handshake with the bike.
The SRAM bond session uses these BLE services/characteristics:
- Service:
d905ee51-90aa-4c7c-b036-1e01fb8eb7ee - Characteristic:
d905ee52-90aa-4c7c-b036-1e01fb8eb7ee
At a high level, BikeBridge does one of two things:
First Connection
- Ask the user to authorize bonding by holding the AXS button on the bike
- Exchange temporary public keys with the bike
- Use the temporary shared key to decrypt the first provisioning message
- Save the final 16-byte SRAM bond key returned by the bike
- Confirm the session was established by decrypting a second provisioning message
Subsequent Connections
- Send
0x73using the stored key - Decrypt the response with the stored key
- If the decrypted message is valid, use that key for encrypted SRAM data reads, otherwise fallback to First Connection steps
Once the session is established, encrypted SRAM characteristic values are decrypted with the 16-byte SRAM bond key.
Diffie-Hellman
First-time bonding uses a small Diffie-Hellman exchange:
public = 5^private mod (2^128 - 713)
secret = devicePublic^private mod (2^128 - 713)
Two helpers cover this:
getPublicKey(privateKey)calculates the public key that is sent to the bikegetIntermediateKey(devicePublicKey, privateKey)calculates the temporary shared key used to decrypt the first provisioning message
AES-EAX
Auth/bonding packets use AES-EAX:
packet[0:16] = nonce
packet[16:-16] = ciphertext
packet[-16:] = authentication tag
decryptEax(key, ciphertext) verifies the tag first. If the tag is valid, it decrypts the ciphertext with AES-CTR.
During first-pairing flow:
- The first encrypted packet decrypts with the intermediate DH key and returns the final 16-byte key
- The second encrypted packet decrypts with that final key and should produce
hello everyone!\0
CMAC
AES-EAX uses AES-CMAC to authenticate the nonce, optional header data, and ciphertext:
tag = CMAC(key, block(0) + nonce)
xor CMAC(key, block(1) + header)
xor CMAC(key, block(2) + ciphertext)
CMAC itself uses raw AES block encryption. In the implementation this is accessed through AES/ECB/NoPadding; this is only the AES block primitive for CMAC, not the payload encryption mode. Payload bytes are decrypted with AES-CTR as required by EAX.
The CMAC double operation derives the standard CMAC subkeys by shifting a 128-bit block left by one bit and applying the 0x87 reduction constant when needed.
Encrypted Writes
SRAM Powertrain settings are written through the same encrypted session used for reads, but writes include an additional per-write token obtained from the bike.
Flow:
- Build the Protocol Buffer payload for the setting being changed
- Read the SRAM bond token characteristic:
d905ee53-90aa-4c7c-b036-1e01fb8eb7ee - Decrypt that token with the active SRAM bond key
- Append the decrypted token bytes to the Protocol Buffer payload
- Encrypt the combined payload with
AES-EAXand the active SRAM bond key - Write the encrypted bytes to the target characteristic
payload = protobuf(setting)
token = eaxDecrypt(srambondKey, read(d905ee53))
write(target, eaxEncrypt(srambondKey, payload + token))
Keepalive
SRAM powertrains require a keepalive operation in order to stay connected for more than a few minutes. A keepalive can be any Apollo read/write operation. In BikeBridge, a read operation is sent every 60 seconds.
Protocol Buffers
After decrypting payloads, SRAM payloads are decoded via Protocol Buffers.
SRAM uses protobuf-c to compile the Protocol Buffers.
Functionality
Drive Unit Status
Drive unit status BLE characteristic: d90500e3-90aa-4c7c-b036-1e01fb8eb7ee
Protocol Buffer:
message ApolloDriveUnitStatus {
optional string serial_number = 20;
optional uint32 firmware_version = 21;
optional uint32 odometer = 22;
optional uint32 motor_temperature = 23;
optional uint32 motor_hardware_version = 24;
optional string drive_unit_hardware_version = 25;
}
odometer value is in meters, and motor_temperature is in Celsius.
serial_number requires some manipulation to get the value shown in the official app:
val serialNumber = serialNumber
.replace(" ", "")
.replace("-", "")
.takeLast(11)
firmware_version is represented as packed bytes so should be parsed as:
major = (firmware_version >> 24) & 0xFF
minor = (firmware_version >> 16) & 0xFF
patch = (firmware_version >> 8) & 0xFF
Battery Status
Battery status BLE characteristic: d90500e1-90aa-4c7c-b036-1e01fb8eb7ee
Protocol Buffer:
message ApolloBatteryStatus {
optional string serial_number = 10;
optional uint32 firmware_version = 11;
optional uint32 charge = 12;
optional uint32 health = 13;
optional uint32 charge_cycles = 14;
optional uint32 temperature = 15;
optional string model_code = 16;
}
charge is the current state of charge and health is the estimated state of health of the battery. Both values are represented as percentages.
temperature is represented in tenths of a degree Celsius.
firmware_version uses the same packed-byte format as drive unit status:
major = (firmware_version >> 24) & 0xFF
minor = (firmware_version >> 16) & 0xFF
patch = (firmware_version >> 8) & 0xFF
model_code is the hardware/model identifier. The official SRAM app has a static map of model_code to battery specifications (e.g. capacity).
Known values:
| Model | App model | Capacity |
|---|---|---|
SRM-001-58L |
BT-ETP-A1 |
680Wh |
Range Extender
Range extender BLE characteristic: d90500e2-90aa-4c7c-b036-1e01fb8eb7ee
The range extender uses the same protobuf as the main battery, just under a different characteristic.
Display
Display BLE characteristic: d90500e4-90aa-4c7c-b036-1e01fb8eb7ee
Protocol Buffer:
message ApolloUtilitiesStatus {
optional uint32 brightness_mode_id = 29;
optional uint32 brightness_level_day = 30;
optional uint32 brightness_level_night = 31;
optional bool auto_dim_status = 32;
optional bool light_ports_enable_status = 33;
}
brightness_mode_id values:
- Day:
0x01 - Night:
0x02
Day mode can only have a brightness value of 32 in decimal. Night mode allows for a brightness between 5 and 18, with an observed default value of 10.
Assist Mode
Assist mode BLE characteristic: d905009d-90aa-4c7c-b036-1e01fb8eb7ee
assist_mode values:
- Range:
0x06 - Rally:
0x07
Protocol Buffer:
message ApolloAssistMode {
optional uint32 assist_mode = 12;
optional uint32 outb_overshoot_deciperc = 13;
optional uint32 outb_debounce_ms = 14;
optional uint32 inb_overshoot_deciperc = 15;
optional uint32 inb_debounce_ms = 16;
optional uint32 coast_offset_rpm = 25;
optional uint32 coast_transition_ms = 26;
}
Assist Mode Config
Assist mode config BLE characteristics:
- Rally:
d905009b-90aa-4c7c-b036-1e01fb8eb7ee - Range:
d905009c-90aa-4c7c-b036-1e01fb8eb7ee
Protocol Buffer:
message ApolloModeConfig {
optional uint32 power_level = 12;
optional uint32 sensitivity_scaling = 13;
optional uint32 gain_level = 14;
optional uint32 flex_power = 15;
}
Default values observed:
| Mode | Power level | Sensitivity scaling | Gain level | Flex power |
|---|---|---|---|---|
| Range | 74 | 30 | 30 | 100 |
| Rally | 100 | 70 | 85 | 200 |
Power
Values are represented in watts, but it is sent over the wire as a percentage of 680W, as the power_level field in the protobuf.
powerLevel = round((watts / 680) * 100)
Valid ranges:
| Mode | Power range |
|---|---|
| Range | 70W - 540W |
| Rally | 550W - 680W |
Assist
Values are represented as percentage, and also sent over the wire as a percentage under the protobuf field gain_level.
Valid ranges:
| Mode | Assist range |
|---|---|
| Range | 10% - 80% |
| Rally | 30% - 100% |
Shift Config
Shift config BLE characteristic: d905009e-90aa-4c7c-b036-1e01fb8eb7ee
Protocol Buffer:
message ApolloShiftConfig {
optional uint32 cadence_setpoint = 16;
optional bool auto_shift = 17;
optional uint32 overlap_min = 18;
optional uint32 overlap_max = 19;
optional uint32 torque_cadence_gain = 20;
optional uint32 torque_cadence_limit = 21;
optional uint32 nom_power = 22;
optional uint32 initial_outb_offset = 23;
optional uint32 outb_decay_factor = 24;
optional uint32 start_gear_idx = 27;
optional uint32 dflt_cadence_sp_rpm = 28;
optional uint32 initial_inb_offset = 29;
optional uint32 inb_decay_factor = 30;
}
auto_shift enables and disables auto-shift on the bike.
cadence_setpoint is the pedal speed target in RPM. The bike represents this value as -3 to +3, where 0 is dflt_cadence_sp_rpm (75 RPM). Each step changes the setpoint by 5 RPM.