feat: revamp sensor add flow
This commit is contained in:
@@ -1,11 +1,20 @@
|
||||
import '../../domain/models/sensor.dart';
|
||||
import '../sources/localiser/realtime_data_client.dart';
|
||||
import '../sources/localiser/sensor_client.dart';
|
||||
import 'sensor_repository.dart';
|
||||
|
||||
class PhoenixSensorRepository implements SensorRepository {
|
||||
const PhoenixSensorRepository({required this.client});
|
||||
const PhoenixSensorRepository({
|
||||
required this.client,
|
||||
required this.realtime,
|
||||
});
|
||||
|
||||
final SensorClient client;
|
||||
final RealtimeDataClient realtime;
|
||||
|
||||
@override
|
||||
Future<Sensor> createSensor(String sensorId, {String? name}) async =>
|
||||
Sensor.fromJson(await client.createSensor(sensorId, name: name));
|
||||
|
||||
@override
|
||||
Future<List<Sensor>> getSensors() async {
|
||||
@@ -35,11 +44,16 @@ class PhoenixSensorRepository implements SensorRepository {
|
||||
|
||||
@override
|
||||
Future<Sensor> placeSensor(int id,
|
||||
{required int roomId, required double x, required double y}) async =>
|
||||
{required int roomId, required double x, required double y}) async =>
|
||||
Sensor.fromJson(
|
||||
await client.placeSensor(id, {'room_id': roomId, 'x': x, 'y': y}));
|
||||
|
||||
@override
|
||||
Future<Sensor> unplaceSensor(int id) async =>
|
||||
Sensor.fromJson(await client.unplaceSensor(id));
|
||||
|
||||
@override
|
||||
Stream<Map<String, dynamic>> sensorEvents() => realtime
|
||||
.channelMessages('sensors')
|
||||
.map((m) => {'event': m.event, ...m.payload});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import '../../domain/models/sensor.dart';
|
||||
|
||||
abstract class SensorRepository {
|
||||
Future<Sensor> createSensor(String sensorId, {String? name});
|
||||
Future<List<Sensor>> getSensors();
|
||||
Future<List<Sensor>> getUnplacedSensors();
|
||||
Future<Sensor> getSensor(int id);
|
||||
@@ -9,4 +10,8 @@ abstract class SensorRepository {
|
||||
Future<Sensor> placeSensor(int id,
|
||||
{required int roomId, required double x, required double y});
|
||||
Future<Sensor> unplaceSensor(int id);
|
||||
|
||||
/// Stream of raw SensorsChannel messages. Each map contains an `event` key
|
||||
/// (`sensor_announced` or `sensor_enrollment_timeout`) plus the payload.
|
||||
Stream<Map<String, dynamic>> sensorEvents();
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user