device infos
This commit is contained in:
133
lib/data/device_telemetry.dart
Normal file
133
lib/data/device_telemetry.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// WHAT THE CONSOLE NEEDS TO SEE ABOUT THE HANDSET
|
||||
///
|
||||
/// The dispatcher watching a rider is not only asking *where is he*. When a
|
||||
/// rider stops reporting, the question is **why** — and the answers are all
|
||||
/// facts about the phone: the battery died, the signal dropped, he turned
|
||||
/// location off, the app went to the background and the OS stopped waking it.
|
||||
/// `POST /miler/logs` has fields for every one of them:
|
||||
///
|
||||
/// ```
|
||||
/// battery is_charging connection location_service accuracy is_background
|
||||
/// ```
|
||||
///
|
||||
/// ── Why the console was showing an em dash for all of it ──
|
||||
///
|
||||
/// Not because nothing was measured. Every one of these was already being read
|
||||
/// on the heartbeat — and published to **MQTT only**, in a block that ran
|
||||
/// *after* the log had been posted. So the telemetry existed, reached a
|
||||
/// different transport, and the rider-log row the console actually reads
|
||||
/// carried nothing but a position.
|
||||
///
|
||||
/// It was two lanes for one fact, and the lane the console reads was the empty
|
||||
/// one. This is the reader both now share: one place that knows how to ask the
|
||||
/// handset, so the MQTT publish and the log post cannot report different
|
||||
/// batteries a second apart.
|
||||
///
|
||||
/// ── Never throws, never blocks ──
|
||||
///
|
||||
/// Telemetry is the least important thing the app does. A permissions prompt, a
|
||||
/// platform channel that hangs, a plugin missing on a desktop test host — none
|
||||
/// of them may cost the rider his heartbeat, so every read is guarded
|
||||
/// individually and an unavailable one simply does not appear in the map.
|
||||
/// A field that is absent renders as the em dash it already rendered as; a
|
||||
/// field that is *wrong* would be read as fact by somebody making a decision.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
class DeviceTelemetry {
|
||||
/// Battery percentage, or null when the platform will not say.
|
||||
final int? battery;
|
||||
|
||||
/// True while on the charger — a rider at 12% and charging is not the same
|
||||
/// problem as a rider at 12% and riding.
|
||||
final bool? isCharging;
|
||||
|
||||
/// `wifi`, `mobile`, `none` … as the platform names it.
|
||||
final String? connection;
|
||||
|
||||
/// `enabled`, `disabled`, `denied`, `denied_forever`, `unknown`.
|
||||
///
|
||||
/// The one field here that is about a *choice the rider made*, which is why
|
||||
/// it is worth a column of its own on the console: a disabled location
|
||||
/// service explains a silent rider completely, and no amount of staring at
|
||||
/// his last position does.
|
||||
final String? locationService;
|
||||
|
||||
const DeviceTelemetry({
|
||||
this.battery,
|
||||
this.isCharging,
|
||||
this.connection,
|
||||
this.locationService,
|
||||
});
|
||||
|
||||
/// Reads what the handset will say about itself, right now.
|
||||
///
|
||||
/// [locationService] can be supplied by a caller that has just resolved a
|
||||
/// fix and already knows — the position lookup has to ask the same question
|
||||
/// to get a fix at all, and asking twice can disagree with itself when the
|
||||
/// rider toggles the setting between the two calls.
|
||||
static Future<DeviceTelemetry> read({String? locationService}) async {
|
||||
int? level;
|
||||
bool? charging;
|
||||
try {
|
||||
final battery = Battery();
|
||||
level = await battery.batteryLevel;
|
||||
final state = await battery.batteryState;
|
||||
charging =
|
||||
state == BatteryState.charging || state == BatteryState.full;
|
||||
} catch (e) {
|
||||
debugPrint('[TELEMETRY] battery unavailable: $e');
|
||||
}
|
||||
|
||||
String? connection;
|
||||
try {
|
||||
final result = await Connectivity().checkConnectivity();
|
||||
connection = result.isNotEmpty
|
||||
? result.first.toString().split('.').last
|
||||
: 'none';
|
||||
} catch (e) {
|
||||
debugPrint('[TELEMETRY] connectivity unavailable: $e');
|
||||
}
|
||||
|
||||
var service = locationService;
|
||||
if (service == null || service.isEmpty || service == 'unknown') {
|
||||
try {
|
||||
service = await Geolocator.isLocationServiceEnabled()
|
||||
? 'enabled'
|
||||
: 'disabled';
|
||||
} catch (e) {
|
||||
debugPrint('[TELEMETRY] location service unavailable: $e');
|
||||
service = null;
|
||||
}
|
||||
}
|
||||
|
||||
return DeviceTelemetry(
|
||||
battery: level,
|
||||
isCharging: charging,
|
||||
connection: connection,
|
||||
locationService: service,
|
||||
);
|
||||
}
|
||||
|
||||
/// The heartbeat payload's own field names, ready to spread into it.
|
||||
///
|
||||
/// Only what was actually read: an absent key is a field the console draws as
|
||||
/// unknown, which is the truth. A `0` battery or a `none` connection standing
|
||||
/// in for "could not ask" is a fact somebody would act on.
|
||||
Map<String, dynamic> toPayload() => <String, dynamic>{
|
||||
if (battery != null) 'battery': '$battery',
|
||||
if (isCharging != null) 'is_charging': isCharging,
|
||||
if (connection != null && connection!.isNotEmpty) 'connection': connection,
|
||||
if (locationService != null && locationService!.isNotEmpty)
|
||||
'location_service': locationService,
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'battery=$battery charging=$isCharging '
|
||||
'connection=$connection location=$locationService';
|
||||
}
|
||||
Reference in New Issue
Block a user