mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-09-01 16:48:21 +00:00
WIP: rescue misbranched LXMF and RNode work
This commit is contained in:
@@ -221,6 +221,12 @@ class AppContext final : public IAppBleFacade
|
||||
config_.meshcore_config = chat::MeshConfig();
|
||||
config_.applyMeshCoreFactoryDefaults();
|
||||
}
|
||||
else if (config_.mesh_protocol == chat::MeshProtocol::RNode ||
|
||||
config_.mesh_protocol == chat::MeshProtocol::LXMF)
|
||||
{
|
||||
config_.rnode_config = chat::MeshConfig();
|
||||
config_.applyRNodeFactoryDefaults();
|
||||
}
|
||||
else
|
||||
{
|
||||
config_.meshtastic_config = chat::MeshConfig();
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
# Reticulum / LXMF Device Mode Plan
|
||||
|
||||
## Why this document exists
|
||||
|
||||
Trail Mate already has two very different things under the "RNode" umbrella:
|
||||
|
||||
- an honest low-level RNode air-layer implementation
|
||||
- a USB CDC RNode KISS bridge so a real Reticulum host can use the device as a modem
|
||||
|
||||
What it does **not** yet have is a real device-side Reticulum/LXMF stack.
|
||||
|
||||
That distinction matters:
|
||||
|
||||
- `RNode modem` means the device forwards raw LoRa bytes and is controlled by an external Reticulum host
|
||||
- `Reticulum/LXMF device mode` means the device itself owns identity, announces, destination discovery, packet encryption, message packing and local chat UX
|
||||
|
||||
The goal of this document is to define the end-state architecture and the first honest implementation slice. It intentionally avoids inventing a private "fake LXMF-like" chat envelope.
|
||||
|
||||
## Target outcome
|
||||
|
||||
Trail Mate should support these runtime roles:
|
||||
|
||||
- `Meshtastic`: native device-side chat over Meshtastic
|
||||
- `MeshCore`: native device-side chat over MeshCore
|
||||
- `LXMF`: native device-side chat over a Reticulum-compatible subset carried on RNode-style raw LoRa packets
|
||||
- `RNode Bridge`: host-controlled modem mode for an external Reticulum/LXMF stack
|
||||
|
||||
In other words:
|
||||
|
||||
- `LXMF` is an application/network stack mode
|
||||
- `RNode Bridge` is a modem/bridge mode
|
||||
|
||||
Today the codebase still couples "active radio backend" and "active user-visible protocol" into one selector. Because of that, the first implementation phase keeps `RNode Bridge` as a separate protocol value instead of fully moving it into the `Data Exchange` page. That split is deferred until the runtime can own more than one radio-facing backend safely.
|
||||
|
||||
## Constraints from the reference implementations
|
||||
|
||||
The local `.tmp/Reticulum` and `.tmp/LXMF` sources make the following points non-negotiable:
|
||||
|
||||
- Reticulum is a full network stack, not just a payload codec
|
||||
- `Identity` uses two keypairs in practice:
|
||||
- Curve25519/X25519 for encryption and ECDH
|
||||
- Ed25519 for signatures
|
||||
- destination addressing is derived from:
|
||||
- `name_hash = SHA256(app_name + aspects)[:10]`
|
||||
- `destination_hash = SHA256(name_hash + identity_hash)[:16]`
|
||||
- opportunistic single-packet encryption uses:
|
||||
- ephemeral Curve25519 key exchange
|
||||
- HKDF-SHA256 derived 64-byte token key
|
||||
- AES-256-CBC + PKCS7
|
||||
- HMAC-SHA256 token authentication
|
||||
- packet delivery proofs are real protocol behavior, not optional UI sugar
|
||||
- LXMF wire format is:
|
||||
- 16 bytes destination hash
|
||||
- 16 bytes source hash
|
||||
- 64 bytes signature
|
||||
- msgpack payload
|
||||
- LXMF routing above that depends on announces, destination recall, proofs, and later links/resources/propagation
|
||||
|
||||
## Honest phase boundary
|
||||
|
||||
The full Reticulum feature set is too large to land in one pass without high risk. The first device-side implementation therefore targets a **real interoperable subset**:
|
||||
|
||||
- local Reticulum identity storage
|
||||
- `lxmf.delivery` destination derivation
|
||||
- announce transmit/receive and signature validation
|
||||
- peer discovery from direct announces
|
||||
- opportunistic LXMF text delivery in single packets
|
||||
- proof generation for received single-packet deliveries
|
||||
- contact list integration from announces
|
||||
- device-side unicast text chat to directly discovered peers
|
||||
|
||||
This first slice is intentionally **not** yet:
|
||||
|
||||
- multi-hop transport/path finding
|
||||
- path request / path response handling
|
||||
- link establishment
|
||||
- resource transfer for large LXMF messages
|
||||
- propagation nodes
|
||||
- ticket/stamp enforcement
|
||||
- ratchets
|
||||
- full shared-instance parity with Python Reticulum
|
||||
|
||||
Even with those limits, the phase-1 result is still a real protocol subset:
|
||||
|
||||
- packet headers are real Reticulum packet headers
|
||||
- announces are real Reticulum announces
|
||||
- payload encryption is the real token scheme
|
||||
- LXMF bodies are real LXMF bodies
|
||||
|
||||
## Proposed architecture
|
||||
|
||||
### 1. Radio carrier layer
|
||||
|
||||
Reuse the current RNode raw air layer:
|
||||
|
||||
- one-byte RNode air header
|
||||
- packet fragmentation / reassembly
|
||||
- raw LoRa payload send/receive
|
||||
- radio parameter application
|
||||
|
||||
This remains the LoRa carrier for both:
|
||||
|
||||
- `RNode Bridge`
|
||||
- `LXMF`
|
||||
|
||||
### 2. Reticulum core subset
|
||||
|
||||
Add a shared Reticulum wire layer with:
|
||||
|
||||
- constants for MTU, header sizing and hash sizes
|
||||
- packet encode/decode for header-1 packets
|
||||
- packet hash calculation
|
||||
- destination hash helpers
|
||||
- announce validation helpers
|
||||
- HKDF + token encryption helpers
|
||||
|
||||
This layer must stay protocol-accurate and not depend on UI concerns.
|
||||
|
||||
### 3. LXMF wire layer
|
||||
|
||||
Add a shared LXMF wire layer with:
|
||||
|
||||
- minimal msgpack encoder/decoder for the LXMF structures used in phase 1
|
||||
- peer announce app-data codec for `[display_name, stamp_cost]`
|
||||
- text-message pack/unpack for `[timestamp, title, content, fields]`
|
||||
- support for an optional fifth payload element so future stamp support does not break parsing
|
||||
|
||||
### 4. Platform identity layer
|
||||
|
||||
Add an ESP-side identity service that persists:
|
||||
|
||||
- Curve25519 public/private keypair
|
||||
- Ed25519 public/private keypair
|
||||
- local delivery destination hash
|
||||
|
||||
This layer should reuse the existing preferences/blob storage style already used by MeshCore identity handling.
|
||||
|
||||
### 5. LXMF adapter layer
|
||||
|
||||
Add a new `LxmfAdapter` that:
|
||||
|
||||
- composes the existing `RNodeAdapter` as its raw carrier
|
||||
- owns local identity and peer recall tables
|
||||
- periodically emits announces
|
||||
- turns valid announces into `NodeInfoUpdateEvent` and `NodeProtocolUpdateEvent`
|
||||
- sends opportunistic LXMF text packets
|
||||
- receives and verifies opportunistic LXMF text packets
|
||||
- emits proofs for received packets
|
||||
- exposes incoming text to `ChatService`
|
||||
|
||||
### 6. UI / service integration
|
||||
|
||||
Phase 1 UI integration should be minimal and honest:
|
||||
|
||||
- add `LXMF` as a first-class protocol option
|
||||
- keep `RNode Bridge` explicit for the host-controlled modem role
|
||||
- allow Contacts and Chat pages to work when `LxmfAdapter` reports text support
|
||||
- continue showing the existing RNode host-only warnings only for `RNode Bridge`
|
||||
|
||||
## Data model implications
|
||||
|
||||
### Protocol enum
|
||||
|
||||
Add:
|
||||
|
||||
- `MeshProtocol::LXMF`
|
||||
|
||||
Keep:
|
||||
|
||||
- `MeshProtocol::RNode`
|
||||
|
||||
for bridge mode until radio ownership and bridge/runtime selection are separated.
|
||||
|
||||
### Contact identity mapping
|
||||
|
||||
The current app-wide contact/chat model only has a 32-bit `NodeId`, while Reticulum/LXMF identities are addressed by 16-byte destination hashes.
|
||||
|
||||
Phase 1 therefore introduces a stable surrogate:
|
||||
|
||||
- `node_id = lower_32_bits(destination_hash)`
|
||||
|
||||
This is acceptable for a first pass, but it is not collision-proof. The long-term fix is to add a protocol-native peer identifier model to contacts/chat storage.
|
||||
|
||||
### Radio settings
|
||||
|
||||
Phase 1 reuses the existing `rnode_config` as the carrier configuration for `LXMF`. This avoids adding a second identical LoRa profile while the runtime still treats RNode as the Reticulum-compatible carrier.
|
||||
|
||||
## Phase-1 implementation details
|
||||
|
||||
### Identity
|
||||
|
||||
- generate Curve25519 keys with the already available Arduino `Curve25519` library
|
||||
- generate Ed25519 keys with the existing shared Ed25519 implementation already used by MeshCore helpers
|
||||
- persist under a dedicated preferences namespace
|
||||
|
||||
### Local destination
|
||||
|
||||
The local delivery destination is:
|
||||
|
||||
- app name: `lxmf`
|
||||
- aspect: `delivery`
|
||||
- direction: inbound single destination
|
||||
|
||||
### Announce behavior
|
||||
|
||||
The device should:
|
||||
|
||||
- send an announce shortly after startup/config apply
|
||||
- re-announce periodically while active
|
||||
- include `display_name` in LXMF announce app-data
|
||||
- validate inbound announce signatures
|
||||
- remember peer public keys and display names
|
||||
|
||||
### Text send
|
||||
|
||||
Phase 1 sends only opportunistic single-packet LXMF messages:
|
||||
|
||||
- empty title
|
||||
- text content in the LXMF content field
|
||||
- empty fields map
|
||||
- no stamps or tickets
|
||||
- fail fast if the packed message exceeds the single-packet budget
|
||||
|
||||
### Text receive
|
||||
|
||||
On inbound Reticulum data packets:
|
||||
|
||||
- decrypt if the destination hash matches local delivery destination
|
||||
- reconstruct the full LXMF frame
|
||||
- validate the LXMF signature if the source identity is known from prior announce
|
||||
- queue a `MeshIncomingText`
|
||||
- immediately emit a Reticulum proof packet for the received packet
|
||||
|
||||
## Deferred work after phase 1
|
||||
|
||||
### Phase 2 current slice
|
||||
|
||||
The current phase-2 implementation extends the phase-1 subset with:
|
||||
|
||||
- outbound `rnstransport.path.request` packets for known LXMF peers
|
||||
- inbound path-request handling for the local `lxmf.delivery` destination
|
||||
- `PATH_RESPONSE` announce replies for the local destination
|
||||
- persisted peer recall so known LXMF peers survive reboot/config reload
|
||||
- generic Reticulum announce validation and path learning, not only `lxmf.delivery`
|
||||
- third-party announce cache storage and cache-request replay
|
||||
- immediate announce rebroadcast as a minimal propagation mechanism
|
||||
- `HEADER_2` transport packet parsing/building for multi-hop forwarding
|
||||
- blind forwarding of transported non-local packets based on a local path table
|
||||
- reverse-path tracking for proof relay
|
||||
- opaque link-request relay plus link/resource packet relay over learned link IDs
|
||||
|
||||
This is still intentionally narrower than full Reticulum transport parity:
|
||||
|
||||
- no dedicated local `Link` API or destination-owned link termination on device
|
||||
- no local resource sender/receiver state machine equivalent to Python `RNS.Resource`
|
||||
- no propagation-node store/forward policy layer
|
||||
- no shared-instance parity, tunnel handling, or management destinations
|
||||
- no ratchets, ticket/stamp enforcement, or deep interop test coverage yet
|
||||
|
||||
### Phase 2
|
||||
|
||||
- path request / path response
|
||||
- peer/path table persistence
|
||||
- better peer ID model than 32-bit surrogate
|
||||
|
||||
### Phase 3
|
||||
|
||||
- Reticulum links
|
||||
- direct link-based LXMF delivery
|
||||
- resource transfer for messages larger than the opportunistic limit
|
||||
|
||||
### Phase 4
|
||||
|
||||
- propagation nodes
|
||||
- ticket and stamp handling
|
||||
- ratchets
|
||||
- deeper parity testing against Python Reticulum/LXMF
|
||||
|
||||
## Code changes planned in this round
|
||||
|
||||
This round should land the following:
|
||||
|
||||
- shared Reticulum packet/token helpers
|
||||
- shared LXMF wire/msgpack helpers
|
||||
- ESP-side LXMF identity persistence
|
||||
- ESP-side `LxmfAdapter` over the current RNode raw carrier
|
||||
- protocol enum/UI/config updates for `LXMF`
|
||||
- compile validation on `tlora_pager_sx1262`
|
||||
|
||||
## Non-goal for this round
|
||||
|
||||
This round does **not** claim "complete Reticulum parity".
|
||||
|
||||
It does claim something narrower and honest:
|
||||
|
||||
- Trail Mate gains a real device-side Reticulum/LXMF foundation
|
||||
- the implementation uses protocol-accurate wire formats
|
||||
- the shipped feature set is intentionally the opportunistic direct-neighbor subset first
|
||||
@@ -41,7 +41,9 @@ using MessageId = uint32_t;
|
||||
enum class MeshProtocol : uint8_t
|
||||
{
|
||||
Meshtastic = 1,
|
||||
MeshCore = 2
|
||||
MeshCore = 2,
|
||||
RNode = 3,
|
||||
LXMF = 4
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,9 @@ enum class NodeProtocolType : uint8_t
|
||||
{
|
||||
Unknown = 0,
|
||||
Meshtastic = 1,
|
||||
MeshCore = 2
|
||||
MeshCore = 2,
|
||||
RNode = 3,
|
||||
LXMF = 4
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @file lxmf_wire.h
|
||||
* @brief Shared LXMF wire helpers for direct text-message subsets
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "chat/infra/reticulum/reticulum_wire.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace chat::lxmf
|
||||
{
|
||||
|
||||
struct DecodedMessage
|
||||
{
|
||||
uint8_t destination_hash[reticulum::kTruncatedHashSize] = {};
|
||||
uint8_t source_hash[reticulum::kTruncatedHashSize] = {};
|
||||
uint8_t signature[reticulum::kSignatureSize] = {};
|
||||
double timestamp = 0.0;
|
||||
std::string title;
|
||||
std::string content;
|
||||
std::vector<uint8_t> packed_payload;
|
||||
bool has_stamp = false;
|
||||
std::vector<uint8_t> stamp;
|
||||
bool fields_empty = true;
|
||||
};
|
||||
|
||||
bool packPeerAnnounceAppData(const char* display_name,
|
||||
bool has_stamp_cost,
|
||||
uint8_t stamp_cost,
|
||||
uint8_t* out_data,
|
||||
size_t* inout_len);
|
||||
|
||||
bool unpackPeerAnnounceAppData(const uint8_t* data, size_t len,
|
||||
char* out_display_name, size_t display_name_len,
|
||||
bool* out_has_stamp_cost,
|
||||
uint8_t* out_stamp_cost);
|
||||
|
||||
bool encodeTextPayload(double timestamp,
|
||||
const char* title,
|
||||
const char* content,
|
||||
uint8_t* out_payload,
|
||||
size_t* inout_len);
|
||||
|
||||
void computeMessageHash(const uint8_t destination_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t source_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t* packed_payload,
|
||||
size_t packed_payload_len,
|
||||
uint8_t out_hash[reticulum::kFullHashSize]);
|
||||
|
||||
bool buildSignedPart(const uint8_t destination_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t source_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t* packed_payload,
|
||||
size_t packed_payload_len,
|
||||
uint8_t* out_signed_part,
|
||||
size_t* inout_len,
|
||||
uint8_t out_message_hash[reticulum::kFullHashSize]);
|
||||
|
||||
bool packMessage(const uint8_t destination_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t source_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t signature[reticulum::kSignatureSize],
|
||||
const uint8_t* packed_payload,
|
||||
size_t packed_payload_len,
|
||||
uint8_t* out_message,
|
||||
size_t* inout_len);
|
||||
|
||||
bool unpackMessage(const uint8_t* data, size_t len, DecodedMessage* out_message);
|
||||
|
||||
} // namespace chat::lxmf
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* @file reticulum_wire.h
|
||||
* @brief Shared Reticulum packet and token helpers for device-side subsets
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace chat::reticulum
|
||||
{
|
||||
|
||||
constexpr size_t kFullHashSize = 32;
|
||||
constexpr size_t kTruncatedHashSize = 16;
|
||||
constexpr size_t kNameHashSize = 10;
|
||||
constexpr size_t kEncryptionPublicKeySize = 32;
|
||||
constexpr size_t kSigningPublicKeySize = 32;
|
||||
constexpr size_t kCombinedPublicKeySize = kEncryptionPublicKeySize + kSigningPublicKeySize;
|
||||
constexpr size_t kSignatureSize = 64;
|
||||
constexpr size_t kPacketHeader1Size = 2 + kTruncatedHashSize + 1;
|
||||
constexpr size_t kPacketHeader2Size = 2 + kTruncatedHashSize + kTruncatedHashSize + 1;
|
||||
constexpr size_t kReticulumMtu = 500;
|
||||
constexpr size_t kTokenIvSize = 16;
|
||||
constexpr size_t kTokenHmacSize = 32;
|
||||
constexpr size_t kTokenOverhead = kTokenIvSize + kTokenHmacSize;
|
||||
constexpr size_t kDerivedTokenKeySize = 64;
|
||||
constexpr size_t kReticulumMdu = kReticulumMtu - (2 + 1 + (kTruncatedHashSize * 2));
|
||||
|
||||
enum class PacketType : uint8_t
|
||||
{
|
||||
Data = 0x00,
|
||||
Announce = 0x01,
|
||||
LinkRequest = 0x02,
|
||||
Proof = 0x03
|
||||
};
|
||||
|
||||
enum class DestinationType : uint8_t
|
||||
{
|
||||
Single = 0x00,
|
||||
Group = 0x01,
|
||||
Plain = 0x02,
|
||||
Link = 0x03
|
||||
};
|
||||
|
||||
enum class TransportType : uint8_t
|
||||
{
|
||||
Broadcast = 0x00,
|
||||
Transport = 0x01,
|
||||
Relay = 0x02,
|
||||
Tunnel = 0x03
|
||||
};
|
||||
|
||||
enum class PacketContext : uint8_t
|
||||
{
|
||||
None = 0x00,
|
||||
Resource = 0x01,
|
||||
ResourceAdv = 0x02,
|
||||
ResourceReq = 0x03,
|
||||
ResourceHmu = 0x04,
|
||||
ResourcePrf = 0x05,
|
||||
ResourceIcl = 0x06,
|
||||
ResourceRcl = 0x07,
|
||||
CacheRequest = 0x08,
|
||||
Request = 0x09,
|
||||
Response = 0x0A,
|
||||
PathResponse = 0x0B,
|
||||
Command = 0x0C,
|
||||
CommandStatus = 0x0D,
|
||||
Channel = 0x0E,
|
||||
Keepalive = 0xFA,
|
||||
LinkIdentify = 0xFB,
|
||||
LinkClose = 0xFC,
|
||||
LinkProof = 0xFD,
|
||||
LrRtt = 0xFE,
|
||||
LrProof = 0xFF
|
||||
};
|
||||
|
||||
struct ParsedPacket
|
||||
{
|
||||
bool valid = false;
|
||||
uint8_t raw_flags = 0;
|
||||
uint8_t header_type = 0;
|
||||
uint8_t hops = 0;
|
||||
PacketType packet_type = PacketType::Data;
|
||||
DestinationType destination_type = DestinationType::Single;
|
||||
TransportType transport_type = TransportType::Broadcast;
|
||||
uint8_t context = 0;
|
||||
uint8_t context_flag = 0;
|
||||
const uint8_t* transport_id = nullptr;
|
||||
const uint8_t* destination_hash = nullptr;
|
||||
const uint8_t* payload = nullptr;
|
||||
size_t payload_len = 0;
|
||||
size_t header_len = 0;
|
||||
};
|
||||
|
||||
struct ParsedAnnounce
|
||||
{
|
||||
bool valid = false;
|
||||
bool has_ratchet = false;
|
||||
const uint8_t* public_key = nullptr;
|
||||
const uint8_t* name_hash = nullptr;
|
||||
const uint8_t* random_hash = nullptr;
|
||||
const uint8_t* ratchet = nullptr;
|
||||
size_t ratchet_len = 0;
|
||||
const uint8_t* signature = nullptr;
|
||||
const uint8_t* app_data = nullptr;
|
||||
size_t app_data_len = 0;
|
||||
};
|
||||
|
||||
size_t paddedTokenPlaintextSize(size_t plaintext_len);
|
||||
size_t tokenSizeForPlaintext(size_t plaintext_len);
|
||||
|
||||
void fullHash(const uint8_t* data, size_t len, uint8_t out_hash[kFullHashSize]);
|
||||
void truncatedHash(const uint8_t* data, size_t len, uint8_t out_hash[kTruncatedHashSize]);
|
||||
void computeNameHash(const char* app_name, const char* aspect,
|
||||
uint8_t out_hash[kNameHashSize]);
|
||||
void computeIdentityHash(const uint8_t public_key[kCombinedPublicKeySize],
|
||||
uint8_t out_hash[kTruncatedHashSize]);
|
||||
void computePlainDestinationHash(const uint8_t name_hash[kNameHashSize],
|
||||
uint8_t out_hash[kTruncatedHashSize]);
|
||||
void computeDestinationHash(const uint8_t name_hash[kNameHashSize],
|
||||
const uint8_t identity_hash[kTruncatedHashSize],
|
||||
uint8_t out_hash[kTruncatedHashSize]);
|
||||
void computePacketHash(const uint8_t* raw_packet, size_t len,
|
||||
uint8_t out_hash[kFullHashSize]);
|
||||
void computeTruncatedPacketHash(const uint8_t* raw_packet, size_t len,
|
||||
uint8_t out_hash[kTruncatedHashSize]);
|
||||
uint32_t nodeIdFromDestinationHash(const uint8_t destination_hash[kTruncatedHashSize]);
|
||||
|
||||
bool parsePacket(const uint8_t* data, size_t len, ParsedPacket* out_packet);
|
||||
bool buildHeader1Packet(PacketType packet_type,
|
||||
DestinationType destination_type,
|
||||
PacketContext context,
|
||||
bool context_flag,
|
||||
const uint8_t destination_hash[kTruncatedHashSize],
|
||||
const uint8_t* payload, size_t payload_len,
|
||||
uint8_t* out_packet, size_t* inout_len,
|
||||
uint8_t hops = 0,
|
||||
TransportType transport_type = TransportType::Broadcast);
|
||||
bool buildHeader2Packet(PacketType packet_type,
|
||||
DestinationType destination_type,
|
||||
PacketContext context,
|
||||
bool context_flag,
|
||||
const uint8_t transport_id[kTruncatedHashSize],
|
||||
const uint8_t destination_hash[kTruncatedHashSize],
|
||||
const uint8_t* payload, size_t payload_len,
|
||||
uint8_t* out_packet, size_t* inout_len,
|
||||
uint8_t hops = 0,
|
||||
TransportType transport_type = TransportType::Transport);
|
||||
|
||||
bool parseAnnounce(const ParsedPacket& packet, ParsedAnnounce* out_announce);
|
||||
|
||||
bool hkdfSha256(const uint8_t* ikm, size_t ikm_len,
|
||||
const uint8_t* salt, size_t salt_len,
|
||||
const uint8_t* info, size_t info_len,
|
||||
uint8_t* out_key, size_t out_len);
|
||||
|
||||
bool tokenEncrypt(const uint8_t derived_key[kDerivedTokenKeySize],
|
||||
const uint8_t iv[kTokenIvSize],
|
||||
const uint8_t* plaintext, size_t plaintext_len,
|
||||
uint8_t* out_token, size_t* inout_len);
|
||||
|
||||
bool tokenDecrypt(const uint8_t derived_key[kDerivedTokenKeySize],
|
||||
const uint8_t* token, size_t token_len,
|
||||
uint8_t* out_plaintext, size_t* inout_len);
|
||||
|
||||
} // namespace chat::reticulum
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @file rnode_packet_wire.h
|
||||
* @brief Shared RNode over-air packet framing helpers
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace chat
|
||||
{
|
||||
namespace rnode
|
||||
{
|
||||
|
||||
constexpr size_t kRNodeHeaderSize = 1;
|
||||
constexpr size_t kRNodeSingleAirPacketSize = 255;
|
||||
constexpr size_t kRNodeFragmentPayloadSize = kRNodeSingleAirPacketSize - kRNodeHeaderSize;
|
||||
constexpr size_t kRNodeMaxPayloadSize = kRNodeFragmentPayloadSize * 2;
|
||||
constexpr uint8_t kRNodeFlagSplit = 0x01;
|
||||
constexpr uint8_t kRNodeSeqUnset = 0xFF;
|
||||
|
||||
struct ParsedAirPacket
|
||||
{
|
||||
uint8_t header = 0;
|
||||
uint8_t sequence = 0;
|
||||
bool split = false;
|
||||
const uint8_t* payload = nullptr;
|
||||
size_t payload_len = 0;
|
||||
};
|
||||
|
||||
struct EncodedAirPacketSet
|
||||
{
|
||||
uint8_t first[kRNodeSingleAirPacketSize] = {};
|
||||
size_t first_len = 0;
|
||||
uint8_t second[kRNodeSingleAirPacketSize] = {};
|
||||
size_t second_len = 0;
|
||||
size_t count = 0;
|
||||
uint8_t header = 0;
|
||||
};
|
||||
|
||||
struct ReassemblyState
|
||||
{
|
||||
uint8_t sequence = kRNodeSeqUnset;
|
||||
size_t buffered_len = 0;
|
||||
uint8_t buffered[kRNodeMaxPayloadSize] = {};
|
||||
|
||||
void reset()
|
||||
{
|
||||
sequence = kRNodeSeqUnset;
|
||||
buffered_len = 0;
|
||||
}
|
||||
};
|
||||
|
||||
bool parseAirPacket(const uint8_t* data, size_t len, ParsedAirPacket* out);
|
||||
bool encodeAirPacketSet(const uint8_t* payload, size_t payload_len,
|
||||
uint8_t sequence, EncodedAirPacketSet* out);
|
||||
bool feedAirPacket(ReassemblyState* state,
|
||||
const uint8_t* data, size_t len,
|
||||
uint8_t* out_payload, size_t* inout_payload_len,
|
||||
bool* out_complete = nullptr);
|
||||
|
||||
uint32_t estimateBitrateBps(uint32_t bandwidth_hz, uint8_t spreading_factor, uint8_t coding_rate);
|
||||
float estimateSymbolTimeMs(uint32_t bandwidth_hz, uint8_t spreading_factor);
|
||||
uint16_t recommendPreambleSymbols(uint32_t bandwidth_hz, uint8_t spreading_factor, uint8_t coding_rate);
|
||||
|
||||
} // namespace rnode
|
||||
} // namespace chat
|
||||
@@ -0,0 +1,671 @@
|
||||
/**
|
||||
* @file lxmf_wire.cpp
|
||||
* @brief Shared LXMF wire helpers for direct text-message subsets
|
||||
*/
|
||||
|
||||
#include "chat/infra/lxmf/lxmf_wire.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace chat::lxmf
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct Cursor
|
||||
{
|
||||
const uint8_t* data = nullptr;
|
||||
size_t len = 0;
|
||||
size_t pos = 0;
|
||||
};
|
||||
|
||||
bool appendByte(uint8_t value, uint8_t* out, size_t out_len, size_t& used)
|
||||
{
|
||||
if (!out || used >= out_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out[used++] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool appendBytes(const uint8_t* data, size_t len, uint8_t* out, size_t out_len, size_t& used)
|
||||
{
|
||||
if ((!data && len != 0) || !out || used + len > out_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (len != 0)
|
||||
{
|
||||
memcpy(out + used, data, len);
|
||||
}
|
||||
used += len;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool appendArrayHeader(uint8_t count, uint8_t* out, size_t out_len, size_t& used)
|
||||
{
|
||||
return appendByte(static_cast<uint8_t>(0x90U | (count & 0x0FU)), out, out_len, used);
|
||||
}
|
||||
|
||||
bool appendMapHeader(uint8_t count, uint8_t* out, size_t out_len, size_t& used)
|
||||
{
|
||||
return appendByte(static_cast<uint8_t>(0x80U | (count & 0x0FU)), out, out_len, used);
|
||||
}
|
||||
|
||||
bool appendNil(uint8_t* out, size_t out_len, size_t& used)
|
||||
{
|
||||
return appendByte(0xC0, out, out_len, used);
|
||||
}
|
||||
|
||||
bool appendUint(uint32_t value, uint8_t* out, size_t out_len, size_t& used)
|
||||
{
|
||||
if (value <= 0x7FU)
|
||||
{
|
||||
return appendByte(static_cast<uint8_t>(value), out, out_len, used);
|
||||
}
|
||||
if (value <= 0xFFU)
|
||||
{
|
||||
return appendByte(0xCC, out, out_len, used) &&
|
||||
appendByte(static_cast<uint8_t>(value), out, out_len, used);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool appendFloat64(double value, uint8_t* out, size_t out_len, size_t& used)
|
||||
{
|
||||
union
|
||||
{
|
||||
double d;
|
||||
uint8_t b[8];
|
||||
} bits{};
|
||||
bits.d = value;
|
||||
|
||||
if (!appendByte(0xCB, out, out_len, used))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int i = 7; i >= 0; --i)
|
||||
{
|
||||
if (!appendByte(bits.b[i], out, out_len, used))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool appendBin(const uint8_t* data, size_t len, uint8_t* out, size_t out_len, size_t& used)
|
||||
{
|
||||
if (len <= 0xFFU)
|
||||
{
|
||||
return appendByte(0xC4, out, out_len, used) &&
|
||||
appendByte(static_cast<uint8_t>(len), out, out_len, used) &&
|
||||
appendBytes(data, len, out, out_len, used);
|
||||
}
|
||||
if (len <= 0xFFFFU)
|
||||
{
|
||||
return appendByte(0xC5, out, out_len, used) &&
|
||||
appendByte(static_cast<uint8_t>((len >> 8) & 0xFFU), out, out_len, used) &&
|
||||
appendByte(static_cast<uint8_t>(len & 0xFFU), out, out_len, used) &&
|
||||
appendBytes(data, len, out, out_len, used);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool readByte(Cursor& cursor, uint8_t* out)
|
||||
{
|
||||
if (!out || !cursor.data || cursor.pos >= cursor.len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*out = cursor.data[cursor.pos++];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool peekByte(const Cursor& cursor, uint8_t* out)
|
||||
{
|
||||
if (!out || !cursor.data || cursor.pos >= cursor.len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*out = cursor.data[cursor.pos];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readArrayHeader(Cursor& cursor, size_t* out_count)
|
||||
{
|
||||
uint8_t tag = 0;
|
||||
if (!readByte(cursor, &tag) || (tag & 0xF0U) != 0x90U || !out_count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*out_count = static_cast<size_t>(tag & 0x0FU);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readMapHeader(Cursor& cursor, size_t* out_count)
|
||||
{
|
||||
uint8_t tag = 0;
|
||||
if (!readByte(cursor, &tag) || (tag & 0xF0U) != 0x80U || !out_count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*out_count = static_cast<size_t>(tag & 0x0FU);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readUint(Cursor& cursor, uint32_t* out_value)
|
||||
{
|
||||
uint8_t tag = 0;
|
||||
if (!readByte(cursor, &tag) || !out_value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (tag <= 0x7F)
|
||||
{
|
||||
*out_value = tag;
|
||||
return true;
|
||||
}
|
||||
if (tag == 0xCC)
|
||||
{
|
||||
uint8_t value = 0;
|
||||
if (!readByte(cursor, &value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*out_value = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool readFloat64(Cursor& cursor, double* out_value)
|
||||
{
|
||||
uint8_t tag = 0;
|
||||
if (!readByte(cursor, &tag) || tag != 0xCB || !out_value || cursor.pos + 8 > cursor.len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
union
|
||||
{
|
||||
double d;
|
||||
uint8_t b[8];
|
||||
} bits{};
|
||||
for (int i = 7; i >= 0; --i)
|
||||
{
|
||||
bits.b[i] = cursor.data[cursor.pos++];
|
||||
}
|
||||
*out_value = bits.d;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readNil(Cursor& cursor)
|
||||
{
|
||||
uint8_t tag = 0;
|
||||
return readByte(cursor, &tag) && tag == 0xC0;
|
||||
}
|
||||
|
||||
bool readBinary(Cursor& cursor, std::vector<uint8_t>* out_data)
|
||||
{
|
||||
if (!out_data)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t tag = 0;
|
||||
if (!readByte(cursor, &tag))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t len = 0;
|
||||
if (tag == 0xC4)
|
||||
{
|
||||
uint8_t len8 = 0;
|
||||
if (!readByte(cursor, &len8))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
len = len8;
|
||||
}
|
||||
else if (tag == 0xC5)
|
||||
{
|
||||
uint8_t hi = 0;
|
||||
uint8_t lo = 0;
|
||||
if (!readByte(cursor, &hi) || !readByte(cursor, &lo))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
len = static_cast<size_t>((static_cast<uint16_t>(hi) << 8) | lo);
|
||||
}
|
||||
else if ((tag & 0xE0U) == 0xA0U)
|
||||
{
|
||||
len = static_cast<size_t>(tag & 0x1FU);
|
||||
}
|
||||
else if (tag == 0xD9)
|
||||
{
|
||||
uint8_t len8 = 0;
|
||||
if (!readByte(cursor, &len8))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
len = len8;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cursor.pos + len > cursor.len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
out_data->assign(cursor.data + cursor.pos, cursor.data + cursor.pos + len);
|
||||
cursor.pos += len;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool skipObject(Cursor& cursor)
|
||||
{
|
||||
uint8_t tag = 0;
|
||||
if (!peekByte(cursor, &tag))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tag == 0xC0)
|
||||
{
|
||||
return readNil(cursor);
|
||||
}
|
||||
if (tag == 0xCB)
|
||||
{
|
||||
double ignored = 0.0;
|
||||
return readFloat64(cursor, &ignored);
|
||||
}
|
||||
if (tag == 0xCC || tag <= 0x7F)
|
||||
{
|
||||
uint32_t ignored = 0;
|
||||
return readUint(cursor, &ignored);
|
||||
}
|
||||
if ((tag & 0xF0U) == 0x80U)
|
||||
{
|
||||
size_t count = 0;
|
||||
if (!readMapHeader(cursor, &count))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
{
|
||||
if (!skipObject(cursor) || !skipObject(cursor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if ((tag & 0xF0U) == 0x90U)
|
||||
{
|
||||
size_t count = 0;
|
||||
if (!readArrayHeader(cursor, &count))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
{
|
||||
if (!skipObject(cursor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ignored;
|
||||
return readBinary(cursor, &ignored);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool packPeerAnnounceAppData(const char* display_name,
|
||||
bool has_stamp_cost,
|
||||
uint8_t stamp_cost,
|
||||
uint8_t* out_data,
|
||||
size_t* inout_len)
|
||||
{
|
||||
if (!out_data || !inout_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t used = 0;
|
||||
const uint8_t* name_bytes = reinterpret_cast<const uint8_t*>(display_name ? display_name : "");
|
||||
const size_t name_len = (display_name != nullptr) ? strlen(display_name) : 0;
|
||||
|
||||
if (!appendArrayHeader(2, out_data, *inout_len, used))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (name_len == 0)
|
||||
{
|
||||
if (!appendNil(out_data, *inout_len, used))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!appendBin(name_bytes, name_len, out_data, *inout_len, used))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (has_stamp_cost)
|
||||
{
|
||||
if (!appendUint(stamp_cost, out_data, *inout_len, used))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!appendNil(out_data, *inout_len, used))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*inout_len = used;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unpackPeerAnnounceAppData(const uint8_t* data, size_t len,
|
||||
char* out_display_name, size_t display_name_len,
|
||||
bool* out_has_stamp_cost,
|
||||
uint8_t* out_stamp_cost)
|
||||
{
|
||||
if (!data || len == 0 || !out_display_name || display_name_len == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
out_display_name[0] = '\0';
|
||||
if (out_has_stamp_cost)
|
||||
{
|
||||
*out_has_stamp_cost = false;
|
||||
}
|
||||
if (out_stamp_cost)
|
||||
{
|
||||
*out_stamp_cost = 0;
|
||||
}
|
||||
|
||||
Cursor cursor;
|
||||
cursor.data = data;
|
||||
cursor.len = len;
|
||||
cursor.pos = 0;
|
||||
size_t count = 0;
|
||||
if (!readArrayHeader(cursor, &count) || count != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t next = 0;
|
||||
if (!peekByte(cursor, &next))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (next == 0xC0)
|
||||
{
|
||||
if (!readNil(cursor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::vector<uint8_t> name;
|
||||
if (!readBinary(cursor, &name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const size_t copy_len = std::min(name.size(), display_name_len - 1);
|
||||
memcpy(out_display_name, name.data(), copy_len);
|
||||
out_display_name[copy_len] = '\0';
|
||||
}
|
||||
|
||||
if (!peekByte(cursor, &next))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (next == 0xC0)
|
||||
{
|
||||
return readNil(cursor);
|
||||
}
|
||||
|
||||
uint32_t stamp = 0;
|
||||
if (!readUint(cursor, &stamp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (out_has_stamp_cost)
|
||||
{
|
||||
*out_has_stamp_cost = true;
|
||||
}
|
||||
if (out_stamp_cost)
|
||||
{
|
||||
*out_stamp_cost = static_cast<uint8_t>(stamp);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool encodeTextPayload(double timestamp,
|
||||
const char* title,
|
||||
const char* content,
|
||||
uint8_t* out_payload,
|
||||
size_t* inout_len)
|
||||
{
|
||||
if (!out_payload || !inout_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t* title_bytes = reinterpret_cast<const uint8_t*>(title ? title : "");
|
||||
const size_t title_len = (title != nullptr) ? strlen(title) : 0;
|
||||
const uint8_t* content_bytes = reinterpret_cast<const uint8_t*>(content ? content : "");
|
||||
const size_t content_len = (content != nullptr) ? strlen(content) : 0;
|
||||
|
||||
size_t used = 0;
|
||||
if (!appendArrayHeader(4, out_payload, *inout_len, used) ||
|
||||
!appendFloat64(timestamp, out_payload, *inout_len, used) ||
|
||||
!appendBin(title_bytes, title_len, out_payload, *inout_len, used) ||
|
||||
!appendBin(content_bytes, content_len, out_payload, *inout_len, used) ||
|
||||
!appendMapHeader(0, out_payload, *inout_len, used))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*inout_len = used;
|
||||
return true;
|
||||
}
|
||||
|
||||
void computeMessageHash(const uint8_t destination_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t source_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t* packed_payload,
|
||||
size_t packed_payload_len,
|
||||
uint8_t out_hash[reticulum::kFullHashSize])
|
||||
{
|
||||
uint8_t material[reticulum::kReticulumMtu] = {};
|
||||
size_t used = 0;
|
||||
memcpy(material + used, destination_hash, reticulum::kTruncatedHashSize);
|
||||
used += reticulum::kTruncatedHashSize;
|
||||
memcpy(material + used, source_hash, reticulum::kTruncatedHashSize);
|
||||
used += reticulum::kTruncatedHashSize;
|
||||
if (packed_payload && packed_payload_len != 0)
|
||||
{
|
||||
memcpy(material + used, packed_payload, packed_payload_len);
|
||||
used += packed_payload_len;
|
||||
}
|
||||
reticulum::fullHash(material, used, out_hash);
|
||||
}
|
||||
|
||||
bool buildSignedPart(const uint8_t destination_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t source_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t* packed_payload,
|
||||
size_t packed_payload_len,
|
||||
uint8_t* out_signed_part,
|
||||
size_t* inout_len,
|
||||
uint8_t out_message_hash[reticulum::kFullHashSize])
|
||||
{
|
||||
if (!destination_hash || !source_hash || !out_signed_part || !inout_len || !out_message_hash)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
computeMessageHash(destination_hash, source_hash, packed_payload, packed_payload_len, out_message_hash);
|
||||
|
||||
const size_t total_len = (reticulum::kTruncatedHashSize * 2) +
|
||||
packed_payload_len +
|
||||
reticulum::kFullHashSize;
|
||||
if (*inout_len < total_len)
|
||||
{
|
||||
*inout_len = total_len;
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t used = 0;
|
||||
memcpy(out_signed_part + used, destination_hash, reticulum::kTruncatedHashSize);
|
||||
used += reticulum::kTruncatedHashSize;
|
||||
memcpy(out_signed_part + used, source_hash, reticulum::kTruncatedHashSize);
|
||||
used += reticulum::kTruncatedHashSize;
|
||||
if (packed_payload && packed_payload_len != 0)
|
||||
{
|
||||
memcpy(out_signed_part + used, packed_payload, packed_payload_len);
|
||||
used += packed_payload_len;
|
||||
}
|
||||
memcpy(out_signed_part + used, out_message_hash, reticulum::kFullHashSize);
|
||||
used += reticulum::kFullHashSize;
|
||||
|
||||
*inout_len = used;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool packMessage(const uint8_t destination_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t source_hash[reticulum::kTruncatedHashSize],
|
||||
const uint8_t signature[reticulum::kSignatureSize],
|
||||
const uint8_t* packed_payload,
|
||||
size_t packed_payload_len,
|
||||
uint8_t* out_message,
|
||||
size_t* inout_len)
|
||||
{
|
||||
if (!destination_hash || !source_hash || !signature || !out_message || !inout_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t total_len = (reticulum::kTruncatedHashSize * 2) +
|
||||
reticulum::kSignatureSize +
|
||||
packed_payload_len;
|
||||
if (*inout_len < total_len)
|
||||
{
|
||||
*inout_len = total_len;
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t used = 0;
|
||||
memcpy(out_message + used, destination_hash, reticulum::kTruncatedHashSize);
|
||||
used += reticulum::kTruncatedHashSize;
|
||||
memcpy(out_message + used, source_hash, reticulum::kTruncatedHashSize);
|
||||
used += reticulum::kTruncatedHashSize;
|
||||
memcpy(out_message + used, signature, reticulum::kSignatureSize);
|
||||
used += reticulum::kSignatureSize;
|
||||
if (packed_payload && packed_payload_len != 0)
|
||||
{
|
||||
memcpy(out_message + used, packed_payload, packed_payload_len);
|
||||
used += packed_payload_len;
|
||||
}
|
||||
*inout_len = used;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unpackMessage(const uint8_t* data, size_t len, DecodedMessage* out_message)
|
||||
{
|
||||
if (!data || len < ((reticulum::kTruncatedHashSize * 2) + reticulum::kSignatureSize + 4) || !out_message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DecodedMessage decoded{};
|
||||
memcpy(decoded.destination_hash, data, reticulum::kTruncatedHashSize);
|
||||
memcpy(decoded.source_hash, data + reticulum::kTruncatedHashSize, reticulum::kTruncatedHashSize);
|
||||
memcpy(decoded.signature,
|
||||
data + (reticulum::kTruncatedHashSize * 2),
|
||||
reticulum::kSignatureSize);
|
||||
|
||||
const uint8_t* payload_ptr = data + (reticulum::kTruncatedHashSize * 2) + reticulum::kSignatureSize;
|
||||
const size_t payload_len = len - ((reticulum::kTruncatedHashSize * 2) + reticulum::kSignatureSize);
|
||||
Cursor cursor;
|
||||
cursor.data = payload_ptr;
|
||||
cursor.len = payload_len;
|
||||
cursor.pos = 0;
|
||||
|
||||
size_t element_count = 0;
|
||||
if (!readArrayHeader(cursor, &element_count) || element_count < 4 || element_count > 5)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!readFloat64(cursor, &decoded.timestamp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> title_bytes;
|
||||
std::vector<uint8_t> content_bytes;
|
||||
if (!readBinary(cursor, &title_bytes) || !readBinary(cursor, &content_bytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
decoded.title.assign(title_bytes.begin(), title_bytes.end());
|
||||
decoded.content.assign(content_bytes.begin(), content_bytes.end());
|
||||
|
||||
size_t map_count = 0;
|
||||
if (!readMapHeader(cursor, &map_count))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
decoded.fields_empty = (map_count == 0);
|
||||
for (size_t i = 0; i < map_count; ++i)
|
||||
{
|
||||
if (!skipObject(cursor) || !skipObject(cursor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (element_count == 5)
|
||||
{
|
||||
uint8_t next = 0;
|
||||
if (!peekByte(cursor, &next))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (next == 0xC0)
|
||||
{
|
||||
if (!readNil(cursor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
decoded.has_stamp = true;
|
||||
if (!readBinary(cursor, &decoded.stamp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decoded.packed_payload.assign(payload_ptr, payload_ptr + cursor.pos);
|
||||
*out_message = std::move(decoded);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace chat::lxmf
|
||||
@@ -14,6 +14,8 @@ bool isValidMeshProtocol(MeshProtocol protocol)
|
||||
{
|
||||
case MeshProtocol::Meshtastic:
|
||||
case MeshProtocol::MeshCore:
|
||||
case MeshProtocol::RNode:
|
||||
case MeshProtocol::LXMF:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -37,6 +39,10 @@ const char* meshProtocolName(MeshProtocol protocol)
|
||||
{
|
||||
case MeshProtocol::MeshCore:
|
||||
return "MeshCore";
|
||||
case MeshProtocol::RNode:
|
||||
return "RNode";
|
||||
case MeshProtocol::LXMF:
|
||||
return "LXMF";
|
||||
case MeshProtocol::Meshtastic:
|
||||
default:
|
||||
return "Meshtastic";
|
||||
@@ -49,6 +55,10 @@ const char* meshProtocolShortName(MeshProtocol protocol)
|
||||
{
|
||||
case MeshProtocol::MeshCore:
|
||||
return "MC";
|
||||
case MeshProtocol::RNode:
|
||||
return "RN";
|
||||
case MeshProtocol::LXMF:
|
||||
return "LX";
|
||||
case MeshProtocol::Meshtastic:
|
||||
default:
|
||||
return "MT";
|
||||
@@ -61,6 +71,10 @@ const char* meshProtocolSlug(MeshProtocol protocol)
|
||||
{
|
||||
case MeshProtocol::MeshCore:
|
||||
return "meshcore";
|
||||
case MeshProtocol::RNode:
|
||||
return "rnode";
|
||||
case MeshProtocol::LXMF:
|
||||
return "lxmf";
|
||||
case MeshProtocol::Meshtastic:
|
||||
default:
|
||||
return "meshtastic";
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
/**
|
||||
* @file reticulum_wire.cpp
|
||||
* @brief Shared Reticulum packet and token helpers for device-side subsets
|
||||
*/
|
||||
|
||||
#include "chat/infra/reticulum/reticulum_wire.h"
|
||||
|
||||
#include <AES.h>
|
||||
#include <Crypto.h>
|
||||
#include <SHA256.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace chat::reticulum
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr size_t kAesBlockSize = 16;
|
||||
constexpr size_t kHeader1Size = 2 + kTruncatedHashSize + 1;
|
||||
constexpr size_t kHeader2Size = 2 + kTruncatedHashSize + kTruncatedHashSize + 1;
|
||||
constexpr uint8_t kHeaderType1 = 0x00;
|
||||
constexpr uint8_t kHeaderType2 = 0x01;
|
||||
|
||||
class Aes256CbcCipher
|
||||
{
|
||||
public:
|
||||
void setKey(const uint8_t* key, size_t len)
|
||||
{
|
||||
valid_ = (key != nullptr && len == 32);
|
||||
if (valid_)
|
||||
{
|
||||
aes_.setKey(key, len);
|
||||
}
|
||||
}
|
||||
|
||||
bool valid() const
|
||||
{
|
||||
return valid_;
|
||||
}
|
||||
|
||||
void encryptBlock(uint8_t* out, const uint8_t* in)
|
||||
{
|
||||
if (!out || !in)
|
||||
{
|
||||
return;
|
||||
}
|
||||
aes_.encryptBlock(out, in);
|
||||
}
|
||||
|
||||
void decryptBlock(uint8_t* out, const uint8_t* in)
|
||||
{
|
||||
if (!out || !in)
|
||||
{
|
||||
return;
|
||||
}
|
||||
aes_.decryptBlock(out, in);
|
||||
}
|
||||
|
||||
private:
|
||||
AESSmall256 aes_;
|
||||
bool valid_ = false;
|
||||
};
|
||||
|
||||
void hmacSha256(const uint8_t* key, size_t key_len,
|
||||
const uint8_t* data, size_t data_len,
|
||||
uint8_t out_hash[kFullHashSize])
|
||||
{
|
||||
if (!out_hash)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SHA256 sha;
|
||||
sha.resetHMAC(key, key_len);
|
||||
if (data && data_len != 0)
|
||||
{
|
||||
sha.update(data, data_len);
|
||||
}
|
||||
sha.finalizeHMAC(key, key_len, out_hash, kFullHashSize);
|
||||
}
|
||||
|
||||
void xorBlock(uint8_t* dst, const uint8_t* src)
|
||||
{
|
||||
if (!dst || !src)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < kAesBlockSize; ++i)
|
||||
{
|
||||
dst[i] ^= src[i];
|
||||
}
|
||||
}
|
||||
|
||||
bool constantTimeEquals(const uint8_t* a, const uint8_t* b, size_t len)
|
||||
{
|
||||
if ((!a || !b) && len != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
uint8_t diff = 0;
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
{
|
||||
diff |= static_cast<uint8_t>(a[i] ^ b[i]);
|
||||
}
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
size_t pkcs7Pad(const uint8_t* input, size_t input_len,
|
||||
uint8_t* out, size_t out_len)
|
||||
{
|
||||
const size_t pad_len = kAesBlockSize - (input_len % kAesBlockSize);
|
||||
const size_t total_len = input_len + ((pad_len == 0) ? kAesBlockSize : pad_len);
|
||||
if (!out || out_len < total_len)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (input && input_len != 0)
|
||||
{
|
||||
memcpy(out, input, input_len);
|
||||
}
|
||||
const uint8_t applied = static_cast<uint8_t>((pad_len == 0) ? kAesBlockSize : pad_len);
|
||||
for (size_t i = input_len; i < total_len; ++i)
|
||||
{
|
||||
out[i] = applied;
|
||||
}
|
||||
return total_len;
|
||||
}
|
||||
|
||||
bool pkcs7Unpad(const uint8_t* input, size_t input_len,
|
||||
uint8_t* out, size_t* inout_len)
|
||||
{
|
||||
if (!input || input_len == 0 || !out || !inout_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t pad_len = input[input_len - 1];
|
||||
if (pad_len == 0 || pad_len > kAesBlockSize || pad_len > input_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < pad_len; ++i)
|
||||
{
|
||||
if (input[input_len - 1 - i] != pad_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const size_t plain_len = input_len - pad_len;
|
||||
if (*inout_len < plain_len)
|
||||
{
|
||||
*inout_len = plain_len;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (plain_len != 0)
|
||||
{
|
||||
memcpy(out, input, plain_len);
|
||||
}
|
||||
*inout_len = plain_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
void aesCbcEncrypt(const uint8_t* key, size_t key_len,
|
||||
const uint8_t iv[kTokenIvSize],
|
||||
const uint8_t* plaintext, size_t plaintext_len,
|
||||
uint8_t* out_ciphertext)
|
||||
{
|
||||
Aes256CbcCipher cipher;
|
||||
cipher.setKey(key, key_len);
|
||||
if (!cipher.valid() || !iv || !out_ciphertext)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t previous[kAesBlockSize] = {};
|
||||
memcpy(previous, iv, sizeof(previous));
|
||||
|
||||
for (size_t offset = 0; offset < plaintext_len; offset += kAesBlockSize)
|
||||
{
|
||||
uint8_t block[kAesBlockSize] = {};
|
||||
memcpy(block, plaintext + offset, kAesBlockSize);
|
||||
xorBlock(block, previous);
|
||||
cipher.encryptBlock(out_ciphertext + offset, block);
|
||||
memcpy(previous, out_ciphertext + offset, kAesBlockSize);
|
||||
}
|
||||
}
|
||||
|
||||
void aesCbcDecrypt(const uint8_t* key, size_t key_len,
|
||||
const uint8_t iv[kTokenIvSize],
|
||||
const uint8_t* ciphertext, size_t ciphertext_len,
|
||||
uint8_t* out_plaintext)
|
||||
{
|
||||
Aes256CbcCipher cipher;
|
||||
cipher.setKey(key, key_len);
|
||||
if (!cipher.valid() || !iv || !out_plaintext)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t previous[kAesBlockSize] = {};
|
||||
memcpy(previous, iv, sizeof(previous));
|
||||
|
||||
for (size_t offset = 0; offset < ciphertext_len; offset += kAesBlockSize)
|
||||
{
|
||||
uint8_t block[kAesBlockSize] = {};
|
||||
cipher.decryptBlock(block, ciphertext + offset);
|
||||
xorBlock(block, previous);
|
||||
memcpy(out_plaintext + offset, block, kAesBlockSize);
|
||||
memcpy(previous, ciphertext + offset, kAesBlockSize);
|
||||
}
|
||||
}
|
||||
|
||||
void appendAscii(char* out, size_t out_len, size_t& index, const char* text)
|
||||
{
|
||||
if (!out || out_len == 0 || !text)
|
||||
{
|
||||
return;
|
||||
}
|
||||
while (*text != '\0' && index + 1 < out_len)
|
||||
{
|
||||
out[index++] = *text++;
|
||||
}
|
||||
out[index] = '\0';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
size_t paddedTokenPlaintextSize(size_t plaintext_len)
|
||||
{
|
||||
const size_t remainder = plaintext_len % kAesBlockSize;
|
||||
return plaintext_len + ((remainder == 0) ? kAesBlockSize : (kAesBlockSize - remainder));
|
||||
}
|
||||
|
||||
size_t tokenSizeForPlaintext(size_t plaintext_len)
|
||||
{
|
||||
return kTokenOverhead + paddedTokenPlaintextSize(plaintext_len);
|
||||
}
|
||||
|
||||
void fullHash(const uint8_t* data, size_t len, uint8_t out_hash[kFullHashSize])
|
||||
{
|
||||
if (!out_hash)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SHA256 sha;
|
||||
if (data && len != 0)
|
||||
{
|
||||
sha.update(data, len);
|
||||
}
|
||||
sha.finalize(out_hash, kFullHashSize);
|
||||
}
|
||||
|
||||
void truncatedHash(const uint8_t* data, size_t len, uint8_t out_hash[kTruncatedHashSize])
|
||||
{
|
||||
uint8_t hash[kFullHashSize] = {};
|
||||
fullHash(data, len, hash);
|
||||
memcpy(out_hash, hash, kTruncatedHashSize);
|
||||
}
|
||||
|
||||
void computeNameHash(const char* app_name, const char* aspect,
|
||||
uint8_t out_hash[kNameHashSize])
|
||||
{
|
||||
char expanded[64] = {};
|
||||
size_t index = 0;
|
||||
appendAscii(expanded, sizeof(expanded), index, app_name ? app_name : "");
|
||||
if (aspect && aspect[0] != '\0' && index + 1 < sizeof(expanded))
|
||||
{
|
||||
expanded[index++] = '.';
|
||||
expanded[index] = '\0';
|
||||
appendAscii(expanded, sizeof(expanded), index, aspect);
|
||||
}
|
||||
|
||||
uint8_t full[kFullHashSize] = {};
|
||||
fullHash(reinterpret_cast<const uint8_t*>(expanded), strlen(expanded), full);
|
||||
memcpy(out_hash, full, kNameHashSize);
|
||||
}
|
||||
|
||||
void computeIdentityHash(const uint8_t public_key[kCombinedPublicKeySize],
|
||||
uint8_t out_hash[kTruncatedHashSize])
|
||||
{
|
||||
truncatedHash(public_key, kCombinedPublicKeySize, out_hash);
|
||||
}
|
||||
|
||||
void computePlainDestinationHash(const uint8_t name_hash[kNameHashSize],
|
||||
uint8_t out_hash[kTruncatedHashSize])
|
||||
{
|
||||
truncatedHash(name_hash, kNameHashSize, out_hash);
|
||||
}
|
||||
|
||||
void computeDestinationHash(const uint8_t name_hash[kNameHashSize],
|
||||
const uint8_t identity_hash[kTruncatedHashSize],
|
||||
uint8_t out_hash[kTruncatedHashSize])
|
||||
{
|
||||
uint8_t material[kNameHashSize + kTruncatedHashSize] = {};
|
||||
memcpy(material, name_hash, kNameHashSize);
|
||||
memcpy(material + kNameHashSize, identity_hash, kTruncatedHashSize);
|
||||
truncatedHash(material, sizeof(material), out_hash);
|
||||
}
|
||||
|
||||
void computePacketHash(const uint8_t* raw_packet, size_t len,
|
||||
uint8_t out_hash[kFullHashSize])
|
||||
{
|
||||
if (!raw_packet || len < kHeader1Size || !out_hash)
|
||||
{
|
||||
if (out_hash)
|
||||
{
|
||||
memset(out_hash, 0, kFullHashSize);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t hashable[kReticulumMtu] = {};
|
||||
size_t hashable_len = 0;
|
||||
hashable[hashable_len++] = static_cast<uint8_t>(raw_packet[0] & 0x0FU);
|
||||
|
||||
const uint8_t header_type = static_cast<uint8_t>((raw_packet[0] >> 6) & 0x01U);
|
||||
if (header_type == kHeaderType2)
|
||||
{
|
||||
if (len < kHeader2Size)
|
||||
{
|
||||
memset(out_hash, 0, kFullHashSize);
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(hashable + hashable_len,
|
||||
raw_packet + 2 + kTruncatedHashSize,
|
||||
len - (2 + kTruncatedHashSize));
|
||||
hashable_len += (len - (2 + kTruncatedHashSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
memcpy(hashable + hashable_len, raw_packet + 2, len - 2);
|
||||
hashable_len += (len - 2);
|
||||
}
|
||||
|
||||
fullHash(hashable, hashable_len, out_hash);
|
||||
}
|
||||
|
||||
void computeTruncatedPacketHash(const uint8_t* raw_packet, size_t len,
|
||||
uint8_t out_hash[kTruncatedHashSize])
|
||||
{
|
||||
uint8_t full[kFullHashSize] = {};
|
||||
computePacketHash(raw_packet, len, full);
|
||||
memcpy(out_hash, full, kTruncatedHashSize);
|
||||
}
|
||||
|
||||
uint32_t nodeIdFromDestinationHash(const uint8_t destination_hash[kTruncatedHashSize])
|
||||
{
|
||||
if (!destination_hash)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return (static_cast<uint32_t>(destination_hash[12]) << 24) |
|
||||
(static_cast<uint32_t>(destination_hash[13]) << 16) |
|
||||
(static_cast<uint32_t>(destination_hash[14]) << 8) |
|
||||
static_cast<uint32_t>(destination_hash[15]);
|
||||
}
|
||||
|
||||
bool parsePacket(const uint8_t* data, size_t len, ParsedPacket* out_packet)
|
||||
{
|
||||
if (!data || len < kHeader1Size || !out_packet)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ParsedPacket parsed{};
|
||||
parsed.raw_flags = data[0];
|
||||
parsed.hops = data[1];
|
||||
|
||||
parsed.header_type = static_cast<uint8_t>((parsed.raw_flags >> 6) & 0x01U);
|
||||
if (parsed.header_type != kHeaderType1 && parsed.header_type != kHeaderType2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
parsed.context_flag = static_cast<uint8_t>((parsed.raw_flags >> 5) & 0x01U);
|
||||
parsed.transport_type = static_cast<TransportType>((parsed.raw_flags >> 4) & 0x01U);
|
||||
parsed.destination_type = static_cast<DestinationType>((parsed.raw_flags >> 2) & 0x03U);
|
||||
parsed.packet_type = static_cast<PacketType>(parsed.raw_flags & 0x03U);
|
||||
|
||||
if (parsed.header_type == kHeaderType2)
|
||||
{
|
||||
if (len < kHeader2Size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
parsed.transport_id = data + 2;
|
||||
parsed.destination_hash = data + 2 + kTruncatedHashSize;
|
||||
parsed.context = data[2 + (kTruncatedHashSize * 2)];
|
||||
parsed.payload = data + kHeader2Size;
|
||||
parsed.payload_len = len - kHeader2Size;
|
||||
parsed.header_len = kHeader2Size;
|
||||
}
|
||||
else
|
||||
{
|
||||
parsed.transport_id = nullptr;
|
||||
parsed.destination_hash = data + 2;
|
||||
parsed.context = data[2 + kTruncatedHashSize];
|
||||
parsed.payload = data + kHeader1Size;
|
||||
parsed.payload_len = len - kHeader1Size;
|
||||
parsed.header_len = kHeader1Size;
|
||||
}
|
||||
|
||||
parsed.valid = true;
|
||||
|
||||
*out_packet = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool buildHeader1Packet(PacketType packet_type,
|
||||
DestinationType destination_type,
|
||||
PacketContext context,
|
||||
bool context_flag,
|
||||
const uint8_t destination_hash[kTruncatedHashSize],
|
||||
const uint8_t* payload, size_t payload_len,
|
||||
uint8_t* out_packet, size_t* inout_len,
|
||||
uint8_t hops,
|
||||
TransportType transport_type)
|
||||
{
|
||||
if (!destination_hash || !out_packet || !inout_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t total_len = kHeader1Size + payload_len;
|
||||
if (*inout_len < total_len || total_len > kReticulumMtu)
|
||||
{
|
||||
*inout_len = total_len;
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t flags =
|
||||
static_cast<uint8_t>((0U << 6) |
|
||||
((context_flag ? 1U : 0U) << 5) |
|
||||
((static_cast<uint8_t>(transport_type) & 0x01U) << 4) |
|
||||
((static_cast<uint8_t>(destination_type) & 0x03U) << 2) |
|
||||
(static_cast<uint8_t>(packet_type) & 0x03U));
|
||||
|
||||
out_packet[0] = flags;
|
||||
out_packet[1] = hops;
|
||||
memcpy(out_packet + 2, destination_hash, kTruncatedHashSize);
|
||||
out_packet[2 + kTruncatedHashSize] = static_cast<uint8_t>(context);
|
||||
if (payload && payload_len != 0)
|
||||
{
|
||||
memcpy(out_packet + kHeader1Size, payload, payload_len);
|
||||
}
|
||||
*inout_len = total_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool buildHeader2Packet(PacketType packet_type,
|
||||
DestinationType destination_type,
|
||||
PacketContext context,
|
||||
bool context_flag,
|
||||
const uint8_t transport_id[kTruncatedHashSize],
|
||||
const uint8_t destination_hash[kTruncatedHashSize],
|
||||
const uint8_t* payload, size_t payload_len,
|
||||
uint8_t* out_packet, size_t* inout_len,
|
||||
uint8_t hops,
|
||||
TransportType transport_type)
|
||||
{
|
||||
if (!transport_id || !destination_hash || !out_packet || !inout_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t total_len = kHeader2Size + payload_len;
|
||||
if (*inout_len < total_len || total_len > kReticulumMtu)
|
||||
{
|
||||
*inout_len = total_len;
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t flags =
|
||||
static_cast<uint8_t>((1U << 6) |
|
||||
((context_flag ? 1U : 0U) << 5) |
|
||||
((static_cast<uint8_t>(transport_type) & 0x01U) << 4) |
|
||||
((static_cast<uint8_t>(destination_type) & 0x03U) << 2) |
|
||||
(static_cast<uint8_t>(packet_type) & 0x03U));
|
||||
|
||||
out_packet[0] = flags;
|
||||
out_packet[1] = hops;
|
||||
memcpy(out_packet + 2, transport_id, kTruncatedHashSize);
|
||||
memcpy(out_packet + 2 + kTruncatedHashSize, destination_hash, kTruncatedHashSize);
|
||||
out_packet[2 + (kTruncatedHashSize * 2)] = static_cast<uint8_t>(context);
|
||||
if (payload && payload_len != 0)
|
||||
{
|
||||
memcpy(out_packet + kHeader2Size, payload, payload_len);
|
||||
}
|
||||
*inout_len = total_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseAnnounce(const ParsedPacket& packet, ParsedAnnounce* out_announce)
|
||||
{
|
||||
if (!packet.valid || !out_announce ||
|
||||
packet.packet_type != PacketType::Announce ||
|
||||
packet.payload == nullptr ||
|
||||
packet.payload_len < (kCombinedPublicKeySize + kNameHashSize + 10 + kSignatureSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ParsedAnnounce parsed{};
|
||||
parsed.valid = true;
|
||||
parsed.has_ratchet = (packet.context_flag != 0);
|
||||
parsed.public_key = packet.payload;
|
||||
parsed.name_hash = packet.payload + kCombinedPublicKeySize;
|
||||
parsed.random_hash = parsed.name_hash + kNameHashSize;
|
||||
|
||||
if (parsed.has_ratchet)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
parsed.signature = parsed.random_hash + 10;
|
||||
parsed.app_data = parsed.signature + kSignatureSize;
|
||||
parsed.app_data_len = packet.payload_len - (kCombinedPublicKeySize + kNameHashSize + 10 + kSignatureSize);
|
||||
|
||||
*out_announce = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool hkdfSha256(const uint8_t* ikm, size_t ikm_len,
|
||||
const uint8_t* salt, size_t salt_len,
|
||||
const uint8_t* info, size_t info_len,
|
||||
uint8_t* out_key, size_t out_len)
|
||||
{
|
||||
if (!ikm || ikm_len == 0 || !out_key || out_len == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t zero_salt[kFullHashSize] = {};
|
||||
const uint8_t* actual_salt = (salt && salt_len != 0) ? salt : zero_salt;
|
||||
const size_t actual_salt_len = (salt && salt_len != 0) ? salt_len : sizeof(zero_salt);
|
||||
|
||||
uint8_t prk[kFullHashSize] = {};
|
||||
hmacSha256(actual_salt, actual_salt_len, ikm, ikm_len, prk);
|
||||
|
||||
uint8_t previous[kFullHashSize] = {};
|
||||
size_t generated = 0;
|
||||
uint8_t counter = 1;
|
||||
size_t previous_len = 0;
|
||||
|
||||
while (generated < out_len)
|
||||
{
|
||||
uint8_t block_input[kFullHashSize + 64 + 1] = {};
|
||||
size_t block_len = 0;
|
||||
if (previous_len != 0)
|
||||
{
|
||||
memcpy(block_input + block_len, previous, previous_len);
|
||||
block_len += previous_len;
|
||||
}
|
||||
if (info && info_len != 0)
|
||||
{
|
||||
memcpy(block_input + block_len, info, info_len);
|
||||
block_len += info_len;
|
||||
}
|
||||
block_input[block_len++] = counter++;
|
||||
|
||||
hmacSha256(prk, sizeof(prk), block_input, block_len, previous);
|
||||
previous_len = sizeof(previous);
|
||||
|
||||
const size_t remaining = out_len - generated;
|
||||
const size_t chunk = std::min(remaining, sizeof(previous));
|
||||
memcpy(out_key + generated, previous, chunk);
|
||||
generated += chunk;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool tokenEncrypt(const uint8_t derived_key[kDerivedTokenKeySize],
|
||||
const uint8_t iv[kTokenIvSize],
|
||||
const uint8_t* plaintext, size_t plaintext_len,
|
||||
uint8_t* out_token, size_t* inout_len)
|
||||
{
|
||||
if (!derived_key || !iv || !out_token || !inout_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t padded_len = paddedTokenPlaintextSize(plaintext_len);
|
||||
const size_t total_len = tokenSizeForPlaintext(plaintext_len);
|
||||
if (*inout_len < total_len)
|
||||
{
|
||||
*inout_len = total_len;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t padded[kReticulumMtu] = {};
|
||||
if (padded_len > sizeof(padded))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (pkcs7Pad(plaintext, plaintext_len, padded, sizeof(padded)) != padded_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(out_token, iv, kTokenIvSize);
|
||||
aesCbcEncrypt(derived_key + 32, 32, iv, padded, padded_len, out_token + kTokenIvSize);
|
||||
|
||||
uint8_t mac[kFullHashSize] = {};
|
||||
hmacSha256(derived_key, 32, out_token, kTokenIvSize + padded_len, mac);
|
||||
memcpy(out_token + kTokenIvSize + padded_len, mac, sizeof(mac));
|
||||
*inout_len = total_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool tokenDecrypt(const uint8_t derived_key[kDerivedTokenKeySize],
|
||||
const uint8_t* token, size_t token_len,
|
||||
uint8_t* out_plaintext, size_t* inout_len)
|
||||
{
|
||||
if (!derived_key || !token || token_len <= kTokenOverhead || !out_plaintext || !inout_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t cipher_len = token_len - kTokenOverhead;
|
||||
if ((cipher_len % kAesBlockSize) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t* iv = token;
|
||||
const uint8_t* ciphertext = token + kTokenIvSize;
|
||||
const uint8_t* received_hmac = token + kTokenIvSize + cipher_len;
|
||||
|
||||
uint8_t expected_hmac[kFullHashSize] = {};
|
||||
hmacSha256(derived_key, 32, token, kTokenIvSize + cipher_len, expected_hmac);
|
||||
if (!constantTimeEquals(received_hmac, expected_hmac, sizeof(expected_hmac)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t padded[kReticulumMtu] = {};
|
||||
if (cipher_len > sizeof(padded))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
aesCbcDecrypt(derived_key + 32, 32, iv, ciphertext, cipher_len, padded);
|
||||
return pkcs7Unpad(padded, cipher_len, out_plaintext, inout_len);
|
||||
}
|
||||
|
||||
} // namespace chat::reticulum
|
||||
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* @file rnode_packet_wire.cpp
|
||||
* @brief Shared RNode over-air packet framing helpers
|
||||
*/
|
||||
|
||||
#include "chat/infra/rnode/rnode_packet_wire.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace chat
|
||||
{
|
||||
namespace rnode
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float kPreambleSymbolsMin = 18.0f;
|
||||
constexpr float kPreambleTargetMs = 24.0f;
|
||||
constexpr float kPreambleFastDeltaMs = 18.0f;
|
||||
constexpr uint32_t kFastThresholdBps = 30000U;
|
||||
|
||||
template <typename T>
|
||||
T clampValue(T value, T min_value, T max_value)
|
||||
{
|
||||
if (value < min_value)
|
||||
{
|
||||
return min_value;
|
||||
}
|
||||
if (value > max_value)
|
||||
{
|
||||
return max_value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool parseAirPacket(const uint8_t* data, size_t len, ParsedAirPacket* out)
|
||||
{
|
||||
if (!data || len <= kRNodeHeaderSize || !out)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
out->header = data[0];
|
||||
out->sequence = static_cast<uint8_t>(data[0] >> 4);
|
||||
out->split = (data[0] & kRNodeFlagSplit) != 0;
|
||||
out->payload = data + kRNodeHeaderSize;
|
||||
out->payload_len = len - kRNodeHeaderSize;
|
||||
return out->payload_len > 0;
|
||||
}
|
||||
|
||||
bool encodeAirPacketSet(const uint8_t* payload, size_t payload_len,
|
||||
uint8_t sequence, EncodedAirPacketSet* out)
|
||||
{
|
||||
if (!payload || payload_len == 0 || payload_len > kRNodeMaxPayloadSize || !out)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t base_header = static_cast<uint8_t>((sequence & 0x0FU) << 4);
|
||||
const bool split = payload_len > kRNodeFragmentPayloadSize;
|
||||
out->header = static_cast<uint8_t>(base_header | (split ? kRNodeFlagSplit : 0U));
|
||||
out->count = split ? 2U : 1U;
|
||||
|
||||
out->first[0] = out->header;
|
||||
const size_t first_payload_len =
|
||||
split ? kRNodeFragmentPayloadSize : payload_len;
|
||||
memcpy(out->first + kRNodeHeaderSize, payload, first_payload_len);
|
||||
out->first_len = kRNodeHeaderSize + first_payload_len;
|
||||
|
||||
if (split)
|
||||
{
|
||||
const size_t second_payload_len = payload_len - first_payload_len;
|
||||
out->second[0] = out->header;
|
||||
memcpy(out->second + kRNodeHeaderSize, payload + first_payload_len, second_payload_len);
|
||||
out->second_len = kRNodeHeaderSize + second_payload_len;
|
||||
}
|
||||
else
|
||||
{
|
||||
out->second_len = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool feedAirPacket(ReassemblyState* state,
|
||||
const uint8_t* data, size_t len,
|
||||
uint8_t* out_payload, size_t* inout_payload_len,
|
||||
bool* out_complete)
|
||||
{
|
||||
if (out_complete)
|
||||
{
|
||||
*out_complete = false;
|
||||
}
|
||||
|
||||
if (!state || !data || !out_payload || !inout_payload_len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ParsedAirPacket parsed{};
|
||||
if (!parseAirPacket(data, len, &parsed))
|
||||
{
|
||||
state->reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto emit_payload = [&](const uint8_t* src, size_t src_len) -> bool
|
||||
{
|
||||
if (*inout_payload_len < src_len)
|
||||
{
|
||||
*inout_payload_len = src_len;
|
||||
return false;
|
||||
}
|
||||
memcpy(out_payload, src, src_len);
|
||||
*inout_payload_len = src_len;
|
||||
if (out_complete)
|
||||
{
|
||||
*out_complete = true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!parsed.split)
|
||||
{
|
||||
state->reset();
|
||||
return emit_payload(parsed.payload, parsed.payload_len);
|
||||
}
|
||||
|
||||
if (state->sequence == kRNodeSeqUnset)
|
||||
{
|
||||
if (parsed.payload_len > sizeof(state->buffered))
|
||||
{
|
||||
state->reset();
|
||||
return false;
|
||||
}
|
||||
memcpy(state->buffered, parsed.payload, parsed.payload_len);
|
||||
state->buffered_len = parsed.payload_len;
|
||||
state->sequence = parsed.sequence;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state->sequence != parsed.sequence)
|
||||
{
|
||||
if (parsed.payload_len > sizeof(state->buffered))
|
||||
{
|
||||
state->reset();
|
||||
return false;
|
||||
}
|
||||
memcpy(state->buffered, parsed.payload, parsed.payload_len);
|
||||
state->buffered_len = parsed.payload_len;
|
||||
state->sequence = parsed.sequence;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state->buffered_len + parsed.payload_len > sizeof(state->buffered))
|
||||
{
|
||||
state->reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(state->buffered + state->buffered_len, parsed.payload, parsed.payload_len);
|
||||
state->buffered_len += parsed.payload_len;
|
||||
|
||||
const size_t complete_len = state->buffered_len;
|
||||
const bool ok = emit_payload(state->buffered, complete_len);
|
||||
state->reset();
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint32_t estimateBitrateBps(uint32_t bandwidth_hz, uint8_t spreading_factor, uint8_t coding_rate)
|
||||
{
|
||||
if (bandwidth_hz == 0 || spreading_factor < 5 || spreading_factor > 12 ||
|
||||
coding_rate < 5 || coding_rate > 8)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const float sf = static_cast<float>(spreading_factor);
|
||||
const float cr = static_cast<float>(coding_rate);
|
||||
const float bw_khz = static_cast<float>(bandwidth_hz) / 1000.0f;
|
||||
const float bitrate =
|
||||
sf * ((4.0f / cr) / (std::pow(2.0f, sf) / bw_khz)) * 1000.0f;
|
||||
if (!std::isfinite(bitrate) || bitrate <= 0.0f)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return static_cast<uint32_t>(std::lround(bitrate));
|
||||
}
|
||||
|
||||
float estimateSymbolTimeMs(uint32_t bandwidth_hz, uint8_t spreading_factor)
|
||||
{
|
||||
if (bandwidth_hz == 0 || spreading_factor < 5 || spreading_factor > 12)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float symbol_rate =
|
||||
static_cast<float>(bandwidth_hz) / std::pow(2.0f, static_cast<float>(spreading_factor));
|
||||
if (!std::isfinite(symbol_rate) || symbol_rate <= 0.0f)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
return (1.0f / symbol_rate) * 1000.0f;
|
||||
}
|
||||
|
||||
uint16_t recommendPreambleSymbols(uint32_t bandwidth_hz, uint8_t spreading_factor, uint8_t coding_rate)
|
||||
{
|
||||
const float symbol_time_ms = estimateSymbolTimeMs(bandwidth_hz, spreading_factor);
|
||||
if (symbol_time_ms <= 0.0f)
|
||||
{
|
||||
return static_cast<uint16_t>(kPreambleSymbolsMin);
|
||||
}
|
||||
|
||||
const uint32_t bitrate_bps = estimateBitrateBps(bandwidth_hz, spreading_factor, coding_rate);
|
||||
float target_ms = kPreambleTargetMs;
|
||||
if (bitrate_bps > kFastThresholdBps)
|
||||
{
|
||||
target_ms -= kPreambleFastDeltaMs;
|
||||
}
|
||||
|
||||
float preamble_symbols = target_ms / symbol_time_ms;
|
||||
if (!std::isfinite(preamble_symbols))
|
||||
{
|
||||
preamble_symbols = kPreambleSymbolsMin;
|
||||
}
|
||||
|
||||
preamble_symbols = std::max(kPreambleSymbolsMin, std::ceil(preamble_symbols));
|
||||
return clampValue<uint16_t>(static_cast<uint16_t>(preamble_symbols), 18U, 255U);
|
||||
}
|
||||
|
||||
} // namespace rnode
|
||||
} // namespace chat
|
||||
@@ -62,6 +62,11 @@ struct AppConfig
|
||||
static constexpr uint8_t kMeshCoreDefaultSf = 11;
|
||||
static constexpr uint8_t kMeshCoreDefaultCr = 5;
|
||||
static constexpr int8_t kMeshCoreDefaultTxPowerDbm = 20;
|
||||
static constexpr float kRNodeDefaultFreqMHz = 869.525f;
|
||||
static constexpr float kRNodeDefaultBwKHz = 125.0f;
|
||||
static constexpr uint8_t kRNodeDefaultSf = 9;
|
||||
static constexpr uint8_t kRNodeDefaultCr = 5;
|
||||
static constexpr int8_t kRNodeDefaultTxPowerDbm = 17;
|
||||
static constexpr int8_t kTxPowerMinDbm = -9;
|
||||
#if defined(TRAIL_MATE_LORA_TX_POWER_MAX_DBM)
|
||||
// Board/module capability must be declared per build target.
|
||||
@@ -77,6 +82,7 @@ struct AppConfig
|
||||
chat::ChatPolicy chat_policy;
|
||||
chat::MeshConfig meshtastic_config;
|
||||
chat::MeshConfig meshcore_config;
|
||||
chat::MeshConfig rnode_config;
|
||||
chat::MeshProtocol mesh_protocol;
|
||||
|
||||
// Device settings
|
||||
@@ -150,6 +156,8 @@ struct AppConfig
|
||||
|
||||
meshcore_config = chat::MeshConfig();
|
||||
applyMeshCoreFactoryDefaults();
|
||||
rnode_config = chat::MeshConfig();
|
||||
applyRNodeFactoryDefaults();
|
||||
mesh_protocol = chat::MeshProtocol::Meshtastic;
|
||||
node_name[0] = '\0';
|
||||
short_name[0] = '\0';
|
||||
@@ -209,14 +217,46 @@ struct AppConfig
|
||||
meshcore_config.meshcore_channel_name[sizeof(meshcore_config.meshcore_channel_name) - 1] = '\0';
|
||||
}
|
||||
|
||||
void applyRNodeFactoryDefaults()
|
||||
{
|
||||
rnode_config.use_preset = false;
|
||||
rnode_config.bandwidth_khz = kRNodeDefaultBwKHz;
|
||||
rnode_config.spread_factor = kRNodeDefaultSf;
|
||||
rnode_config.coding_rate = kRNodeDefaultCr;
|
||||
rnode_config.tx_power = kRNodeDefaultTxPowerDbm;
|
||||
rnode_config.tx_enabled = true;
|
||||
rnode_config.override_duty_cycle = false;
|
||||
rnode_config.override_frequency_mhz = kRNodeDefaultFreqMHz;
|
||||
}
|
||||
|
||||
chat::MeshConfig& activeMeshConfig()
|
||||
{
|
||||
return (mesh_protocol == chat::MeshProtocol::MeshCore) ? meshcore_config : meshtastic_config;
|
||||
switch (mesh_protocol)
|
||||
{
|
||||
case chat::MeshProtocol::MeshCore:
|
||||
return meshcore_config;
|
||||
case chat::MeshProtocol::LXMF:
|
||||
case chat::MeshProtocol::RNode:
|
||||
return rnode_config;
|
||||
case chat::MeshProtocol::Meshtastic:
|
||||
default:
|
||||
return meshtastic_config;
|
||||
}
|
||||
}
|
||||
|
||||
const chat::MeshConfig& activeMeshConfig() const
|
||||
{
|
||||
return (mesh_protocol == chat::MeshProtocol::MeshCore) ? meshcore_config : meshtastic_config;
|
||||
switch (mesh_protocol)
|
||||
{
|
||||
case chat::MeshProtocol::MeshCore:
|
||||
return meshcore_config;
|
||||
case chat::MeshProtocol::LXMF:
|
||||
case chat::MeshProtocol::RNode:
|
||||
return rnode_config;
|
||||
case chat::MeshProtocol::Meshtastic:
|
||||
default:
|
||||
return meshtastic_config;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "app/app_config.h"
|
||||
#include "app/app_facade_access.h"
|
||||
#include "chat/infra/mesh_protocol_utils.h"
|
||||
#include "chat/ports/i_mesh_adapter.h"
|
||||
#include "chat/usecase/contact_service.h"
|
||||
#include "platform/ui/gps_runtime.h"
|
||||
#include "platform/ui/screen_runtime.h"
|
||||
@@ -130,6 +131,36 @@ std::string base_conversation_name(const chat::ConversationId& conv)
|
||||
return buf;
|
||||
}
|
||||
|
||||
chat::MeshCapabilities active_mesh_capabilities()
|
||||
{
|
||||
chat::IMeshAdapter* adapter = app::messagingFacade().getMeshAdapter();
|
||||
return adapter ? adapter->getCapabilities() : chat::MeshCapabilities{};
|
||||
}
|
||||
|
||||
bool supports_local_text_chat()
|
||||
{
|
||||
return active_mesh_capabilities().supports_unicast_text;
|
||||
}
|
||||
|
||||
bool supports_team_chat()
|
||||
{
|
||||
return active_mesh_capabilities().supports_unicast_appdata;
|
||||
}
|
||||
|
||||
const char* local_text_chat_unavailable_message()
|
||||
{
|
||||
return (active_mesh_protocol() == chat::MeshProtocol::RNode)
|
||||
? "RNode text chat runs on host"
|
||||
: "Text chat unavailable";
|
||||
}
|
||||
|
||||
const char* team_chat_unavailable_message()
|
||||
{
|
||||
return (active_mesh_protocol() == chat::MeshProtocol::RNode)
|
||||
? "Team chat unavailable in RNode mode"
|
||||
: "Team chat unavailable";
|
||||
}
|
||||
|
||||
chat::ConversationId teamConversationId()
|
||||
{
|
||||
return chat::ConversationId(kTeamChatChannel, 0, active_mesh_protocol());
|
||||
@@ -597,7 +628,9 @@ void UiController::switchToConversation(chat::ConversationId conv)
|
||||
conversation_->setActionCallback(handle_conversation_action, this);
|
||||
conversation_->setBackCallback(handle_conversation_back, this);
|
||||
}
|
||||
const bool can_reply = team_conv_active_ || (conv.protocol == active_mesh_protocol());
|
||||
const bool can_reply = team_conv_active_
|
||||
? supports_team_chat()
|
||||
: (conv.protocol == active_mesh_protocol() && supports_local_text_chat());
|
||||
conversation_->setReplyEnabled(can_reply);
|
||||
|
||||
if (team_conv_active_)
|
||||
@@ -668,6 +701,16 @@ void UiController::switchToCompose(chat::ConversationId conv)
|
||||
::ui::SystemNotification::show("Conversation protocol mismatch", 2000);
|
||||
return;
|
||||
}
|
||||
if (!is_team_conv && !supports_local_text_chat())
|
||||
{
|
||||
::ui::SystemNotification::show(local_text_chat_unavailable_message(), 2200);
|
||||
return;
|
||||
}
|
||||
if (is_team_conv && !supports_team_chat())
|
||||
{
|
||||
::ui::SystemNotification::show(team_chat_unavailable_message(), 2200);
|
||||
return;
|
||||
}
|
||||
|
||||
state_ = State::Compose;
|
||||
current_channel_ = conv.channel;
|
||||
@@ -788,6 +831,11 @@ void UiController::handleSendMessage(const std::string& text)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!supports_local_text_chat())
|
||||
{
|
||||
::ui::SystemNotification::show(local_text_chat_unavailable_message(), 2200);
|
||||
return;
|
||||
}
|
||||
service_.sendText(current_channel_, text, current_conv_.peer);
|
||||
}
|
||||
|
||||
@@ -1873,6 +1921,16 @@ void UiController::handleConversationAction(ChatConversationScreen::ActionIntent
|
||||
::ui::SystemNotification::show("Reply disabled for this protocol", 2000);
|
||||
return;
|
||||
}
|
||||
if (!team_conv_active_ && !supports_local_text_chat())
|
||||
{
|
||||
::ui::SystemNotification::show(local_text_chat_unavailable_message(), 2200);
|
||||
return;
|
||||
}
|
||||
if (team_conv_active_ && !supports_team_chat())
|
||||
{
|
||||
::ui::SystemNotification::show(team_chat_unavailable_message(), 2200);
|
||||
return;
|
||||
}
|
||||
switchToCompose(current_conv_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,10 @@ static const char* node_protocol_short_label(chat::contacts::NodeProtocolType pr
|
||||
{
|
||||
switch (protocol)
|
||||
{
|
||||
case chat::contacts::NodeProtocolType::LXMF:
|
||||
return "LX";
|
||||
case chat::contacts::NodeProtocolType::RNode:
|
||||
return "RN";
|
||||
case chat::contacts::NodeProtocolType::MeshCore:
|
||||
return "MC";
|
||||
case chat::contacts::NodeProtocolType::Meshtastic:
|
||||
@@ -333,6 +337,12 @@ static bool node_protocol_to_mesh(chat::contacts::NodeProtocolType protocol, cha
|
||||
}
|
||||
switch (protocol)
|
||||
{
|
||||
case chat::contacts::NodeProtocolType::LXMF:
|
||||
*out = chat::MeshProtocol::LXMF;
|
||||
return true;
|
||||
case chat::contacts::NodeProtocolType::RNode:
|
||||
*out = chat::MeshProtocol::RNode;
|
||||
return true;
|
||||
case chat::contacts::NodeProtocolType::MeshCore:
|
||||
*out = chat::MeshProtocol::MeshCore;
|
||||
return true;
|
||||
@@ -375,9 +385,62 @@ static constexpr DiscoveryActionSpec kDiscoveryActionSpecs[] = {
|
||||
{"Cancel", "Back", DiscoveryActionCommand::Cancel},
|
||||
};
|
||||
|
||||
static chat::MeshCapabilities active_mesh_capabilities()
|
||||
{
|
||||
chat::IMeshAdapter* adapter = app::messagingFacade().getMeshAdapter();
|
||||
return adapter ? adapter->getCapabilities() : chat::MeshCapabilities{};
|
||||
}
|
||||
|
||||
static bool supports_local_text_chat()
|
||||
{
|
||||
return active_mesh_capabilities().supports_unicast_text;
|
||||
}
|
||||
|
||||
static bool supports_team_chat()
|
||||
{
|
||||
return active_mesh_capabilities().supports_unicast_appdata;
|
||||
}
|
||||
|
||||
static const char* local_text_chat_unavailable_message()
|
||||
{
|
||||
return (active_mesh_protocol() == chat::MeshProtocol::RNode)
|
||||
? "RNode text chat runs on host"
|
||||
: "Text chat unavailable";
|
||||
}
|
||||
|
||||
static const char* team_chat_unavailable_message()
|
||||
{
|
||||
return (active_mesh_protocol() == chat::MeshProtocol::RNode)
|
||||
? "Team chat unavailable in RNode mode"
|
||||
: "Team chat unavailable";
|
||||
}
|
||||
|
||||
static const char* broadcast_chat_unavailable_message(const BroadcastTargetSpec& spec)
|
||||
{
|
||||
if (spec.protocol == chat::MeshProtocol::Meshtastic)
|
||||
{
|
||||
return "MT send uses slot 0/1 only";
|
||||
}
|
||||
if (spec.protocol == chat::MeshProtocol::RNode)
|
||||
{
|
||||
return "RNode text chat runs on host";
|
||||
}
|
||||
return "Chat unavailable";
|
||||
}
|
||||
|
||||
static size_t get_broadcast_target_count()
|
||||
{
|
||||
return (active_mesh_protocol() == chat::MeshProtocol::Meshtastic) ? 8U : 2U;
|
||||
switch (active_mesh_protocol())
|
||||
{
|
||||
case chat::MeshProtocol::Meshtastic:
|
||||
return 8U;
|
||||
case chat::MeshProtocol::MeshCore:
|
||||
return 2U;
|
||||
case chat::MeshProtocol::RNode:
|
||||
return 1U;
|
||||
default:
|
||||
return 0U;
|
||||
}
|
||||
}
|
||||
|
||||
static bool get_broadcast_target_spec(int index, BroadcastTargetSpec* out)
|
||||
@@ -403,6 +466,20 @@ static bool get_broadcast_target_spec(int index, BroadcastTargetSpec* out)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (active_mesh_protocol() == chat::MeshProtocol::RNode)
|
||||
{
|
||||
if (index != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out->protocol = chat::MeshProtocol::RNode;
|
||||
out->channel = chat::ChannelId::PRIMARY;
|
||||
out->channel_index = 0;
|
||||
out->enabled = true;
|
||||
out->chat_supported = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
@@ -432,6 +509,10 @@ static std::string format_broadcast_target_label(const BroadcastTargetSpec& spec
|
||||
snprintf(buf, sizeof(buf), "[MT] Slot %u", static_cast<unsigned>(spec.channel_index));
|
||||
return std::string(buf);
|
||||
}
|
||||
if (spec.protocol == chat::MeshProtocol::RNode)
|
||||
{
|
||||
return "[RN] Modem Bridge";
|
||||
}
|
||||
return (spec.channel == chat::ChannelId::SECONDARY) ? "[MC] Secondary" : "[MC] Primary";
|
||||
}
|
||||
|
||||
@@ -453,6 +534,10 @@ static std::string format_broadcast_target_status(const BroadcastTargetSpec& spe
|
||||
}
|
||||
return spec.chat_supported ? "Ready" : "Slot";
|
||||
}
|
||||
if (spec.protocol == chat::MeshProtocol::RNode)
|
||||
{
|
||||
return "Host bridge";
|
||||
}
|
||||
return "Ready";
|
||||
}
|
||||
|
||||
@@ -736,7 +821,7 @@ static void on_list_item_clicked(lv_event_t* e)
|
||||
BroadcastTargetSpec spec{};
|
||||
if (get_selected_broadcast_target(&spec, nullptr) && !spec.chat_supported)
|
||||
{
|
||||
::ui::SystemNotification::show("MT send uses slot 0/1 only", 2200);
|
||||
::ui::SystemNotification::show(broadcast_chat_unavailable_message(spec), 2200);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1225,7 +1310,7 @@ static void open_chat_compose()
|
||||
}
|
||||
if (!target_spec.chat_supported)
|
||||
{
|
||||
::ui::SystemNotification::show("MT send uses slot 0/1 only", 2200);
|
||||
::ui::SystemNotification::show(broadcast_chat_unavailable_message(target_spec), 2200);
|
||||
return;
|
||||
}
|
||||
protocol = target_spec.protocol;
|
||||
@@ -1235,6 +1320,11 @@ static void open_chat_compose()
|
||||
}
|
||||
else if (g_contacts_state.current_mode == ContactsMode::Team)
|
||||
{
|
||||
if (!supports_team_chat())
|
||||
{
|
||||
::ui::SystemNotification::show(team_chat_unavailable_message(), 2200);
|
||||
return;
|
||||
}
|
||||
channel = chat::ChannelId::PRIMARY;
|
||||
peer_id = 0;
|
||||
title = team::ui::g_team_state.team_name.empty()
|
||||
@@ -1243,6 +1333,11 @@ static void open_chat_compose()
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!supports_local_text_chat())
|
||||
{
|
||||
::ui::SystemNotification::show(local_text_chat_unavailable_message(), 2200);
|
||||
return;
|
||||
}
|
||||
channel = chat::ChannelId::PRIMARY;
|
||||
peer_id = node->node_id;
|
||||
chat::MeshProtocol node_protocol = protocol;
|
||||
@@ -1251,7 +1346,7 @@ static void open_chat_compose()
|
||||
{
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "Switch to %s to chat",
|
||||
(node_protocol == chat::MeshProtocol::MeshCore) ? "MeshCore" : "Meshtastic");
|
||||
chat::infra::meshProtocolName(node_protocol));
|
||||
::ui::SystemNotification::show(buf, 2200);
|
||||
return;
|
||||
}
|
||||
@@ -1531,6 +1626,13 @@ static void on_compose_action(chat::ui::ChatComposeScreen::ActionIntent intent,
|
||||
return;
|
||||
}
|
||||
|
||||
if (!supports_local_text_chat())
|
||||
{
|
||||
::ui::SystemNotification::show(local_text_chat_unavailable_message(), 2200);
|
||||
close_chat_compose();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string text = g_contacts_state.compose_screen->getText();
|
||||
if (!text.empty())
|
||||
{
|
||||
@@ -2155,7 +2257,9 @@ static void open_action_menu_modal()
|
||||
(g_contacts_state.current_mode == ContactsMode::Contacts ||
|
||||
g_contacts_state.current_mode == ContactsMode::Nearby);
|
||||
|
||||
int action_count = 2; // Chat + Cancel
|
||||
const bool allow_chat_action =
|
||||
(g_contacts_state.current_mode == ContactsMode::Team) ? supports_team_chat() : supports_local_text_chat();
|
||||
int action_count = allow_chat_action ? 2 : 1; // Chat + Cancel
|
||||
if (g_contacts_state.current_mode == ContactsMode::Contacts)
|
||||
{
|
||||
action_count += 3; // Edit/Delete/Info
|
||||
@@ -2255,7 +2359,10 @@ static void open_action_menu_modal()
|
||||
}
|
||||
};
|
||||
|
||||
add_action(ActionMenuCommand::Chat, "Chat");
|
||||
if (allow_chat_action)
|
||||
{
|
||||
add_action(ActionMenuCommand::Chat, "Chat");
|
||||
}
|
||||
if (g_contacts_state.current_mode == ContactsMode::Contacts)
|
||||
{
|
||||
add_action(ActionMenuCommand::Edit, "Edit");
|
||||
@@ -2324,7 +2431,7 @@ void refresh_ui()
|
||||
lv_obj_clear_flag(g_contacts_state.sub_container, LV_OBJ_FLAG_SCROLLABLE);
|
||||
}
|
||||
|
||||
bool team_available = is_team_available();
|
||||
bool team_available = is_team_available() && supports_team_chat();
|
||||
const bool meshcore_mode = (active_mesh_protocol() == chat::MeshProtocol::MeshCore);
|
||||
if (g_contacts_state.team_btn)
|
||||
{
|
||||
@@ -2450,7 +2557,11 @@ void refresh_ui()
|
||||
target.display_name = format_broadcast_target_label(spec);
|
||||
target.protocol = (spec.protocol == chat::MeshProtocol::MeshCore)
|
||||
? chat::contacts::NodeProtocolType::MeshCore
|
||||
: chat::contacts::NodeProtocolType::Meshtastic;
|
||||
: ((spec.protocol == chat::MeshProtocol::LXMF)
|
||||
? chat::contacts::NodeProtocolType::LXMF
|
||||
: ((spec.protocol == chat::MeshProtocol::RNode)
|
||||
? chat::contacts::NodeProtocolType::RNode
|
||||
: chat::contacts::NodeProtocolType::Meshtastic));
|
||||
target.channel = spec.channel_index;
|
||||
broadcast_list.push_back(target);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "app/app_config.h"
|
||||
#include "app/app_facade_access.h"
|
||||
#include "chat/domain/chat_types.h"
|
||||
#include "chat/infra/mesh_protocol_utils.h"
|
||||
#include "chat/infra/meshtastic/mt_region.h"
|
||||
#include "ui/components/info_card.h"
|
||||
#include "ui/components/two_pane_layout.h"
|
||||
@@ -51,7 +52,33 @@ void format_contacts_title(char* out, size_t out_len)
|
||||
snprintf(out, out_len, "Contacts (MeshCore)");
|
||||
return;
|
||||
}
|
||||
snprintf(out, out_len, "Contacts");
|
||||
if (protocol == chat::MeshProtocol::RNode)
|
||||
{
|
||||
const chat::MeshConfig& rnode = app_ctx.getConfig().rnode_config;
|
||||
if (rnode.override_frequency_mhz > 0.0f)
|
||||
{
|
||||
snprintf(out, out_len, "Contacts (RNode - %.3fMHz)", rnode.override_frequency_mhz);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(out, out_len, "Contacts (RNode)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (protocol == chat::MeshProtocol::LXMF)
|
||||
{
|
||||
const chat::MeshConfig& lxmf = app_ctx.getConfig().rnode_config;
|
||||
if (lxmf.override_frequency_mhz > 0.0f)
|
||||
{
|
||||
snprintf(out, out_len, "Contacts (LXMF - %.3fMHz)", lxmf.override_frequency_mhz);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(out, out_len, "Contacts (LXMF)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
snprintf(out, out_len, "Contacts (%s)", chat::infra::meshProtocolName(protocol));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#if defined(ARDUINO) || defined(ESP_PLATFORM)
|
||||
|
||||
#include "app/app_config.h"
|
||||
#include "app/app_facade_access.h"
|
||||
#include "platform/ui/hostlink_runtime.h"
|
||||
#include "ui/app_runtime.h"
|
||||
#include "ui/assets/fonts/fonts.h"
|
||||
@@ -37,8 +39,42 @@ void request_exit()
|
||||
ui_request_exit_to_menu();
|
||||
}
|
||||
|
||||
bool use_rnode_bridge()
|
||||
{
|
||||
return app::appFacade().getConfig().mesh_protocol == chat::MeshProtocol::RNode;
|
||||
}
|
||||
|
||||
const char* page_title()
|
||||
{
|
||||
return use_rnode_bridge() ? "RNode Bridge" : "Data Exchange";
|
||||
}
|
||||
|
||||
const char* page_subtitle()
|
||||
{
|
||||
return use_rnode_bridge() ? "USB CDC KISS modem for Reticulum" : "Data Exchange";
|
||||
}
|
||||
|
||||
const char* status_text(platform::ui::hostlink::LinkState state)
|
||||
{
|
||||
if (use_rnode_bridge())
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case platform::ui::hostlink::LinkState::Stopped:
|
||||
case platform::ui::hostlink::LinkState::Waiting:
|
||||
return "Waiting for Reticulum host...";
|
||||
case platform::ui::hostlink::LinkState::Connected:
|
||||
case platform::ui::hostlink::LinkState::Handshaking:
|
||||
return "Host connected, probing modem...";
|
||||
case platform::ui::hostlink::LinkState::Ready:
|
||||
return "RNode modem ready";
|
||||
case platform::ui::hostlink::LinkState::Error:
|
||||
return "Bridge error";
|
||||
default:
|
||||
return "Waiting for Reticulum host...";
|
||||
}
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case platform::ui::hostlink::LinkState::Stopped:
|
||||
@@ -133,7 +169,7 @@ void enter(const shell::Host* host, lv_obj_t* parent)
|
||||
lv_obj_add_event_cb(s_root, root_key_event_cb, LV_EVENT_KEY, nullptr);
|
||||
|
||||
::ui::widgets::top_bar_init(s_top_bar, s_root);
|
||||
::ui::widgets::top_bar_set_title(s_top_bar, "Data Exchange");
|
||||
::ui::widgets::top_bar_set_title(s_top_bar, page_title());
|
||||
::ui::widgets::top_bar_set_back_callback(s_top_bar, on_back, nullptr);
|
||||
if (s_top_bar.back_btn)
|
||||
{
|
||||
@@ -173,7 +209,7 @@ void enter(const shell::Host* host, lv_obj_t* parent)
|
||||
lv_obj_center(stack);
|
||||
|
||||
lv_obj_t* title = lv_label_create(stack);
|
||||
lv_label_set_text(title, "Data Exchange");
|
||||
lv_label_set_text(title, page_subtitle());
|
||||
lv_obj_set_style_text_font(title, &lv_font_montserrat_18, 0);
|
||||
|
||||
s_status_label = lv_label_create(stack);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "app/app_facade_access.h"
|
||||
#include "board/BoardBase.h"
|
||||
#include "chat/domain/chat_types.h"
|
||||
#include "chat/infra/mesh_protocol_utils.h"
|
||||
#include "chat/infra/meshcore/mc_region_presets.h"
|
||||
#include "chat/infra/meshtastic/mt_region.h"
|
||||
#include "meshtastic/config.pb.h"
|
||||
@@ -399,12 +400,31 @@ static bool parse_float_text(const char* text, float* out_value)
|
||||
return true;
|
||||
}
|
||||
|
||||
static chat::MeshProtocol selected_protocol()
|
||||
{
|
||||
return static_cast<chat::MeshProtocol>(g_settings.chat_protocol);
|
||||
}
|
||||
|
||||
static bool is_meshcore_protocol_selected()
|
||||
{
|
||||
return selected_protocol() == chat::MeshProtocol::MeshCore;
|
||||
}
|
||||
|
||||
static bool is_rnode_protocol_selected()
|
||||
{
|
||||
return selected_protocol() == chat::MeshProtocol::RNode ||
|
||||
selected_protocol() == chat::MeshProtocol::LXMF;
|
||||
}
|
||||
|
||||
static void reset_mesh_settings()
|
||||
{
|
||||
app::IAppFacade& app_ctx = app::appFacade();
|
||||
app_ctx.getConfig().meshtastic_config = chat::MeshConfig();
|
||||
app_ctx.getConfig().meshtastic_config.region = app::AppConfig::kDefaultRegionCode;
|
||||
app_ctx.getConfig().meshcore_config = chat::MeshConfig();
|
||||
app_ctx.getConfig().applyMeshCoreFactoryDefaults();
|
||||
app_ctx.getConfig().rnode_config = chat::MeshConfig();
|
||||
app_ctx.getConfig().applyRNodeFactoryDefaults();
|
||||
strncpy(app_ctx.getConfig().meshcore_config.meshcore_channel_name, "Public",
|
||||
sizeof(app_ctx.getConfig().meshcore_config.meshcore_channel_name) - 1);
|
||||
app_ctx.getConfig().meshcore_config.meshcore_channel_name[sizeof(app_ctx.getConfig().meshcore_config.meshcore_channel_name) - 1] = '\0';
|
||||
@@ -417,12 +437,9 @@ static void reset_mesh_settings()
|
||||
g_settings.chat_psk[0] = '\0';
|
||||
g_settings.net_use_preset = app_ctx.getConfig().meshtastic_config.use_preset;
|
||||
g_settings.net_modem_preset = app_ctx.getConfig().meshtastic_config.modem_preset;
|
||||
g_settings.net_tx_power = app_ctx.getConfig().meshtastic_config.tx_power;
|
||||
g_settings.net_tx_power = app_ctx.getConfig().activeMeshConfig().tx_power;
|
||||
g_settings.net_hop_limit = app_ctx.getConfig().meshtastic_config.hop_limit;
|
||||
const chat::MeshConfig& active_cfg =
|
||||
(app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::MeshCore)
|
||||
? app_ctx.getConfig().meshcore_config
|
||||
: app_ctx.getConfig().meshtastic_config;
|
||||
const chat::MeshConfig& active_cfg = app_ctx.getConfig().activeMeshConfig();
|
||||
g_settings.net_tx_enabled = active_cfg.tx_enabled;
|
||||
g_settings.net_relay = app_ctx.getConfig().meshtastic_config.enable_relay;
|
||||
g_settings.net_duty_cycle = true;
|
||||
@@ -576,12 +593,20 @@ static void settings_load()
|
||||
const app::AppConfig& cfg = app_ctx.getConfig();
|
||||
const chat::MeshConfig& mt_cfg = cfg.meshtastic_config;
|
||||
const chat::MeshConfig& mc_cfg = cfg.meshcore_config;
|
||||
const chat::MeshConfig& rn_cfg = cfg.rnode_config;
|
||||
|
||||
g_settings.chat_region = mt_cfg.region;
|
||||
g_settings.chat_channel = cfg.chat_channel;
|
||||
const uint8_t* active_psk =
|
||||
(cfg.mesh_protocol == chat::MeshProtocol::MeshCore) ? mc_cfg.secondary_key : mt_cfg.secondary_key;
|
||||
if (is_zero_key(active_psk, sizeof(mt_cfg.secondary_key)))
|
||||
const uint8_t* active_psk = nullptr;
|
||||
if (cfg.mesh_protocol == chat::MeshProtocol::MeshCore)
|
||||
{
|
||||
active_psk = mc_cfg.secondary_key;
|
||||
}
|
||||
else if (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic)
|
||||
{
|
||||
active_psk = mt_cfg.secondary_key;
|
||||
}
|
||||
if (!active_psk || is_zero_key(active_psk, sizeof(mt_cfg.secondary_key)))
|
||||
{
|
||||
g_settings.chat_psk[0] = '\0';
|
||||
}
|
||||
@@ -593,24 +618,38 @@ static void settings_load()
|
||||
sizeof(g_settings.chat_psk));
|
||||
}
|
||||
|
||||
g_settings.net_use_preset = mt_cfg.use_preset;
|
||||
g_settings.net_modem_preset = mt_cfg.modem_preset;
|
||||
g_settings.net_manual_bw = static_cast<int>(std::lround(mt_cfg.bandwidth_khz));
|
||||
g_settings.net_manual_sf = mt_cfg.spread_factor;
|
||||
g_settings.net_manual_cr = mt_cfg.coding_rate;
|
||||
int tx_power = mt_cfg.tx_power;
|
||||
if (cfg.mesh_protocol == chat::MeshProtocol::RNode ||
|
||||
cfg.mesh_protocol == chat::MeshProtocol::LXMF)
|
||||
{
|
||||
g_settings.net_use_preset = 0;
|
||||
g_settings.net_modem_preset = 0;
|
||||
g_settings.net_manual_bw = static_cast<int>(std::lround(rn_cfg.bandwidth_khz));
|
||||
g_settings.net_manual_sf = rn_cfg.spread_factor;
|
||||
g_settings.net_manual_cr = rn_cfg.coding_rate;
|
||||
float_to_text(rn_cfg.override_frequency_mhz, g_settings.net_override_freq,
|
||||
sizeof(g_settings.net_override_freq), 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
g_settings.net_use_preset = mt_cfg.use_preset;
|
||||
g_settings.net_modem_preset = mt_cfg.modem_preset;
|
||||
g_settings.net_manual_bw = static_cast<int>(std::lround(mt_cfg.bandwidth_khz));
|
||||
g_settings.net_manual_sf = mt_cfg.spread_factor;
|
||||
g_settings.net_manual_cr = mt_cfg.coding_rate;
|
||||
float_to_text(mt_cfg.override_frequency_mhz, g_settings.net_override_freq,
|
||||
sizeof(g_settings.net_override_freq), 3);
|
||||
}
|
||||
|
||||
int tx_power = cfg.activeMeshConfig().tx_power;
|
||||
if (tx_power < kNetTxPowerMin) tx_power = kNetTxPowerMin;
|
||||
if (tx_power > kNetTxPowerMax) tx_power = kNetTxPowerMax;
|
||||
g_settings.net_tx_power = tx_power;
|
||||
g_settings.net_hop_limit = mt_cfg.hop_limit;
|
||||
g_settings.net_tx_enabled = (cfg.mesh_protocol == chat::MeshProtocol::MeshCore)
|
||||
? mc_cfg.tx_enabled
|
||||
: mt_cfg.tx_enabled;
|
||||
g_settings.net_tx_enabled = cfg.activeMeshConfig().tx_enabled;
|
||||
g_settings.net_override_duty_cycle = mt_cfg.override_duty_cycle;
|
||||
g_settings.net_channel_num = mt_cfg.channel_num;
|
||||
g_settings.net_relay = mt_cfg.enable_relay;
|
||||
float_to_text(mt_cfg.frequency_offset_mhz, g_settings.net_freq_offset, sizeof(g_settings.net_freq_offset), 3);
|
||||
float_to_text(mt_cfg.override_frequency_mhz, g_settings.net_override_freq, sizeof(g_settings.net_override_freq), 3);
|
||||
g_settings.net_duty_cycle = cfg.net_duty_cycle;
|
||||
g_settings.net_channel_util = cfg.net_channel_util;
|
||||
|
||||
@@ -900,7 +939,15 @@ static void on_text_save_clicked(lv_event_t* e)
|
||||
modal_close();
|
||||
return;
|
||||
}
|
||||
app_ctx.getConfig().meshtastic_config.override_frequency_mhz = value;
|
||||
if (app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::RNode ||
|
||||
app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::LXMF)
|
||||
{
|
||||
app_ctx.getConfig().rnode_config.override_frequency_mhz = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
app_ctx.getConfig().meshtastic_config.override_frequency_mhz = value;
|
||||
}
|
||||
app_ctx.saveConfig();
|
||||
app_ctx.applyMeshConfig();
|
||||
}
|
||||
@@ -1169,10 +1216,18 @@ static void on_option_clicked(lv_event_t* e)
|
||||
if (payload->item->pref_key && strcmp(payload->item->pref_key, "net_bw") == 0)
|
||||
{
|
||||
app::IAppFacade& app_ctx = app::appFacade();
|
||||
app_ctx.getConfig().meshtastic_config.bandwidth_khz = static_cast<float>(payload->value);
|
||||
app_ctx.getConfig().meshtastic_config.use_preset = false;
|
||||
g_settings.net_use_preset = false;
|
||||
prefs_put_int("net_use_preset", 0);
|
||||
if (app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::RNode ||
|
||||
app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::LXMF)
|
||||
{
|
||||
app_ctx.getConfig().rnode_config.bandwidth_khz = static_cast<float>(payload->value);
|
||||
}
|
||||
else
|
||||
{
|
||||
app_ctx.getConfig().meshtastic_config.bandwidth_khz = static_cast<float>(payload->value);
|
||||
app_ctx.getConfig().meshtastic_config.use_preset = false;
|
||||
g_settings.net_use_preset = false;
|
||||
prefs_put_int("net_use_preset", 0);
|
||||
}
|
||||
app_ctx.saveConfig();
|
||||
app_ctx.applyMeshConfig();
|
||||
rebuild_list = true;
|
||||
@@ -1180,10 +1235,18 @@ static void on_option_clicked(lv_event_t* e)
|
||||
if (payload->item->pref_key && strcmp(payload->item->pref_key, "net_sf") == 0)
|
||||
{
|
||||
app::IAppFacade& app_ctx = app::appFacade();
|
||||
app_ctx.getConfig().meshtastic_config.spread_factor = static_cast<uint8_t>(payload->value);
|
||||
app_ctx.getConfig().meshtastic_config.use_preset = false;
|
||||
g_settings.net_use_preset = false;
|
||||
prefs_put_int("net_use_preset", 0);
|
||||
if (app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::RNode ||
|
||||
app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::LXMF)
|
||||
{
|
||||
app_ctx.getConfig().rnode_config.spread_factor = static_cast<uint8_t>(payload->value);
|
||||
}
|
||||
else
|
||||
{
|
||||
app_ctx.getConfig().meshtastic_config.spread_factor = static_cast<uint8_t>(payload->value);
|
||||
app_ctx.getConfig().meshtastic_config.use_preset = false;
|
||||
g_settings.net_use_preset = false;
|
||||
prefs_put_int("net_use_preset", 0);
|
||||
}
|
||||
app_ctx.saveConfig();
|
||||
app_ctx.applyMeshConfig();
|
||||
rebuild_list = true;
|
||||
@@ -1191,10 +1254,18 @@ static void on_option_clicked(lv_event_t* e)
|
||||
if (payload->item->pref_key && strcmp(payload->item->pref_key, "net_cr") == 0)
|
||||
{
|
||||
app::IAppFacade& app_ctx = app::appFacade();
|
||||
app_ctx.getConfig().meshtastic_config.coding_rate = static_cast<uint8_t>(payload->value);
|
||||
app_ctx.getConfig().meshtastic_config.use_preset = false;
|
||||
g_settings.net_use_preset = false;
|
||||
prefs_put_int("net_use_preset", 0);
|
||||
if (app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::RNode ||
|
||||
app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::LXMF)
|
||||
{
|
||||
app_ctx.getConfig().rnode_config.coding_rate = static_cast<uint8_t>(payload->value);
|
||||
}
|
||||
else
|
||||
{
|
||||
app_ctx.getConfig().meshtastic_config.coding_rate = static_cast<uint8_t>(payload->value);
|
||||
app_ctx.getConfig().meshtastic_config.use_preset = false;
|
||||
g_settings.net_use_preset = false;
|
||||
prefs_put_int("net_use_preset", 0);
|
||||
}
|
||||
app_ctx.saveConfig();
|
||||
app_ctx.applyMeshConfig();
|
||||
rebuild_list = true;
|
||||
@@ -1322,7 +1393,15 @@ static void on_option_clicked(lv_event_t* e)
|
||||
if (payload->item->pref_key && strcmp(payload->item->pref_key, "net_tx_power") == 0)
|
||||
{
|
||||
app::IAppFacade& app_ctx = app::appFacade();
|
||||
app_ctx.getConfig().meshtastic_config.tx_power = static_cast<int8_t>(payload->value);
|
||||
if (app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::RNode ||
|
||||
app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::LXMF)
|
||||
{
|
||||
app_ctx.getConfig().rnode_config.tx_power = static_cast<int8_t>(payload->value);
|
||||
}
|
||||
else
|
||||
{
|
||||
app_ctx.getConfig().meshtastic_config.tx_power = static_cast<int8_t>(payload->value);
|
||||
}
|
||||
app_ctx.saveConfig();
|
||||
app_ctx.applyMeshConfig();
|
||||
}
|
||||
@@ -1607,6 +1686,8 @@ static const settings::ui::SettingOption kChatChannelOptions[] = {
|
||||
static const settings::ui::SettingOption kChatProtocolOptions[] = {
|
||||
{"Meshtastic", static_cast<int>(chat::MeshProtocol::Meshtastic)},
|
||||
{"MeshCore", static_cast<int>(chat::MeshProtocol::MeshCore)},
|
||||
{"LXMF", static_cast<int>(chat::MeshProtocol::LXMF)},
|
||||
{"RNode Bridge", static_cast<int>(chat::MeshProtocol::RNode)},
|
||||
};
|
||||
|
||||
static const settings::ui::SettingOption kNetPresetOptions[] = {
|
||||
@@ -1801,7 +1882,7 @@ static settings::ui::SettingItem kMapItems[] = {
|
||||
static settings::ui::SettingItem kChatItems[] = {
|
||||
{"User Name", settings::ui::SettingType::Text, nullptr, 0, nullptr, nullptr, g_settings.user_name, sizeof(g_settings.user_name), false, "chat_user"},
|
||||
{"Short Name", settings::ui::SettingType::Text, nullptr, 0, nullptr, nullptr, g_settings.short_name, sizeof(g_settings.short_name), false, "chat_short"},
|
||||
{"Protocol", settings::ui::SettingType::Enum, kChatProtocolOptions, 2, &g_settings.chat_protocol, nullptr, nullptr, 0, false, "mesh_protocol"},
|
||||
{"Protocol", settings::ui::SettingType::Enum, kChatProtocolOptions, 4, &g_settings.chat_protocol, nullptr, nullptr, 0, false, "mesh_protocol"},
|
||||
{"Region", settings::ui::SettingType::Enum, kChatRegionOptions, 0, &g_settings.chat_region, nullptr, nullptr, 0, false, "chat_region"},
|
||||
{"Channel", settings::ui::SettingType::Enum, kChatChannelOptions, 2, &g_settings.chat_channel, nullptr, nullptr, 0, false, "chat_channel"},
|
||||
{"Channel Key / PSK", settings::ui::SettingType::Text, nullptr, 0, nullptr, nullptr, g_settings.chat_psk, sizeof(g_settings.chat_psk), true, "chat_psk"},
|
||||
@@ -1954,7 +2035,8 @@ static bool should_show_item(const settings::ui::SettingItem& item)
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool meshcore = (g_settings.chat_protocol == static_cast<int>(chat::MeshProtocol::MeshCore));
|
||||
const bool meshcore = is_meshcore_protocol_selected();
|
||||
const bool rnode = is_rnode_protocol_selected();
|
||||
|
||||
// Relay is currently not implemented as real forwarding in Meshtastic path.
|
||||
if (has_pref_key(item, "net_relay"))
|
||||
@@ -1986,6 +2068,38 @@ static bool should_show_item(const settings::ui::SettingItem& item)
|
||||
if (has_pref_key(item, "net_freq_offset")) return false;
|
||||
if (has_pref_key(item, "net_override_freq")) return false;
|
||||
}
|
||||
else if (rnode)
|
||||
{
|
||||
if (has_pref_key(item, "chat_region")) return false;
|
||||
if (has_pref_key(item, "chat_channel")) return false;
|
||||
if (has_pref_key(item, "chat_psk")) return false;
|
||||
if (has_pref_key(item, "privacy_encrypt")) return false;
|
||||
if (has_pref_key(item, "privacy_pki")) return false;
|
||||
|
||||
if (has_pref_key(item, "net_use_preset")) return false;
|
||||
if (has_pref_key(item, "net_preset")) return false;
|
||||
if (has_pref_key(item, "net_hop_limit")) return false;
|
||||
if (has_pref_key(item, "net_override_duty")) return false;
|
||||
if (has_pref_key(item, "net_channel_num")) return false;
|
||||
if (has_pref_key(item, "net_freq_offset")) return false;
|
||||
if (has_pref_key(item, "net_duty_cycle")) return false;
|
||||
if (has_pref_key(item, "net_util")) return false;
|
||||
|
||||
if (has_pref_key(item, "mc_region_preset")) return false;
|
||||
if (has_pref_key(item, "mc_freq")) return false;
|
||||
if (has_pref_key(item, "mc_bw")) return false;
|
||||
if (has_pref_key(item, "mc_sf")) return false;
|
||||
if (has_pref_key(item, "mc_cr")) return false;
|
||||
if (has_pref_key(item, "mc_tx_power")) return false;
|
||||
if (has_pref_key(item, "mc_repeat")) return false;
|
||||
if (has_pref_key(item, "mc_rx_delay")) return false;
|
||||
if (has_pref_key(item, "mc_airtime")) return false;
|
||||
if (has_pref_key(item, "mc_flood_max")) return false;
|
||||
if (has_pref_key(item, "mc_multi_acks")) return false;
|
||||
if (has_pref_key(item, "mc_channel_slot")) return false;
|
||||
if (has_pref_key(item, "mc_channel_name")) return false;
|
||||
if (has_pref_key(item, "mc_channel_key")) return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (has_pref_key(item, "mc_region_preset")) return false;
|
||||
@@ -2149,6 +2263,11 @@ static bool activate_item_widget(settings::ui::ItemWidget& widget)
|
||||
{
|
||||
app_ctx.getConfig().meshcore_config.tx_enabled = *item.bool_value;
|
||||
}
|
||||
else if (app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::RNode ||
|
||||
app_ctx.getConfig().mesh_protocol == chat::MeshProtocol::LXMF)
|
||||
{
|
||||
app_ctx.getConfig().rnode_config.tx_enabled = *item.bool_value;
|
||||
}
|
||||
else
|
||||
{
|
||||
app_ctx.getConfig().meshtastic_config.tx_enabled = *item.bool_value;
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @file lxmf_adapter.h
|
||||
* @brief Device-side LXMF adapter over the existing RNode raw carrier
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "board/LoraBoard.h"
|
||||
#include "chat/infra/lxmf/lxmf_wire.h"
|
||||
#include "chat/ports/i_mesh_adapter.h"
|
||||
#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_identity.h"
|
||||
#include "platform/esp/arduino_common/chat/infra/rnode/rnode_adapter.h"
|
||||
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
|
||||
namespace chat::lxmf
|
||||
{
|
||||
|
||||
class LxmfAdapter : public IMeshAdapter
|
||||
{
|
||||
public:
|
||||
explicit LxmfAdapter(LoraBoard& board);
|
||||
|
||||
MeshCapabilities getCapabilities() const override;
|
||||
bool sendText(ChannelId channel, const std::string& text,
|
||||
MessageId* out_msg_id, NodeId peer = 0) override;
|
||||
bool pollIncomingText(MeshIncomingText* out) override;
|
||||
bool sendAppData(ChannelId channel, uint32_t portnum,
|
||||
const uint8_t* payload, size_t len,
|
||||
NodeId dest = 0, bool want_ack = false,
|
||||
MessageId packet_id = 0,
|
||||
bool want_response = false) override;
|
||||
bool pollIncomingData(MeshIncomingData* out) override;
|
||||
bool requestNodeInfo(NodeId dest, bool want_response) override;
|
||||
NodeId getNodeId() const override;
|
||||
void applyConfig(const MeshConfig& config) override;
|
||||
void setUserInfo(const char* long_name, const char* short_name) override;
|
||||
bool isReady() const override;
|
||||
bool pollIncomingRawPacket(uint8_t* out_data, size_t& out_len, size_t max_len) override;
|
||||
void handleRawPacket(const uint8_t* data, size_t size) override;
|
||||
void setLastRxStats(float rssi, float snr) override;
|
||||
|
||||
private:
|
||||
struct PeerInfo
|
||||
{
|
||||
uint32_t node_id = 0;
|
||||
uint8_t destination_hash[reticulum::kTruncatedHashSize] = {};
|
||||
uint8_t identity_hash[reticulum::kTruncatedHashSize] = {};
|
||||
uint8_t enc_pub[LxmfIdentity::kEncPubKeySize] = {};
|
||||
uint8_t sig_pub[LxmfIdentity::kSigPubKeySize] = {};
|
||||
char display_name[32] = {};
|
||||
uint32_t last_seen_s = 0;
|
||||
uint32_t last_path_request_ms = 0;
|
||||
};
|
||||
|
||||
struct PathEntry
|
||||
{
|
||||
uint8_t destination_hash[reticulum::kTruncatedHashSize] = {};
|
||||
uint8_t next_hop_transport[reticulum::kTruncatedHashSize] = {};
|
||||
uint8_t cached_packet_hash[reticulum::kFullHashSize] = {};
|
||||
uint8_t cached_announce[reticulum::kReticulumMtu] = {};
|
||||
size_t cached_announce_len = 0;
|
||||
uint8_t hops = 0;
|
||||
uint32_t last_seen_s = 0;
|
||||
bool direct = false;
|
||||
};
|
||||
|
||||
struct PacketFilterEntry
|
||||
{
|
||||
uint8_t packet_hash[reticulum::kFullHashSize] = {};
|
||||
uint32_t seen_ms = 0;
|
||||
};
|
||||
|
||||
struct ReverseEntry
|
||||
{
|
||||
uint8_t proof_hash[reticulum::kTruncatedHashSize] = {};
|
||||
uint32_t created_ms = 0;
|
||||
};
|
||||
|
||||
struct LinkRelayEntry
|
||||
{
|
||||
uint8_t link_id[reticulum::kTruncatedHashSize] = {};
|
||||
uint8_t initiator_hops = 0;
|
||||
uint8_t responder_hops = 0;
|
||||
uint32_t last_seen_ms = 0;
|
||||
};
|
||||
|
||||
static constexpr uint32_t kAnnounceIntervalMs = 120000;
|
||||
static constexpr uint32_t kInitialAnnounceDelayMs = 1500;
|
||||
|
||||
rnode::RNodeAdapter raw_;
|
||||
LxmfIdentity identity_;
|
||||
MeshConfig config_{};
|
||||
std::queue<MeshIncomingText> text_receive_queue_;
|
||||
std::vector<PeerInfo> peers_;
|
||||
std::vector<PathEntry> paths_;
|
||||
std::vector<PacketFilterEntry> packet_filter_;
|
||||
std::vector<ReverseEntry> reverse_table_;
|
||||
std::vector<LinkRelayEntry> link_relays_;
|
||||
std::string user_long_name_;
|
||||
std::string user_short_name_;
|
||||
uint32_t last_announce_ms_ = 0;
|
||||
bool announce_pending_ = true;
|
||||
bool peers_loaded_ = false;
|
||||
|
||||
void processRadioPackets();
|
||||
void maybeAnnounce();
|
||||
bool sendAnnounce(reticulum::PacketContext context = reticulum::PacketContext::None);
|
||||
bool handleAnnouncePacket(const uint8_t* raw_packet, size_t raw_len,
|
||||
const reticulum::ParsedPacket& packet);
|
||||
bool handleDataPacket(const uint8_t* raw_packet, size_t raw_len,
|
||||
const reticulum::ParsedPacket& packet);
|
||||
bool handleProofPacket(const uint8_t* raw_packet, size_t raw_len,
|
||||
const reticulum::ParsedPacket& packet);
|
||||
bool handleLinkRequestPacket(const uint8_t* raw_packet, size_t raw_len,
|
||||
const reticulum::ParsedPacket& packet);
|
||||
bool handlePathRequestPacket(const reticulum::ParsedPacket& packet);
|
||||
bool handleCacheRequestPacket(const reticulum::ParsedPacket& packet);
|
||||
bool maybeForwardTransportPacket(const uint8_t* raw_packet, size_t raw_len,
|
||||
const reticulum::ParsedPacket& packet);
|
||||
bool maybeForwardLinkPacket(const uint8_t* raw_packet, size_t raw_len,
|
||||
const reticulum::ParsedPacket& packet);
|
||||
bool sendProofForPacket(const uint8_t* raw_packet, size_t raw_len);
|
||||
bool sendPathRequest(PeerInfo& peer);
|
||||
bool shouldRequestPath(const PeerInfo& peer) const;
|
||||
bool buildEncryptedPacketForPeer(const PeerInfo& peer,
|
||||
const uint8_t* plaintext, size_t plaintext_len,
|
||||
uint8_t* out_packet, size_t* inout_len);
|
||||
bool routeAndSendPacket(const uint8_t* raw_packet, size_t raw_len,
|
||||
bool allow_transport);
|
||||
bool sendCachedAnnounceResponse(const PathEntry& path,
|
||||
reticulum::PacketContext context);
|
||||
bool sendCachedPacketReplay(const uint8_t packet_hash[reticulum::kFullHashSize]);
|
||||
bool shouldRebroadcastAnnounce(const reticulum::ParsedPacket& packet) const;
|
||||
bool rebroadcastAnnounce(const PathEntry& path, const reticulum::ParsedPacket& packet);
|
||||
bool isDuplicatePacket(const uint8_t packet_hash[reticulum::kFullHashSize]);
|
||||
void rememberPacket(const uint8_t packet_hash[reticulum::kFullHashSize]);
|
||||
void rememberReversePath(const uint8_t proof_hash[reticulum::kTruncatedHashSize]);
|
||||
ReverseEntry* findReversePath(const uint8_t proof_hash[reticulum::kTruncatedHashSize]);
|
||||
void cullTransportState();
|
||||
PathEntry& upsertPath(const uint8_t destination_hash[reticulum::kTruncatedHashSize]);
|
||||
const PathEntry* findPath(const uint8_t destination_hash[reticulum::kTruncatedHashSize]) const;
|
||||
LinkRelayEntry& upsertLinkRelay(const uint8_t link_id[reticulum::kTruncatedHashSize]);
|
||||
LinkRelayEntry* findLinkRelay(const uint8_t link_id[reticulum::kTruncatedHashSize]);
|
||||
PeerInfo* findPeerByNodeId(NodeId node_id);
|
||||
const PeerInfo* findPeerByDestinationHash(const uint8_t hash[reticulum::kTruncatedHashSize]) const;
|
||||
PeerInfo& upsertPeer(const uint8_t destination_hash[reticulum::kTruncatedHashSize]);
|
||||
void publishPeerUpdate(const PeerInfo& peer) const;
|
||||
void loadPersistedPeers();
|
||||
bool persistPeers() const;
|
||||
uint32_t currentTimestampSeconds() const;
|
||||
const char* effectiveDisplayName() const;
|
||||
static uint32_t messageIdFromHash(const uint8_t hash[reticulum::kFullHashSize]);
|
||||
static void pathRequestDestinationHash(uint8_t out_hash[reticulum::kTruncatedHashSize]);
|
||||
};
|
||||
|
||||
} // namespace chat::lxmf
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @file lxmf_identity.h
|
||||
* @brief Reticulum/LXMF identity persistence for ESP Arduino targets
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "chat/infra/reticulum/reticulum_wire.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace chat::lxmf
|
||||
{
|
||||
|
||||
class LxmfIdentity
|
||||
{
|
||||
public:
|
||||
static constexpr size_t kEncPubKeySize = reticulum::kEncryptionPublicKeySize;
|
||||
static constexpr size_t kEncPrivKeySize = reticulum::kEncryptionPublicKeySize;
|
||||
static constexpr size_t kSigPubKeySize = reticulum::kSigningPublicKeySize;
|
||||
static constexpr size_t kSigPrivKeySize = reticulum::kSignatureSize;
|
||||
static constexpr size_t kSignatureSize = reticulum::kSignatureSize;
|
||||
|
||||
bool init();
|
||||
bool isReady() const { return ready_; }
|
||||
|
||||
const uint8_t* encryptionPublicKey() const { return enc_pub_.data(); }
|
||||
const uint8_t* signingPublicKey() const { return sig_pub_.data(); }
|
||||
const uint8_t* identityHash() const { return identity_hash_.data(); }
|
||||
const uint8_t* destinationHash() const { return destination_hash_.data(); }
|
||||
uint32_t nodeId() const { return node_id_; }
|
||||
|
||||
void combinedPublicKey(uint8_t out_key[reticulum::kCombinedPublicKeySize]) const;
|
||||
|
||||
bool sign(const uint8_t* message, size_t message_len,
|
||||
uint8_t out_signature[kSignatureSize]) const;
|
||||
|
||||
static bool verify(const uint8_t sign_pub[kSigPubKeySize],
|
||||
const uint8_t signature[kSignatureSize],
|
||||
const uint8_t* message, size_t message_len);
|
||||
|
||||
bool deriveSharedSecret(const uint8_t peer_public_key[kEncPubKeySize],
|
||||
uint8_t out_secret[kEncPubKeySize]) const;
|
||||
|
||||
private:
|
||||
bool loadFromPrefs();
|
||||
bool saveToPrefs() const;
|
||||
bool generateAndPersist();
|
||||
void recomputeDerivedFields();
|
||||
|
||||
bool ready_ = false;
|
||||
uint32_t node_id_ = 0;
|
||||
std::array<uint8_t, kEncPubKeySize> enc_pub_ = {};
|
||||
std::array<uint8_t, kEncPrivKeySize> enc_priv_ = {};
|
||||
std::array<uint8_t, kSigPubKeySize> sig_pub_ = {};
|
||||
std::array<uint8_t, kSigPrivKeySize> sig_priv_ = {};
|
||||
std::array<uint8_t, reticulum::kTruncatedHashSize> identity_hash_ = {};
|
||||
std::array<uint8_t, reticulum::kTruncatedHashSize> destination_hash_ = {};
|
||||
};
|
||||
|
||||
} // namespace chat::lxmf
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @file rnode_adapter.h
|
||||
* @brief Minimal RNode raw-payload mesh adapter
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "board/LoraBoard.h"
|
||||
#include "chat/infra/rnode/rnode_packet_wire.h"
|
||||
#include "chat/ports/i_mesh_adapter.h"
|
||||
#include <queue>
|
||||
|
||||
namespace chat
|
||||
{
|
||||
namespace rnode
|
||||
{
|
||||
|
||||
class RNodeAdapter : public IMeshAdapter
|
||||
{
|
||||
public:
|
||||
explicit RNodeAdapter(LoraBoard& board);
|
||||
|
||||
MeshCapabilities getCapabilities() const override;
|
||||
bool sendText(ChannelId channel, const std::string& text,
|
||||
MessageId* out_msg_id, NodeId peer = 0) override;
|
||||
bool pollIncomingText(MeshIncomingText* out) override;
|
||||
bool sendAppData(ChannelId channel, uint32_t portnum,
|
||||
const uint8_t* payload, size_t len,
|
||||
NodeId dest = 0, bool want_ack = false,
|
||||
MessageId packet_id = 0,
|
||||
bool want_response = false) override;
|
||||
bool pollIncomingData(MeshIncomingData* out) override;
|
||||
void applyConfig(const MeshConfig& config) override;
|
||||
void setLastRxStats(float rssi, float snr) override;
|
||||
bool isReady() const override;
|
||||
bool pollIncomingRawPacket(uint8_t* out_data, size_t& out_len, size_t max_len) override;
|
||||
void handleRawPacket(const uint8_t* data, size_t size) override;
|
||||
float lastRxRssi() const { return last_rx_rssi_; }
|
||||
float lastRxSnr() const { return last_rx_snr_; }
|
||||
|
||||
private:
|
||||
static constexpr uint8_t kSyncWord = 0x12;
|
||||
static constexpr uint8_t kCrcLen = 2;
|
||||
|
||||
struct PendingRawPacket
|
||||
{
|
||||
uint8_t data[chat::rnode::kRNodeMaxPayloadSize] = {};
|
||||
size_t len = 0;
|
||||
};
|
||||
|
||||
LoraBoard& board_;
|
||||
MeshConfig config_;
|
||||
bool ready_ = false;
|
||||
float last_rx_rssi_ = 0.0f;
|
||||
float last_rx_snr_ = 0.0f;
|
||||
uint32_t radio_freq_hz_ = 0;
|
||||
uint32_t radio_bw_hz_ = 0;
|
||||
uint8_t radio_sf_ = 0;
|
||||
uint8_t radio_cr_ = 0;
|
||||
uint8_t next_sequence_ = 0;
|
||||
chat::rnode::ReassemblyState reassembly_;
|
||||
PendingRawPacket last_raw_packet_;
|
||||
bool has_pending_raw_packet_ = false;
|
||||
std::queue<MeshIncomingData> app_receive_queue_;
|
||||
|
||||
void startRadioReceive();
|
||||
void enqueueIncomingData(const uint8_t* payload, size_t len);
|
||||
};
|
||||
|
||||
} // namespace rnode
|
||||
} // namespace chat
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "hostlink/hostlink_session.h"
|
||||
|
||||
namespace rnode_kiss
|
||||
{
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
bool is_active();
|
||||
hostlink::Status get_status();
|
||||
|
||||
} // namespace rnode_kiss
|
||||
@@ -84,6 +84,7 @@ bool loadAppConfigFromPreferences(AppConfig& config, Preferences& prefs)
|
||||
auto& chat_policy = config.chat_policy;
|
||||
auto& meshtastic_config = config.meshtastic_config;
|
||||
auto& meshcore_config = config.meshcore_config;
|
||||
auto& rnode_config = config.rnode_config;
|
||||
auto& mesh_protocol = config.mesh_protocol;
|
||||
auto& node_name = config.node_name;
|
||||
auto& short_name = config.short_name;
|
||||
@@ -171,6 +172,13 @@ bool loadAppConfigFromPreferences(AppConfig& config, Preferences& prefs)
|
||||
meshcore_config.meshcore_channel_name[sizeof(meshcore_config.meshcore_channel_name) - 1] = '\0';
|
||||
prefs.getBytes("mc_ch_key", meshcore_config.secondary_key, sizeof(meshcore_config.secondary_key));
|
||||
|
||||
rnode_config.override_frequency_mhz = prefs.getFloat("rn_freq", rnode_config.override_frequency_mhz);
|
||||
rnode_config.bandwidth_khz = prefs.getFloat("rn_bw", rnode_config.bandwidth_khz);
|
||||
rnode_config.spread_factor = prefs.getUChar("rn_sf", rnode_config.spread_factor);
|
||||
rnode_config.coding_rate = prefs.getUChar("rn_cr", rnode_config.coding_rate);
|
||||
rnode_config.tx_power = prefs.getChar("rn_tx", rnode_config.tx_power);
|
||||
rnode_config.tx_enabled = prefs.getBool("rn_tx_en", rnode_config.tx_enabled);
|
||||
|
||||
uint8_t mesh_protocol_raw = prefs.getUChar("mesh_protocol", 0xFF);
|
||||
if (chat::infra::isValidMeshProtocolValue(mesh_protocol_raw))
|
||||
{
|
||||
@@ -215,6 +223,7 @@ bool loadAppConfigFromPreferences(AppConfig& config, Preferences& prefs)
|
||||
};
|
||||
meshtastic_config.tx_power = clamp_tx_power(meshtastic_config.tx_power);
|
||||
meshcore_config.tx_power = clamp_tx_power(meshcore_config.tx_power);
|
||||
rnode_config.tx_power = clamp_tx_power(rnode_config.tx_power);
|
||||
|
||||
prefs.end();
|
||||
|
||||
@@ -309,6 +318,7 @@ bool saveAppConfigToPreferences(AppConfig& config, Preferences& prefs)
|
||||
auto& chat_policy = config.chat_policy;
|
||||
auto& meshtastic_config = config.meshtastic_config;
|
||||
auto& meshcore_config = config.meshcore_config;
|
||||
auto& rnode_config = config.rnode_config;
|
||||
auto& mesh_protocol = config.mesh_protocol;
|
||||
auto& node_name = config.node_name;
|
||||
auto& short_name = config.short_name;
|
||||
@@ -387,6 +397,13 @@ bool saveAppConfigToPreferences(AppConfig& config, Preferences& prefs)
|
||||
prefs.putBool("mc_tx_en", meshcore_config.tx_enabled);
|
||||
prefs.putString("mc_ch_name", meshcore_config.meshcore_channel_name);
|
||||
prefs.putBytes("mc_ch_key", meshcore_config.secondary_key, sizeof(meshcore_config.secondary_key));
|
||||
|
||||
prefs.putFloat("rn_freq", rnode_config.override_frequency_mhz);
|
||||
prefs.putFloat("rn_bw", rnode_config.bandwidth_khz);
|
||||
prefs.putUChar("rn_sf", rnode_config.spread_factor);
|
||||
prefs.putUChar("rn_cr", rnode_config.coding_rate);
|
||||
prefs.putChar("rn_tx", rnode_config.tx_power);
|
||||
prefs.putBool("rn_tx_en", rnode_config.tx_enabled);
|
||||
// Remove first so legacy key type mismatches cannot block updating this value.
|
||||
prefs.remove("mesh_protocol");
|
||||
prefs.putUChar("mesh_protocol", static_cast<uint8_t>(mesh_protocol));
|
||||
|
||||
@@ -99,6 +99,14 @@ void BleManager::restartService(chat::MeshProtocol protocol)
|
||||
|
||||
shutdownNimble();
|
||||
|
||||
if (protocol == chat::MeshProtocol::RNode || protocol == chat::MeshProtocol::LXMF)
|
||||
{
|
||||
active_protocol_ = protocol;
|
||||
Serial.printf("[BLE] protocol=%s has no BLE service yet\n",
|
||||
chat::infra::meshProtocolSlug(active_protocol_));
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string device_name = buildDeviceName(protocol);
|
||||
|
||||
NimBLEDevice::init(device_name);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* @file lxmf_identity.cpp
|
||||
* @brief Reticulum/LXMF identity persistence for ESP Arduino targets
|
||||
*/
|
||||
|
||||
#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_identity.h"
|
||||
|
||||
#include "../../internal/blob_store_io.h"
|
||||
#include "chat/infra/meshcore/crypto/ed25519/ed_25519.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Curve25519.h>
|
||||
#include <RNG.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace chat::lxmf
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr const char* kPrefsNs = "lxmf_ident";
|
||||
constexpr const char* kEncPubKey = "enc_pub";
|
||||
constexpr const char* kEncPrivKey = "enc_priv";
|
||||
constexpr const char* kSigPubKey = "sig_pub";
|
||||
constexpr const char* kSigPrivKey = "sig_priv";
|
||||
|
||||
void fillRandomBytes(uint8_t* out, size_t len)
|
||||
{
|
||||
if (!out || len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
while (offset < len)
|
||||
{
|
||||
const uint32_t rnd = static_cast<uint32_t>(esp_random());
|
||||
const size_t chunk = (len - offset >= sizeof(rnd)) ? sizeof(rnd) : (len - offset);
|
||||
memcpy(out + offset, &rnd, chunk);
|
||||
offset += chunk;
|
||||
}
|
||||
}
|
||||
|
||||
bool isAllZero(const uint8_t* data, size_t len)
|
||||
{
|
||||
if (!data)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
{
|
||||
if (data[i] != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool LxmfIdentity::init()
|
||||
{
|
||||
if (ready_)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (loadFromPrefs())
|
||||
{
|
||||
ready_ = true;
|
||||
return true;
|
||||
}
|
||||
return generateAndPersist();
|
||||
}
|
||||
|
||||
void LxmfIdentity::combinedPublicKey(uint8_t out_key[reticulum::kCombinedPublicKeySize]) const
|
||||
{
|
||||
if (!out_key)
|
||||
{
|
||||
return;
|
||||
}
|
||||
memcpy(out_key, enc_pub_.data(), enc_pub_.size());
|
||||
memcpy(out_key + enc_pub_.size(), sig_pub_.data(), sig_pub_.size());
|
||||
}
|
||||
|
||||
bool LxmfIdentity::sign(const uint8_t* message, size_t message_len,
|
||||
uint8_t out_signature[kSignatureSize]) const
|
||||
{
|
||||
if (!ready_ || !message || !out_signature)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ed25519_sign(out_signature, message, message_len, sig_pub_.data(), sig_priv_.data());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LxmfIdentity::verify(const uint8_t sign_pub[kSigPubKeySize],
|
||||
const uint8_t signature[kSignatureSize],
|
||||
const uint8_t* message, size_t message_len)
|
||||
{
|
||||
if (!sign_pub || !signature || !message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return ed25519_verify(signature, message, message_len, sign_pub) != 0;
|
||||
}
|
||||
|
||||
bool LxmfIdentity::deriveSharedSecret(const uint8_t peer_public_key[kEncPubKeySize],
|
||||
uint8_t out_secret[kEncPubKeySize]) const
|
||||
{
|
||||
if (!ready_ || !peer_public_key || !out_secret)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(out_secret, peer_public_key, kEncPubKeySize);
|
||||
uint8_t local_priv[kEncPrivKeySize] = {};
|
||||
memcpy(local_priv, enc_priv_.data(), sizeof(local_priv));
|
||||
return Curve25519::dh2(out_secret, local_priv);
|
||||
}
|
||||
|
||||
bool LxmfIdentity::loadFromPrefs()
|
||||
{
|
||||
std::vector<uint8_t> blob;
|
||||
if (!chat::infra::loadRawBlobFromPreferences(kPrefsNs, kEncPubKey, blob) ||
|
||||
blob.size() != enc_pub_.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
memcpy(enc_pub_.data(), blob.data(), enc_pub_.size());
|
||||
|
||||
if (!chat::infra::loadRawBlobFromPreferences(kPrefsNs, kEncPrivKey, blob) ||
|
||||
blob.size() != enc_priv_.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
memcpy(enc_priv_.data(), blob.data(), enc_priv_.size());
|
||||
|
||||
if (!chat::infra::loadRawBlobFromPreferences(kPrefsNs, kSigPubKey, blob) ||
|
||||
blob.size() != sig_pub_.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
memcpy(sig_pub_.data(), blob.data(), sig_pub_.size());
|
||||
|
||||
if (!chat::infra::loadRawBlobFromPreferences(kPrefsNs, kSigPrivKey, blob) ||
|
||||
blob.size() != sig_priv_.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
memcpy(sig_priv_.data(), blob.data(), sig_priv_.size());
|
||||
|
||||
if (isAllZero(enc_pub_.data(), enc_pub_.size()) ||
|
||||
isAllZero(enc_priv_.data(), enc_priv_.size()) ||
|
||||
isAllZero(sig_pub_.data(), sig_pub_.size()) ||
|
||||
isAllZero(sig_priv_.data(), sig_priv_.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t derived_sig_pub[kSigPubKeySize] = {};
|
||||
ed25519_derive_pub(derived_sig_pub, sig_priv_.data());
|
||||
if (memcmp(derived_sig_pub, sig_pub_.data(), sizeof(derived_sig_pub)) != 0)
|
||||
{
|
||||
memcpy(sig_pub_.data(), derived_sig_pub, sizeof(derived_sig_pub));
|
||||
saveToPrefs();
|
||||
}
|
||||
|
||||
recomputeDerivedFields();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LxmfIdentity::saveToPrefs() const
|
||||
{
|
||||
const bool enc_pub_ok = chat::infra::saveRawBlobToPreferences(
|
||||
kPrefsNs, kEncPubKey, enc_pub_.data(), enc_pub_.size());
|
||||
const bool enc_priv_ok = chat::infra::saveRawBlobToPreferences(
|
||||
kPrefsNs, kEncPrivKey, enc_priv_.data(), enc_priv_.size());
|
||||
const bool sig_pub_ok = chat::infra::saveRawBlobToPreferences(
|
||||
kPrefsNs, kSigPubKey, sig_pub_.data(), sig_pub_.size());
|
||||
const bool sig_priv_ok = chat::infra::saveRawBlobToPreferences(
|
||||
kPrefsNs, kSigPrivKey, sig_priv_.data(), sig_priv_.size());
|
||||
return enc_pub_ok && enc_priv_ok && sig_pub_ok && sig_priv_ok;
|
||||
}
|
||||
|
||||
bool LxmfIdentity::generateAndPersist()
|
||||
{
|
||||
RNG.begin("trail-mate-lxmf");
|
||||
|
||||
for (size_t attempt = 0; attempt < 16; ++attempt)
|
||||
{
|
||||
memset(enc_pub_.data(), 0, enc_pub_.size());
|
||||
memset(enc_priv_.data(), 0, enc_priv_.size());
|
||||
Curve25519::dh1(enc_pub_.data(), enc_priv_.data());
|
||||
if (!isAllZero(enc_priv_.data(), enc_priv_.size()))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isAllZero(enc_priv_.data(), enc_priv_.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t seed[32] = {};
|
||||
fillRandomBytes(seed, sizeof(seed));
|
||||
ed25519_create_keypair(sig_pub_.data(), sig_priv_.data(), seed);
|
||||
memset(seed, 0, sizeof(seed));
|
||||
|
||||
if (isAllZero(sig_priv_.data(), sig_priv_.size()) || isAllZero(sig_pub_.data(), sig_pub_.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
recomputeDerivedFields();
|
||||
ready_ = saveToPrefs();
|
||||
return ready_;
|
||||
}
|
||||
|
||||
void LxmfIdentity::recomputeDerivedFields()
|
||||
{
|
||||
uint8_t combined[reticulum::kCombinedPublicKeySize] = {};
|
||||
combinedPublicKey(combined);
|
||||
reticulum::computeIdentityHash(combined, identity_hash_.data());
|
||||
|
||||
uint8_t name_hash[reticulum::kNameHashSize] = {};
|
||||
reticulum::computeNameHash("lxmf", "delivery", name_hash);
|
||||
reticulum::computeDestinationHash(name_hash, identity_hash_.data(), destination_hash_.data());
|
||||
node_id_ = reticulum::nodeIdFromDestinationHash(destination_hash_.data());
|
||||
}
|
||||
|
||||
} // namespace chat::lxmf
|
||||
@@ -5,8 +5,10 @@
|
||||
|
||||
#include "platform/esp/arduino_common/chat/infra/protocol_factory.h"
|
||||
#include "board/LoraBoard.h"
|
||||
#include "platform/esp/arduino_common/chat/infra/lxmf/lxmf_adapter.h"
|
||||
#include "platform/esp/arduino_common/chat/infra/meshcore/meshcore_adapter.h"
|
||||
#include "platform/esp/arduino_common/chat/infra/meshtastic/mt_adapter.h"
|
||||
#include "platform/esp/arduino_common/chat/infra/rnode/rnode_adapter.h"
|
||||
|
||||
namespace chat
|
||||
{
|
||||
@@ -18,6 +20,10 @@ std::unique_ptr<IMeshAdapter> ProtocolFactory::createAdapter(MeshProtocol protoc
|
||||
{
|
||||
case MeshProtocol::MeshCore:
|
||||
return std::unique_ptr<IMeshAdapter>(new chat::meshcore::MeshCoreAdapter(board));
|
||||
case MeshProtocol::LXMF:
|
||||
return std::unique_ptr<IMeshAdapter>(new chat::lxmf::LxmfAdapter(board));
|
||||
case MeshProtocol::RNode:
|
||||
return std::unique_ptr<IMeshAdapter>(new chat::rnode::RNodeAdapter(board));
|
||||
case MeshProtocol::Meshtastic:
|
||||
default:
|
||||
return std::unique_ptr<IMeshAdapter>(new chat::meshtastic::MtAdapter(board));
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* @file rnode_adapter.cpp
|
||||
* @brief Minimal RNode raw-payload mesh adapter
|
||||
*/
|
||||
|
||||
#include "platform/esp/arduino_common/chat/infra/rnode/rnode_adapter.h"
|
||||
|
||||
#include "chat/time_utils.h"
|
||||
#include <Arduino.h>
|
||||
#include <RadioLib.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace chat
|
||||
{
|
||||
namespace rnode
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float kDefaultFrequencyMHz = 869.525f;
|
||||
constexpr float kDefaultBandwidthKHz = 125.0f;
|
||||
constexpr uint8_t kDefaultSpreadingFactor = 9;
|
||||
constexpr uint8_t kDefaultCodingRate = 5;
|
||||
constexpr int8_t kDefaultTxPowerDbm = 17;
|
||||
|
||||
template <typename T>
|
||||
T clampValue(T value, T min_value, T max_value)
|
||||
{
|
||||
if (value < min_value)
|
||||
{
|
||||
return min_value;
|
||||
}
|
||||
if (value > max_value)
|
||||
{
|
||||
return max_value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RNodeAdapter::RNodeAdapter(LoraBoard& board)
|
||||
: board_(board)
|
||||
{
|
||||
}
|
||||
|
||||
MeshCapabilities RNodeAdapter::getCapabilities() const
|
||||
{
|
||||
return MeshCapabilities{};
|
||||
}
|
||||
|
||||
bool RNodeAdapter::sendText(ChannelId channel, const std::string& text,
|
||||
MessageId* out_msg_id, NodeId peer)
|
||||
{
|
||||
(void)channel;
|
||||
(void)text;
|
||||
(void)peer;
|
||||
if (out_msg_id)
|
||||
{
|
||||
*out_msg_id = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RNodeAdapter::pollIncomingText(MeshIncomingText* out)
|
||||
{
|
||||
(void)out;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RNodeAdapter::sendAppData(ChannelId channel, uint32_t portnum,
|
||||
const uint8_t* payload, size_t len,
|
||||
NodeId dest, bool want_ack,
|
||||
MessageId packet_id,
|
||||
bool want_response)
|
||||
{
|
||||
(void)channel;
|
||||
(void)dest;
|
||||
(void)want_ack;
|
||||
(void)want_response;
|
||||
|
||||
// RNode air payloads are raw Reticulum/TNC bytes. We reserve port 0
|
||||
// for pass-through raw payload transmission and reject higher-level
|
||||
// app-data semantics until a Reticulum-compatible upper layer exists.
|
||||
if (!payload || len == 0 || portnum != 0 || !ready_ || !board_.isRadioOnline())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EncodedAirPacketSet air_packets{};
|
||||
const uint8_t sequence = static_cast<uint8_t>(((packet_id != 0 ? packet_id : next_sequence_) & 0x0FU));
|
||||
next_sequence_ = static_cast<uint8_t>((sequence + 1U) & 0x0FU);
|
||||
if (!encodeAirPacketSet(payload, len, sequence, &air_packets))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int first_state = board_.transmitRadio(air_packets.first, air_packets.first_len);
|
||||
if (first_state != RADIOLIB_ERR_NONE)
|
||||
{
|
||||
startRadioReceive();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (air_packets.count > 1U)
|
||||
{
|
||||
const int second_state = board_.transmitRadio(air_packets.second, air_packets.second_len);
|
||||
if (second_state != RADIOLIB_ERR_NONE)
|
||||
{
|
||||
startRadioReceive();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
startRadioReceive();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RNodeAdapter::pollIncomingData(MeshIncomingData* out)
|
||||
{
|
||||
if (!out || app_receive_queue_.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*out = std::move(app_receive_queue_.front());
|
||||
app_receive_queue_.pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RNodeAdapter::applyConfig(const MeshConfig& config)
|
||||
{
|
||||
config_ = config;
|
||||
|
||||
const float freq_mhz =
|
||||
(config_.override_frequency_mhz > 0.0f) ? config_.override_frequency_mhz : kDefaultFrequencyMHz;
|
||||
const float bw_khz =
|
||||
(config_.bandwidth_khz > 0.0f) ? config_.bandwidth_khz : kDefaultBandwidthKHz;
|
||||
const uint8_t sf =
|
||||
clampValue<uint8_t>(config_.spread_factor != 0 ? config_.spread_factor : kDefaultSpreadingFactor, 5U, 12U);
|
||||
const uint8_t cr =
|
||||
clampValue<uint8_t>(config_.coding_rate != 0 ? config_.coding_rate : kDefaultCodingRate, 5U, 8U);
|
||||
const int8_t tx_power =
|
||||
clampValue<int8_t>(config_.tx_power != 0 ? config_.tx_power : kDefaultTxPowerDbm, -9, 22);
|
||||
|
||||
radio_freq_hz_ = static_cast<uint32_t>(std::lround(freq_mhz * 1000000.0f));
|
||||
radio_bw_hz_ = static_cast<uint32_t>(std::lround(bw_khz * 1000.0f));
|
||||
radio_sf_ = sf;
|
||||
radio_cr_ = cr;
|
||||
|
||||
const uint16_t preamble =
|
||||
chat::rnode::recommendPreambleSymbols(radio_bw_hz_, radio_sf_, radio_cr_);
|
||||
|
||||
board_.configureLoraRadio(freq_mhz, bw_khz, sf, cr, tx_power, preamble, kSyncWord, kCrcLen);
|
||||
ready_ = true;
|
||||
startRadioReceive();
|
||||
}
|
||||
|
||||
void RNodeAdapter::setLastRxStats(float rssi, float snr)
|
||||
{
|
||||
last_rx_rssi_ = rssi;
|
||||
last_rx_snr_ = snr;
|
||||
}
|
||||
|
||||
bool RNodeAdapter::isReady() const
|
||||
{
|
||||
return ready_ && board_.isRadioOnline();
|
||||
}
|
||||
|
||||
bool RNodeAdapter::pollIncomingRawPacket(uint8_t* out_data, size_t& out_len, size_t max_len)
|
||||
{
|
||||
if (!has_pending_raw_packet_ || !out_data || max_len == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t copy_len = std::min(last_raw_packet_.len, max_len);
|
||||
memcpy(out_data, last_raw_packet_.data, copy_len);
|
||||
out_len = copy_len;
|
||||
has_pending_raw_packet_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RNodeAdapter::handleRawPacket(const uint8_t* data, size_t size)
|
||||
{
|
||||
if (!data || size == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t payload[chat::rnode::kRNodeMaxPayloadSize] = {};
|
||||
size_t payload_len = sizeof(payload);
|
||||
bool complete = false;
|
||||
if (!feedAirPacket(&reassembly_, data, size, payload, &payload_len, &complete) || !complete)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(last_raw_packet_.data, payload, payload_len);
|
||||
last_raw_packet_.len = payload_len;
|
||||
has_pending_raw_packet_ = true;
|
||||
enqueueIncomingData(payload, payload_len);
|
||||
}
|
||||
|
||||
void RNodeAdapter::startRadioReceive()
|
||||
{
|
||||
if (!board_.isRadioOnline())
|
||||
{
|
||||
return;
|
||||
}
|
||||
(void)board_.startRadioReceive();
|
||||
}
|
||||
|
||||
void RNodeAdapter::enqueueIncomingData(const uint8_t* payload, size_t len)
|
||||
{
|
||||
if (!payload || len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MeshIncomingData incoming;
|
||||
incoming.portnum = 0;
|
||||
incoming.from = 0;
|
||||
incoming.to = 0;
|
||||
incoming.packet_id = now_message_timestamp();
|
||||
incoming.request_id = 0;
|
||||
incoming.channel = ChannelId::PRIMARY;
|
||||
incoming.channel_hash = 0xFF;
|
||||
incoming.hop_limit = 0xFF;
|
||||
incoming.want_response = false;
|
||||
incoming.payload.assign(payload, payload + len);
|
||||
|
||||
incoming.rx_meta.rx_timestamp_ms = millis();
|
||||
const uint32_t epoch_s = now_epoch_seconds();
|
||||
if (is_valid_epoch(epoch_s))
|
||||
{
|
||||
incoming.rx_meta.rx_timestamp_s = epoch_s;
|
||||
incoming.rx_meta.time_source = RxTimeSource::DeviceUtc;
|
||||
}
|
||||
else
|
||||
{
|
||||
incoming.rx_meta.rx_timestamp_s = incoming.rx_meta.rx_timestamp_ms / 1000U;
|
||||
incoming.rx_meta.time_source = RxTimeSource::Uptime;
|
||||
}
|
||||
incoming.rx_meta.origin = RxOrigin::Mesh;
|
||||
incoming.rx_meta.direct = true;
|
||||
incoming.rx_meta.from_is = false;
|
||||
incoming.rx_meta.rssi_dbm_x10 = static_cast<int16_t>(std::lround(last_rx_rssi_ * 10.0f));
|
||||
incoming.rx_meta.snr_db_x10 = static_cast<int16_t>(std::lround(last_rx_snr_ * 10.0f));
|
||||
incoming.rx_meta.freq_hz = radio_freq_hz_;
|
||||
incoming.rx_meta.bw_hz = radio_bw_hz_;
|
||||
incoming.rx_meta.sf = radio_sf_;
|
||||
incoming.rx_meta.cr = radio_cr_;
|
||||
|
||||
app_receive_queue_.push(std::move(incoming));
|
||||
}
|
||||
|
||||
} // namespace rnode
|
||||
} // namespace chat
|
||||
@@ -1,9 +1,22 @@
|
||||
#include "platform/ui/hostlink_runtime.h"
|
||||
|
||||
#include "app/app_config.h"
|
||||
#include "platform/esp/arduino_common/hostlink/hostlink_service.h"
|
||||
#include "platform/esp/arduino_common/rnode_kiss/rnode_kiss_service.h"
|
||||
|
||||
#include "app/app_facade_access.h"
|
||||
|
||||
namespace platform::ui::hostlink
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
bool use_rnode_bridge()
|
||||
{
|
||||
return app::appFacade().getConfig().mesh_protocol == chat::MeshProtocol::RNode;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool is_supported()
|
||||
{
|
||||
@@ -12,21 +25,39 @@ bool is_supported()
|
||||
|
||||
void start()
|
||||
{
|
||||
if (use_rnode_bridge())
|
||||
{
|
||||
::rnode_kiss::start();
|
||||
return;
|
||||
}
|
||||
::hostlink::start();
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
if (use_rnode_bridge())
|
||||
{
|
||||
::rnode_kiss::stop();
|
||||
return;
|
||||
}
|
||||
::hostlink::stop();
|
||||
}
|
||||
|
||||
bool is_active()
|
||||
{
|
||||
if (use_rnode_bridge())
|
||||
{
|
||||
return ::rnode_kiss::is_active();
|
||||
}
|
||||
return ::hostlink::is_active();
|
||||
}
|
||||
|
||||
Status get_status()
|
||||
{
|
||||
if (use_rnode_bridge())
|
||||
{
|
||||
return ::rnode_kiss::get_status();
|
||||
}
|
||||
return ::hostlink::get_status();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,642 @@
|
||||
#include "platform/esp/arduino_common/rnode_kiss/rnode_kiss_service.h"
|
||||
|
||||
#include "app/app_config.h"
|
||||
#include "app/app_facade_access.h"
|
||||
#include "app/app_facades.h"
|
||||
#include "hostlink/hostlink_session.h"
|
||||
#include "platform/esp/arduino_common/chat/infra/rnode/rnode_adapter.h"
|
||||
#include "usb/usb_cdc_transport.h"
|
||||
|
||||
#include "esp_system.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace rnode_kiss
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint8_t kFend = 0xC0;
|
||||
constexpr uint8_t kFesc = 0xDB;
|
||||
constexpr uint8_t kTfend = 0xDC;
|
||||
constexpr uint8_t kTfesc = 0xDD;
|
||||
|
||||
constexpr uint8_t kCmdData = 0x00;
|
||||
constexpr uint8_t kCmdFrequency = 0x01;
|
||||
constexpr uint8_t kCmdBandwidth = 0x02;
|
||||
constexpr uint8_t kCmdTxPower = 0x03;
|
||||
constexpr uint8_t kCmdSf = 0x04;
|
||||
constexpr uint8_t kCmdCr = 0x05;
|
||||
constexpr uint8_t kCmdRadioState = 0x06;
|
||||
constexpr uint8_t kCmdDetect = 0x08;
|
||||
constexpr uint8_t kCmdLeave = 0x0A;
|
||||
constexpr uint8_t kCmdStALock = 0x0B;
|
||||
constexpr uint8_t kCmdLtALock = 0x0C;
|
||||
constexpr uint8_t kCmdReady = 0x0F;
|
||||
constexpr uint8_t kCmdStatRx = 0x21;
|
||||
constexpr uint8_t kCmdStatTx = 0x22;
|
||||
constexpr uint8_t kCmdStatRssi = 0x23;
|
||||
constexpr uint8_t kCmdStatSnr = 0x24;
|
||||
constexpr uint8_t kCmdRandom = 0x40;
|
||||
constexpr uint8_t kCmdFbExt = 0x41;
|
||||
constexpr uint8_t kCmdBoard = 0x47;
|
||||
constexpr uint8_t kCmdPlatform = 0x48;
|
||||
constexpr uint8_t kCmdMcu = 0x49;
|
||||
constexpr uint8_t kCmdFwVersion = 0x50;
|
||||
constexpr uint8_t kCmdError = 0x90;
|
||||
|
||||
constexpr uint8_t kDetectReq = 0x73;
|
||||
constexpr uint8_t kDetectResp = 0x46;
|
||||
|
||||
constexpr uint8_t kRadioStateOff = 0x00;
|
||||
constexpr uint8_t kRadioStateOn = 0x01;
|
||||
constexpr uint8_t kRadioStateAsk = 0xFF;
|
||||
|
||||
constexpr uint8_t kPlatformEsp32 = 0x80;
|
||||
constexpr uint8_t kMcuEsp32 = 0x81;
|
||||
constexpr uint8_t kBoardGenericEsp32 = 0x35;
|
||||
|
||||
constexpr uint8_t kErrorTxFailed = 0x02;
|
||||
constexpr uint8_t kFwVersionMajor = 1;
|
||||
constexpr uint8_t kFwVersionMinor = 52;
|
||||
constexpr uint32_t kTaskPollMs = 20;
|
||||
constexpr size_t kMaxFrameSize = 600;
|
||||
constexpr size_t kUsbReadChunk = 96;
|
||||
|
||||
struct KissParser
|
||||
{
|
||||
bool in_frame = false;
|
||||
bool escape = false;
|
||||
uint8_t buffer[kMaxFrameSize] = {};
|
||||
size_t length = 0;
|
||||
|
||||
void reset()
|
||||
{
|
||||
in_frame = false;
|
||||
escape = false;
|
||||
length = 0;
|
||||
}
|
||||
};
|
||||
|
||||
TaskHandle_t s_task = nullptr;
|
||||
volatile bool s_stop = false;
|
||||
hostlink::SessionRuntime s_session{};
|
||||
uint8_t s_radio_state = kRadioStateOn;
|
||||
uint16_t s_short_airtime_limit = 0;
|
||||
uint16_t s_long_airtime_limit = 0;
|
||||
bool s_host_seen = false;
|
||||
uint32_t s_radio_rx_count = 0;
|
||||
uint32_t s_radio_tx_count = 0;
|
||||
|
||||
void set_state(hostlink::LinkState state)
|
||||
{
|
||||
hostlink::set_link_state(s_session, state);
|
||||
}
|
||||
|
||||
chat::rnode::RNodeAdapter* get_backend()
|
||||
{
|
||||
chat::IMeshAdapter* mesh = app::messagingFacade().getMeshAdapter();
|
||||
if (!mesh)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
chat::IMeshAdapter* backend = mesh->backendForProtocol(chat::MeshProtocol::RNode);
|
||||
return backend ? static_cast<chat::rnode::RNodeAdapter*>(backend) : nullptr;
|
||||
}
|
||||
|
||||
bool apply_live_config()
|
||||
{
|
||||
chat::rnode::RNodeAdapter* backend = get_backend();
|
||||
if (!backend)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
backend->applyConfig(app::appFacade().getConfig().rnode_config);
|
||||
return true;
|
||||
}
|
||||
|
||||
void note_rx()
|
||||
{
|
||||
hostlink::note_rx(s_session);
|
||||
}
|
||||
|
||||
void note_tx()
|
||||
{
|
||||
hostlink::note_tx(s_session);
|
||||
}
|
||||
|
||||
void note_error(uint32_t code)
|
||||
{
|
||||
hostlink::note_error(s_session, code);
|
||||
set_state(hostlink::LinkState::Error);
|
||||
}
|
||||
|
||||
void write_escaped(std::vector<uint8_t>& frame, const uint8_t* data, size_t len)
|
||||
{
|
||||
if (!data || len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
{
|
||||
const uint8_t byte = data[i];
|
||||
if (byte == kFend)
|
||||
{
|
||||
frame.push_back(kFesc);
|
||||
frame.push_back(kTfend);
|
||||
}
|
||||
else if (byte == kFesc)
|
||||
{
|
||||
frame.push_back(kFesc);
|
||||
frame.push_back(kTfesc);
|
||||
}
|
||||
else
|
||||
{
|
||||
frame.push_back(byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool send_frame(uint8_t command, const uint8_t* payload, size_t len)
|
||||
{
|
||||
if (!usb_cdc::get_status().started)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> frame;
|
||||
frame.reserve(len + 4);
|
||||
frame.push_back(kFend);
|
||||
frame.push_back(command);
|
||||
write_escaped(frame, payload, len);
|
||||
frame.push_back(kFend);
|
||||
|
||||
if (usb_cdc::write(frame.data(), frame.size()) != frame.size())
|
||||
{
|
||||
note_error(kErrorTxFailed);
|
||||
return false;
|
||||
}
|
||||
|
||||
note_tx();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool send_u32(uint8_t command, uint32_t value)
|
||||
{
|
||||
const uint8_t payload[4] = {
|
||||
static_cast<uint8_t>((value >> 24) & 0xFF),
|
||||
static_cast<uint8_t>((value >> 16) & 0xFF),
|
||||
static_cast<uint8_t>((value >> 8) & 0xFF),
|
||||
static_cast<uint8_t>(value & 0xFF),
|
||||
};
|
||||
return send_frame(command, payload, sizeof(payload));
|
||||
}
|
||||
|
||||
bool send_u16(uint8_t command, uint16_t value)
|
||||
{
|
||||
const uint8_t payload[2] = {
|
||||
static_cast<uint8_t>((value >> 8) & 0xFF),
|
||||
static_cast<uint8_t>(value & 0xFF),
|
||||
};
|
||||
return send_frame(command, payload, sizeof(payload));
|
||||
}
|
||||
|
||||
bool send_u8(uint8_t command, uint8_t value)
|
||||
{
|
||||
return send_frame(command, &value, 1);
|
||||
}
|
||||
|
||||
int clamp_int(int value, int min_value, int max_value)
|
||||
{
|
||||
if (value < min_value)
|
||||
{
|
||||
return min_value;
|
||||
}
|
||||
if (value > max_value)
|
||||
{
|
||||
return max_value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void send_ready()
|
||||
{
|
||||
const uint8_t ready = 0x01;
|
||||
(void)send_frame(kCmdReady, &ready, 1);
|
||||
}
|
||||
|
||||
void reply_current_config(uint8_t command)
|
||||
{
|
||||
const chat::MeshConfig& cfg = app::appFacade().getConfig().rnode_config;
|
||||
switch (command)
|
||||
{
|
||||
case kCmdFrequency:
|
||||
(void)send_u32(kCmdFrequency,
|
||||
static_cast<uint32_t>(std::lround(cfg.override_frequency_mhz * 1000000.0f)));
|
||||
break;
|
||||
case kCmdBandwidth:
|
||||
(void)send_u32(kCmdBandwidth,
|
||||
static_cast<uint32_t>(std::lround(cfg.bandwidth_khz * 1000.0f)));
|
||||
break;
|
||||
case kCmdTxPower:
|
||||
(void)send_u8(kCmdTxPower, static_cast<uint8_t>(cfg.tx_power));
|
||||
break;
|
||||
case kCmdSf:
|
||||
(void)send_u8(kCmdSf, cfg.spread_factor);
|
||||
break;
|
||||
case kCmdCr:
|
||||
(void)send_u8(kCmdCr, cfg.coding_rate);
|
||||
break;
|
||||
case kCmdRadioState:
|
||||
(void)send_u8(kCmdRadioState, s_radio_state);
|
||||
break;
|
||||
case kCmdStALock:
|
||||
(void)send_u16(kCmdStALock, s_short_airtime_limit);
|
||||
break;
|
||||
case kCmdLtALock:
|
||||
(void)send_u16(kCmdLtALock, s_long_airtime_limit);
|
||||
break;
|
||||
case kCmdFwVersion:
|
||||
{
|
||||
const uint8_t version[2] = {kFwVersionMajor, kFwVersionMinor};
|
||||
(void)send_frame(kCmdFwVersion, version, sizeof(version));
|
||||
break;
|
||||
}
|
||||
case kCmdPlatform:
|
||||
(void)send_u8(kCmdPlatform, kPlatformEsp32);
|
||||
break;
|
||||
case kCmdMcu:
|
||||
(void)send_u8(kCmdMcu, kMcuEsp32);
|
||||
break;
|
||||
case kCmdBoard:
|
||||
(void)send_u8(kCmdBoard, kBoardGenericEsp32);
|
||||
break;
|
||||
case kCmdStatRx:
|
||||
(void)send_u32(kCmdStatRx, s_radio_rx_count);
|
||||
break;
|
||||
case kCmdStatTx:
|
||||
(void)send_u32(kCmdStatTx, s_radio_tx_count);
|
||||
break;
|
||||
case kCmdRandom:
|
||||
(void)send_u8(kCmdRandom, static_cast<uint8_t>(esp_random() & 0xFF));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t decode_u32(const uint8_t* payload, size_t len)
|
||||
{
|
||||
if (!payload || len < 4)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return (static_cast<uint32_t>(payload[0]) << 24) |
|
||||
(static_cast<uint32_t>(payload[1]) << 16) |
|
||||
(static_cast<uint32_t>(payload[2]) << 8) |
|
||||
static_cast<uint32_t>(payload[3]);
|
||||
}
|
||||
|
||||
void send_last_rx_stats(chat::rnode::RNodeAdapter& backend)
|
||||
{
|
||||
const float rssi = backend.lastRxRssi();
|
||||
const float snr = backend.lastRxSnr();
|
||||
|
||||
int rssi_encoded = static_cast<int>(std::lround(rssi + 157.0f));
|
||||
rssi_encoded = clamp_int(rssi_encoded, 0, 255);
|
||||
|
||||
int snr_encoded = static_cast<int>(std::lround(snr * 4.0f));
|
||||
snr_encoded = clamp_int(snr_encoded, -128, 127);
|
||||
|
||||
(void)send_u8(kCmdStatRssi, static_cast<uint8_t>(rssi_encoded));
|
||||
(void)send_u8(kCmdStatSnr, static_cast<uint8_t>(static_cast<int8_t>(snr_encoded)));
|
||||
}
|
||||
|
||||
void process_command(uint8_t command, const uint8_t* payload, size_t len)
|
||||
{
|
||||
s_host_seen = true;
|
||||
if (s_session.status.state != hostlink::LinkState::Error)
|
||||
{
|
||||
set_state(hostlink::LinkState::Ready);
|
||||
}
|
||||
note_rx();
|
||||
|
||||
app::IAppFacade& app_ctx = app::appFacade();
|
||||
chat::MeshConfig& cfg = app_ctx.getConfig().rnode_config;
|
||||
chat::rnode::RNodeAdapter* backend = get_backend();
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case kCmdDetect:
|
||||
if (len == 0 || payload[0] == kDetectReq)
|
||||
{
|
||||
(void)send_u8(kCmdDetect, kDetectResp);
|
||||
}
|
||||
break;
|
||||
case kCmdFwVersion:
|
||||
case kCmdPlatform:
|
||||
case kCmdMcu:
|
||||
case kCmdBoard:
|
||||
case kCmdStatRx:
|
||||
case kCmdStatTx:
|
||||
case kCmdRandom:
|
||||
reply_current_config(command);
|
||||
break;
|
||||
case kCmdFrequency:
|
||||
if (len >= 4)
|
||||
{
|
||||
cfg.override_frequency_mhz = static_cast<float>(decode_u32(payload, len)) / 1000000.0f;
|
||||
if (s_radio_state == kRadioStateOn)
|
||||
{
|
||||
(void)apply_live_config();
|
||||
}
|
||||
}
|
||||
reply_current_config(kCmdFrequency);
|
||||
break;
|
||||
case kCmdBandwidth:
|
||||
if (len >= 4)
|
||||
{
|
||||
cfg.bandwidth_khz = static_cast<float>(decode_u32(payload, len)) / 1000.0f;
|
||||
if (s_radio_state == kRadioStateOn)
|
||||
{
|
||||
(void)apply_live_config();
|
||||
}
|
||||
}
|
||||
reply_current_config(kCmdBandwidth);
|
||||
break;
|
||||
case kCmdTxPower:
|
||||
if (len >= 1)
|
||||
{
|
||||
cfg.tx_power = static_cast<int8_t>(payload[0]);
|
||||
if (s_radio_state == kRadioStateOn)
|
||||
{
|
||||
(void)apply_live_config();
|
||||
}
|
||||
}
|
||||
reply_current_config(kCmdTxPower);
|
||||
break;
|
||||
case kCmdSf:
|
||||
if (len >= 1)
|
||||
{
|
||||
cfg.spread_factor = payload[0];
|
||||
if (s_radio_state == kRadioStateOn)
|
||||
{
|
||||
(void)apply_live_config();
|
||||
}
|
||||
}
|
||||
reply_current_config(kCmdSf);
|
||||
break;
|
||||
case kCmdCr:
|
||||
if (len >= 1)
|
||||
{
|
||||
cfg.coding_rate = payload[0];
|
||||
if (s_radio_state == kRadioStateOn)
|
||||
{
|
||||
(void)apply_live_config();
|
||||
}
|
||||
}
|
||||
reply_current_config(kCmdCr);
|
||||
break;
|
||||
case kCmdStALock:
|
||||
if (len >= 2)
|
||||
{
|
||||
s_short_airtime_limit = static_cast<uint16_t>((payload[0] << 8) | payload[1]);
|
||||
}
|
||||
reply_current_config(kCmdStALock);
|
||||
break;
|
||||
case kCmdLtALock:
|
||||
if (len >= 2)
|
||||
{
|
||||
s_long_airtime_limit = static_cast<uint16_t>((payload[0] << 8) | payload[1]);
|
||||
}
|
||||
reply_current_config(kCmdLtALock);
|
||||
break;
|
||||
case kCmdRadioState:
|
||||
if (len >= 1 && payload[0] != kRadioStateAsk)
|
||||
{
|
||||
s_radio_state = (payload[0] == kRadioStateOff) ? kRadioStateOff : kRadioStateOn;
|
||||
if (s_radio_state == kRadioStateOn)
|
||||
{
|
||||
(void)apply_live_config();
|
||||
}
|
||||
}
|
||||
reply_current_config(kCmdRadioState);
|
||||
send_ready();
|
||||
break;
|
||||
case kCmdFbExt:
|
||||
send_ready();
|
||||
break;
|
||||
case kCmdLeave:
|
||||
s_host_seen = false;
|
||||
if (s_session.status.state != hostlink::LinkState::Error)
|
||||
{
|
||||
set_state(hostlink::LinkState::Connected);
|
||||
}
|
||||
break;
|
||||
case kCmdData:
|
||||
if (s_radio_state != kRadioStateOn || !backend || !payload || len == 0)
|
||||
{
|
||||
(void)send_u8(kCmdError, kErrorTxFailed);
|
||||
break;
|
||||
}
|
||||
if (backend->sendAppData(chat::ChannelId::PRIMARY, 0, payload, len))
|
||||
{
|
||||
s_radio_tx_count++;
|
||||
(void)send_u32(kCmdStatTx, s_radio_tx_count);
|
||||
send_ready();
|
||||
}
|
||||
else
|
||||
{
|
||||
(void)send_u8(kCmdError, kErrorTxFailed);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void feed_parser(KissParser& parser, uint8_t byte)
|
||||
{
|
||||
if (byte == kFend)
|
||||
{
|
||||
if (parser.in_frame && parser.length > 0)
|
||||
{
|
||||
const uint8_t command = parser.buffer[0];
|
||||
const uint8_t* payload = (parser.length > 1) ? &parser.buffer[1] : nullptr;
|
||||
const size_t payload_len = (parser.length > 1) ? (parser.length - 1) : 0;
|
||||
process_command(command, payload, payload_len);
|
||||
}
|
||||
|
||||
parser.in_frame = true;
|
||||
parser.escape = false;
|
||||
parser.length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parser.in_frame)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.escape)
|
||||
{
|
||||
if (byte == kTfend)
|
||||
{
|
||||
byte = kFend;
|
||||
}
|
||||
else if (byte == kTfesc)
|
||||
{
|
||||
byte = kFesc;
|
||||
}
|
||||
parser.escape = false;
|
||||
}
|
||||
else if (byte == kFesc)
|
||||
{
|
||||
parser.escape = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.length < sizeof(parser.buffer))
|
||||
{
|
||||
parser.buffer[parser.length++] = byte;
|
||||
}
|
||||
}
|
||||
|
||||
void pump_host_rx(KissParser& parser)
|
||||
{
|
||||
uint8_t buffer[kUsbReadChunk] = {};
|
||||
const size_t len = usb_cdc::read(buffer, sizeof(buffer));
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
{
|
||||
feed_parser(parser, buffer[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void pump_radio_rx()
|
||||
{
|
||||
if (s_radio_state != kRadioStateOn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
chat::rnode::RNodeAdapter* backend = get_backend();
|
||||
if (!backend)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t packet[chat::rnode::kRNodeMaxPayloadSize] = {};
|
||||
size_t packet_len = 0;
|
||||
if (!backend->pollIncomingRawPacket(packet, packet_len, sizeof(packet)) || packet_len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
send_last_rx_stats(*backend);
|
||||
if (send_frame(kCmdData, packet, packet_len))
|
||||
{
|
||||
s_radio_rx_count++;
|
||||
(void)send_u32(kCmdStatRx, s_radio_rx_count);
|
||||
}
|
||||
}
|
||||
|
||||
void reset_runtime()
|
||||
{
|
||||
hostlink::reset_session(s_session, 0);
|
||||
s_radio_state = kRadioStateOn;
|
||||
s_short_airtime_limit = 0;
|
||||
s_long_airtime_limit = 0;
|
||||
s_host_seen = false;
|
||||
s_radio_rx_count = 0;
|
||||
s_radio_tx_count = 0;
|
||||
}
|
||||
|
||||
void rnode_task(void* /*arg*/)
|
||||
{
|
||||
KissParser parser{};
|
||||
reset_runtime();
|
||||
set_state(hostlink::LinkState::Waiting);
|
||||
(void)usb_cdc::start();
|
||||
|
||||
while (!s_stop)
|
||||
{
|
||||
if (!usb_cdc::is_connected())
|
||||
{
|
||||
parser.reset();
|
||||
s_host_seen = false;
|
||||
if (s_session.status.state != hostlink::LinkState::Waiting)
|
||||
{
|
||||
set_state(hostlink::LinkState::Waiting);
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(kTaskPollMs));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!s_host_seen && s_session.status.state != hostlink::LinkState::Connected)
|
||||
{
|
||||
set_state(hostlink::LinkState::Connected);
|
||||
}
|
||||
|
||||
pump_host_rx(parser);
|
||||
pump_radio_rx();
|
||||
vTaskDelay(pdMS_TO_TICKS(kTaskPollMs));
|
||||
}
|
||||
|
||||
hostlink::stop_session(s_session);
|
||||
usb_cdc::stop();
|
||||
s_task = nullptr;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void start()
|
||||
{
|
||||
if (s_task != nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
s_stop = false;
|
||||
xTaskCreate(rnode_task, "rnode_kiss", 6 * 1024, nullptr, 5, &s_task);
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
if (s_task == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
s_stop = true;
|
||||
for (int attempts = 0; attempts < 25 && s_task != nullptr; ++attempts)
|
||||
{
|
||||
vTaskDelay(pdMS_TO_TICKS(kTaskPollMs));
|
||||
}
|
||||
|
||||
if (s_task != nullptr)
|
||||
{
|
||||
vTaskDelete(s_task);
|
||||
s_task = nullptr;
|
||||
hostlink::stop_session(s_session);
|
||||
usb_cdc::stop();
|
||||
}
|
||||
}
|
||||
|
||||
bool is_active()
|
||||
{
|
||||
return s_task != nullptr;
|
||||
}
|
||||
|
||||
hostlink::Status get_status()
|
||||
{
|
||||
return s_session.status;
|
||||
}
|
||||
|
||||
} // namespace rnode_kiss
|
||||
Reference in New Issue
Block a user