feat: revamp sensor add flow

This commit is contained in:
2026-05-16 12:00:55 +02:00
parent be6ac42059
commit f37176cce5
12 changed files with 478 additions and 186 deletions
+52 -17
View File
@@ -31,26 +31,61 @@ class BleProvisioner {
BluetoothDevice? _connectedDevice;
Stream<BleScanResult> scan() async* {
await _requestScanPermissions();
// Continuously scans for nearby ESP32 sensors, restarting after each
// 15-second window, until the returned stream is cancelled.
Stream<BleScanResult> scan() {
StreamSubscription<List<ScanResult>>? resultsSub;
StreamSubscription<bool>? stateSub;
bool started = false;
late StreamController<BleScanResult> controller;
await FlutterBluePlus.startScan(
withServices: [Guid(_serviceUuid)],
timeout: const Duration(seconds: 30),
Future<void> startScan() async {
if (controller.isClosed) return;
started = true;
try {
await FlutterBluePlus.startScan(
withServices: [Guid(_serviceUuid)],
);
} catch (_) {}
}
controller = StreamController<BleScanResult>(
onListen: () async {
try {
await _requestScanPermissions();
} catch (e) {
controller.addError(e);
await controller.close();
return;
}
resultsSub = FlutterBluePlus.scanResults.listen((results) {
for (final r in results) {
final name = r.device.platformName;
if (name.startsWith('anchor_') && !controller.isClosed) {
controller.add(BleScanResult(
deviceId: r.device.remoteId.str,
name: name,
rssi: r.rssi,
));
}
}
});
if(!FlutterBluePlus.isScanningNow) {
await startScan();
}
},
onCancel: () {
resultsSub?.cancel();
stateSub?.cancel();
FlutterBluePlus.stopScan();
controller.close();
},
);
await for (final results in FlutterBluePlus.scanResults) {
for (final r in results) {
final name = r.device.platformName;
if (name.startsWith('anchor_')) {
yield BleScanResult(
deviceId: r.device.remoteId.str,
name: name,
rssi: r.rssi,
);
}
}
}
return controller.stream;
}
Future<void> stopScan() => FlutterBluePlus.stopScan();
@@ -56,6 +56,28 @@ class RealtimeDataClient {
.map((msg) => msg.payload ?? const {});
}
/// Like [channel], but includes the Phoenix event name in each emission.
Stream<({String event, Map<String, dynamic> payload})> channelMessages(
String topic, {
Map<String, dynamic> params = const {},
}) {
final socket = _socket;
if (socket == null) throw StateError('RealtimeDataClient not connected');
final ch = _channels.putIfAbsent(
topic,
() {
final c = socket.addChannel(topic: topic, parameters: params);
c.join();
return c;
},
);
return ch.messages
.where((msg) => msg.event.value != 'phx_reply')
.map((msg) => (event: msg.event.value, payload: msg.payload ?? const {}));
}
/// Pushes [event] on [topic] and waits for the server reply.
/// The channel must have been joined first via [channel].
Future<Map<String, dynamic>> push(
@@ -25,6 +25,13 @@ class SensorClient extends LocaliserdClient {
Future<Map<String, dynamic>> unplaceSensor(int id) async =>
await deleteBody('/api/sensors/$id/place') as Map<String, dynamic>;
Future<Map<String, dynamic>> createSensor(String sensorId,
{String? name}) async =>
await post('/api/sensors', {
'sensor_id': sensorId,
if (name != null) 'name': name,
}) as Map<String, dynamic>;
Future<Map<String, dynamic>> startCalibration(
int id, double referenceDistance) async =>
await post('/api/sensors/$id/calibration/start',