fix: Normalize raw transport path for firmware v1.15 photo/audio transfer

CMD_SEND_RAW_DATA (0x19) was passed the encoded MeshCore route descriptor
(e.g. 0x41 for a 1-hop 2-byte-hash route), but the v1.15 companion firmware
raw-data handler treats the path byte as a legacy literal hop count, not an
encoded descriptor. It misread 0x41 as "65 path bytes" and rejected the
command (ERROR: Unsupported command), so media fetch always failed while
normal messaging worked.

- Add ContactRouteCodec.toLegacyRawPath() converting encoded descriptor +
  path into legacy format (literal hop count + first byte of each hop hash).
- Apply it in ConnectionProvider.sendRawVoicePacket, the single chokepoint
  all raw media (voice/image fragments, swarm, route probes) flows through.
- Pace served fragments 350ms apart to avoid firmware "ERROR: Table full"
  when bursting many raw packets.
- Tests for the descriptor conversion.

Fixes #43

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Janez T
2026-06-24 10:38:05 +02:00
parent 365bd76cbc
commit bdbceece75
4 changed files with 122 additions and 2 deletions

View File

@@ -1833,15 +1833,23 @@ class ConnectionProvider with ChangeNotifier {
/// Send a raw binary voice packet directly to a contact (cmdSendRawData, code 25).
/// Only works for contacts with a known direct route (outPathLen >= 0).
///
/// Callers pass the contact's *encoded* route descriptor ([routeEncodedPathLen])
/// and encoded [outPath]. The `CMD_SEND_RAW_DATA` handler in companion firmware
/// v1.15 misinterprets encoded descriptors as a literal path-byte count, so we
/// normalise to the legacy raw path format (literal hop count + one byte per
/// hop) before handing the frame to the transport. See
/// [ContactRouteCodec.toLegacyRawPath].
Future<void> sendRawVoicePacket({
required Uint8List contactPath,
required int contactPathLen,
required Uint8List payload,
}) async {
if (!_activeService.isConnected) return;
final legacy = ContactRouteCodec.toLegacyRawPath(contactPathLen, contactPath);
await _activeService.sendRawVoicePacket(
contactPathLen: contactPathLen,
contactPath: contactPath,
contactPathLen: legacy.length,
contactPath: Uint8List.fromList(legacy.path),
payload: payload,
);
}

View File

@@ -9,6 +9,11 @@ typedef RawPacketSender =
required Uint8List payload,
});
/// Pacing between consecutive raw fragments. Firmware v1.15 rejects bursts of
/// raw packets with `ERROR: Table full` when fragments are blasted too quickly,
/// so we space them out to keep the radio's raw transmit queue from overflowing.
const Duration _interFragmentDelay = Duration(milliseconds: 350);
Future<bool> serveCachedSessionFragments<T>({
required String providerLabel,
required String sessionId,
@@ -19,6 +24,7 @@ Future<bool> serveCachedSessionFragments<T>({
required Uint8List Function(T fragment) encodeBinary,
required RawPacketSender? sendRawPacket,
Set<int>? requestedIndices,
Duration interFragmentDelay = _interFragmentDelay,
}) async {
if (fragments.isEmpty) {
debugPrint('⚠️ [$providerLabel] No cached fragments for $sessionId');
@@ -56,6 +62,9 @@ Future<bool> serveCachedSessionFragments<T>({
continue;
}
try {
if (servedCount > 0 && interFragmentDelay > Duration.zero) {
await Future<void>.delayed(interFragmentDelay);
}
await sendRawPacket(
contactPath: requester.outPath,
contactPathLen: requester.routeEncodedPathLen,