What follows is the analysis of BLE data packets sent between the Ergdata app and the PM5 during a short fixed 100 meter distance workout. The packets were captured with the nRF52840 and Wireshark. After capturing the session, the packet dissections were exported to a JSON file (File-> Export Packet Dissections -> As Plain Text).
%jq 'group_by(.["_source"]["layers"]["btatt"]["btatt.handle_tree"]["btatt.uuid128"]) | map({uuid128: .[0]["_source"]["layers"]["btatt"]["btatt.handle_tree"]["btatt.uuid128"], count: length})' pm5-ergdata-airplus-extract.json
[
{
"uuid128": "ce:06:00:22:43:e5:11:e4:91:6c:08:00:20:0c:9a:66",
"count": 8
},
{
"uuid128": "ce:06:00:31:43:e5:11:e4:91:6c:08:00:20:0c:9a:66",
"count": 172
},
{
"uuid128": "ce:06:00:32:43:e5:11:e4:91:6c:08:00:20:0c:9a:66",
"count": 169
},
{
"uuid128": "ce:06:00:33:43:e5:11:e4:91:6c:08:00:20:0c:9a:66",
"count": 169
},
{
"uuid128": "ce:06:00:35:43:e5:11:e4:91:6c:08:00:20:0c:9a:66",
"count": 21
},
{
"uuid128": "ce:06:00:36:43:e5:11:e4:91:6c:08:00:20:0c:9a:66",
"count": 11
},
{
"uuid128": "ce:06:00:37:43:e5:11:e4:91:6c:08:00:20:0c:9a:66",
"count": 1
},
{
"uuid128": "ce:06:00:38:43:e5:11:e4:91:6c:08:00:20:0c:9a:66",
"count": 1
},
{
"uuid128": "ce:06:00:39:43:e5:11:e4:91:6c:08:00:20:0c:9a:66",
"count": 1
},
{
"uuid128": "ce:06:00:3a:43:e5:11:e4:91:6c:08:00:20:0c:9a:66",
"count": 1
},
{
"uuid128": "ce:06:00:80:43:e5:11:e4:91:6c:08:00:20:0c:9a:66",
"count": 1
}
]
One group of packets is related to the characteristic: ce:06:00:33:43:e5:11:e4:91:6c:08:00:20:0c:9a:66. The PM5 layout of the corresponding notification can be found in the concept2 SDK documentation as shown below.
void _subscribeNotifications() {
//_notificationEnablerCharacteristic.write(List.from([0x01,0x00]));
_outputTextController.text += 'clicked _subscribeNotifications\n';
_strokeDataCharacteristic.setNotifyValue(true);
_strokeDataCharacteristic.onValueReceived.listen((data) {
String hexData = '${intArrayToHex(data)}\n';
String strokeData = json.encode(intListToStrokeData(data).toJson());
print("$hexData -> $strokeData");
}, onError: (error, stackTrace) {
_outputTextController.text += "Error $error\n";
}, onDone: () {
_outputTextController.text += "listen Done\n";
});
}
The intArrayToHex above looks like this:
String intArrayToHex(List<int> intArray) {
for (var value in intArray) {
if (value < 0 || value > 255) {
throw ArgumentError('All integers must be in the range 0-255.');
}
}
// Convert each integer to a two-character hex string and concatenate them
final hexStringBuffer = StringBuffer();
for (var value in intArray) {
hexStringBuffer.write(value.toRadixString(16).padLeft(2, '0'));
}
return hexStringBuffer.toString();
}
StrokeData intListToStrokeData(List<int> intList) {
StrokeData strokeData = StrokeData();
// @formatter:off
strokeData.elapsedTime = DataConvUtils.getUint24(intList, StrokeDataBLEPayload.ELAPSED_TIME_LO.index) * 10;
strokeData.distance = DataConvUtils.getUint24(intList, StrokeDataBLEPayload.DISTANCE_LO.index) / 10;
strokeData.driveLength = DataConvUtils.getUint8(intList, StrokeDataBLEPayload.DRIVE_LENGTH.index) / 100;
strokeData.driveTime = DataConvUtils.getUint8(intList, StrokeDataBLEPayload.DRIVE_TIME.index) * 10;
strokeData.strokeRecoveryTime = (DataConvUtils.getUint8(intList, StrokeDataBLEPayload.STROKE_RECOVERY_TIME_LO.index)
+ DataConvUtils.getUint8(intList, StrokeDataBLEPayload.STROKE_RECOVERY_TIME_HI.index) * 256) * 10;
strokeData.strokeDistance = DataConvUtils.getUint16(intList, StrokeDataBLEPayload.STROKE_DISTANCE_LO.index) / 100;
strokeData.peakDriveForce = DataConvUtils.getUint16(intList, StrokeDataBLEPayload.PEAK_DRIVE_FORCE_LO.index) / 10;
strokeData.averageDriveForce = DataConvUtils.getUint16(intList, StrokeDataBLEPayload.AVG_DRIVE_FORCE_LO.index) / 10;
strokeData.workPerStroke = DataConvUtils.getUint16(intList, StrokeDataBLEPayload.WORK_PER_STROKE_LO.index) / 10;
strokeData.strokeCount = DataConvUtils.getUint8(intList, StrokeDataBLEPayload.STROKE_COUNT_LO.index)
+ DataConvUtils.getUint8(intList, StrokeDataBLEPayload.STROKE_COUNT_HI.index) * 256;
// @formatter:on
return strokeData;
}
class StrokeData {
int elapsedTime = 0;
double distance = 0;
double driveLength = 0;
int driveTime = 0;
int strokeRecoveryTime = 0;
double strokeDistance = 0;
double peakDriveForce = 0;
double averageDriveForce = 0;
double workPerStroke = 0;
int strokeCount = 0;
Map<String, dynamic> toJson() {
return {
'elapsedTime': elapsedTime,
'distance': distance,
'driveLength': driveLength,
'driveTime': driveTime,
'strokeRecoveryTime': strokeRecoveryTime,
'strokeDistance': strokeDistance,
'peakDriveForce': peakDriveForce,
'averageDriveForce': averageDriveForce,
'workPerStroke': workPerStroke,
'strokeCount': strokeCount
};
}
}
Library still in-work is here: https://github.com/wmmnpr/ergc2_pm_csafe
The demo app is here: https://github.com/wmmnpr/ergpm_diagnostics
No comments:
Post a Comment