mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-26 13:36:39 +10:00
Added telemetry to repeater interface.
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
|
import 'dart:collection';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:ffi';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
// Command codes (to device)
|
// Command codes (to device)
|
||||||
@@ -29,6 +31,8 @@ const int cmdGetContactByKey = 30;
|
|||||||
const int cmdGetChannel = 31;
|
const int cmdGetChannel = 31;
|
||||||
const int cmdSetChannel = 32;
|
const int cmdSetChannel = 32;
|
||||||
const int cmdGetRadioSettings = 57;
|
const int cmdGetRadioSettings = 57;
|
||||||
|
const int cmdGetTelemetryReq = 39;
|
||||||
|
const int cmdSendBinaryReq = 50;
|
||||||
|
|
||||||
// Text message types
|
// Text message types
|
||||||
const int txtTypePlain = 0;
|
const int txtTypePlain = 0;
|
||||||
@@ -73,6 +77,9 @@ const int pushCodeLoginFail = 0x86;
|
|||||||
const int pushCodeStatusResponse = 0x87;
|
const int pushCodeStatusResponse = 0x87;
|
||||||
const int pushCodeLogRxData = 0x88;
|
const int pushCodeLogRxData = 0x88;
|
||||||
const int pushCodeNewAdvert = 0x8A;
|
const int pushCodeNewAdvert = 0x8A;
|
||||||
|
const int pushCodeTelemetryResponse = 0x8B;
|
||||||
|
const int pushCodeBinaryResponse = 0x8C;
|
||||||
|
|
||||||
|
|
||||||
// Contact/advertisement types
|
// Contact/advertisement types
|
||||||
const int advTypeChat = 1;
|
const int advTypeChat = 1;
|
||||||
@@ -614,3 +621,23 @@ Uint8List buildSendCliCommandFrame(
|
|||||||
frame[frame.length - 1] = 0; // null terminator
|
frame[frame.length - 1] = 0; // null terminator
|
||||||
return frame;
|
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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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_status_screen.dart';
|
||||||
import 'repeater_cli_screen.dart';
|
import 'repeater_cli_screen.dart';
|
||||||
import 'repeater_settings_screen.dart';
|
import 'repeater_settings_screen.dart';
|
||||||
|
import 'telemetry_screen.dart';
|
||||||
|
|
||||||
class RepeaterHubScreen extends StatelessWidget {
|
class RepeaterHubScreen extends StatelessWidget {
|
||||||
final Contact repeater;
|
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),
|
const SizedBox(height: 12),
|
||||||
// CLI button
|
// CLI button
|
||||||
_buildManagementCard(
|
_buildManagementCard(
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user