mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
Added telemetry to repeater interface.
This commit is contained in:
parent
e3d7607db9
commit
c306ad798c
6 changed files with 670 additions and 0 deletions
|
|
@ -1,4 +1,6 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:typed_data';
|
||||
|
||||
// Command codes (to device)
|
||||
|
|
@ -29,6 +31,8 @@ const int cmdGetContactByKey = 30;
|
|||
const int cmdGetChannel = 31;
|
||||
const int cmdSetChannel = 32;
|
||||
const int cmdGetRadioSettings = 57;
|
||||
const int cmdGetTelemetryReq = 39;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
|
||||
// Text message types
|
||||
const int txtTypePlain = 0;
|
||||
|
|
@ -73,6 +77,9 @@ const int pushCodeLoginFail = 0x86;
|
|||
const int pushCodeStatusResponse = 0x87;
|
||||
const int pushCodeLogRxData = 0x88;
|
||||
const int pushCodeNewAdvert = 0x8A;
|
||||
const int pushCodeTelemetryResponse = 0x8B;
|
||||
const int pushCodeBinaryResponse = 0x8C;
|
||||
|
||||
|
||||
// Contact/advertisement types
|
||||
const int advTypeChat = 1;
|
||||
|
|
@ -614,3 +621,23 @@ Uint8List buildSendCliCommandFrame(
|
|||
frame[frame.length - 1] = 0; // null terminator
|
||||
return frame;
|
||||
}
|
||||
|
||||
//Build a telemetry request frame
|
||||
//Format: [cmd][pub_key x32][req_type][payload]
|
||||
Uint8List buildSendBinaryReq(
|
||||
Uint8List repeaterPubKey,
|
||||
{
|
||||
int attempt = 0,
|
||||
int? timestampSeconds,
|
||||
Uint8List? payload,
|
||||
}) {
|
||||
int offset = 0;
|
||||
final frame = Uint8List(1 + 32 + 1 + (payload?.length ?? 0));
|
||||
frame[offset++] = cmdSendBinaryReq;
|
||||
frame.setRange(offset, offset + 32, repeaterPubKey);
|
||||
if (payload != null && payload.isNotEmpty) {
|
||||
offset += 32;
|
||||
frame.setRange(offset, offset + payload.length, payload);
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
103
lib/helpers/buffer_reader.dart
Normal file
103
lib/helpers/buffer_reader.dart
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
class BufferReader {
|
||||
int pointer = 0;
|
||||
final Uint8List buffer;
|
||||
|
||||
BufferReader(Uint8List data) : buffer = Uint8List.fromList(data);
|
||||
|
||||
int getRemainingBytesCount() {
|
||||
return buffer.length - pointer;
|
||||
}
|
||||
|
||||
int readByte() {
|
||||
return readBytes(1)[0];
|
||||
}
|
||||
|
||||
Uint8List readBytes(int count) {
|
||||
final data = buffer.sublist(pointer, pointer + count);
|
||||
pointer += count;
|
||||
return data;
|
||||
}
|
||||
|
||||
Uint8List readRemainingBytes() {
|
||||
return readBytes(getRemainingBytesCount());
|
||||
}
|
||||
|
||||
String readString() {
|
||||
return utf8.decode(readRemainingBytes());
|
||||
}
|
||||
|
||||
String readCString(int maxLength) {
|
||||
final value = <int>[];
|
||||
final bytes = readBytes(maxLength);
|
||||
for (final byte in bytes) {
|
||||
// if we find a null terminator character, we have reached the end of the cstring
|
||||
if (byte == 0) {
|
||||
return utf8.decode(Uint8List.fromList(value));
|
||||
}
|
||||
value.add(byte);
|
||||
}
|
||||
return utf8.decode(Uint8List.fromList(value));
|
||||
}
|
||||
|
||||
int readInt8() {
|
||||
final bytes = readBytes(1);
|
||||
return ByteData.view(bytes.buffer).getInt8(0);
|
||||
}
|
||||
|
||||
int readUInt8() {
|
||||
final bytes = readBytes(1);
|
||||
return ByteData.view(bytes.buffer).getUint8(0);
|
||||
}
|
||||
|
||||
int readUInt16LE() {
|
||||
final bytes = readBytes(2);
|
||||
return ByteData.view(bytes.buffer).getUint16(0, Endian.little);
|
||||
}
|
||||
|
||||
int readUInt16BE() {
|
||||
final bytes = readBytes(2);
|
||||
return ByteData.view(bytes.buffer).getUint16(0, Endian.big);
|
||||
}
|
||||
|
||||
int readUInt32LE() {
|
||||
final bytes = readBytes(4);
|
||||
return ByteData.view(bytes.buffer).getUint32(0, Endian.little);
|
||||
}
|
||||
|
||||
int readUInt32BE() {
|
||||
final bytes = readBytes(4);
|
||||
return ByteData.view(bytes.buffer).getUint32(0, Endian.big);
|
||||
}
|
||||
|
||||
int readInt16LE() {
|
||||
final bytes = readBytes(2);
|
||||
return ByteData.view(bytes.buffer).getInt16(0, Endian.little);
|
||||
}
|
||||
|
||||
int readInt16BE() {
|
||||
final bytes = readBytes(2);
|
||||
return ByteData.view(bytes.buffer).getInt16(0, Endian.big);
|
||||
}
|
||||
|
||||
int readInt32LE() {
|
||||
final bytes = readBytes(4);
|
||||
return ByteData.view(bytes.buffer).getInt32(0, Endian.little);
|
||||
}
|
||||
|
||||
int readInt24BE() {
|
||||
// read 24-bit (3 bytes) big endian integer
|
||||
var value = (readByte() << 16) | (readByte() << 8) | readByte();
|
||||
|
||||
// convert 24-bit signed integer to 32-bit signed integer
|
||||
// 0x800000 is the sign bit for a 24-bit value
|
||||
// if it's set, value is negative in 24-bit two's complement
|
||||
if ((value & 0x800000) != 0) {
|
||||
value -= 0x1000000;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
57
lib/helpers/buffer_writer.dart
Normal file
57
lib/helpers/buffer_writer.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
class BufferWriter {
|
||||
final BytesBuilder _builder = BytesBuilder();
|
||||
|
||||
Uint8List toBytes() {
|
||||
return _builder.toBytes();
|
||||
}
|
||||
|
||||
void writeBytes(Uint8List bytes) {
|
||||
_builder.add(bytes);
|
||||
}
|
||||
|
||||
void writeByte(int byte) {
|
||||
_builder.addByte(byte);
|
||||
}
|
||||
|
||||
void writeUInt16LE(int num) {
|
||||
final bytes = Uint8List(2);
|
||||
final data = ByteData.view(bytes.buffer);
|
||||
data.setUint16(0, num, Endian.little);
|
||||
writeBytes(bytes);
|
||||
}
|
||||
|
||||
void writeUInt32LE(int num) {
|
||||
final bytes = Uint8List(4);
|
||||
final data = ByteData.view(bytes.buffer);
|
||||
data.setUint32(0, num, Endian.little);
|
||||
writeBytes(bytes);
|
||||
}
|
||||
|
||||
void writeInt32LE(int num) {
|
||||
final bytes = Uint8List(4);
|
||||
final data = ByteData.view(bytes.buffer);
|
||||
data.setInt32(0, num, Endian.little);
|
||||
writeBytes(bytes);
|
||||
}
|
||||
|
||||
void writeString(String string) {
|
||||
writeBytes(Uint8List.fromList(utf8.encode(string)));
|
||||
}
|
||||
|
||||
void writeCString(String string, int maxLength) {
|
||||
final bytes = Uint8List(maxLength);
|
||||
final encodedString = utf8.encode(string);
|
||||
|
||||
for (var i = 0; i < maxLength && i < encodedString.length; i++) {
|
||||
bytes[i] = encodedString[i];
|
||||
}
|
||||
|
||||
// ensure the last byte is always a null terminator
|
||||
bytes[maxLength - 1] = 0;
|
||||
|
||||
writeBytes(bytes);
|
||||
}
|
||||
}
|
||||
191
lib/helpers/cayenne_lpp.dart
Normal file
191
lib/helpers/cayenne_lpp.dart
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import 'dart:typed_data';
|
||||
import 'buffer_reader.dart';
|
||||
import 'buffer_writer.dart';
|
||||
|
||||
class CayenneLpp {
|
||||
static const int lppDigitalInput = 0; // 1 byte
|
||||
static const int lppDigitalOutput = 1; // 1 byte
|
||||
static const int lppAnalogInput = 2; // 2 bytes, 0.01 signed
|
||||
static const int lppAnalogOutput = 3; // 2 bytes, 0.01 signed
|
||||
static const int lppGenericSensor = 100; // 4 bytes, unsigned
|
||||
static const int lppLuminosity = 101; // 2 bytes, 1 lux unsigned
|
||||
static const int lppPresence = 102; // 1 byte, bool
|
||||
static const int lppTemperature = 103; // 2 bytes, 0.1°C signed
|
||||
static const int lppRelativeHumidity = 104; // 1 byte, 0.5% unsigned
|
||||
static const int lppAccelerometer = 113; // 2 bytes per axis, 0.001G
|
||||
static const int lppBarometricPressure = 115; // 2 bytes 0.1hPa unsigned
|
||||
static const int lppVoltage = 116; // 2 bytes 0.01V unsigned
|
||||
static const int lppCurrent = 117; // 2 bytes 0.001A unsigned
|
||||
static const int lppFrequency = 118; // 4 bytes 1Hz unsigned
|
||||
static const int lppPercentage = 120; // 1 byte 1-100% unsigned
|
||||
static const int lppAltitude = 121; // 2 byte 1m signed
|
||||
static const int lppConcentration = 125; // 2 bytes, 1 ppm unsigned
|
||||
static const int lppPower = 128; // 2 byte, 1W, unsigned
|
||||
static const int lppDistance = 130; // 4 byte, 0.001m, unsigned
|
||||
static const int lppEnergy = 131; // 4 byte, 0.001kWh, unsigned
|
||||
static const int lppDirection = 132; // 2 bytes, 1deg, unsigned
|
||||
static const int lppUnixTime = 133; // 4 bytes, unsigned
|
||||
static const int lppGyrometer = 134; // 2 bytes per axis, 0.01 °/s
|
||||
static const int lppColour = 135; // 1 byte per RGB Color
|
||||
static const int lppGps = 136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
|
||||
static const int lppSwitch = 142; // 1 byte, 0/1
|
||||
static const int lppPolyline = 240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
|
||||
|
||||
final BufferWriter _writer = BufferWriter();
|
||||
|
||||
Uint8List toBytes() {
|
||||
return _writer.toBytes();
|
||||
}
|
||||
|
||||
void addDigitalInput(int channel, int value) {
|
||||
_writer.writeByte(channel);
|
||||
_writer.writeByte(lppDigitalInput);
|
||||
_writer.writeByte(value);
|
||||
}
|
||||
|
||||
void addTemperature(int channel, double value) {
|
||||
_writer.writeByte(channel);
|
||||
_writer.writeByte(lppTemperature);
|
||||
final val = (value * 10).toInt();
|
||||
_writer.writeBytes(_int16ToBE(val));
|
||||
}
|
||||
|
||||
void addVoltage(int channel, double value) {
|
||||
_writer.writeByte(channel);
|
||||
_writer.writeByte(lppVoltage);
|
||||
final val = (value * 100).toInt();
|
||||
_writer.writeBytes(_int16ToBE(val));
|
||||
}
|
||||
|
||||
void addGps(int channel, double lat, double lon, double alt) {
|
||||
_writer.writeByte(channel);
|
||||
_writer.writeByte(lppGps);
|
||||
_writer.writeBytes(_int24ToBE((lat * 10000).toInt()));
|
||||
_writer.writeBytes(_int24ToBE((lon * 10000).toInt()));
|
||||
_writer.writeBytes(_int24ToBE((alt * 100).toInt()));
|
||||
}
|
||||
|
||||
Uint8List _int16ToBE(int value) {
|
||||
final bytes = Uint8List(2);
|
||||
final data = ByteData.view(bytes.buffer);
|
||||
data.setInt16(0, value, Endian.big);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
Uint8List _int24ToBE(int value) {
|
||||
final bytes = Uint8List(3);
|
||||
bytes[0] = (value >> 16) & 0xFF;
|
||||
bytes[1] = (value >> 8) & 0xFF;
|
||||
bytes[2] = value & 0xFF;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>> parse(Uint8List bytes) {
|
||||
final buffer = BufferReader(bytes);
|
||||
final telemetry = <Map<String, dynamic>>[];
|
||||
|
||||
while (buffer.getRemainingBytesCount() >= 2) {
|
||||
final channel = buffer.readUInt8();
|
||||
final type = buffer.readUInt8();
|
||||
|
||||
if (channel == 0 && type == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case lppGenericSensor:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt32BE(),
|
||||
});
|
||||
break;
|
||||
case lppLuminosity:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppPresence:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppTemperature:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 10,
|
||||
});
|
||||
break;
|
||||
case lppRelativeHumidity:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8() / 2,
|
||||
});
|
||||
break;
|
||||
case lppBarometricPressure:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE() / 10,
|
||||
});
|
||||
break;
|
||||
case lppVoltage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 100,
|
||||
});
|
||||
break;
|
||||
case lppCurrent:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 1000,
|
||||
});
|
||||
break;
|
||||
case lppPercentage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppConcentration:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppPower:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppGps:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': {
|
||||
'latitude': buffer.readInt24BE() / 10000,
|
||||
'longitude': buffer.readInt24BE() / 10000,
|
||||
'altitude': buffer.readInt24BE() / 100,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return telemetry;
|
||||
}
|
||||
}
|
||||
|
||||
return telemetry;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import '../models/contact.dart';
|
|||
import 'repeater_status_screen.dart';
|
||||
import 'repeater_cli_screen.dart';
|
||||
import 'repeater_settings_screen.dart';
|
||||
import 'telemetry_screen.dart';
|
||||
|
||||
class RepeaterHubScreen extends StatelessWidget {
|
||||
final Contact repeater;
|
||||
|
|
@ -100,6 +101,26 @@ class RepeaterHubScreen extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Status button
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.bar_chart_sharp,
|
||||
title: 'Telemetry',
|
||||
subtitle: 'View telemetry of sensors and system stats',
|
||||
color: Colors.teal,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TelemetryScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// CLI button
|
||||
_buildManagementCard(
|
||||
|
|
|
|||
271
lib/screens/telemetry_screen.dart
Normal file
271
lib/screens/telemetry_screen.dart
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/path_selection.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/cayenne_lpp.dart';
|
||||
|
||||
class TelemetryScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
final String password;
|
||||
|
||||
const TelemetryScreen({
|
||||
super.key,
|
||||
required this.repeater,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TelemetryScreen> createState() => _TelemetryScreenState();
|
||||
}
|
||||
|
||||
class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
static const int _statusPayloadOffset = 8;
|
||||
static const int _statusStatsSize = 52;
|
||||
static const int _statusResponseBytes = _statusPayloadOffset + _statusStatsSize;
|
||||
Uint8List _tagData = Uint8List(4);
|
||||
int _timeEstment = 0;
|
||||
|
||||
bool _isLoading = false;
|
||||
Timer? _statusTimeout;
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
RepeaterCommandService? _commandService;
|
||||
PathSelection? _pendingStatusSelection;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
_commandService = RepeaterCommandService(connector);
|
||||
_setupMessageListener();
|
||||
_loadTelemetry();
|
||||
}
|
||||
|
||||
void _setupMessageListener() {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
|
||||
// Listen for incoming text messages from the repeater
|
||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty) return;
|
||||
|
||||
if(frame[0] == respCodeSent){
|
||||
_tagData = frame.sublist(2, 6);
|
||||
_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
|
||||
}
|
||||
|
||||
// Check if it's a binary response
|
||||
if (frame[0] == pushCodeBinaryResponse && listEquals(frame.sublist(2, 6), _tagData)) {
|
||||
_handleStatusResponse(context, frame.sublist(6));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleStatusResponse(BuildContext context, Uint8List frame) {
|
||||
|
||||
final parsedTelemetry = CayenneLpp.parse(frame);
|
||||
for (final entry in parsedTelemetry) {
|
||||
print('Telemetry - Channel: ${entry['channel']}, Type: ${entry['type']}, Value: ${entry['value']}');
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Received status response (not implemented).'),
|
||||
backgroundColor: Colors.green,
|
||||
)
|
||||
);
|
||||
_statusTimeout?.cancel();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadTelemetry() async {
|
||||
if (_commandService == null) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
try {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final repeater = _resolveRepeater(connector);
|
||||
final selection = await connector.preparePathForContactSend(repeater);
|
||||
_pendingStatusSelection = selection;
|
||||
final frame = buildSendBinaryReq(repeater.publicKey, payload: Uint8List.fromList([reqTypeGetTelemetry]));
|
||||
await connector.sendFrame(frame);
|
||||
|
||||
final pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
|
||||
final messageBytes = frame.length >= _statusResponseBytes
|
||||
? frame.length
|
||||
: _statusResponseBytes;
|
||||
final timeoutMs = connector.calculateTimeout(
|
||||
pathLength: pathLengthValue,
|
||||
messageBytes: messageBytes,
|
||||
);
|
||||
_statusTimeout?.cancel();
|
||||
_statusTimeout = Timer(Duration(milliseconds: timeoutMs), () {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Status request timed out.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
_recordStatusResult(false);
|
||||
});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error loading status: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _recordStatusResult(bool success) {
|
||||
final selection = _pendingStatusSelection;
|
||||
if (selection == null) return;
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final repeater = _resolveRepeater(connector);
|
||||
connector.recordRepeaterPathResult(repeater, selection, success, null);
|
||||
_pendingStatusSelection = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_frameSubscription?.cancel();
|
||||
_commandService?.dispose();
|
||||
_statusTimeout?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final repeater = _resolveRepeater(connector);
|
||||
final isFloodMode = repeater.pathOverride == -1;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Repeater Status'),
|
||||
Text(
|
||||
repeater.name,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
tooltip: 'Routing mode',
|
||||
onSelected: (mode) async {
|
||||
if (mode == 'flood') {
|
||||
await connector.setPathOverride(repeater, pathLen: -1);
|
||||
} else {
|
||||
await connector.setPathOverride(repeater, pathLen: null);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'auto',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Auto (use saved path)',
|
||||
style: TextStyle(
|
||||
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'flood',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Force Flood Mode',
|
||||
style: TextStyle(
|
||||
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.timeline),
|
||||
tooltip: 'Path management',
|
||||
onPressed: () => PathManagementDialog.show(context, contact: repeater),
|
||||
),
|
||||
IconButton(
|
||||
icon: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
onPressed: _isLoading ? null : _loadTelemetry,
|
||||
tooltip: 'Refresh',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _loadTelemetry,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Text("Not implemented yet", style: Theme.of(context).textTheme.bodyMedium),
|
||||
//_buildSystemInfoCard(),
|
||||
//const SizedBox(height: 16),
|
||||
//_buildRadioStatsCard(),
|
||||
//const SizedBox(height: 16),
|
||||
//_buildPacketStatsCard(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue