Initial version
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../models/scooter_device.dart';
|
||||
import '../scooters/apollo_protocol.dart';
|
||||
import 'ble_client.dart';
|
||||
|
||||
/// Fake BLE link that behaves like the Apollo Go captured on 2026-09-21.
|
||||
///
|
||||
/// Accepts any PIN, streams the real base frame and a monitor frame with an
|
||||
/// animated speed and current, and only streams while keepalives arrive, just
|
||||
/// like the physical scooter. Used to preview layouts without hardware.
|
||||
class DemoBleClient extends BleClient {
|
||||
static const device = ScooterDevice(
|
||||
id: 'demo',
|
||||
name: 'Demo Apollo Go',
|
||||
rssi: -50,
|
||||
advertisedServiceUuids: {apolloDataServiceUuid},
|
||||
);
|
||||
|
||||
final _conn = StreamController<BleConnectionState>.broadcast();
|
||||
final _data = StreamController<Uint8List>.broadcast();
|
||||
final _at = StreamController<Uint8List>.broadcast();
|
||||
Timer? _stream;
|
||||
DateTime _lastKeepalive = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
double _t = 0;
|
||||
|
||||
static final _base = Uint8List.fromList(
|
||||
[0xAA, 0x01, 0x19, 0x03, 0x0A, 0x0F, 0x1E, 0xD8, 0x80, 0x00, 0x1F, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x68, 0x61]);
|
||||
|
||||
@override
|
||||
Stream<List<ScooterDevice>> scan() => Stream.value([device]);
|
||||
@override
|
||||
Future<void> stopScan() async {}
|
||||
@override
|
||||
Stream<BleConnectionState> get connectionState => _conn.stream;
|
||||
|
||||
@override
|
||||
Future<void> connect(String deviceId) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||
_stream = Timer.periodic(const Duration(milliseconds: 200), (_) => _tick());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
_stream?.cancel();
|
||||
_stream = null;
|
||||
_conn.add(BleConnectionState.disconnected);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, Set<String>>> discoverServices() async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 400));
|
||||
return {
|
||||
apolloDataServiceUuid: {apolloDataTxUuid, apolloDataRxUuid},
|
||||
apolloAtServiceUuid: {apolloAtTxUuid, apolloAtRxUuid},
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Stream<Uint8List>> subscribe({
|
||||
required String serviceUuid,
|
||||
required String characteristicUuid,
|
||||
}) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 250));
|
||||
return characteristicUuid == apolloDataRxUuid ? _data.stream : _at.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write({
|
||||
required String serviceUuid,
|
||||
required String characteristicUuid,
|
||||
required Uint8List value,
|
||||
}) async {
|
||||
if (characteristicUuid == apolloAtTxUuid) {
|
||||
Future<void>.delayed(const Duration(milliseconds: 150),
|
||||
() => _at.add(Uint8List.fromList('OK+PWD:Y'.codeUnits)));
|
||||
} else if (value.length == 4 && value[0] == 0xA5) {
|
||||
_lastKeepalive = DateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
void _tick() {
|
||||
if (DateTime.now().difference(_lastKeepalive) > const Duration(seconds: 3)) return;
|
||||
_t += 0.2;
|
||||
final speedKmh = 14 + 12 * math.sin(_t / 4); // 2..26 km/h
|
||||
final rawSpeed = (speedKmh * 10).round();
|
||||
final currentA = 3 + 8 * math.max(0, math.cos(_t / 4)) - (math.sin(_t / 2) < -0.8 ? 6 : 0);
|
||||
final rawCurrent = (currentA * 64).round() & 0xFFFF;
|
||||
final battery = 78;
|
||||
final flagsA = 0x0E | (math.sin(_t / 6) > 0 ? 0x80 : 0); // headlight blinks slowly
|
||||
final frame = Uint8List.fromList([
|
||||
0xAA, 0x00, 0x19, 0x01, 0x02, battery,
|
||||
rawSpeed >> 8, rawSpeed & 0xFF, 0, 0,
|
||||
0x01, 0x9F, rawCurrent >> 8, rawCurrent & 0xFF,
|
||||
0x16 + (speedKmh / 10).round(), 0x19,
|
||||
0x00, 0x7B, 0x00, 0x0C, 0xC5, flagsA, 0x22, 0, 0,
|
||||
]);
|
||||
final crc = apolloCrc16(frame.sublist(0, 23));
|
||||
frame[23] = crc & 0xFF;
|
||||
frame[24] = crc >> 8;
|
||||
// Mimic the real 20 + 5 byte notification split.
|
||||
_data.add(frame.sublist(0, 20));
|
||||
_data.add(frame.sublist(20));
|
||||
_data.add(_base.sublist(0, 20));
|
||||
_data.add(_base.sublist(20));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user