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.
97 lines
3.5 KiB
97 lines
3.5 KiB
import 'dart:typed_data';
|
|
|
|
import 'meshcore_protocol.dart';
|
|
|
|
/// Outcome of feeding one `0xC4` frame to [CaplogReassembler.accept].
|
|
enum CaplogStatus { started, chunk, completed, truncated, ignored }
|
|
|
|
/// Result of [CaplogReassembler.accept]. [bytes] is set on [CaplogStatus.completed]
|
|
/// and [CaplogStatus.truncated]; [expected] is set on [CaplogStatus.truncated].
|
|
class CaplogEvent {
|
|
const CaplogEvent(this.status, {this.bytes, this.expected});
|
|
final CaplogStatus status;
|
|
final Uint8List? bytes;
|
|
final int? expected;
|
|
}
|
|
|
|
/// Pure state machine that reassembles an Offband caplog (`0xC4`) streamed dump:
|
|
/// START `[0xC4, 0x01, total_len(uint32 LE)]` → CHUNK `[0xC4, 0x02, <bytes>]*`
|
|
/// → END `[0xC4, 0x03]`.
|
|
///
|
|
/// A CHUNK or END arriving without a preceding START, and any non-`0xC4` frame,
|
|
/// is [CaplogStatus.ignored]. Kept free of connector/transport concerns so the
|
|
/// reassembly is unit-testable in isolation. (#430)
|
|
class CaplogReassembler {
|
|
final BytesBuilder _buffer = BytesBuilder();
|
|
int? _expected;
|
|
bool _active = false;
|
|
|
|
CaplogEvent accept(Uint8List frame) {
|
|
if (frame.length < 2 || frame[0] != respCodeOffbandCaplog) {
|
|
return const CaplogEvent(CaplogStatus.ignored);
|
|
}
|
|
switch (frame[1]) {
|
|
case caplogSubStart:
|
|
_buffer.clear();
|
|
// total_len is optional/defensive: an older device might omit it. When
|
|
// absent we can't detect truncation, but still reassemble what arrives.
|
|
_expected = frame.length >= 6 ? readUint32LE(frame, 2) : null;
|
|
_active = true;
|
|
return const CaplogEvent(CaplogStatus.started);
|
|
case caplogSubChunk:
|
|
if (!_active) return const CaplogEvent(CaplogStatus.ignored);
|
|
if (frame.length > 2) _buffer.add(frame.sublist(2));
|
|
return const CaplogEvent(CaplogStatus.chunk);
|
|
case caplogSubEnd:
|
|
if (!_active) return const CaplogEvent(CaplogStatus.ignored);
|
|
_active = false;
|
|
final bytes = _buffer.takeBytes();
|
|
final expected = _expected;
|
|
_expected = null;
|
|
if (expected != null && bytes.length != expected) {
|
|
return CaplogEvent(
|
|
CaplogStatus.truncated,
|
|
bytes: bytes,
|
|
expected: expected,
|
|
);
|
|
}
|
|
return CaplogEvent(CaplogStatus.completed, bytes: bytes);
|
|
default:
|
|
return const CaplogEvent(CaplogStatus.ignored);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Thrown when a caplog download is rejected because another streamed response
|
|
/// is already in flight on the device (firmware answers `RESP_CODE_ERR`). (#430)
|
|
class CaplogBusyException implements Exception {
|
|
const CaplogBusyException();
|
|
@override
|
|
String toString() =>
|
|
'CaplogBusyException: device busy, another stream is in flight';
|
|
}
|
|
|
|
/// Result of a caplog download. On a clean transfer [received] == [expected]
|
|
/// and [truncated] is false; on a short transfer [bytes] still carry everything
|
|
/// that was reassembled and [truncated] is true. A partial capture is kept and
|
|
/// handed back rather than discarded, so the user can still save/share it. (#580)
|
|
class CaplogDownload {
|
|
const CaplogDownload({
|
|
required this.bytes,
|
|
required this.received,
|
|
required this.expected,
|
|
required this.chunks,
|
|
});
|
|
|
|
/// The reassembled bytes (complete, or partial when [truncated]).
|
|
final Uint8List bytes;
|
|
final int received;
|
|
final int expected;
|
|
|
|
/// Number of CHUNK frames accumulated before END, a diagnostic to tell
|
|
/// client/transport frame loss apart from the firmware streaming short.
|
|
final int chunks;
|
|
|
|
bool get truncated => received != expected;
|
|
}
|