mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
Update sensor parsing layout
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
import '../widgets/sensors/sensor_telemetry_card.dart';
|
||||||
import 'connection_provider.dart';
|
import 'connection_provider.dart';
|
||||||
import 'contacts_provider.dart';
|
import 'contacts_provider.dart';
|
||||||
|
|
||||||
@@ -223,6 +224,22 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
_visibleFieldsBySensor[publicKeyHex] ?? _defaultVisibleFields,
|
_visibleFieldsBySensor[publicKeyHex] ?? _defaultVisibleFields,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Set<String> effectiveVisibleFieldsFor(
|
||||||
|
String publicKeyHex,
|
||||||
|
Iterable<String> availableFieldKeys,
|
||||||
|
) {
|
||||||
|
final storedVisibleFields =
|
||||||
|
_visibleFieldsBySensor[publicKeyHex] ?? _defaultVisibleFields;
|
||||||
|
if (!_shouldAutoIncludeAvailableFields(publicKeyHex, storedVisibleFields)) {
|
||||||
|
return Set<String>.unmodifiable(storedVisibleFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Set<String>.unmodifiable(<String>{
|
||||||
|
...storedVisibleFields,
|
||||||
|
...availableFieldKeys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bool showsField(String publicKeyHex, String fieldKey) =>
|
bool showsField(String publicKeyHex, String fieldKey) =>
|
||||||
visibleFieldsFor(publicKeyHex).contains(fieldKey);
|
visibleFieldsFor(publicKeyHex).contains(fieldKey);
|
||||||
|
|
||||||
@@ -440,14 +457,14 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_watchedSensorKeys.add(contact.publicKeyHex);
|
_watchedSensorKeys.add(contact.publicKeyHex);
|
||||||
|
final initialVisibleFields = _initialVisibleFieldsForContact(contact);
|
||||||
await _persistWatchedSensors();
|
await _persistWatchedSensors();
|
||||||
_visibleFieldsBySensor[contact.publicKeyHex] = Set<String>.from(
|
_visibleFieldsBySensor[contact.publicKeyHex] = initialVisibleFields;
|
||||||
_defaultVisibleFields,
|
|
||||||
);
|
|
||||||
_fieldSpansBySensor[contact.publicKeyHex] = <String, int>{'gps': 2};
|
_fieldSpansBySensor[contact.publicKeyHex] = <String, int>{'gps': 2};
|
||||||
_metricLabelsBySensor[contact.publicKeyHex] = <String, String>{};
|
_metricLabelsBySensor[contact.publicKeyHex] = <String, String>{};
|
||||||
_metricOrderBySensor[contact.publicKeyHex] = List<String>.from(
|
_metricOrderBySensor[contact.publicKeyHex] = metricOrderFor(
|
||||||
_defaultMetricOrder,
|
contact.publicKeyHex,
|
||||||
|
initialVisibleFields,
|
||||||
);
|
);
|
||||||
await _persistVisibleMetrics();
|
await _persistVisibleMetrics();
|
||||||
await _persistFieldSpans();
|
await _persistFieldSpans();
|
||||||
@@ -456,6 +473,37 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _shouldAutoIncludeAvailableFields(
|
||||||
|
String publicKeyHex,
|
||||||
|
Set<String> storedVisibleFields,
|
||||||
|
) {
|
||||||
|
if (!setEquals(storedVisibleFields, _defaultVisibleFields)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final storedOrder = _metricOrderBySensor[publicKeyHex];
|
||||||
|
if (storedOrder != null && !listEquals(storedOrder, _defaultMetricOrder)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final storedLabels = _metricLabelsBySensor[publicKeyHex];
|
||||||
|
if (storedLabels != null && storedLabels.isNotEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final storedSpans = _fieldSpansBySensor[publicKeyHex];
|
||||||
|
if (storedSpans != null &&
|
||||||
|
!(storedSpans.length == 1 && storedSpans['gps'] == 2)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> _initialVisibleFieldsForContact(Contact contact) {
|
||||||
|
return <String>{..._defaultVisibleFields, ...sensorMetricKeysFor(contact)};
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> removeSensor(String publicKeyHex) async {
|
Future<void> removeSensor(String publicKeyHex) async {
|
||||||
_watchedSensorKeys.remove(publicKeyHex);
|
_watchedSensorKeys.remove(publicKeyHex);
|
||||||
_refreshStates.remove(publicKeyHex);
|
_refreshStates.remove(publicKeyHex);
|
||||||
|
|||||||
@@ -294,19 +294,20 @@ class _SensorsTabState extends State<SensorsTab> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
final availableFieldKeys = sensorMetricKeysFor(contact);
|
||||||
|
final visibleFields = sensorsProvider
|
||||||
|
.effectiveVisibleFieldsFor(key, availableFieldKeys);
|
||||||
return SensorTelemetryCard(
|
return SensorTelemetryCard(
|
||||||
contact: contact,
|
contact: contact,
|
||||||
state: sensorsProvider.stateFor(key),
|
state: sensorsProvider.stateFor(key),
|
||||||
visibleFields: sensorsProvider.visibleFieldsFor(key),
|
visibleFields: visibleFields,
|
||||||
fieldOrder: sensorsProvider.metricOrderFor(
|
fieldOrder: sensorsProvider.metricOrderFor(
|
||||||
key,
|
key,
|
||||||
sensorsProvider.visibleFieldsFor(key),
|
availableFieldKeys,
|
||||||
),
|
),
|
||||||
labelOverrides: sensorsProvider.labelOverridesFor(key),
|
labelOverrides: sensorsProvider.labelOverridesFor(key),
|
||||||
fieldSpans: {
|
fieldSpans: {
|
||||||
for (final field in sensorsProvider.visibleFieldsFor(
|
for (final field in visibleFields)
|
||||||
key,
|
|
||||||
))
|
|
||||||
field: sensorsProvider.fieldSpanFor(key, field),
|
field: sensorsProvider.fieldSpanFor(key, field),
|
||||||
},
|
},
|
||||||
onRemove: () async {
|
onRemove: () async {
|
||||||
@@ -359,14 +360,17 @@ class _SensorCustomizeView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final visibleFields = sensorsProvider.visibleFieldsFor(publicKeyHex);
|
|
||||||
final autoRefreshMinutes = sensorsProvider.autoRefreshMinutesFor(
|
|
||||||
publicKeyHex,
|
|
||||||
);
|
|
||||||
final options = sensorMetricOptionsFor(
|
final options = sensorMetricOptionsFor(
|
||||||
contact,
|
contact,
|
||||||
labelOverrides: sensorsProvider.labelOverridesFor(publicKeyHex),
|
labelOverrides: sensorsProvider.labelOverridesFor(publicKeyHex),
|
||||||
);
|
);
|
||||||
|
final visibleFields = sensorsProvider.effectiveVisibleFieldsFor(
|
||||||
|
publicKeyHex,
|
||||||
|
options.map((option) => option.key),
|
||||||
|
);
|
||||||
|
final autoRefreshMinutes = sensorsProvider.autoRefreshMinutesFor(
|
||||||
|
publicKeyHex,
|
||||||
|
);
|
||||||
final orderedFieldKeys = sensorsProvider.metricOrderFor(
|
final orderedFieldKeys = sensorsProvider.metricOrderFor(
|
||||||
publicKeyHex,
|
publicKeyHex,
|
||||||
options.map((option) => option.key),
|
options.map((option) => option.key),
|
||||||
|
|||||||
@@ -19,7 +19,61 @@ class CayenneLppParser {
|
|||||||
static const int _lppDirection = 132;
|
static const int _lppDirection = 132;
|
||||||
static const int _lppUnixTime = 133;
|
static const int _lppUnixTime = 133;
|
||||||
static const int _lppColour = 135;
|
static const int _lppColour = 135;
|
||||||
|
static const int _lppGust = 137;
|
||||||
|
static const int _lppDewPoint = 138;
|
||||||
|
static const int _lppRain = 139;
|
||||||
static const int _lppSwitch = 142;
|
static const int _lppSwitch = 142;
|
||||||
|
static const int _lppBinaryBool = 143;
|
||||||
|
static const int _lppBinaryPowerSwitch = 144;
|
||||||
|
static const int _lppBinaryOpen = 145;
|
||||||
|
static const int _lppBinaryBatteryLow = 146;
|
||||||
|
static const int _lppBinaryCharging = 147;
|
||||||
|
static const int _lppBinaryCarbonMonoxide = 148;
|
||||||
|
static const int _lppBinaryCold = 149;
|
||||||
|
static const int _lppBinaryConnectivity = 150;
|
||||||
|
static const int _lppBinaryDoor = 151;
|
||||||
|
static const int _lppBinaryGarageDoor = 152;
|
||||||
|
static const int _lppBinaryGas = 153;
|
||||||
|
static const int _lppBinaryHeat = 154;
|
||||||
|
static const int _lppBinaryLight = 155;
|
||||||
|
static const int _lppBinaryLock = 156;
|
||||||
|
static const int _lppBinaryMoisture = 157;
|
||||||
|
static const int _lppBinaryMotion = 158;
|
||||||
|
static const int _lppBinaryMoving = 159;
|
||||||
|
static const int _lppBinaryOccupancy = 160;
|
||||||
|
static const int _lppBinaryPlug = 161;
|
||||||
|
static const int _lppBinaryPresence = 162;
|
||||||
|
static const int _lppBinaryProblem = 163;
|
||||||
|
static const int _lppBinaryRunning = 164;
|
||||||
|
static const int _lppBinarySafety = 165;
|
||||||
|
static const int _lppBinarySmoke = 166;
|
||||||
|
static const int _lppBinarySound = 167;
|
||||||
|
static const int _lppBinaryTamper = 168;
|
||||||
|
static const int _lppBinaryVibration = 169;
|
||||||
|
static const int _lppBinaryWindow = 170;
|
||||||
|
static const int _lppButtonEvent = 171;
|
||||||
|
static const int _lppDimmer = 172;
|
||||||
|
static const int _lppUv = 173;
|
||||||
|
static const int _lppLightLevel = 174;
|
||||||
|
static const int _lppPm25 = 175;
|
||||||
|
static const int _lppPm10 = 176;
|
||||||
|
static const int _lppCo2 = 177;
|
||||||
|
static const int _lppTvoc = 178;
|
||||||
|
static const int _lppRpm = 179;
|
||||||
|
static const int _lppConductivity = 180;
|
||||||
|
static const int _lppRotation = 181;
|
||||||
|
static const int _lppDuration = 182;
|
||||||
|
static const int _lppAcceleration = 183;
|
||||||
|
static const int _lppGyroRate = 184;
|
||||||
|
static const int _lppVolume = 185;
|
||||||
|
static const int _lppFlowRate = 186;
|
||||||
|
static const int _lppVolumeStorage = 187;
|
||||||
|
static const int _lppWater = 188;
|
||||||
|
static const int _lppGasVolume = 189;
|
||||||
|
static const int _lppMass = 190;
|
||||||
|
static const int _lppSignedSpeed = 191;
|
||||||
|
static const int _lppSignedPower = 192;
|
||||||
|
static const int _lppSignedCurrent = 193;
|
||||||
|
|
||||||
/// Parse Cayenne LPP data into ContactTelemetry
|
/// Parse Cayenne LPP data into ContactTelemetry
|
||||||
static ContactTelemetry parse(Uint8List data) {
|
static ContactTelemetry parse(Uint8List data) {
|
||||||
@@ -345,6 +399,212 @@ class CayenneLppParser {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case _lppGust:
|
||||||
|
final rawValue = reader.readUInt16BE();
|
||||||
|
final value = rawValue / 100.0;
|
||||||
|
debugPrint(' Gust: ${value}m/s');
|
||||||
|
extraSensorData['gust_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppDewPoint:
|
||||||
|
final rawValue = reader.readInt16BE();
|
||||||
|
final value = rawValue / 10.0;
|
||||||
|
debugPrint(' Dew point: ${value.toStringAsFixed(1)}°C');
|
||||||
|
extraSensorData['dew_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppRain:
|
||||||
|
final rawValue = reader.readUInt16BE();
|
||||||
|
final value = rawValue / 10.0;
|
||||||
|
debugPrint(' Rain: ${value}mm');
|
||||||
|
extraSensorData['rain_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppBinaryBool:
|
||||||
|
case _lppBinaryPowerSwitch:
|
||||||
|
case _lppBinaryOpen:
|
||||||
|
case _lppBinaryBatteryLow:
|
||||||
|
case _lppBinaryCharging:
|
||||||
|
case _lppBinaryCarbonMonoxide:
|
||||||
|
case _lppBinaryCold:
|
||||||
|
case _lppBinaryConnectivity:
|
||||||
|
case _lppBinaryDoor:
|
||||||
|
case _lppBinaryGarageDoor:
|
||||||
|
case _lppBinaryGas:
|
||||||
|
case _lppBinaryHeat:
|
||||||
|
case _lppBinaryLight:
|
||||||
|
case _lppBinaryLock:
|
||||||
|
case _lppBinaryMoisture:
|
||||||
|
case _lppBinaryMotion:
|
||||||
|
case _lppBinaryMoving:
|
||||||
|
case _lppBinaryOccupancy:
|
||||||
|
case _lppBinaryPlug:
|
||||||
|
case _lppBinaryPresence:
|
||||||
|
case _lppBinaryProblem:
|
||||||
|
case _lppBinaryRunning:
|
||||||
|
case _lppBinarySafety:
|
||||||
|
case _lppBinarySmoke:
|
||||||
|
case _lppBinarySound:
|
||||||
|
case _lppBinaryTamper:
|
||||||
|
case _lppBinaryVibration:
|
||||||
|
case _lppBinaryWindow:
|
||||||
|
final value = reader.readByte();
|
||||||
|
debugPrint(' Binary state: $value');
|
||||||
|
extraSensorData['${_binaryMetricKeyForType(type)}_$channel'] =
|
||||||
|
value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppButtonEvent:
|
||||||
|
final value = reader.readByte();
|
||||||
|
debugPrint(' Button event: $value');
|
||||||
|
extraSensorData['button_event_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppDimmer:
|
||||||
|
final value = _readInt8(reader);
|
||||||
|
debugPrint(' Dimmer: $value');
|
||||||
|
extraSensorData['dimmer_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppUv:
|
||||||
|
final value = reader.readByte() / 10.0;
|
||||||
|
debugPrint(' UV index: $value');
|
||||||
|
extraSensorData['uv_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppLightLevel:
|
||||||
|
final value = reader.readByte();
|
||||||
|
debugPrint(' Light level: $value');
|
||||||
|
extraSensorData['light_level_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppPm25:
|
||||||
|
final value = reader.readUInt16BE().toDouble();
|
||||||
|
debugPrint(' PM2.5: $value');
|
||||||
|
extraSensorData['pm25_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppPm10:
|
||||||
|
final value = reader.readUInt16BE().toDouble();
|
||||||
|
debugPrint(' PM10: $value');
|
||||||
|
extraSensorData['pm10_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppCo2:
|
||||||
|
final value = reader.readUInt16BE().toDouble();
|
||||||
|
debugPrint(' CO2: $value');
|
||||||
|
extraSensorData['co2_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppTvoc:
|
||||||
|
final value = reader.readUInt16BE().toDouble();
|
||||||
|
debugPrint(' TVOC: $value');
|
||||||
|
extraSensorData['tvoc_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppRpm:
|
||||||
|
final value = reader.readUInt16BE().toDouble();
|
||||||
|
debugPrint(' RPM: $value');
|
||||||
|
extraSensorData['rpm_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppConductivity:
|
||||||
|
final value = reader.readUInt16BE().toDouble();
|
||||||
|
debugPrint(' Conductivity: $value');
|
||||||
|
extraSensorData['conductivity_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppRotation:
|
||||||
|
final rawValue = reader.readInt16BE();
|
||||||
|
final value = rawValue / 10.0;
|
||||||
|
debugPrint(' Rotation: $value');
|
||||||
|
extraSensorData['rotation_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppDuration:
|
||||||
|
final rawValue = _readUInt32BE(reader);
|
||||||
|
final value = rawValue / 1000.0;
|
||||||
|
debugPrint(' Duration: $value s');
|
||||||
|
extraSensorData['duration_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppAcceleration:
|
||||||
|
final rawValue = _readInt32BE(reader);
|
||||||
|
final value = rawValue / 1000000.0;
|
||||||
|
debugPrint(' Acceleration: $value');
|
||||||
|
extraSensorData['acceleration_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppGyroRate:
|
||||||
|
final rawValue = _readInt32BE(reader);
|
||||||
|
final value = rawValue / 1000.0;
|
||||||
|
debugPrint(' Gyro rate: $value');
|
||||||
|
extraSensorData['gyro_rate_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppVolume:
|
||||||
|
final rawValue = _readUInt32BE(reader);
|
||||||
|
final value = rawValue / 1000.0;
|
||||||
|
debugPrint(' Volume: $value');
|
||||||
|
extraSensorData['volume_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppFlowRate:
|
||||||
|
final rawValue = _readUInt32BE(reader);
|
||||||
|
final value = rawValue / 1000.0;
|
||||||
|
debugPrint(' Flow rate: $value');
|
||||||
|
extraSensorData['flow_rate_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppVolumeStorage:
|
||||||
|
final rawValue = _readUInt32BE(reader);
|
||||||
|
final value = rawValue / 1000.0;
|
||||||
|
debugPrint(' Storage volume: $value');
|
||||||
|
extraSensorData['volume_storage_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppWater:
|
||||||
|
final rawValue = _readUInt32BE(reader);
|
||||||
|
final value = rawValue / 1000.0;
|
||||||
|
debugPrint(' Water: $value');
|
||||||
|
extraSensorData['water_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppGasVolume:
|
||||||
|
final rawValue = _readUInt32BE(reader);
|
||||||
|
final value = rawValue / 1000.0;
|
||||||
|
debugPrint(' Gas volume: $value');
|
||||||
|
extraSensorData['gas_volume_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppMass:
|
||||||
|
final rawValue = _readUInt32BE(reader);
|
||||||
|
final value = rawValue / 1000.0;
|
||||||
|
debugPrint(' Mass: $value');
|
||||||
|
extraSensorData['mass_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppSignedSpeed:
|
||||||
|
final rawValue = _readInt32BE(reader);
|
||||||
|
final value = rawValue / 1000000.0;
|
||||||
|
debugPrint(' Signed speed: $value');
|
||||||
|
extraSensorData['signed_speed_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppSignedPower:
|
||||||
|
final rawValue = _readInt32BE(reader);
|
||||||
|
final value = rawValue / 100.0;
|
||||||
|
debugPrint(' Signed power: $value');
|
||||||
|
extraSensorData['signed_power_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case _lppSignedCurrent:
|
||||||
|
final rawValue = _readInt32BE(reader);
|
||||||
|
final value = rawValue / 1000.0;
|
||||||
|
debugPrint(' Signed current: $value');
|
||||||
|
extraSensorData['signed_current_$channel'] = value;
|
||||||
|
break;
|
||||||
|
|
||||||
case _lppSwitch:
|
case _lppSwitch:
|
||||||
final value = reader.readByte();
|
final value = reader.readByte();
|
||||||
debugPrint(' Switch: $value');
|
debugPrint(' Switch: $value');
|
||||||
@@ -352,13 +612,18 @@ class CayenneLppParser {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
final size = _payloadSizeForType(type);
|
||||||
|
if (size == null || reader.remainingBytesCount < size) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
' ⚠️ Unknown type, skipping remaining ${reader.remainingBytesCount} bytes',
|
' ⚠️ Unknown type $type with unsupported size, stopping parse',
|
||||||
);
|
);
|
||||||
// Unknown type, skip remaining to avoid parsing errors
|
|
||||||
reader.skip(reader.remainingBytesCount);
|
reader.skip(reader.remainingBytesCount);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
debugPrint(' ⚠️ Unknown type $type, skipping $size bytes');
|
||||||
|
reader.skip(size);
|
||||||
|
break;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint(' ❌ Parsing error: $e');
|
debugPrint(' ❌ Parsing error: $e');
|
||||||
// If we encounter a parsing error, break and return what we have
|
// If we encounter a parsing error, break and return what we have
|
||||||
@@ -420,7 +685,154 @@ class CayenneLppParser {
|
|||||||
return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
|
return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
static String _sourceChannelKey(String fieldKey) => '__source_channel:$fieldKey';
|
static int _readInt32BE(BufferReader reader) {
|
||||||
|
final value = _readUInt32BE(reader);
|
||||||
|
if ((value & 0x80000000) != 0) {
|
||||||
|
return value - 0x100000000;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _readInt8(BufferReader reader) {
|
||||||
|
final value = reader.readByte();
|
||||||
|
if ((value & 0x80) != 0) {
|
||||||
|
return value - 0x100;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _sourceChannelKey(String fieldKey) =>
|
||||||
|
'__source_channel:$fieldKey';
|
||||||
|
|
||||||
|
static String _binaryMetricKeyForType(int type) {
|
||||||
|
switch (type) {
|
||||||
|
case _lppBinaryBool:
|
||||||
|
return 'binary_bool';
|
||||||
|
case _lppBinaryPowerSwitch:
|
||||||
|
return 'binary_power_switch';
|
||||||
|
case _lppBinaryOpen:
|
||||||
|
return 'binary_open';
|
||||||
|
case _lppBinaryBatteryLow:
|
||||||
|
return 'binary_battery_low';
|
||||||
|
case _lppBinaryCharging:
|
||||||
|
return 'binary_charging';
|
||||||
|
case _lppBinaryCarbonMonoxide:
|
||||||
|
return 'binary_carbon_monoxide';
|
||||||
|
case _lppBinaryCold:
|
||||||
|
return 'binary_cold';
|
||||||
|
case _lppBinaryConnectivity:
|
||||||
|
return 'binary_connectivity';
|
||||||
|
case _lppBinaryDoor:
|
||||||
|
return 'binary_door';
|
||||||
|
case _lppBinaryGarageDoor:
|
||||||
|
return 'binary_garage_door';
|
||||||
|
case _lppBinaryGas:
|
||||||
|
return 'binary_gas';
|
||||||
|
case _lppBinaryHeat:
|
||||||
|
return 'binary_heat';
|
||||||
|
case _lppBinaryLight:
|
||||||
|
return 'binary_light';
|
||||||
|
case _lppBinaryLock:
|
||||||
|
return 'binary_lock';
|
||||||
|
case _lppBinaryMoisture:
|
||||||
|
return 'binary_moisture';
|
||||||
|
case _lppBinaryMotion:
|
||||||
|
return 'binary_motion';
|
||||||
|
case _lppBinaryMoving:
|
||||||
|
return 'binary_moving';
|
||||||
|
case _lppBinaryOccupancy:
|
||||||
|
return 'binary_occupancy';
|
||||||
|
case _lppBinaryPlug:
|
||||||
|
return 'binary_plug';
|
||||||
|
case _lppBinaryPresence:
|
||||||
|
return 'binary_presence';
|
||||||
|
case _lppBinaryProblem:
|
||||||
|
return 'binary_problem';
|
||||||
|
case _lppBinaryRunning:
|
||||||
|
return 'binary_running';
|
||||||
|
case _lppBinarySafety:
|
||||||
|
return 'binary_safety';
|
||||||
|
case _lppBinarySmoke:
|
||||||
|
return 'binary_smoke';
|
||||||
|
case _lppBinarySound:
|
||||||
|
return 'binary_sound';
|
||||||
|
case _lppBinaryTamper:
|
||||||
|
return 'binary_tamper';
|
||||||
|
case _lppBinaryVibration:
|
||||||
|
return 'binary_vibration';
|
||||||
|
case _lppBinaryWindow:
|
||||||
|
return 'binary_window';
|
||||||
|
}
|
||||||
|
return 'binary_state';
|
||||||
|
}
|
||||||
|
|
||||||
|
static int? _payloadSizeForType(int type) {
|
||||||
|
if (type >= _lppBinaryBool && type <= _lppBinaryWindow) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case MeshCoreConstants.lppDigitalInput:
|
||||||
|
case MeshCoreConstants.lppDigitalOutput:
|
||||||
|
case MeshCoreConstants.lppPresenceSensor:
|
||||||
|
case MeshCoreConstants.lppHumiditySensor:
|
||||||
|
case _lppPercentage:
|
||||||
|
case _lppSwitch:
|
||||||
|
case _lppButtonEvent:
|
||||||
|
case _lppDimmer:
|
||||||
|
case _lppUv:
|
||||||
|
case _lppLightLevel:
|
||||||
|
return 1;
|
||||||
|
case MeshCoreConstants.lppAnalogInput:
|
||||||
|
case MeshCoreConstants.lppAnalogOutput:
|
||||||
|
case MeshCoreConstants.lppIlluminanceSensor:
|
||||||
|
case MeshCoreConstants.lppTemperatureSensor:
|
||||||
|
case MeshCoreConstants.lppBarometer:
|
||||||
|
case MeshCoreConstants.lppVoltageSensor:
|
||||||
|
case _lppCurrent:
|
||||||
|
case _lppAltitude:
|
||||||
|
case _lppConcentration:
|
||||||
|
case _lppPower:
|
||||||
|
case _lppSpeed:
|
||||||
|
case _lppDirection:
|
||||||
|
case _lppGust:
|
||||||
|
case _lppDewPoint:
|
||||||
|
case _lppRain:
|
||||||
|
case _lppPm25:
|
||||||
|
case _lppPm10:
|
||||||
|
case _lppCo2:
|
||||||
|
case _lppTvoc:
|
||||||
|
case _lppRpm:
|
||||||
|
case _lppConductivity:
|
||||||
|
case _lppRotation:
|
||||||
|
return 2;
|
||||||
|
case MeshCoreConstants.lppAccelerometer:
|
||||||
|
case MeshCoreConstants.lppGyrometer:
|
||||||
|
return 6;
|
||||||
|
case MeshCoreConstants.lppGps:
|
||||||
|
return 9;
|
||||||
|
case _lppGenericSensor:
|
||||||
|
case _lppFrequency:
|
||||||
|
case _lppDistance:
|
||||||
|
case _lppEnergy:
|
||||||
|
case _lppUnixTime:
|
||||||
|
case _lppDuration:
|
||||||
|
case _lppAcceleration:
|
||||||
|
case _lppGyroRate:
|
||||||
|
case _lppVolume:
|
||||||
|
case _lppFlowRate:
|
||||||
|
case _lppVolumeStorage:
|
||||||
|
case _lppWater:
|
||||||
|
case _lppGasVolume:
|
||||||
|
case _lppMass:
|
||||||
|
case _lppSignedSpeed:
|
||||||
|
case _lppSignedPower:
|
||||||
|
case _lppSignedCurrent:
|
||||||
|
return 4;
|
||||||
|
case _lppColour:
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static bool _isZeroPaddedTail(Uint8List data, int remainingBytes) {
|
static bool _isZeroPaddedTail(Uint8List data, int remainingBytes) {
|
||||||
final start = data.length - remainingBytes;
|
final start = data.length - remainingBytes;
|
||||||
|
|||||||
@@ -253,6 +253,17 @@ SensorMetricCardData? _buildOptionPreviewCardData(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_isBinaryMetricBaseKey(metricKey.baseKey)) {
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: previewValue == 'On' ? Icons.toggle_on : Icons.toggle_off,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF4B7B5A),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
switch (metricKey.baseKey) {
|
switch (metricKey.baseKey) {
|
||||||
case 'altitude':
|
case 'altitude':
|
||||||
return SensorMetricCardData(
|
return SensorMetricCardData(
|
||||||
@@ -350,6 +361,25 @@ SensorMetricCardData? _buildOptionPreviewCardData(
|
|||||||
accent: const Color(0xFF3E657C),
|
accent: const Color(0xFF3E657C),
|
||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
case 'button_event':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.touch_app_outlined,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF6C4F96),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
case 'dimmer':
|
||||||
|
case 'light_level':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.tune,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF5A6C84),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
case 'current':
|
case 'current':
|
||||||
return SensorMetricCardData(
|
return SensorMetricCardData(
|
||||||
fieldKey: fieldKey,
|
fieldKey: fieldKey,
|
||||||
@@ -359,6 +389,15 @@ SensorMetricCardData? _buildOptionPreviewCardData(
|
|||||||
accent: const Color(0xFF1C7C54),
|
accent: const Color(0xFF1C7C54),
|
||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
case 'signed_current':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.electric_bolt,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF1C7C54),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
case 'frequency':
|
case 'frequency':
|
||||||
return SensorMetricCardData(
|
return SensorMetricCardData(
|
||||||
fieldKey: fieldKey,
|
fieldKey: fieldKey,
|
||||||
@@ -389,6 +428,7 @@ SensorMetricCardData? _buildOptionPreviewCardData(
|
|||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
case 'power':
|
case 'power':
|
||||||
|
case 'signed_power':
|
||||||
return SensorMetricCardData(
|
return SensorMetricCardData(
|
||||||
fieldKey: fieldKey,
|
fieldKey: fieldKey,
|
||||||
icon: Icons.flash_on_outlined,
|
icon: Icons.flash_on_outlined,
|
||||||
@@ -406,6 +446,42 @@ SensorMetricCardData? _buildOptionPreviewCardData(
|
|||||||
accent: const Color(0xFF2B78A0),
|
accent: const Color(0xFF2B78A0),
|
||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
case 'signed_speed':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.air,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF2B78A0),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
case 'gust':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.air,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF1E88A8),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
case 'dew':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.device_thermostat,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF2F7AA1),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
case 'rain':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.grain,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF2C6BA0),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
case 'distance':
|
case 'distance':
|
||||||
return SensorMetricCardData(
|
return SensorMetricCardData(
|
||||||
fieldKey: fieldKey,
|
fieldKey: fieldKey,
|
||||||
@@ -415,6 +491,15 @@ SensorMetricCardData? _buildOptionPreviewCardData(
|
|||||||
accent: const Color(0xFF577590),
|
accent: const Color(0xFF577590),
|
||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
case 'duration':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.timer_outlined,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF577590),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
case 'energy':
|
case 'energy':
|
||||||
return SensorMetricCardData(
|
return SensorMetricCardData(
|
||||||
fieldKey: fieldKey,
|
fieldKey: fieldKey,
|
||||||
@@ -424,7 +509,22 @@ SensorMetricCardData? _buildOptionPreviewCardData(
|
|||||||
accent: const Color(0xFF9C6644),
|
accent: const Color(0xFF9C6644),
|
||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
case 'volume':
|
||||||
|
case 'flow_rate':
|
||||||
|
case 'volume_storage':
|
||||||
|
case 'water':
|
||||||
|
case 'gas_volume':
|
||||||
|
case 'mass':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.inventory_2_outlined,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF5F7C8A),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
case 'direction':
|
case 'direction':
|
||||||
|
case 'rotation':
|
||||||
final degrees = _previewAsDouble(value);
|
final degrees = _previewAsDouble(value);
|
||||||
return SensorMetricCardData(
|
return SensorMetricCardData(
|
||||||
fieldKey: fieldKey,
|
fieldKey: fieldKey,
|
||||||
@@ -481,6 +581,33 @@ SensorMetricCardData? _buildOptionPreviewCardData(
|
|||||||
accent: const Color(0xFF0A7D61),
|
accent: const Color(0xFF0A7D61),
|
||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
case 'conductivity':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.science_outlined,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF4D6D9A),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
case 'acceleration':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.speed_outlined,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF5A4C99),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
case 'gyro_rate':
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: fieldKey,
|
||||||
|
icon: Icons.sync,
|
||||||
|
label: label,
|
||||||
|
value: previewValue,
|
||||||
|
accent: const Color(0xFF6C4F96),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
case 'pm25':
|
case 'pm25':
|
||||||
case 'pm10':
|
case 'pm10':
|
||||||
return SensorMetricCardData(
|
return SensorMetricCardData(
|
||||||
@@ -543,6 +670,12 @@ String _previewFormatCardinalDirection(double degrees) {
|
|||||||
String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
|
String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
|
||||||
final metricKey = _parseMetricKey(rawKey);
|
final metricKey = _parseMetricKey(rawKey);
|
||||||
|
|
||||||
|
if (_isBinaryMetricBaseKey(metricKey.baseKey)) {
|
||||||
|
final isOn = _previewAsBool(value);
|
||||||
|
if (isOn == null) return null;
|
||||||
|
return isOn ? 'On' : 'Off';
|
||||||
|
}
|
||||||
|
|
||||||
switch (metricKey.baseKey) {
|
switch (metricKey.baseKey) {
|
||||||
case 'altitude':
|
case 'altitude':
|
||||||
final meters = _previewAsDouble(value);
|
final meters = _previewAsDouble(value);
|
||||||
@@ -568,10 +701,21 @@ String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
|
|||||||
case 'analog_input':
|
case 'analog_input':
|
||||||
case 'analog_output':
|
case 'analog_output':
|
||||||
case 'generic_sensor':
|
case 'generic_sensor':
|
||||||
|
case 'light_level':
|
||||||
final reading = _previewAsDouble(value);
|
final reading = _previewAsDouble(value);
|
||||||
if (reading == null) return null;
|
if (reading == null) return null;
|
||||||
return _formatPreviewNumber(reading, maxFractionDigits: 3);
|
return _formatPreviewNumber(reading, maxFractionDigits: 3);
|
||||||
|
|
||||||
|
case 'button_event':
|
||||||
|
final eventCode = _previewAsInt(value);
|
||||||
|
if (eventCode == null) return null;
|
||||||
|
return _formatButtonEvent(eventCode);
|
||||||
|
|
||||||
|
case 'dimmer':
|
||||||
|
final reading = _previewAsInt(value);
|
||||||
|
if (reading == null) return null;
|
||||||
|
return reading.toString();
|
||||||
|
|
||||||
case 'accelerometer':
|
case 'accelerometer':
|
||||||
final vector = _previewAsVector3(value);
|
final vector = _previewAsVector3(value);
|
||||||
if (vector == null) return null;
|
if (vector == null) return null;
|
||||||
@@ -591,6 +735,11 @@ String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
|
|||||||
if (amps == null) return null;
|
if (amps == null) return null;
|
||||||
return _formatPreviewCurrent(amps);
|
return _formatPreviewCurrent(amps);
|
||||||
|
|
||||||
|
case 'signed_current':
|
||||||
|
final amps = _previewAsDouble(value);
|
||||||
|
if (amps == null) return null;
|
||||||
|
return _formatPreviewCurrent(amps);
|
||||||
|
|
||||||
case 'frequency':
|
case 'frequency':
|
||||||
final hertz = _previewAsDouble(value);
|
final hertz = _previewAsDouble(value);
|
||||||
if (hertz == null) return null;
|
if (hertz == null) return null;
|
||||||
@@ -613,26 +762,75 @@ String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
|
|||||||
if (watts == null) return null;
|
if (watts == null) return null;
|
||||||
return _formatPreviewPower(watts);
|
return _formatPreviewPower(watts);
|
||||||
|
|
||||||
|
case 'signed_power':
|
||||||
|
final watts = _previewAsDouble(value);
|
||||||
|
if (watts == null) return null;
|
||||||
|
return _formatPreviewPower(watts);
|
||||||
|
|
||||||
case 'speed':
|
case 'speed':
|
||||||
final metersPerSecond = _previewAsDouble(value);
|
final metersPerSecond = _previewAsDouble(value);
|
||||||
if (metersPerSecond == null) return null;
|
if (metersPerSecond == null) return null;
|
||||||
return '${_formatPreviewNumber(metersPerSecond, maxFractionDigits: 2)} m/s';
|
return '${_formatPreviewNumber(metersPerSecond, maxFractionDigits: 2)} m/s';
|
||||||
|
|
||||||
|
case 'signed_speed':
|
||||||
|
final metersPerSecond = _previewAsDouble(value);
|
||||||
|
if (metersPerSecond == null) return null;
|
||||||
|
return '${_formatPreviewNumber(metersPerSecond, maxFractionDigits: 2)} m/s';
|
||||||
|
|
||||||
|
case 'gust':
|
||||||
|
final metersPerSecond = _previewAsDouble(value);
|
||||||
|
if (metersPerSecond == null) return null;
|
||||||
|
return '${_formatPreviewNumber(metersPerSecond, maxFractionDigits: 2)} m/s';
|
||||||
|
|
||||||
|
case 'dew':
|
||||||
|
final degreesCelsius = _previewAsDouble(value);
|
||||||
|
if (degreesCelsius == null) return null;
|
||||||
|
return '${_formatPreviewNumber(degreesCelsius, maxFractionDigits: 1)}°C';
|
||||||
|
|
||||||
|
case 'rain':
|
||||||
|
final millimeters = _previewAsDouble(value);
|
||||||
|
if (millimeters == null) return null;
|
||||||
|
return '${_formatPreviewNumber(millimeters, maxFractionDigits: 1)} mm';
|
||||||
|
|
||||||
case 'distance':
|
case 'distance':
|
||||||
final meters = _previewAsDouble(value);
|
final meters = _previewAsDouble(value);
|
||||||
if (meters == null) return null;
|
if (meters == null) return null;
|
||||||
return _formatPreviewDistance(meters);
|
return _formatPreviewDistance(meters);
|
||||||
|
|
||||||
|
case 'duration':
|
||||||
|
final seconds = _previewAsDouble(value);
|
||||||
|
if (seconds == null) return null;
|
||||||
|
return '${_formatPreviewNumber(seconds, maxFractionDigits: 2)} s';
|
||||||
|
|
||||||
case 'energy':
|
case 'energy':
|
||||||
final kilowattHours = _previewAsDouble(value);
|
final kilowattHours = _previewAsDouble(value);
|
||||||
if (kilowattHours == null) return null;
|
if (kilowattHours == null) return null;
|
||||||
return _formatPreviewEnergy(kilowattHours);
|
return _formatPreviewEnergy(kilowattHours);
|
||||||
|
|
||||||
|
case 'volume':
|
||||||
|
case 'flow_rate':
|
||||||
|
case 'volume_storage':
|
||||||
|
case 'water':
|
||||||
|
case 'gas_volume':
|
||||||
|
final liters = _previewAsDouble(value);
|
||||||
|
if (liters == null) return null;
|
||||||
|
return '${_formatPreviewNumber(liters, maxFractionDigits: 3)} L';
|
||||||
|
|
||||||
|
case 'mass':
|
||||||
|
final kilograms = _previewAsDouble(value);
|
||||||
|
if (kilograms == null) return null;
|
||||||
|
return '${_formatPreviewNumber(kilograms, maxFractionDigits: 3)} kg';
|
||||||
|
|
||||||
case 'direction':
|
case 'direction':
|
||||||
final degrees = _previewAsDouble(value);
|
final degrees = _previewAsDouble(value);
|
||||||
if (degrees == null) return null;
|
if (degrees == null) return null;
|
||||||
return '${_formatPreviewNumber(degrees, maxFractionDigits: 0)} deg';
|
return '${_formatPreviewNumber(degrees, maxFractionDigits: 0)} deg';
|
||||||
|
|
||||||
|
case 'rotation':
|
||||||
|
final degrees = _previewAsDouble(value);
|
||||||
|
if (degrees == null) return null;
|
||||||
|
return '${_formatPreviewNumber(degrees, maxFractionDigits: 1)} deg';
|
||||||
|
|
||||||
case 'unixtime':
|
case 'unixtime':
|
||||||
final seconds = _previewAsInt(value);
|
final seconds = _previewAsInt(value);
|
||||||
if (seconds == null) return null;
|
if (seconds == null) return null;
|
||||||
@@ -659,6 +857,21 @@ String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
|
|||||||
if (volts == null) return null;
|
if (volts == null) return null;
|
||||||
return '${_formatPreviewNumber(volts, maxFractionDigits: 3)} V';
|
return '${_formatPreviewNumber(volts, maxFractionDigits: 3)} V';
|
||||||
|
|
||||||
|
case 'conductivity':
|
||||||
|
final reading = _previewAsDouble(value);
|
||||||
|
if (reading == null) return null;
|
||||||
|
return _formatPreviewNumber(reading, maxFractionDigits: 0);
|
||||||
|
|
||||||
|
case 'acceleration':
|
||||||
|
final reading = _previewAsDouble(value);
|
||||||
|
if (reading == null) return null;
|
||||||
|
return '${_formatPreviewNumber(reading, maxFractionDigits: 3)} m/s2';
|
||||||
|
|
||||||
|
case 'gyro_rate':
|
||||||
|
final reading = _previewAsDouble(value);
|
||||||
|
if (reading == null) return null;
|
||||||
|
return '${_formatPreviewNumber(reading, maxFractionDigits: 3)} deg/s';
|
||||||
|
|
||||||
case 'pm25':
|
case 'pm25':
|
||||||
case 'pm10':
|
case 'pm10':
|
||||||
final reading = _previewAsDouble(value);
|
final reading = _previewAsDouble(value);
|
||||||
@@ -1179,6 +1392,19 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
labelOverrides: labelOverrides,
|
labelOverrides: labelOverrides,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (_isBinaryMetricBaseKey(metricKey.baseKey)) {
|
||||||
|
final isOn = _asBool(value);
|
||||||
|
if (isOn == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: isOn ? Icons.toggle_on : Icons.toggle_off,
|
||||||
|
label: label,
|
||||||
|
value: isOn ? 'On' : 'Off',
|
||||||
|
accent: const Color(0xFF4B7B5A),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
switch (metricKey.baseKey) {
|
switch (metricKey.baseKey) {
|
||||||
case 'altitude':
|
case 'altitude':
|
||||||
final meters = _asDouble(value);
|
final meters = _asDouble(value);
|
||||||
@@ -1310,6 +1536,31 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'button_event':
|
||||||
|
final reading = _asInt(value);
|
||||||
|
if (reading == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.touch_app_outlined,
|
||||||
|
label: label,
|
||||||
|
value: _formatButtonEvent(reading),
|
||||||
|
accent: const Color(0xFF6C4F96),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'dimmer':
|
||||||
|
case 'light_level':
|
||||||
|
final reading = _asDouble(value);
|
||||||
|
if (reading == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.tune,
|
||||||
|
label: label,
|
||||||
|
value: _formatNumber(reading, maxFractionDigits: 2),
|
||||||
|
accent: const Color(0xFF5A6C84),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
case 'current':
|
case 'current':
|
||||||
final amps = _asDouble(value);
|
final amps = _asDouble(value);
|
||||||
if (amps == null) return null;
|
if (amps == null) return null;
|
||||||
@@ -1322,6 +1573,18 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'signed_current':
|
||||||
|
final amps = _asDouble(value);
|
||||||
|
if (amps == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.electric_bolt,
|
||||||
|
label: label,
|
||||||
|
value: _formatCurrent(amps),
|
||||||
|
accent: const Color(0xFF1C7C54),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
case 'frequency':
|
case 'frequency':
|
||||||
final hz = _asDouble(value);
|
final hz = _asDouble(value);
|
||||||
if (hz == null) return null;
|
if (hz == null) return null;
|
||||||
@@ -1370,6 +1633,18 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'signed_power':
|
||||||
|
final watts = _asDouble(value);
|
||||||
|
if (watts == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.flash_on_outlined,
|
||||||
|
label: label,
|
||||||
|
value: _formatPower(watts),
|
||||||
|
accent: const Color(0xFFB5622E),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
case 'speed':
|
case 'speed':
|
||||||
final metersPerSecond = _asDouble(value);
|
final metersPerSecond = _asDouble(value);
|
||||||
if (metersPerSecond == null) return null;
|
if (metersPerSecond == null) return null;
|
||||||
@@ -1382,6 +1657,54 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'signed_speed':
|
||||||
|
final metersPerSecond = _asDouble(value);
|
||||||
|
if (metersPerSecond == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.air,
|
||||||
|
label: label,
|
||||||
|
value: '${_formatNumber(metersPerSecond, maxFractionDigits: 2)} m/s',
|
||||||
|
accent: const Color(0xFF2B78A0),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'gust':
|
||||||
|
final metersPerSecond = _asDouble(value);
|
||||||
|
if (metersPerSecond == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.air,
|
||||||
|
label: label,
|
||||||
|
value: '${_formatNumber(metersPerSecond, maxFractionDigits: 2)} m/s',
|
||||||
|
accent: const Color(0xFF1E88A8),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'dew':
|
||||||
|
final degreesCelsius = _asDouble(value);
|
||||||
|
if (degreesCelsius == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.device_thermostat,
|
||||||
|
label: label,
|
||||||
|
value: '${_formatNumber(degreesCelsius, maxFractionDigits: 1)}°C',
|
||||||
|
accent: const Color(0xFF2F7AA1),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'rain':
|
||||||
|
final millimeters = _asDouble(value);
|
||||||
|
if (millimeters == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.grain,
|
||||||
|
label: label,
|
||||||
|
value: '${_formatNumber(millimeters, maxFractionDigits: 1)} mm',
|
||||||
|
accent: const Color(0xFF2C6BA0),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
case 'distance':
|
case 'distance':
|
||||||
final meters = _asDouble(value);
|
final meters = _asDouble(value);
|
||||||
if (meters == null) return null;
|
if (meters == null) return null;
|
||||||
@@ -1394,6 +1717,18 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'duration':
|
||||||
|
final seconds = _asDouble(value);
|
||||||
|
if (seconds == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.timer_outlined,
|
||||||
|
label: label,
|
||||||
|
value: '${_formatNumber(seconds, maxFractionDigits: 2)} s',
|
||||||
|
accent: const Color(0xFF577590),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
case 'energy':
|
case 'energy':
|
||||||
final kwh = _asDouble(value);
|
final kwh = _asDouble(value);
|
||||||
if (kwh == null) return null;
|
if (kwh == null) return null;
|
||||||
@@ -1406,6 +1741,24 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'volume':
|
||||||
|
case 'flow_rate':
|
||||||
|
case 'volume_storage':
|
||||||
|
case 'water':
|
||||||
|
case 'gas_volume':
|
||||||
|
case 'mass':
|
||||||
|
final reading = _asDouble(value);
|
||||||
|
if (reading == null) return null;
|
||||||
|
final unit = metricKey.baseKey == 'mass' ? 'kg' : 'L';
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.inventory_2_outlined,
|
||||||
|
label: label,
|
||||||
|
value: '${_formatNumber(reading, maxFractionDigits: 3)} $unit',
|
||||||
|
accent: const Color(0xFF5F7C8A),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
case 'direction':
|
case 'direction':
|
||||||
final degrees = _asDouble(value);
|
final degrees = _asDouble(value);
|
||||||
if (degrees == null) return null;
|
if (degrees == null) return null;
|
||||||
@@ -1419,6 +1772,18 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'rotation':
|
||||||
|
final degrees = _asDouble(value);
|
||||||
|
if (degrees == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.explore_outlined,
|
||||||
|
label: label,
|
||||||
|
value: '${_formatNumber(degrees, maxFractionDigits: 1)} deg',
|
||||||
|
accent: const Color(0xFF8A5A44),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
case 'unixtime':
|
case 'unixtime':
|
||||||
final seconds = _asInt(value);
|
final seconds = _asInt(value);
|
||||||
if (seconds == null) return null;
|
if (seconds == null) return null;
|
||||||
@@ -1475,6 +1840,42 @@ class SensorTelemetryCard extends StatelessWidget {
|
|||||||
accent: const Color(0xFF0A7D61),
|
accent: const Color(0xFF0A7D61),
|
||||||
channel: metricKey.channel,
|
channel: metricKey.channel,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'conductivity':
|
||||||
|
final reading = _asDouble(value);
|
||||||
|
if (reading == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.science_outlined,
|
||||||
|
label: label,
|
||||||
|
value: _formatNumber(reading, maxFractionDigits: 0),
|
||||||
|
accent: const Color(0xFF4D6D9A),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'acceleration':
|
||||||
|
final reading = _asDouble(value);
|
||||||
|
if (reading == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.speed_outlined,
|
||||||
|
label: label,
|
||||||
|
value: '${_formatNumber(reading, maxFractionDigits: 3)} m/s2',
|
||||||
|
accent: const Color(0xFF5A4C99),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'gyro_rate':
|
||||||
|
final reading = _asDouble(value);
|
||||||
|
if (reading == null) return null;
|
||||||
|
return SensorMetricCardData(
|
||||||
|
fieldKey: _extraFieldKey(rawKey),
|
||||||
|
icon: Icons.sync,
|
||||||
|
label: label,
|
||||||
|
value: '${_formatNumber(reading, maxFractionDigits: 3)} deg/s',
|
||||||
|
accent: const Color(0xFF6C4F96),
|
||||||
|
channel: metricKey.channel,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (metricKey.baseKey) {
|
switch (metricKey.baseKey) {
|
||||||
@@ -2135,6 +2536,32 @@ String _selectorMetricLabel(String label, int? channel) {
|
|||||||
return '$label (ch $channel)';
|
return '$label (ch $channel)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isBinaryMetricBaseKey(String baseKey) {
|
||||||
|
return baseKey.startsWith('binary_');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatButtonEvent(int value) {
|
||||||
|
switch (value) {
|
||||||
|
case 0:
|
||||||
|
return 'None';
|
||||||
|
case 1:
|
||||||
|
return 'Press';
|
||||||
|
case 2:
|
||||||
|
return 'Double press';
|
||||||
|
case 3:
|
||||||
|
return 'Triple press';
|
||||||
|
case 4:
|
||||||
|
return 'Long press';
|
||||||
|
case 5:
|
||||||
|
return 'Long double';
|
||||||
|
case 6:
|
||||||
|
return 'Long triple';
|
||||||
|
case 128:
|
||||||
|
return 'Hold';
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
String _formatExtraFieldLabel(String rawKey) {
|
String _formatExtraFieldLabel(String rawKey) {
|
||||||
final metricKey = _parseMetricKey(rawKey);
|
final metricKey = _parseMetricKey(rawKey);
|
||||||
return _knownMetricLabels[metricKey.baseKey] ??
|
return _knownMetricLabels[metricKey.baseKey] ??
|
||||||
@@ -2168,13 +2595,60 @@ const List<String> _knownMetricBaseKeys = <String>[
|
|||||||
'energy',
|
'energy',
|
||||||
'power',
|
'power',
|
||||||
'speed',
|
'speed',
|
||||||
|
'gust',
|
||||||
|
'dew',
|
||||||
|
'rain',
|
||||||
'pm25',
|
'pm25',
|
||||||
'pm10',
|
'pm10',
|
||||||
'tvoc',
|
'tvoc',
|
||||||
'co2',
|
'co2',
|
||||||
'rpm',
|
'rpm',
|
||||||
'cond',
|
'conductivity',
|
||||||
'uv',
|
'uv',
|
||||||
|
'button_event',
|
||||||
|
'dimmer',
|
||||||
|
'light_level',
|
||||||
|
'rotation',
|
||||||
|
'duration',
|
||||||
|
'acceleration',
|
||||||
|
'gyro_rate',
|
||||||
|
'volume',
|
||||||
|
'flow_rate',
|
||||||
|
'volume_storage',
|
||||||
|
'water',
|
||||||
|
'gas_volume',
|
||||||
|
'mass',
|
||||||
|
'signed_speed',
|
||||||
|
'signed_power',
|
||||||
|
'signed_current',
|
||||||
|
'binary_bool',
|
||||||
|
'binary_power_switch',
|
||||||
|
'binary_open',
|
||||||
|
'binary_battery_low',
|
||||||
|
'binary_charging',
|
||||||
|
'binary_carbon_monoxide',
|
||||||
|
'binary_cold',
|
||||||
|
'binary_connectivity',
|
||||||
|
'binary_door',
|
||||||
|
'binary_garage_door',
|
||||||
|
'binary_gas',
|
||||||
|
'binary_heat',
|
||||||
|
'binary_light',
|
||||||
|
'binary_lock',
|
||||||
|
'binary_moisture',
|
||||||
|
'binary_motion',
|
||||||
|
'binary_moving',
|
||||||
|
'binary_occupancy',
|
||||||
|
'binary_plug',
|
||||||
|
'binary_presence',
|
||||||
|
'binary_problem',
|
||||||
|
'binary_running',
|
||||||
|
'binary_safety',
|
||||||
|
'binary_smoke',
|
||||||
|
'binary_sound',
|
||||||
|
'binary_tamper',
|
||||||
|
'binary_vibration',
|
||||||
|
'binary_window',
|
||||||
];
|
];
|
||||||
|
|
||||||
const Map<String, String> _knownMetricLabels = <String, String>{
|
const Map<String, String> _knownMetricLabels = <String, String>{
|
||||||
@@ -2185,32 +2659,78 @@ const Map<String, String> _knownMetricLabels = <String, String>{
|
|||||||
'co2': 'CO2',
|
'co2': 'CO2',
|
||||||
'colour': 'Color',
|
'colour': 'Color',
|
||||||
'concentration': 'Concentration',
|
'concentration': 'Concentration',
|
||||||
'cond': 'Conductivity',
|
'conductivity': 'Conductivity',
|
||||||
'current': 'Current',
|
'current': 'Current',
|
||||||
|
'button_event': 'Button',
|
||||||
|
'dimmer': 'Dimmer',
|
||||||
'digital_input': 'Digital input',
|
'digital_input': 'Digital input',
|
||||||
'digital_output': 'Digital output',
|
'digital_output': 'Digital output',
|
||||||
'direction': 'Direction',
|
'direction': 'Direction',
|
||||||
'distance': 'Distance',
|
'distance': 'Distance',
|
||||||
|
'duration': 'Duration',
|
||||||
'energy': 'Energy',
|
'energy': 'Energy',
|
||||||
'frequency': 'Frequency',
|
'frequency': 'Frequency',
|
||||||
|
'flow_rate': 'Flow rate',
|
||||||
|
'gas_volume': 'Gas volume',
|
||||||
'generic_sensor': 'Generic sensor',
|
'generic_sensor': 'Generic sensor',
|
||||||
|
'gyro_rate': 'Gyro rate',
|
||||||
'gyrometer': 'Gyrometer',
|
'gyrometer': 'Gyrometer',
|
||||||
'humidity': 'Humidity',
|
'humidity': 'Humidity',
|
||||||
'illuminance': 'Illuminance',
|
'illuminance': 'Illuminance',
|
||||||
|
'light_level': 'Light level',
|
||||||
|
'mass': 'Mass',
|
||||||
'percentage': 'Percentage',
|
'percentage': 'Percentage',
|
||||||
'pm10': 'PM10',
|
'pm10': 'PM10',
|
||||||
'pm25': 'PM2.5',
|
'pm25': 'PM2.5',
|
||||||
'power': 'Power',
|
'power': 'Power',
|
||||||
'presence': 'Presence',
|
'presence': 'Presence',
|
||||||
'pressure': 'Pressure',
|
'pressure': 'Pressure',
|
||||||
|
'rain': 'Rain',
|
||||||
|
'rotation': 'Rotation',
|
||||||
'rpm': 'RPM',
|
'rpm': 'RPM',
|
||||||
|
'gust': 'Wind gust',
|
||||||
|
'signed_current': 'Signed current',
|
||||||
|
'signed_power': 'Signed power',
|
||||||
|
'signed_speed': 'Signed speed',
|
||||||
'speed': 'Speed',
|
'speed': 'Speed',
|
||||||
'switch': 'Switch',
|
'switch': 'Switch',
|
||||||
|
'dew': 'Dew point',
|
||||||
'temperature': 'Temperature',
|
'temperature': 'Temperature',
|
||||||
'tvoc': 'TVOC',
|
'tvoc': 'TVOC',
|
||||||
'unixtime': 'Time',
|
'unixtime': 'Time',
|
||||||
'uv': 'UV index',
|
'uv': 'UV index',
|
||||||
|
'volume': 'Volume',
|
||||||
|
'volume_storage': 'Storage volume',
|
||||||
'voltage': 'Voltage',
|
'voltage': 'Voltage',
|
||||||
|
'water': 'Water',
|
||||||
|
'binary_battery_low': 'Battery low',
|
||||||
|
'binary_bool': 'Binary',
|
||||||
|
'binary_carbon_monoxide': 'Carbon monoxide',
|
||||||
|
'binary_charging': 'Charging',
|
||||||
|
'binary_cold': 'Cold',
|
||||||
|
'binary_connectivity': 'Connectivity',
|
||||||
|
'binary_door': 'Door',
|
||||||
|
'binary_garage_door': 'Garage door',
|
||||||
|
'binary_gas': 'Gas',
|
||||||
|
'binary_heat': 'Heat',
|
||||||
|
'binary_light': 'Light',
|
||||||
|
'binary_lock': 'Lock',
|
||||||
|
'binary_moisture': 'Moisture',
|
||||||
|
'binary_motion': 'Motion',
|
||||||
|
'binary_moving': 'Moving',
|
||||||
|
'binary_occupancy': 'Occupancy',
|
||||||
|
'binary_open': 'Open',
|
||||||
|
'binary_plug': 'Plug',
|
||||||
|
'binary_power_switch': 'Power switch',
|
||||||
|
'binary_presence': 'Presence',
|
||||||
|
'binary_problem': 'Problem',
|
||||||
|
'binary_running': 'Running',
|
||||||
|
'binary_safety': 'Safety',
|
||||||
|
'binary_smoke': 'Smoke',
|
||||||
|
'binary_sound': 'Sound',
|
||||||
|
'binary_tamper': 'Tamper',
|
||||||
|
'binary_vibration': 'Vibration',
|
||||||
|
'binary_window': 'Window',
|
||||||
};
|
};
|
||||||
|
|
||||||
_ParsedMetricKey _parseMetricKey(String rawKey) {
|
_ParsedMetricKey _parseMetricKey(String rawKey) {
|
||||||
|
|||||||
@@ -75,6 +75,42 @@ void main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Contact buildTelemetrySensorContact() {
|
||||||
|
final publicKey = Uint8List(32);
|
||||||
|
publicKey[0] = 0x55;
|
||||||
|
|
||||||
|
return Contact(
|
||||||
|
publicKey: publicKey,
|
||||||
|
type: ContactType.sensor,
|
||||||
|
flags: 0,
|
||||||
|
outPathLen: 0,
|
||||||
|
outPath: Uint8List(64),
|
||||||
|
advName: 'Weather Station',
|
||||||
|
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
advLat: 0,
|
||||||
|
advLon: 0,
|
||||||
|
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
telemetry: ContactTelemetry(
|
||||||
|
temperature: 11.8,
|
||||||
|
humidity: 51,
|
||||||
|
pressure: 921.9,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
extraSensorData: const {
|
||||||
|
'__source_channel:temperature': 3,
|
||||||
|
'__source_channel:humidity': 2,
|
||||||
|
'__source_channel:pressure': 2,
|
||||||
|
'illuminance_2': 19150.0,
|
||||||
|
'temperature_2': 1.99,
|
||||||
|
'voltage_2': 5.2,
|
||||||
|
'switch_2': 0,
|
||||||
|
'speed_2': 2.8,
|
||||||
|
'speed_3': 3.7,
|
||||||
|
'uv_2': 1.0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
test('metric label overrides persist across reloads', () async {
|
test('metric label overrides persist across reloads', () async {
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
final contact = buildSensorContact();
|
final contact = buildSensorContact();
|
||||||
@@ -206,4 +242,32 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(connectionProvider.pingCalls, 2);
|
expect(connectionProvider.pingCalls, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'addSensor includes available extra telemetry fields by default',
|
||||||
|
() async {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
final contact = buildTelemetrySensorContact();
|
||||||
|
|
||||||
|
final provider = SensorsProvider();
|
||||||
|
await waitUntilLoaded(provider);
|
||||||
|
await provider.addSensor(contact);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
provider.visibleFieldsFor(contact.publicKeyHex),
|
||||||
|
containsAll(<String>{
|
||||||
|
'temperature',
|
||||||
|
'humidity',
|
||||||
|
'pressure',
|
||||||
|
'extra:illuminance_2',
|
||||||
|
'extra:temperature_2',
|
||||||
|
'extra:voltage_2',
|
||||||
|
'extra:switch_2',
|
||||||
|
'extra:speed_2',
|
||||||
|
'extra:speed_3',
|
||||||
|
'extra:uv_2',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -373,6 +373,9 @@ void main() {
|
|||||||
const lppDirection = 132;
|
const lppDirection = 132;
|
||||||
const lppUnixTime = 133;
|
const lppUnixTime = 133;
|
||||||
const lppColour = 135;
|
const lppColour = 135;
|
||||||
|
const lppGust = 137;
|
||||||
|
const lppDewPoint = 138;
|
||||||
|
const lppRain = 139;
|
||||||
const lppSwitch = 142;
|
const lppSwitch = 142;
|
||||||
|
|
||||||
final payload = Uint8List.fromList([
|
final payload = Uint8List.fromList([
|
||||||
@@ -388,6 +391,9 @@ void main() {
|
|||||||
11, lppDirection, 0x01, 0x0E, // 270 deg
|
11, lppDirection, 0x01, 0x0E, // 270 deg
|
||||||
12, lppUnixTime, 0x65, 0xF0, 0x00, 0x00, // 1710221312
|
12, lppUnixTime, 0x65, 0xF0, 0x00, 0x00, // 1710221312
|
||||||
13, lppColour, 0xFF, 0x80, 0x40, // #FF8040
|
13, lppColour, 0xFF, 0x80, 0x40, // #FF8040
|
||||||
|
15, lppGust, 0x01, 0x72, // 3.70 m/s
|
||||||
|
16, lppDewPoint, 0x00, 0x14, // 2.0 C
|
||||||
|
17, lppRain, 0x00, 0x7B, // 12.3 mm
|
||||||
14, lppSwitch, 0x01, // on
|
14, lppSwitch, 0x01, // on
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -409,9 +415,59 @@ void main() {
|
|||||||
decoded.extraSensorData!['colour_13'],
|
decoded.extraSensorData!['colour_13'],
|
||||||
equals({'r': 255, 'g': 128, 'b': 64}),
|
equals({'r': 255, 'g': 128, 'b': 64}),
|
||||||
);
|
);
|
||||||
|
expect(decoded.extraSensorData!['gust_15'], closeTo(3.7, 0.001));
|
||||||
|
expect(decoded.extraSensorData!['dew_16'], closeTo(2.0, 0.001));
|
||||||
|
expect(decoded.extraSensorData!['rain_17'], closeTo(12.3, 0.001));
|
||||||
expect(decoded.extraSensorData!['switch_14'], equals(1));
|
expect(decoded.extraSensorData!['switch_14'], equals(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('new MeshCore custom telemetry types decode without truncation', () {
|
||||||
|
const lppBinaryMoisture = 157;
|
||||||
|
const lppButtonEvent = 171;
|
||||||
|
const lppDimmer = 172;
|
||||||
|
const lppUv = 173;
|
||||||
|
const lppPm25 = 175;
|
||||||
|
const lppConductivity = 180;
|
||||||
|
const lppDuration = 182;
|
||||||
|
const lppSignedSpeed = 191;
|
||||||
|
const lppSignedPower = 192;
|
||||||
|
const lppSignedCurrent = 193;
|
||||||
|
|
||||||
|
final payload = Uint8List.fromList([
|
||||||
|
2, lppBinaryMoisture, 0x01,
|
||||||
|
3, lppButtonEvent, 0x04,
|
||||||
|
4, lppDimmer, 0xFB, // -5
|
||||||
|
5, lppUv, 0x0A, // 1.0
|
||||||
|
6, lppPm25, 0x00, 0x0C, // 12
|
||||||
|
7, lppConductivity, 0x01, 0xF4, // 500
|
||||||
|
8, lppDuration, 0x00, 0x00, 0x13, 0x88, // 5.0 s
|
||||||
|
9, lppSignedSpeed, 0xFF, 0xE1, 0x7B, 0x80, // -2.0 m/s
|
||||||
|
10, lppSignedPower, 0xFF, 0xFF, 0xFC, 0x18, // -10.0 W
|
||||||
|
11, lppSignedCurrent, 0xFF, 0xFF, 0xFC, 0x18, // -1.0 A
|
||||||
|
12, MeshCoreConstants.lppTemperatureSensor, 0x00, 0x76, // 11.8 C
|
||||||
|
]);
|
||||||
|
|
||||||
|
final decoded = CayenneLppParser.parse(payload);
|
||||||
|
|
||||||
|
expect(decoded.extraSensorData!['binary_moisture_2'], equals(1));
|
||||||
|
expect(decoded.extraSensorData!['button_event_3'], equals(4));
|
||||||
|
expect(decoded.extraSensorData!['dimmer_4'], equals(-5));
|
||||||
|
expect(decoded.extraSensorData!['uv_5'], closeTo(1.0, 0.001));
|
||||||
|
expect(decoded.extraSensorData!['pm25_6'], equals(12.0));
|
||||||
|
expect(decoded.extraSensorData!['conductivity_7'], equals(500.0));
|
||||||
|
expect(decoded.extraSensorData!['duration_8'], closeTo(5.0, 0.001));
|
||||||
|
expect(decoded.extraSensorData!['signed_speed_9'], closeTo(-2.0, 0.001));
|
||||||
|
expect(
|
||||||
|
decoded.extraSensorData!['signed_power_10'],
|
||||||
|
closeTo(-10.0, 0.001),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
decoded.extraSensorData!['signed_current_11'],
|
||||||
|
closeTo(-1.0, 0.001),
|
||||||
|
);
|
||||||
|
expect(decoded.temperature, closeTo(11.8, 0.1));
|
||||||
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'percentage battery and non-battery voltage channels are preserved separately',
|
'percentage battery and non-battery voltage channels are preserved separately',
|
||||||
() {
|
() {
|
||||||
|
|||||||
@@ -50,9 +50,10 @@ void main() {
|
|||||||
'temperature': 'Ambient',
|
'temperature': 'Ambient',
|
||||||
'extra:illuminance_2': 'Light',
|
'extra:illuminance_2': 'Light',
|
||||||
},
|
},
|
||||||
fieldSpans: sensorFullWidthFieldSpans(
|
fieldSpans: sensorFullWidthFieldSpans(const {
|
||||||
const {'temperature', 'extra:illuminance_2'},
|
'temperature',
|
||||||
),
|
'extra:illuminance_2',
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -111,18 +112,12 @@ void main() {
|
|||||||
'temperature',
|
'temperature',
|
||||||
'extra:illuminance_2',
|
'extra:illuminance_2',
|
||||||
},
|
},
|
||||||
fieldOrder: const [
|
fieldOrder: const ['extra:illuminance_2', 'temperature', 'battery'],
|
||||||
'extra:illuminance_2',
|
fieldSpans: sensorFullWidthFieldSpans(const {
|
||||||
'temperature',
|
|
||||||
'battery',
|
|
||||||
],
|
|
||||||
fieldSpans: sensorFullWidthFieldSpans(
|
|
||||||
const {
|
|
||||||
'battery',
|
'battery',
|
||||||
'temperature',
|
'temperature',
|
||||||
'extra:illuminance_2',
|
'extra:illuminance_2',
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -141,4 +136,55 @@ void main() {
|
|||||||
expect(illuminanceTop.dy, lessThan(temperatureTop.dy));
|
expect(illuminanceTop.dy, lessThan(temperatureTop.dy));
|
||||||
expect(temperatureTop.dy, lessThan(batteryTop.dy));
|
expect(temperatureTop.dy, lessThan(batteryTop.dy));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('renders MeshCore custom weather metrics', (tester) async {
|
||||||
|
final publicKey = Uint8List(32);
|
||||||
|
publicKey[0] = 0x46;
|
||||||
|
final contact = Contact(
|
||||||
|
publicKey: publicKey,
|
||||||
|
type: ContactType.sensor,
|
||||||
|
flags: 0,
|
||||||
|
outPathLen: 0,
|
||||||
|
outPath: Uint8List(64),
|
||||||
|
advName: 'WX Station',
|
||||||
|
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
advLat: 0,
|
||||||
|
advLon: 0,
|
||||||
|
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
telemetry: ContactTelemetry(
|
||||||
|
timestamp: DateTime.now().subtract(const Duration(minutes: 1)),
|
||||||
|
extraSensorData: const {'gust_2': 3.7, 'dew_2': 2.0, 'rain_2': 12.3},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: SensorTelemetryCard(
|
||||||
|
contact: contact,
|
||||||
|
state: SensorRefreshState.idle,
|
||||||
|
visibleFields: const {
|
||||||
|
'extra:gust_2',
|
||||||
|
'extra:dew_2',
|
||||||
|
'extra:rain_2',
|
||||||
|
},
|
||||||
|
fieldSpans: sensorFullWidthFieldSpans(const {
|
||||||
|
'extra:gust_2',
|
||||||
|
'extra:dew_2',
|
||||||
|
'extra:rain_2',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Wind gust'), findsOneWidget);
|
||||||
|
expect(find.text('Dew point'), findsOneWidget);
|
||||||
|
expect(find.text('Rain'), findsOneWidget);
|
||||||
|
expect(find.text('3.7 m/s'), findsOneWidget);
|
||||||
|
expect(find.text('2°C'), findsOneWidget);
|
||||||
|
expect(find.text('12.3 mm'), findsOneWidget);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user