You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
337 lines
13 KiB
337 lines
13 KiB
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:drift/native.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:meshcore_open/connector/device_population.dart';
|
|
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
|
import 'package:meshcore_open/storage/drift/blob_store.dart';
|
|
import 'package:meshcore_open/storage/drift/offband_database.dart';
|
|
import 'package:meshcore_open/storage/prefs_manager.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// Builds a device-info reply frame of [length] bytes with [verCode] at
|
|
/// byte 1, mirroring RESP_CODE_DEVICE_INFO layouts:
|
|
/// stock ships exactly 82 bytes; the Offband fork appends offband_caps at
|
|
/// offset 82 (83 bytes) and caps byte 2 at offset 84 (85 bytes).
|
|
Uint8List deviceInfoFrame({required int length, required int verCode}) {
|
|
final frame = Uint8List(length);
|
|
if (length > 0) frame[0] = 13; // RESP_CODE_DEVICE_INFO
|
|
if (length > 1) frame[1] = verCode;
|
|
return frame;
|
|
}
|
|
|
|
void main() {
|
|
group('classifyDevicePopulation', () {
|
|
test('stock 1.17.x: 82-byte frame, ver 13', () {
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 82, verCode: 13),
|
|
firmwareVersion: 'v1.17.0',
|
|
);
|
|
expect(c.population, DevicePopulation.stockLegacy);
|
|
expect(c.allowsOffbandFrames, isFalse);
|
|
expect(c.allowsCliFrame, isFalse);
|
|
});
|
|
|
|
test('stock 1.18.0: 82-byte frame, ver 14', () {
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 82, verCode: 14),
|
|
firmwareVersion: 'v1.18.0',
|
|
);
|
|
expect(c.population, DevicePopulation.stockModern);
|
|
expect(c.allowsOffbandFrames, isFalse);
|
|
expect(c.allowsCliFrame, isTrue);
|
|
});
|
|
|
|
test('Offband ver 22: 83-byte frame (caps byte 1 only)', () {
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 83, verCode: 22),
|
|
firmwareVersion: '1.4.0-1.17.0',
|
|
);
|
|
expect(c.population, DevicePopulation.offband);
|
|
expect(c.allowsOffbandFrames, isTrue);
|
|
// No frame-66 CLI on the 1.17.0-base fork; flips only post-absorb.
|
|
expect(c.allowsCliFrame, isFalse);
|
|
});
|
|
|
|
test('Offband ver 22: 85-byte frame (caps byte 2 present)', () {
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 85, verCode: 22),
|
|
firmwareVersion: '1.4.0-1.17.0',
|
|
);
|
|
expect(c.population, DevicePopulation.offband);
|
|
expect(c.allowsOffbandFrames, isTrue);
|
|
});
|
|
|
|
test(
|
|
'Offband release image X-X version string (#691) still classifies',
|
|
() {
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 85, verCode: 22),
|
|
firmwareVersion: '1.4.0-1.4.0',
|
|
);
|
|
expect(c.population, DevicePopulation.offband);
|
|
},
|
|
);
|
|
|
|
test('ver >= 14 with a LONG frame but stock version string is a conflict:'
|
|
' fails closed to stockLegacy', () {
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 83, verCode: 14),
|
|
firmwareVersion: 'v1.18.0',
|
|
);
|
|
expect(c.population, DevicePopulation.stockLegacy);
|
|
expect(c.allowsOffbandFrames, isFalse);
|
|
expect(c.allowsCliFrame, isFalse);
|
|
expect(c.conflict, isTrue);
|
|
});
|
|
|
|
test('malformed short frame fails closed', () {
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 3, verCode: 0),
|
|
firmwareVersion: null,
|
|
);
|
|
expect(c.population, DevicePopulation.stockLegacy);
|
|
expect(c.allowsOffbandFrames, isFalse);
|
|
expect(c.allowsCliFrame, isFalse);
|
|
});
|
|
|
|
test('fork-shape frame with NO readable version fails closed'
|
|
' (uncorroborated shape never unlocks fork frames)', () {
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 83, verCode: 22),
|
|
firmwareVersion: null,
|
|
);
|
|
expect(c.population, DevicePopulation.stockLegacy);
|
|
expect(c.conflict, isTrue);
|
|
expect(c.allowsOffbandFrames, isFalse);
|
|
});
|
|
|
|
test('fork-shape frame with an unrecognizable version fails closed', () {
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 83, verCode: 22),
|
|
firmwareVersion: 'my-custom-build',
|
|
);
|
|
// #878: 'my-custom-build' is no semver, so it corroborates nothing -
|
|
// the fork shape stays uncorroborated and the device fails closed.
|
|
expect(c.population, DevicePopulation.stockLegacy);
|
|
expect(c.conflict, isTrue);
|
|
});
|
|
|
|
group('classifyVersionString (#878)', () {
|
|
test('stock forms', () {
|
|
expect(classifyVersionString('v1.18.0'), VersionStringShape.stock);
|
|
expect(classifyVersionString('1.18.0'), VersionStringShape.stock);
|
|
expect(classifyVersionString(' v1.17.1 '), VersionStringShape.stock);
|
|
});
|
|
test('Offband dual forms', () {
|
|
expect(
|
|
classifyVersionString('1.4.0-1.17.0'),
|
|
VersionStringShape.offbandDual,
|
|
);
|
|
expect(
|
|
classifyVersionString('1.4.0-1.4.0'), // release image X-X (#691)
|
|
VersionStringShape.offbandDual,
|
|
);
|
|
expect(
|
|
classifyVersionString('1.1.0-4-g9f2e1c3-dirty'), // bench build
|
|
VersionStringShape.offbandDual,
|
|
);
|
|
});
|
|
test('unknown forms', () {
|
|
expect(classifyVersionString(null), VersionStringShape.unknown);
|
|
expect(classifyVersionString(''), VersionStringShape.unknown);
|
|
expect(
|
|
classifyVersionString('my-custom-build'),
|
|
VersionStringShape.unknown,
|
|
);
|
|
expect(classifyVersionString('v1.18'), VersionStringShape.unknown);
|
|
// A stock pre-release / git-suffixed string reads as dual at the parser
|
|
// level; that is harmless now, because a stock-shape frame is classified
|
|
// by ver code regardless of the version string (#946).
|
|
expect(
|
|
classifyVersionString('v1.19.0-rc1'),
|
|
VersionStringShape.offbandDual,
|
|
);
|
|
expect(classifyVersionString('-1.17.0'), VersionStringShape.unknown);
|
|
});
|
|
});
|
|
|
|
test('real stock 1.18.0 with a git-suffixed version classifies'
|
|
' stockModern (#946, hardware)', () {
|
|
// The field device reports "v1.18.0-81dbae5" (semver + git-describe
|
|
// hash), which classifyVersionString reads as the dual form. An 82-byte
|
|
// frame is definitively stock (Offband always appends caps bytes), so the
|
|
// version string must not override the shape: the console must be offered.
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 82, verCode: 14),
|
|
firmwareVersion: 'v1.18.0-81dbae5',
|
|
);
|
|
expect(c.population, DevicePopulation.stockModern);
|
|
expect(c.allowsCliFrame, isTrue);
|
|
expect(c.conflict, isFalse);
|
|
});
|
|
|
|
test('stock-shape frame with a dual-looking version is still stockModern:'
|
|
' the 82-byte shape is definitive (#946)', () {
|
|
// Previously an inverse conflict (fail closed); the shape is definitive,
|
|
// so a stock-shape frame is never forced to a conflict by its version.
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 82, verCode: 14),
|
|
firmwareVersion: '1.4.0-1.17.0',
|
|
);
|
|
expect(c.population, DevicePopulation.stockModern);
|
|
expect(c.conflict, isFalse);
|
|
expect(c.allowsCliFrame, isTrue);
|
|
});
|
|
|
|
test('length 84 with a dual version string classifies offband', () {
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 84, verCode: 22),
|
|
firmwareVersion: '1.4.0-1.17.0',
|
|
);
|
|
expect(c.population, DevicePopulation.offband);
|
|
});
|
|
|
|
test('ver at maxKnownVerCode with stock shape and stock string stays'
|
|
' stockModern; one above fails closed', () {
|
|
final at = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 82, verCode: 32),
|
|
firmwareVersion: 'v9.9.9',
|
|
);
|
|
expect(at.population, DevicePopulation.stockModern);
|
|
final above = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 82, verCode: 33),
|
|
firmwareVersion: 'v9.9.9',
|
|
);
|
|
expect(above.population, DevicePopulation.stockLegacy);
|
|
});
|
|
|
|
test('stock ver wildly out of range fails closed', () {
|
|
final c = classifyDevicePopulation(
|
|
deviceInfo: deviceInfoFrame(length: 82, verCode: 255),
|
|
firmwareVersion: 'v9.9.9',
|
|
);
|
|
expect(c.population, DevicePopulation.stockLegacy);
|
|
});
|
|
});
|
|
|
|
group('connector routes caps parsing through the classifier (#876)', () {
|
|
late OffbandDatabase db;
|
|
late MeshCoreConnector connector;
|
|
|
|
/// Full RESP_CODE_DEVICE_INFO frame with the version string at its real
|
|
/// offset (60, 20 bytes NUL-padded) and optional fork tail bytes.
|
|
Uint8List deviceInfo({
|
|
required int verCode,
|
|
required String version,
|
|
int length = 82,
|
|
int caps = 0x21,
|
|
int caps2 = 0x0F,
|
|
}) {
|
|
final frame = Uint8List(length);
|
|
frame[0] = 13; // RESP_CODE_DEVICE_INFO
|
|
frame[1] = verCode;
|
|
final v = ascii.encode(version);
|
|
frame.setRange(60, 60 + v.length, v);
|
|
if (length > 82) frame[82] = caps;
|
|
if (length > 84) frame[84] = caps2;
|
|
return frame;
|
|
}
|
|
|
|
setUp(() async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
PrefsManager.reset();
|
|
await PrefsManager.initialize();
|
|
db = OffbandDatabase(NativeDatabase.memory());
|
|
BlobStore.overrideForTest(BlobStore(db));
|
|
connector = MeshCoreConnector();
|
|
connector.sendFrameOverrideForTest = (_) {};
|
|
connector.setConnectedForTest();
|
|
});
|
|
|
|
tearDown(() async {
|
|
BlobStore.clearTestOverride();
|
|
await db.close();
|
|
});
|
|
|
|
test('Offband frame with dual version populates caps', () {
|
|
connector.handleFrameForTest(
|
|
deviceInfo(verCode: 22, version: '1.4.0-1.17.0', length: 85),
|
|
);
|
|
expect(connector.classification?.population, DevicePopulation.offband);
|
|
expect(connector.offbandCaps, 0x21);
|
|
expect(connector.offbandCaps2, 0x0F);
|
|
// femLna byte at offset 83 defaults to 0 in the fixture = bypass.
|
|
expect(connector.femLnaEnabled, isFalse);
|
|
});
|
|
|
|
test('83-byte Offband frame: caps byte 1 present, byte 2 and FEM state'
|
|
' absent', () {
|
|
connector.handleFrameForTest(
|
|
deviceInfo(verCode: 22, version: '1.4.0-1.17.0', length: 83),
|
|
);
|
|
expect(connector.classification?.population, DevicePopulation.offband);
|
|
expect(connector.offbandCaps, 0x21);
|
|
expect(connector.offbandCaps2, isNull);
|
|
expect(connector.femLnaEnabled, isNull);
|
|
});
|
|
|
|
test('fork-length frame with a stock version string leaves every Offband'
|
|
' field null', () {
|
|
connector.handleFrameForTest(
|
|
deviceInfo(verCode: 14, version: 'v1.18.0', length: 85),
|
|
);
|
|
expect(
|
|
connector.classification?.population,
|
|
DevicePopulation.stockLegacy,
|
|
);
|
|
expect(connector.classification?.conflict, isTrue);
|
|
expect(connector.offbandCaps, isNull);
|
|
expect(connector.offbandCaps2, isNull);
|
|
});
|
|
|
|
test('stock 1.18 frame classifies stockModern with null caps', () {
|
|
connector.handleFrameForTest(deviceInfo(verCode: 14, version: 'v1.18.0'));
|
|
expect(
|
|
connector.classification?.population,
|
|
DevicePopulation.stockModern,
|
|
);
|
|
expect(connector.offbandCaps, isNull);
|
|
});
|
|
|
|
test('fork gates never open on a stock device, whatever its ver code'
|
|
' (#877): stock reuses the fork range 14+ with other meanings', () {
|
|
// A hypothetical future stock device reporting ver 22 (the fork's
|
|
// current contract number) must not light any Offband capability.
|
|
connector.handleFrameForTest(deviceInfo(verCode: 22, version: 'v9.9.9'));
|
|
expect(connector.supportsOffbandGps, isFalse);
|
|
expect(connector.supportsOffbandBlock, isFalse);
|
|
expect(connector.supportsOffbandFemLna, isFalse);
|
|
expect(connector.supportsButtonMatrix, isFalse);
|
|
expect(connector.supportsNotifyScope, isFalse);
|
|
expect(connector.supportsPktHash, isFalse);
|
|
// Pre-split floors (same meaning on every population) stay available.
|
|
expect(connector.supportsCompanionRadioStats, isTrue);
|
|
});
|
|
|
|
test('stock at ver 14 lights nothing either (#1065): the real stock code,'
|
|
' now that the fork rolls back onto the same number', () {
|
|
// The case above uses 22, which no stock device reports. 14 is what real
|
|
// stock 1.18 sends AND what the fork reports after the rollback, so this
|
|
// is the collision in its actual form. The three gates whose version
|
|
// floors were removed in #1065 must still be false here, and they are,
|
|
// because the 82-byte shape classifies stockModern and the connector
|
|
// nulls both caps bytes.
|
|
connector.handleFrameForTest(
|
|
deviceInfo(verCode: 14, version: 'v1.18.0-81dbae5'),
|
|
);
|
|
expect(connector.offbandCaps, isNull);
|
|
expect(connector.offbandCaps2, isNull);
|
|
expect(connector.supportsOffbandBlock, isFalse);
|
|
expect(connector.supportsOffbandCaplog, isFalse);
|
|
expect(connector.supportsPktHash, isFalse);
|
|
});
|
|
});
|
|
}
|