feat: ui rework and gear generator
This commit is contained in:
72
test/model/gear_configurator_test.dart
Normal file
72
test/model/gear_configurator_test.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'package:abawo_bt_app/model/gear_configurator.dart';
|
||||
import 'package:abawo_bt_app/model/gear_ratio_codec.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('gear ratio codec', () {
|
||||
test('maps one byte across the configured drivetrain range', () {
|
||||
expect(decodeGearRatioByte(1), gearRatioMin);
|
||||
expect(decodeGearRatioByte(255), gearRatioMax);
|
||||
expect(encodeGearRatioByte(0), 0);
|
||||
expect(encodeGearRatioByte(gearRatioMax + 1), 255);
|
||||
});
|
||||
|
||||
test('quantizes ratios through the byte encoding', () {
|
||||
final quantized = quantizeGearRatio(50 / 11);
|
||||
|
||||
expect(quantized, closeTo(4.55, 0.02));
|
||||
expect(encodeGearRatioByte(quantized), encodeGearRatioByte(50 / 11));
|
||||
});
|
||||
});
|
||||
|
||||
group('calculateGearRatios', () {
|
||||
test('keep all deduplicates and sorts ascending', () {
|
||||
final result = calculateGearRatios(
|
||||
chainrings: [40, 20],
|
||||
sprockets: [20, 10],
|
||||
mode: GearRetentionMode.keepAll,
|
||||
);
|
||||
|
||||
expect(result.ratios, orderedEquals(result.ratios.toList()..sort()));
|
||||
expect(result.ratios.length, 3);
|
||||
expect(result.duplicateCount, 1);
|
||||
});
|
||||
|
||||
test('keep highest drops lower overlapping larger-chainring ratios', () {
|
||||
final result = calculateGearRatios(
|
||||
chainrings: [30, 50],
|
||||
sprockets: [10, 20, 30],
|
||||
mode: GearRetentionMode.keepHighest,
|
||||
);
|
||||
|
||||
expect(result.ratios.first, closeTo(1, 0.02));
|
||||
expect(result.ratios.last, closeTo(5, 0.02));
|
||||
expect(result.ratios.length, 4);
|
||||
expect(result.ratios.where((ratio) => ratio < 1).length, 0);
|
||||
});
|
||||
|
||||
test('discard ratios outside the one-byte range', () {
|
||||
final result = calculateGearRatios(
|
||||
chainrings: [20, 60],
|
||||
sprockets: [5, 100],
|
||||
mode: GearRetentionMode.keepAll,
|
||||
);
|
||||
|
||||
expect(result.discardedBelowRange, 1);
|
||||
expect(result.discardedAboveRange, 1);
|
||||
expect(result.ratios.length, 2);
|
||||
});
|
||||
|
||||
test('truncates to the configured maximum', () {
|
||||
final result = calculateGearRatios(
|
||||
chainrings: [30, 40, 50],
|
||||
sprockets: [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21],
|
||||
mode: GearRetentionMode.keepAll,
|
||||
maxRatios: 8,
|
||||
);
|
||||
|
||||
expect(result.ratios.length, 8);
|
||||
expect(result.truncatedCount, greaterThan(0));
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1,12 +1,11 @@
|
||||
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||
import 'package:cbor/simple.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('CentralStatus.fromCborBytes', () {
|
||||
test('decodes firmware packed status with FTMS ready', () {
|
||||
final status = CentralStatus.fromCborBytes(
|
||||
_packedStatusBytes(
|
||||
group('CentralStatus.fromBytes', () {
|
||||
test('decodes status with FTMS ready', () {
|
||||
final status = CentralStatus.fromBytes(
|
||||
_statusBytes(
|
||||
controlVariant: 1,
|
||||
trainerVariant: 5,
|
||||
hasSavedBond: true,
|
||||
@ -22,7 +21,7 @@ void main() {
|
||||
expect(status.lastFailure, isNull);
|
||||
});
|
||||
|
||||
test('decodes all firmware packed trainer unit variants', () {
|
||||
test('decodes all trainer state variants', () {
|
||||
final expectedStates = <int, TrainerConnectionState>{
|
||||
0: TrainerConnectionState.idle,
|
||||
1: TrainerConnectionState.connecting,
|
||||
@ -34,8 +33,8 @@ void main() {
|
||||
};
|
||||
|
||||
for (final entry in expectedStates.entries) {
|
||||
final status = CentralStatus.fromCborBytes(
|
||||
_packedStatusBytes(
|
||||
final status = CentralStatus.fromBytes(
|
||||
_statusBytes(
|
||||
controlVariant: 1,
|
||||
trainerVariant: entry.key,
|
||||
),
|
||||
@ -49,11 +48,12 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
test('decodes firmware packed trainer error newtype variant', () {
|
||||
final status = CentralStatus.fromCborBytes(
|
||||
_packedStatusBytes(
|
||||
test('decodes trainer error code and last failure', () {
|
||||
final status = CentralStatus.fromBytes(
|
||||
_statusBytes(
|
||||
controlVariant: 1,
|
||||
trainerRaw: [6, errorFtmsRequiredCharMissing],
|
||||
trainerVariant: 6,
|
||||
trainerErrorCode: errorFtmsRequiredCharMissing,
|
||||
lastFailure: errorFtmsRequiredCharMissing,
|
||||
),
|
||||
);
|
||||
@ -63,48 +63,99 @@ void main() {
|
||||
expect(status.lastFailure, errorFtmsRequiredCharMissing);
|
||||
});
|
||||
|
||||
test('decodes non-packed status maps with text keys', () {
|
||||
final status = CentralStatus.fromCborBytes(
|
||||
cbor.encode({
|
||||
'control': 'Connected',
|
||||
'trainer': 'FtmsReady',
|
||||
'has_saved_bond': false,
|
||||
'connected_trainer_addr': [10, 11, 12, 13, 14, 15],
|
||||
'last_failure': null,
|
||||
}),
|
||||
test('omits optional values when flags are absent', () {
|
||||
final status = CentralStatus.fromBytes(
|
||||
_statusBytes(
|
||||
controlVariant: 1,
|
||||
trainerVariant: 5,
|
||||
connectedTrainerAddr: [10, 11, 12, 13, 14, 15],
|
||||
includeAddressFlag: false,
|
||||
lastFailure: errorSequence,
|
||||
includeLastFailureFlag: false,
|
||||
),
|
||||
);
|
||||
|
||||
expect(status.control, ControlConnectionState.connected);
|
||||
expect(status.trainer.state, TrainerConnectionState.ftmsReady);
|
||||
expect(status.connectedTrainerAddr, [10, 11, 12, 13, 14, 15]);
|
||||
expect(status.connectedTrainerAddr, isNull);
|
||||
expect(status.lastFailure, isNull);
|
||||
});
|
||||
|
||||
test('throws for invalid status payloads instead of hiding them', () {
|
||||
expect(
|
||||
() => CentralStatus.fromCborBytes(const []),
|
||||
() => CentralStatus.fromBytes(const []),
|
||||
throwsFormatException,
|
||||
);
|
||||
expect(
|
||||
() => CentralStatus.fromCborBytes(cbor.encode([1, 2, 3])),
|
||||
() => CentralStatus.fromBytes(const [1, 1, 5]),
|
||||
throwsFormatException,
|
||||
);
|
||||
expect(
|
||||
() => CentralStatus.fromBytes(
|
||||
_statusBytes(controlVariant: 1, trainerVariant: 5)..[0] = 2,
|
||||
),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('standard GATT telemetry parsing', () {
|
||||
test('decodes battery level percentage', () {
|
||||
expect(parseBatteryLevelPercent([0]), 0);
|
||||
expect(parseBatteryLevelPercent([87]), 87);
|
||||
expect(parseBatteryLevelPercent([100]), 100);
|
||||
});
|
||||
|
||||
test('rejects invalid battery payloads', () {
|
||||
expect(
|
||||
() => parseBatteryLevelPercent(const []),
|
||||
throwsFormatException,
|
||||
);
|
||||
expect(
|
||||
() => parseBatteryLevelPercent([101]),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
|
||||
test('decodes trimmed firmware revision strings', () {
|
||||
expect(parseGattUtf8String(' 1.2.3 '.codeUnits), '1.2.3');
|
||||
expect(parseGattUtf8String([0x31, 0x2e, 0x32, 0x00]), '1.2');
|
||||
expect(parseGattUtf8String(const []), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
List<int> _packedStatusBytes({
|
||||
List<int> _statusBytes({
|
||||
required int controlVariant,
|
||||
int? trainerVariant,
|
||||
Object? trainerRaw,
|
||||
required int trainerVariant,
|
||||
int trainerErrorCode = 0,
|
||||
bool hasSavedBond = false,
|
||||
List<int>? connectedTrainerAddr,
|
||||
int? lastFailure,
|
||||
bool includeAddressFlag = true,
|
||||
bool includeLastFailureFlag = true,
|
||||
}) {
|
||||
return cbor.encode({
|
||||
0: controlVariant,
|
||||
1: trainerRaw ?? trainerVariant,
|
||||
2: hasSavedBond,
|
||||
3: connectedTrainerAddr,
|
||||
4: lastFailure,
|
||||
});
|
||||
var flags = 0;
|
||||
if (hasSavedBond) {
|
||||
flags |= 0x01;
|
||||
}
|
||||
if (connectedTrainerAddr != null && includeAddressFlag) {
|
||||
flags |= 0x02;
|
||||
}
|
||||
if (lastFailure != null && includeLastFailureFlag) {
|
||||
flags |= 0x04;
|
||||
}
|
||||
|
||||
final payload = List<int>.filled(12, 0, growable: false);
|
||||
payload[0] = 1;
|
||||
payload[1] = controlVariant;
|
||||
payload[2] = trainerVariant;
|
||||
payload[3] = trainerErrorCode;
|
||||
payload[4] = flags;
|
||||
final addr = connectedTrainerAddr;
|
||||
if (addr != null) {
|
||||
payload.setRange(5, 11, addr.take(6));
|
||||
}
|
||||
payload[11] = lastFailure ?? 0;
|
||||
return payload;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user