Initial commit: MeshCore Open Flutter client

Open-source Flutter client for MeshCore LoRa mesh networking devices.

Features:
- BLE device scanning and connection
- Nordic UART Service (NUS) integration
- Material 3 design with system theme support
- Provider-based state management
- Placeholder screens for chat, contacts, and settings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
zach
2025-12-26 11:42:02 -07:00
commit e7a5b9e209
177 changed files with 20129 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
class AppSettings {
final bool clearPathOnMaxRetry;
final bool mapShowRepeaters;
final bool mapShowChatNodes;
final bool mapShowOtherNodes;
final double mapTimeFilterHours; // 0 = all time
final bool mapKeyPrefixEnabled;
final String mapKeyPrefix;
final bool mapShowMarkers;
final bool notificationsEnabled;
final bool notifyOnNewMessage;
final bool notifyOnNewAdvert;
final bool autoRouteRotationEnabled;
final String themeMode;
final Map<String, String> batteryChemistryByDeviceId;
AppSettings({
this.clearPathOnMaxRetry = false,
this.mapShowRepeaters = true,
this.mapShowChatNodes = true,
this.mapShowOtherNodes = true,
this.mapTimeFilterHours = 0, // Default to all time
this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '',
this.mapShowMarkers = true,
this.notificationsEnabled = true,
this.notifyOnNewMessage = true,
this.notifyOnNewAdvert = true,
this.autoRouteRotationEnabled = false,
this.themeMode = 'system',
Map<String, String>? batteryChemistryByDeviceId,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {};
Map<String, dynamic> toJson() {
return {
'clear_path_on_max_retry': clearPathOnMaxRetry,
'map_show_repeaters': mapShowRepeaters,
'map_show_chat_nodes': mapShowChatNodes,
'map_show_other_nodes': mapShowOtherNodes,
'map_time_filter_hours': mapTimeFilterHours,
'map_key_prefix_enabled': mapKeyPrefixEnabled,
'map_key_prefix': mapKeyPrefix,
'map_show_markers': mapShowMarkers,
'notifications_enabled': notificationsEnabled,
'notify_on_new_message': notifyOnNewMessage,
'notify_on_new_advert': notifyOnNewAdvert,
'auto_route_rotation_enabled': autoRouteRotationEnabled,
'theme_mode': themeMode,
'battery_chemistry_by_device_id': batteryChemistryByDeviceId,
};
}
factory AppSettings.fromJson(Map<String, dynamic> json) {
return AppSettings(
clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false,
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
mapTimeFilterHours: (json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
mapShowMarkers: json['map_show_markers'] as bool? ?? true,
notificationsEnabled: json['notifications_enabled'] as bool? ?? true,
notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true,
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
autoRouteRotationEnabled: json['auto_route_rotation_enabled'] as bool? ?? false,
themeMode: json['theme_mode'] as String? ?? 'system',
batteryChemistryByDeviceId: (json['battery_chemistry_by_device_id'] as Map?)?.map(
(key, value) => MapEntry(key.toString(), value.toString()),
) ??
{},
);
}
AppSettings copyWith({
bool? clearPathOnMaxRetry,
bool? mapShowRepeaters,
bool? mapShowChatNodes,
bool? mapShowOtherNodes,
double? mapTimeFilterHours,
bool? mapKeyPrefixEnabled,
String? mapKeyPrefix,
bool? mapShowMarkers,
bool? notificationsEnabled,
bool? notifyOnNewMessage,
bool? notifyOnNewAdvert,
bool? autoRouteRotationEnabled,
String? themeMode,
Map<String, String>? batteryChemistryByDeviceId,
}) {
return AppSettings(
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
mapShowRepeaters: mapShowRepeaters ?? this.mapShowRepeaters,
mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes,
mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes,
mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours,
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage,
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
autoRouteRotationEnabled: autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
themeMode: themeMode ?? this.themeMode,
batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
);
}
}
+57
View File
@@ -0,0 +1,57 @@
import 'dart:convert';
import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
class Channel {
final int index;
final String name;
final Uint8List psk; // 16 bytes
Channel({
required this.index,
required this.name,
required this.psk,
});
String get pskBase64 => base64Encode(psk);
bool get isEmpty => name.isEmpty && psk.every((b) => b == 0);
bool get isPublicChannel => pskBase64 == publicChannelPsk;
static Channel? fromFrame(Uint8List data) {
// CHANNEL_INFO format:
// [0] = RESP_CODE_CHANNEL_INFO (18)
// [1] = channel_idx
// [2-33] = name (32 bytes, null-terminated)
// [34-49] = psk (16 bytes)
if (data.length < 50) return null;
if (data[0] != respCodeChannelInfo) return null;
final index = data[1];
final name = readCString(data, 2, 32);
final psk = Uint8List.fromList(data.sublist(34, 50));
return Channel(index: index, name: name, psk: psk);
}
static Channel empty(int index) {
return Channel(
index: index,
name: '',
psk: Uint8List(16),
);
}
static Channel fromPsk(int index, String name, String pskBase64) {
final pskBytes = base64Decode(pskBase64);
final psk = Uint8List(16);
for (int i = 0; i < pskBytes.length && i < 16; i++) {
psk[i] = pskBytes[i];
}
return Channel(index: index, name: name, psk: psk);
}
static const String publicChannelPsk = 'izOH6cXN6mrJ5e26oRXNcg==';
}
+170
View File
@@ -0,0 +1,170 @@
import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
import '../helpers/smaz.dart';
enum ChannelMessageStatus { pending, sent, failed }
class Repeat {
final Uint8List? repeaterKey;
final String repeaterName;
final int tripTimeMs;
final List<Uint8List>? path;
Repeat({
this.repeaterKey,
required this.repeaterName,
required this.tripTimeMs,
this.path,
});
String? get repeaterKeyHex =>
repeaterKey != null ? pubKeyToHex(repeaterKey!) : null;
}
class ChannelMessage {
final Uint8List? senderKey;
final String senderName;
final String text;
final DateTime timestamp;
final bool isOutgoing;
final ChannelMessageStatus status;
final List<Repeat> repeats;
final int repeatCount;
final int? pathLength;
final Uint8List pathBytes;
final int? channelIndex;
ChannelMessage({
this.senderKey,
required this.senderName,
required this.text,
required this.timestamp,
required this.isOutgoing,
this.status = ChannelMessageStatus.pending,
this.repeats = const [],
this.repeatCount = 0,
this.pathLength,
Uint8List? pathBytes,
this.channelIndex,
}) : pathBytes = pathBytes ?? Uint8List(0);
String? get senderKeyHex => senderKey != null ? pubKeyToHex(senderKey!) : null;
ChannelMessage copyWith({
ChannelMessageStatus? status,
List<Repeat>? repeats,
int? repeatCount,
int? pathLength,
Uint8List? pathBytes,
}) {
return ChannelMessage(
senderKey: senderKey,
senderName: senderName,
text: text,
timestamp: timestamp,
isOutgoing: isOutgoing,
status: status ?? this.status,
repeats: repeats ?? this.repeats,
repeatCount: repeatCount ?? this.repeatCount,
pathLength: pathLength ?? this.pathLength,
pathBytes: pathBytes ?? this.pathBytes,
channelIndex: channelIndex,
);
}
static ChannelMessage? fromFrame(Uint8List data) {
// CHANNEL_MSG_RECV format varies by version:
// V3: [0]=code [1]=SNR [2]=rsv1 [3]=rsv2 [4]=channel_idx [5]=path_len [path... optional] [txt_type] [timestamp x4] [text...]
// Non-V3: [0]=code [1]=channel_idx [2]=path_len [3]=txt_type [4-7]=timestamp [8+]=text
if (data.length < 8) return null;
final code = data[0];
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
return null;
}
int timestampOffset, textOffset, pathLenOffset, txtTypeOffset;
Uint8List pathBytes = Uint8List(0);
int channelIdx;
if (code == respCodeChannelMsgRecvV3) {
channelIdx = data[4];
pathLenOffset = 5;
final pathLen = data[pathLenOffset].toSigned(8);
var cursor = 6;
final hasPathBytesFlag = (data[2] & 0x01) != 0;
final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5;
final hasValidTxtType =
cursor < data.length && (data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) && canFitPath) {
pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
cursor += pathLen;
}
txtTypeOffset = cursor;
cursor += 1; // txt_type
timestampOffset = cursor;
textOffset = cursor + 4;
} else {
channelIdx = data[1];
pathLenOffset = 2;
txtTypeOffset = 3;
timestampOffset = 4;
textOffset = 8;
}
if (data.length < textOffset + 1) return null;
final txtType = data[txtTypeOffset];
if (txtType != txtTypePlain) {
return null;
}
final pathLen = data[pathLenOffset].toSigned(8);
final timestampRaw = readUint32LE(data, timestampOffset);
final text = readCString(data, textOffset, data.length - textOffset);
// Extract sender name and actual message from "name: msg" format
String senderName = 'Unknown';
String actualText = text;
final colonIndex = text.indexOf(':');
if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
final potentialSender = text.substring(0, colonIndex);
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
senderName = potentialSender;
final offset = (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
? colonIndex + 2
: colonIndex + 1;
actualText = text.substring(offset);
}
}
final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
return ChannelMessage(
senderKey: null,
senderName: senderName,
text: decodedText,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
isOutgoing: false,
status: ChannelMessageStatus.sent,
pathLength: pathLen,
pathBytes: pathBytes,
channelIndex: channelIdx,
);
}
static ChannelMessage outgoing(String text, String senderName, int channelIndex) {
return ChannelMessage(
senderKey: null,
senderName: senderName,
text: text,
timestamp: DateTime.now(),
isOutgoing: true,
status: ChannelMessageStatus.pending,
pathLength: null,
pathBytes: Uint8List(0),
channelIndex: channelIndex,
);
}
}
+110
View File
@@ -0,0 +1,110 @@
import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
class Contact {
final Uint8List publicKey;
final String name;
final int type;
final int pathLength; // -1 = flood, 0+ = direct hops
final Uint8List path;
final double? latitude;
final double? longitude;
final DateTime lastSeen;
Contact({
required this.publicKey,
required this.name,
required this.type,
required this.pathLength,
required this.path,
this.latitude,
this.longitude,
required this.lastSeen,
});
String get publicKeyHex => pubKeyToHex(publicKey);
String get typeLabel {
switch (type) {
case advTypeChat:
return 'Chat';
case advTypeRepeater:
return 'Repeater';
case advTypeRoom:
return 'Room';
case advTypeSensor:
return 'Sensor';
default:
return 'Unknown';
}
}
String get pathLabel {
if (pathLength < 0) return 'Flood';
if (pathLength == 0) return 'Direct';
return '$pathLength hops';
}
bool get hasLocation => latitude != null && longitude != null;
String get pathIdList {
if (path.isEmpty) return '';
final parts = <String>[];
final groupSize = pathHashSize;
for (int i = 0; i < path.length; i += groupSize) {
final end = (i + groupSize) <= path.length ? (i + groupSize) : path.length;
final chunk = path.sublist(i, end);
parts.add(
chunk.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()).join(),
);
}
return parts.join(',');
}
static Contact? fromFrame(Uint8List data) {
if (data.length < contactFrameSize) return null;
if (data[0] != respCodeContact) return null;
final pubKey = Uint8List.fromList(
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
);
final type = data[contactTypeOffset];
final pathLen = data[contactPathLenOffset].toSigned(8);
final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
: 0;
final pathBytes = safePathLen > 0
? Uint8List.fromList(
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
)
: Uint8List(0);
final name = readCString(data, contactNameOffset, maxNameSize);
final lastmod = readUint32LE(data, contactLastmodOffset);
double? lat, lon;
final latRaw = readInt32LE(data, contactLatOffset);
final lonRaw = readInt32LE(data, contactLonOffset);
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
}
return Contact(
publicKey: pubKey,
name: name.isEmpty ? 'Unknown' : name,
type: type,
pathLength: pathLen,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
);
}
@override
bool operator ==(Object other) =>
other is Contact && publicKeyHex == other.publicKeyHex;
@override
int get hashCode => publicKeyHex.hashCode;
}
+37
View File
@@ -0,0 +1,37 @@
class ContactGroup {
final String name;
final List<String> memberKeys;
const ContactGroup({
required this.name,
required this.memberKeys,
});
ContactGroup copyWith({
String? name,
List<String>? memberKeys,
}) {
return ContactGroup(
name: name ?? this.name,
memberKeys: memberKeys ?? List<String>.from(this.memberKeys),
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'members': memberKeys,
};
}
factory ContactGroup.fromJson(Map<String, dynamic> json) {
final members = (json['members'] as List?)
?.map((value) => value.toString())
.toList() ??
<String>[];
return ContactGroup(
name: json['name'] as String? ?? '',
memberKeys: members,
);
}
}
+125
View File
@@ -0,0 +1,125 @@
import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
enum MessageStatus { pending, sent, delivered, failed }
class Message {
final Uint8List senderKey;
final String text;
final DateTime timestamp;
final bool isOutgoing;
final bool isCli;
final MessageStatus status;
// NEW: Retry logic fields
final String? messageId;
final int retryCount;
final int? estimatedTimeoutMs;
final Uint8List? expectedAckHash;
final DateTime? sentAt;
final DateTime? deliveredAt;
final int? tripTimeMs;
final bool forceFlood;
final int? pathLength;
final Uint8List pathBytes;
Message({
required this.senderKey,
required this.text,
required this.timestamp,
required this.isOutgoing,
this.isCli = false,
this.status = MessageStatus.pending,
this.messageId,
this.retryCount = 0,
this.estimatedTimeoutMs,
this.expectedAckHash,
this.sentAt,
this.deliveredAt,
this.tripTimeMs,
this.forceFlood = false,
this.pathLength,
Uint8List? pathBytes,
}) : pathBytes = pathBytes ?? Uint8List(0);
String get senderKeyHex => pubKeyToHex(senderKey);
Message copyWith({
MessageStatus? status,
int? retryCount,
int? estimatedTimeoutMs,
Uint8List? expectedAckHash,
DateTime? sentAt,
DateTime? deliveredAt,
int? tripTimeMs,
int? pathLength,
Uint8List? pathBytes,
bool? isCli,
}) {
return Message(
senderKey: senderKey,
text: text,
timestamp: timestamp,
isOutgoing: isOutgoing,
isCli: isCli ?? this.isCli,
status: status ?? this.status,
messageId: messageId,
retryCount: retryCount ?? this.retryCount,
estimatedTimeoutMs: estimatedTimeoutMs ?? this.estimatedTimeoutMs,
expectedAckHash: expectedAckHash ?? this.expectedAckHash,
sentAt: sentAt ?? this.sentAt,
deliveredAt: deliveredAt ?? this.deliveredAt,
tripTimeMs: tripTimeMs ?? this.tripTimeMs,
forceFlood: forceFlood,
pathLength: pathLength ?? this.pathLength,
pathBytes: pathBytes ?? this.pathBytes,
);
}
static Message? fromFrame(Uint8List data, Uint8List selfPubKey) {
if (data.length < msgTextOffset + 1) return null;
final code = data[0];
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
return null;
}
final senderKey = Uint8List.fromList(
data.sublist(msgPubKeyOffset, msgPubKeyOffset + pubKeySize),
);
final timestampRaw = readUint32LE(data, msgTimestampOffset);
final flags = data[msgFlagsOffset];
if ((flags >> 2) != txtTypePlain) {
return null;
}
final text = readCString(data, msgTextOffset, data.length - msgTextOffset);
return Message(
senderKey: senderKey,
text: text,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
isOutgoing: false,
isCli: false,
status: MessageStatus.delivered,
pathBytes: Uint8List(0),
);
}
static Message outgoing(
Uint8List recipientKey,
String text, {
int? pathLength,
Uint8List? pathBytes,
}) {
return Message(
senderKey: recipientKey,
text: text,
timestamp: DateTime.now(),
isOutgoing: true,
isCli: false,
status: MessageStatus.pending,
pathLength: pathLength,
pathBytes: pathBytes,
);
}
}
+85
View File
@@ -0,0 +1,85 @@
class PathRecord {
final int hopCount;
final int tripTimeMs;
final DateTime timestamp;
final bool wasFloodDiscovery;
final List<int> pathBytes;
final int successCount;
final int failureCount;
PathRecord({
required this.hopCount,
required this.tripTimeMs,
required this.timestamp,
required this.wasFloodDiscovery,
required this.pathBytes,
required this.successCount,
required this.failureCount,
});
String get displayText =>
'$hopCount ${hopCount == 1 ? 'hop' : 'hops'} - ${(tripTimeMs / 1000).toStringAsFixed(2)}s';
Map<String, dynamic> toJson() {
return {
'hop_count': hopCount,
'trip_time_ms': tripTimeMs,
'timestamp': timestamp.toIso8601String(),
'was_flood': wasFloodDiscovery,
'path_bytes': pathBytes,
'success_count': successCount,
'failure_count': failureCount,
};
}
factory PathRecord.fromJson(Map<String, dynamic> json) {
return PathRecord(
hopCount: json['hop_count'] as int,
tripTimeMs: json['trip_time_ms'] as int,
timestamp: DateTime.parse(json['timestamp'] as String),
wasFloodDiscovery: json['was_flood'] as bool,
pathBytes: (json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
successCount: json['success_count'] as int? ?? 0,
failureCount: json['failure_count'] as int? ?? 0,
);
}
}
class ContactPathHistory {
final String contactPubKeyHex;
final List<PathRecord> recentPaths;
ContactPathHistory({
required this.contactPubKeyHex,
required this.recentPaths,
});
PathRecord? get fastest {
if (recentPaths.isEmpty) return null;
return recentPaths.reduce((a, b) => a.tripTimeMs < b.tripTimeMs ? a : b);
}
PathRecord? get mostRecent {
if (recentPaths.isEmpty) return null;
return recentPaths.first;
}
Map<String, dynamic> toJson() {
return {
'recent_paths': recentPaths.map((p) => p.toJson()).toList(),
};
}
factory ContactPathHistory.fromJson(
String contactPubKeyHex, Map<String, dynamic> json) {
final pathsList = (json['recent_paths'] as List?)
?.map((p) => PathRecord.fromJson(p as Map<String, dynamic>))
.toList() ??
[];
return ContactPathHistory(
contactPubKeyHex: contactPubKeyHex,
recentPaths: pathsList,
);
}
}
+11
View File
@@ -0,0 +1,11 @@
class PathSelection {
final List<int> pathBytes;
final int hopCount;
final bool useFlood;
const PathSelection({
required this.pathBytes,
required this.hopCount,
required this.useFlood,
});
}
+107
View File
@@ -0,0 +1,107 @@
enum LoRaBandwidth {
bw7_8(7800, '7.8 kHz'),
bw10_4(10400, '10.4 kHz'),
bw15_6(15600, '15.6 kHz'),
bw20_8(20800, '20.8 kHz'),
bw31_25(31250, '31.25 kHz'),
bw41_7(41700, '41.7 kHz'),
bw62_5(62500, '62.5 kHz'),
bw125(125000, '125 kHz'),
bw250(250000, '250 kHz'),
bw500(500000, '500 kHz');
final int hz;
final String label;
const LoRaBandwidth(this.hz, this.label);
}
enum LoRaSpreadingFactor {
sf5(5, 'SF5'),
sf6(6, 'SF6'),
sf7(7, 'SF7'),
sf8(8, 'SF8'),
sf9(9, 'SF9'),
sf10(10, 'SF10'),
sf11(11, 'SF11'),
sf12(12, 'SF12');
final int value;
final String label;
const LoRaSpreadingFactor(this.value, this.label);
}
enum LoRaCodingRate {
cr4_5(5, '4/5'),
cr4_6(6, '4/6'),
cr4_7(7, '4/7'),
cr4_8(8, '4/8');
final int value;
final String label;
const LoRaCodingRate(this.value, this.label);
}
class RadioSettings {
final double frequencyMHz;
final LoRaBandwidth bandwidth;
final LoRaSpreadingFactor spreadingFactor;
final LoRaCodingRate codingRate;
final int txPowerDbm;
RadioSettings({
required this.frequencyMHz,
required this.bandwidth,
required this.spreadingFactor,
required this.codingRate,
required this.txPowerDbm,
});
// Preset configurations
static RadioSettings get preset915MHz => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
static RadioSettings get preset868MHz => RadioSettings(
frequencyMHz: 868.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
);
static RadioSettings get preset433MHz => RadioSettings(
frequencyMHz: 433.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
static RadioSettings get presetLongRange => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf12,
codingRate: LoRaCodingRate.cr4_8,
txPowerDbm: 20,
);
static RadioSettings get presetFastSpeed => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw500,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
int get frequencyHz => (frequencyMHz * 1000).round();
int get bandwidthHz => bandwidth.hz;
int get sf => spreadingFactor.value;
int get cr => codingRate.value;
}