mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
256 lines
8.4 KiB
Markdown
256 lines
8.4 KiB
Markdown
# Voice Mode Technical Design
|
|
|
|
## 1. Overview
|
|
|
|
Voice mode uses a **two-plane architecture** with optional swarm-assisted
|
|
recovery:
|
|
|
|
- **Control plane (text messages):**
|
|
- `VE3:` voice envelope announces voice availability in chat.
|
|
- **Control plane (raw binary request):**
|
|
- Binary voice fetch request (same raw route as voice packets).
|
|
- **Data plane (raw binary packets):**
|
|
- `VoicePacket` payload streamed via `cmdSendRawData` and received through `pushRawData`.
|
|
|
|
This design avoids broadcasting full voice payloads to channels/rooms. Chat carries only metadata; audio is fetched on demand when user presses play.
|
|
|
|
Swarm fallback is documented in [Swarm Mode Technical Design](./swarm-mode-technical.md).
|
|
|
|
## 2. Key Modules
|
|
|
|
- `lib/utils/voice_message_parser.dart`
|
|
- `VoicePacket` (binary direct-packet format)
|
|
- `VoiceEnvelope` (`VE3`)
|
|
- `VoiceFetchRequest` (binary)
|
|
- `lib/screens/messages_tab.dart`
|
|
- Capture/encode voice, cache encoded packets, send envelope only
|
|
- `lib/providers/voice_provider.dart`
|
|
- Reassembly/playback sessions
|
|
- Outgoing session cache + deferred serving
|
|
- `lib/providers/app_provider.dart`
|
|
- Incoming routing for `VE3`, binary voice fetch requests, and raw swarm control payloads
|
|
- Handles raw packet ingestion
|
|
- `lib/widgets/messages/voice_message_bubble.dart`
|
|
- Play behavior (immediate play if complete, otherwise fetch + auto-play)
|
|
- `lib/providers/messages_provider.dart`
|
|
- Message-level voice detection (`VE3`)
|
|
- `lib/services/message_storage_service.dart`
|
|
- Persists `isVoice` and `voiceId`
|
|
|
|
## 3. Wire Formats
|
|
|
|
### 3.1 Voice Envelope (`VE3`)
|
|
|
|
Prefix: `VE3:` + colon-delimited compact payload (base36 numeric fields)
|
|
|
|
Fields:
|
|
|
|
- `sid` (string): base36 token for 32-bit session ID
|
|
- `mode` (base36): codec mode ID (`VoicePacketMode.id`)
|
|
- `total` (base36): packet count (1..255)
|
|
- `durS` (base36): estimated duration in seconds
|
|
|
|
`sid` is base36 on wire and expands to 8-hex internally.
|
|
|
|
Compact format:
|
|
|
|
```text
|
|
VE3:{sid}:{mode}:{total}:{durS}
|
|
```
|
|
|
|
Example:
|
|
|
|
```text
|
|
VE3:a:1:4:4
|
|
```
|
|
|
|
### 3.2 Voice Fetch Request (binary)
|
|
|
|
Binary payload format:
|
|
|
|
```text
|
|
[magic=0x72][sid:4B][flags:1B][requesterKey6:6B][missingCount:1B][missingIndices...]
|
|
```
|
|
|
|
### 3.3 Raw Voice Packet (data plane)
|
|
|
|
Binary payload structure:
|
|
|
|
- Byte 0: magic `0x56` (`'V'`)
|
|
- Bytes 1..4: session ID (4 bytes)
|
|
- Byte 5: packet index
|
|
- Bytes 6..N: codec2 data
|
|
|
|
Header is 6 bytes. Mode and total packet count come from the `VE3` envelope.
|
|
|
|
## 4. Outgoing Flow (Send)
|
|
|
|
1. Recorder captures PCM chunks.
|
|
2. Each chunk is codec2-encoded into `VoicePacket` objects.
|
|
3. Packets are cached in `VoiceProvider` outgoing cache (TTL 15 min).
|
|
4. Sender inserts local voice placeholder message (`isVoice=true`, `voiceId=sessionId`).
|
|
5. Sender sends one envelope (`VE3`) through normal message path:
|
|
- channel/room: `sendChannelMessage`
|
|
- direct: `sendTextMessage`
|
|
6. **No raw audio packets are sent during initial send.**
|
|
|
|
## 5. Incoming Routing
|
|
|
|
### 5.1 `VE3` envelope received
|
|
|
|
`AppProvider` records sender identity from message metadata, registers the voice
|
|
session envelope, marks the message as voice (`isVoice`, `voiceId`), and adds it to chat.
|
|
|
|
### 5.2 Binary voice fetch request received
|
|
|
|
`AppProvider` treats it as control-plane only:
|
|
|
|
- request is not added to chat
|
|
- resolves requester contact via key prefix
|
|
- calls `voiceProvider.serveSessionTo(...)`
|
|
|
|
### 5.3 Swarm control messages received
|
|
|
|
`AppProvider` also handles raw swarm control payloads:
|
|
|
|
- binary swarm requests advertise which voice fragments are still missing
|
|
- binary swarm availability responses advertise which fragments a peer can relay
|
|
- swarm control payloads arrive via `pushRawData` and are not added to chat history
|
|
|
|
Swarm semantics are shared with image mode and documented in
|
|
[Swarm Mode Technical Design](./swarm-mode-technical.md).
|
|
|
|
### 5.4 Raw packet received (`pushRawData`)
|
|
|
|
`AppProvider.onRawDataReceived` parses `VoicePacket` binary and appends to session in `VoiceProvider`.
|
|
|
|
## 6. Play / Fetch Behavior
|
|
|
|
In `VoiceMessageBubble`:
|
|
|
|
- If session already complete: play immediately.
|
|
- If incomplete/missing:
|
|
1. Resolve sender contact from message sender metadata
|
|
2. Prefer a direct fetch from the original sender if its raw route is healthy
|
|
3. If the sender path does not respond, fan out a raw swarm request with the
|
|
exact missing packet indices to reachable peers
|
|
4. Wait up to 10 seconds for raw peer availability responses
|
|
5. Send a direct binary fetch request to the best alternate peer for the
|
|
missing subset it advertised
|
|
6. Show requesting state in UI
|
|
7. Auto-play when session becomes complete
|
|
|
|
If sender cannot be resolved or request cannot be sent, bubble remains and shows: **"Voice unavailable right now"**.
|
|
|
|
## 7. Outgoing Cache Details
|
|
|
|
`VoiceProvider` outgoing cache:
|
|
|
|
- key: `sessionId`
|
|
- value: encoded packet list + cached timestamp
|
|
- TTL: 15 minutes (`_outgoingSessionTtl`)
|
|
- eviction: lazy (on cache access/add/serve)
|
|
|
|
Serving prerequisites:
|
|
|
|
- session exists in outgoing cache or already-received session state
|
|
- `sendRawPacketCallback` configured
|
|
- requester has direct path (`outPathLen >= 0`)
|
|
|
|
Received partial sessions can therefore act as relay sources during swarm
|
|
recovery.
|
|
|
|
## 8. Persistence
|
|
|
|
`MessageStorageService` now stores and restores:
|
|
|
|
- `isVoice`
|
|
- `voiceId`
|
|
|
|
This ensures envelope messages remain voice bubbles across app restart.
|
|
|
|
## 9. Validation and Safety
|
|
|
|
Parser validation enforces:
|
|
|
|
- strict hex lengths for IDs and key prefixes
|
|
- valid mode range
|
|
- valid packet counts and duration bounds
|
|
- compact base36 numeric fields in envelope/request
|
|
- Binary request flags specify `all` or `missing` indices.
|
|
- Request payload includes `requesterKey6` to resolve return route.
|
|
|
|
## 10. Transmit Time Estimate (UI)
|
|
|
|
Voice bubbles and Message Technical Details show an **estimated transmit time** (`~... tx`).
|
|
|
|
The estimate is airtime-based (LoRa packet model), not file-duration-only:
|
|
|
|
- Source inputs:
|
|
- `packetCount` and `durationMs` from `VE3` envelope, or
|
|
- numeric envelope values decoded from base36
|
|
- actual received `VoicePacket.codec2Data.length` bytes when local session packets exist
|
|
- `pathLen` from message metadata
|
|
- current radio params from `deviceInfo`: `radioBw`, `radioSf`, `radioCr`
|
|
- Per-packet payload model:
|
|
- `meshHeader(2)` + `pathLen` + `voiceHeader(6)` + `codec2Bytes`
|
|
- LoRa airtime:
|
|
- standard symbol-time formula (preamble + payload symbols)
|
|
- Mesh pacing/hops:
|
|
- multiplied by `(1 + airtimeBudgetFactor)` where default factor is `1.0`
|
|
- multiplied by hop count `(pathLen + 1)`
|
|
- Total estimate:
|
|
- sum over all packets
|
|
|
|
BW handling:
|
|
|
|
- If `radioBw` is index `0..9`, app maps it to Hz (`7.8k` .. `500k`)
|
|
- If `radioBw > 1000`, it is treated as Hz directly
|
|
|
|
Fallback defaults are used when radio params are unavailable: `SF10`, `BW250kHz`, `CR5`.
|
|
|
|
## 11. Operational Constraints
|
|
|
|
- No firmware changes required.
|
|
- On-demand fetch prefers the original sender, but a partial session can also be
|
|
completed from alternate peers that already hold packets.
|
|
- Raw return path needs a currently valid direct route to requester.
|
|
- Voice capture is available on iOS and Android (`Platform.isIOS || Platform.isAndroid`).
|
|
- Swarm discovery uses the same `cmdSendRawData` / `pushRawData` path as voice
|
|
fetch and packet delivery.
|
|
|
|
### 11.1 Raw Binary Routing Semantics
|
|
|
|
- Voice payload packets use companion command `CMD_SEND_RAW_DATA` (`25` / `0x19`).
|
|
- Companion push back to the app is `PUSH_CODE_RAW_DATA` (`0x84`).
|
|
- Over-the-air packet type for this flow is `PAYLOAD_TYPE_RAW_CUSTOM` (`0x0F`).
|
|
- This flow is direct-route only, not flood/broadcast:
|
|
- it is sent to one destination path;
|
|
- only nodes on that path relay it;
|
|
- it is **not** received by everyone in the mesh.
|
|
|
|
## 12. High-Level Sequence
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant A as Sender App
|
|
participant P as Peer With Packets
|
|
participant N as Reachable Peers
|
|
participant B as Receiver App
|
|
|
|
A->>A: Record + encode voice packets
|
|
A->>A: Cache session packets (TTL 15m)
|
|
A->>M: Send VE3 envelope
|
|
M->>B: Deliver VE3
|
|
B->>B: Render voice bubble (metadata only)
|
|
B->>A: Send binary fetch request on Play
|
|
A->>B: Stream raw VoicePacket packets (partial)
|
|
Note over A,B: Sender path stops responding
|
|
B->>N: Raw swarm requests with missing voice packet indices
|
|
P->>B: Raw swarm availability response
|
|
B->>P: Direct binary fetch request for missing subset
|
|
P->>B: Stream remaining raw VoicePacket packets
|
|
B->>B: Reassemble session
|
|
B->>B: Auto-play when complete
|
|
```
|