mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
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>
411 lines
7.1 KiB
Dart
411 lines
7.1 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
class Smaz {
|
|
static const int _verbatimSingle = 254;
|
|
static const int _verbatimRun = 255;
|
|
|
|
static const List<String> _rcb = [
|
|
" ",
|
|
"the",
|
|
"e",
|
|
"t",
|
|
"a",
|
|
"of",
|
|
"o",
|
|
"and",
|
|
"i",
|
|
"n",
|
|
"s",
|
|
"e ",
|
|
"r",
|
|
" th",
|
|
" t",
|
|
"in",
|
|
"he",
|
|
"th",
|
|
"h",
|
|
"he ",
|
|
"to",
|
|
"\r\n",
|
|
"l",
|
|
"s ",
|
|
"d",
|
|
" a",
|
|
"an",
|
|
"er",
|
|
"c",
|
|
" o",
|
|
"d ",
|
|
"on",
|
|
" of",
|
|
"re",
|
|
"of ",
|
|
"t ",
|
|
", ",
|
|
"is",
|
|
"u",
|
|
"at",
|
|
" ",
|
|
"n ",
|
|
"or",
|
|
"which",
|
|
"f",
|
|
"m",
|
|
"as",
|
|
"it",
|
|
"that",
|
|
"\n",
|
|
"was",
|
|
"en",
|
|
" ",
|
|
" w",
|
|
"es",
|
|
" an",
|
|
" i",
|
|
"\r",
|
|
"f ",
|
|
"g",
|
|
"p",
|
|
"nd",
|
|
" s",
|
|
"nd ",
|
|
"ed ",
|
|
"w",
|
|
"ed",
|
|
"http://",
|
|
"for",
|
|
"te",
|
|
"ing",
|
|
"y ",
|
|
"The",
|
|
" c",
|
|
"ti",
|
|
"r ",
|
|
"his",
|
|
"st",
|
|
" in",
|
|
"ar",
|
|
"nt",
|
|
",",
|
|
" to",
|
|
"y",
|
|
"ng",
|
|
" h",
|
|
"with",
|
|
"le",
|
|
"al",
|
|
"to ",
|
|
"b",
|
|
"ou",
|
|
"be",
|
|
"were",
|
|
" b",
|
|
"se",
|
|
"o ",
|
|
"ent",
|
|
"ha",
|
|
"ng ",
|
|
"their",
|
|
"\"",
|
|
"hi",
|
|
"from",
|
|
" f",
|
|
"in ",
|
|
"de",
|
|
"ion",
|
|
"me",
|
|
"v",
|
|
".",
|
|
"ve",
|
|
"all",
|
|
"re ",
|
|
"ri",
|
|
"ro",
|
|
"is ",
|
|
"co",
|
|
"f t",
|
|
"are",
|
|
"ea",
|
|
". ",
|
|
"her",
|
|
" m",
|
|
"er ",
|
|
" p",
|
|
"es ",
|
|
"by",
|
|
"they",
|
|
"di",
|
|
"ra",
|
|
"ic",
|
|
"not",
|
|
"s, ",
|
|
"d t",
|
|
"at ",
|
|
"ce",
|
|
"la",
|
|
"h ",
|
|
"ne",
|
|
"as ",
|
|
"tio",
|
|
"on ",
|
|
"n t",
|
|
"io",
|
|
"we",
|
|
" a ",
|
|
"om",
|
|
", a",
|
|
"s o",
|
|
"ur",
|
|
"li",
|
|
"ll",
|
|
"ch",
|
|
"had",
|
|
"this",
|
|
"e t",
|
|
"g ",
|
|
"e\r\n",
|
|
" wh",
|
|
"ere",
|
|
" co",
|
|
"e o",
|
|
"a ",
|
|
"us",
|
|
" d",
|
|
"ss",
|
|
"\n\r\n",
|
|
"\r\n\r",
|
|
"=\"",
|
|
" be",
|
|
" e",
|
|
"s a",
|
|
"ma",
|
|
"one",
|
|
"t t",
|
|
"or ",
|
|
"but",
|
|
"el",
|
|
"so",
|
|
"l ",
|
|
"e s",
|
|
"s,",
|
|
"no",
|
|
"ter",
|
|
" wa",
|
|
"iv",
|
|
"ho",
|
|
"e a",
|
|
" r",
|
|
"hat",
|
|
"s t",
|
|
"ns",
|
|
"ch ",
|
|
"wh",
|
|
"tr",
|
|
"ut",
|
|
"/",
|
|
"have",
|
|
"ly ",
|
|
"ta",
|
|
" ha",
|
|
" on",
|
|
"tha",
|
|
"-",
|
|
" l",
|
|
"ati",
|
|
"en ",
|
|
"pe",
|
|
" re",
|
|
"there",
|
|
"ass",
|
|
"si",
|
|
" fo",
|
|
"wa",
|
|
"ec",
|
|
"our",
|
|
"who",
|
|
"its",
|
|
"z",
|
|
"fo",
|
|
"rs",
|
|
">",
|
|
"ot",
|
|
"un",
|
|
"<",
|
|
"im",
|
|
"th ",
|
|
"nc",
|
|
"ate",
|
|
"><",
|
|
"ver",
|
|
"ad",
|
|
" we",
|
|
"ly",
|
|
"ee",
|
|
" n",
|
|
"id",
|
|
" cl",
|
|
"ac",
|
|
"il",
|
|
"</",
|
|
"rt",
|
|
" wi",
|
|
"div",
|
|
"e, ",
|
|
" it",
|
|
"whi",
|
|
" ma",
|
|
"ge",
|
|
"x",
|
|
"e c",
|
|
"men",
|
|
".com",
|
|
];
|
|
|
|
static final List<Uint8List> _rcbBytes =
|
|
_rcb.map((s) => Uint8List.fromList(ascii.encode(s))).toList(growable: false);
|
|
static final int _maxEntryLen = _rcbBytes.fold(0, (maxLen, entry) {
|
|
return entry.length > maxLen ? entry.length : maxLen;
|
|
});
|
|
|
|
static String encodeIfSmaller(String text) {
|
|
if (text.isEmpty || text.startsWith('s:')) return text;
|
|
final originalBytes = Uint8List.fromList(utf8.encode(text));
|
|
final compressed = compressBytes(originalBytes);
|
|
final encoded = base64Encode(compressed);
|
|
final candidate = 's:$encoded';
|
|
if (utf8.encode(candidate).length < originalBytes.length) {
|
|
return candidate;
|
|
}
|
|
return text;
|
|
}
|
|
|
|
static String? tryDecodePrefixed(String text) {
|
|
final trimmedLeft = text.trimLeft();
|
|
if (!trimmedLeft.startsWith('s:') || trimmedLeft.length <= 2) return null;
|
|
final encoded = trimmedLeft.substring(2);
|
|
try {
|
|
final compressed = _decodeBase64Flexible(encoded);
|
|
final decompressed = decompressBytes(compressed);
|
|
return utf8.decode(decompressed, allowMalformed: true);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Uint8List compressBytes(Uint8List input) {
|
|
final out = BytesBuilder(copy: false);
|
|
final verbatim = <int>[];
|
|
int index = 0;
|
|
|
|
void flushVerbatim() {
|
|
if (verbatim.isEmpty) return;
|
|
if (verbatim.length == 1) {
|
|
out.addByte(_verbatimSingle);
|
|
out.addByte(verbatim[0]);
|
|
} else {
|
|
out.addByte(_verbatimRun);
|
|
out.addByte(verbatim.length - 1);
|
|
out.add(verbatim);
|
|
}
|
|
verbatim.clear();
|
|
}
|
|
|
|
while (index < input.length) {
|
|
int bestLen = 0;
|
|
int bestCode = -1;
|
|
final remaining = input.length - index;
|
|
final maxLen = remaining < _maxEntryLen ? remaining : _maxEntryLen;
|
|
|
|
for (int code = 0; code < _rcbBytes.length; code++) {
|
|
final entry = _rcbBytes[code];
|
|
final entryLen = entry.length;
|
|
if (entryLen == 0 || entryLen > maxLen || entryLen <= bestLen) {
|
|
continue;
|
|
}
|
|
if (_matches(input, index, entry)) {
|
|
bestLen = entryLen;
|
|
bestCode = code;
|
|
if (bestLen == maxLen) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestCode >= 0) {
|
|
flushVerbatim();
|
|
out.addByte(bestCode);
|
|
index += bestLen;
|
|
continue;
|
|
}
|
|
|
|
verbatim.add(input[index]);
|
|
index++;
|
|
if (verbatim.length == 256) {
|
|
flushVerbatim();
|
|
}
|
|
}
|
|
|
|
flushVerbatim();
|
|
return out.toBytes();
|
|
}
|
|
|
|
static Uint8List decompressBytes(Uint8List input) {
|
|
final out = BytesBuilder(copy: false);
|
|
int index = 0;
|
|
|
|
while (index < input.length) {
|
|
final code = input[index];
|
|
if (code == _verbatimSingle) {
|
|
if (index + 1 >= input.length) {
|
|
throw const FormatException('Invalid SMAZ stream: truncated verbatim byte.');
|
|
}
|
|
out.addByte(input[index + 1]);
|
|
index += 2;
|
|
} else if (code == _verbatimRun) {
|
|
if (index + 1 >= input.length) {
|
|
throw const FormatException('Invalid SMAZ stream: truncated verbatim length.');
|
|
}
|
|
final len = input[index + 1] + 1;
|
|
final end = index + 2 + len;
|
|
if (end > input.length) {
|
|
throw const FormatException('Invalid SMAZ stream: truncated verbatim run.');
|
|
}
|
|
out.add(input.sublist(index + 2, end));
|
|
index = end;
|
|
} else {
|
|
if (code >= _rcbBytes.length) {
|
|
throw const FormatException('Invalid SMAZ stream: code out of range.');
|
|
}
|
|
out.add(_rcbBytes[code]);
|
|
index += 1;
|
|
}
|
|
}
|
|
|
|
return out.toBytes();
|
|
}
|
|
|
|
static bool _matches(Uint8List input, int offset, Uint8List entry) {
|
|
final len = entry.length;
|
|
if (offset + len > input.length) return false;
|
|
for (int i = 0; i < len; i++) {
|
|
if (input[offset + i] != entry[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static Uint8List _decodeBase64Flexible(String encoded) {
|
|
final trimmed = encoded.trim();
|
|
try {
|
|
return base64Decode(trimmed);
|
|
} catch (_) {
|
|
// Try base64url with missing padding.
|
|
var normalized = trimmed.replaceAll('-', '+').replaceAll('_', '/');
|
|
final pad = normalized.length % 4;
|
|
if (pad != 0) {
|
|
normalized = normalized.padRight(normalized.length + (4 - pad), '=');
|
|
}
|
|
return base64Decode(normalized);
|
|
}
|
|
}
|
|
}
|