Add MediaMesh radio2 presets and automatic media routing
This commit is contained in:
1 parent
38f68596f7
commit
53c7bb141a
14 files changed
+358
-19
No files matched your search
@@ -7,6 +7,12 @@
|
||||
See [two LoRa profiles](radio_profiles.md) for `radio2`, `tempradio2`, scheduling,
|
||||
RX-only mode, cross-transmission policy, and optional radio preambles.
|
||||
|
||||
For a saved MediaMesh second profile, use `set radio2 mediamesh`
|
||||
(911.150 MHz, 250 kHz, SF7, CR 4/5, `rxtx`, automatic preamble), or
|
||||
`set radio2 mediamesh-range` for 125 kHz. These leave the primary profile
|
||||
and crossing policy unchanged. Both radio2 and `radio2.cross` default to `off`.
|
||||
See [MediaMesh presets](radio_profiles.md#mediamesh-presets).
|
||||
|
||||
For supported Full Companions, see [Meshtext channel chat](radio_profiles.md#meshtext-channel-chat)
|
||||
for `mt-` channels and the explicit `send mt.nodeinfo <channel slot>` command.
|
||||
Chat remains plain text. A separate GPS-free NodeInfo is sent after the first
|
||||
@@ -995,6 +1001,9 @@ set tx.ssh auto|radio|radio2|both|off
|
||||
Choose one value after `set`. Both default to `auto` and are saved across
|
||||
reboot. Explicit choices override a channel route for the matching application
|
||||
data only; they do not change ordinary text, adverts or contact ACKs.
|
||||
`tx.media auto` selects only the active `rxtx` MeshCore second profile,
|
||||
including `tempradio2`, when available. Otherwise, it uses the normal
|
||||
channel/global route. `tx.ssh auto` keeps that normal route.
|
||||
`radio2` requires an active `rxtx` MeshCore profile and never falls back to
|
||||
the primary. `both` uses the available profiles; `off` blocks sending only.
|
||||
Queries include the effective `active=` selection and warn if radio2 is
|
||||
|
||||
@@ -92,6 +92,7 @@ Examples use private channel index `3`:
|
||||
|
||||
```text
|
||||
fleet send 3 get radio2
|
||||
fleet send 3 set radio2 mediamesh
|
||||
fleet send 3 set tempradio2 910.5,500,8,5,rxtx,120
|
||||
fleet send 3 set radio2.cross auto
|
||||
fleet send 3 set flood.rule.4 type=grp_txt hops=6+ drop
|
||||
@@ -295,7 +296,7 @@ Reboot or changing enrollment discards the partial transfers.
|
||||
|
||||
| Control | Fleet commands |
|
||||
| --- | --- |
|
||||
| Saved second profile | `get radio2`, `set radio2 <tuple|off>` |
|
||||
| Saved second profile | `get radio2`, `set radio2 <tuple|mediamesh|mediamesh-range|off>` |
|
||||
| Temporary second profile | `get tempradio2`, `set tempradio2 <tuple|off>` |
|
||||
| Scheduled primary profile | `get/set/del radioat`, `get/set/del tempradioat` |
|
||||
| Scheduled second profile | `get/set/del radioat2`, `get/set/del tempradioat2` |
|
||||
@@ -313,6 +314,11 @@ Each role still checks whether it supports the requested setting. Immediate
|
||||
primary radio changes, identity export, passwords, ACL edits, filesystem operations, firmware
|
||||
installation, reboot, and unrestricted CLI are outside fleet capability.
|
||||
|
||||
`set radio2 mediamesh` saves 911.150 MHz, 250 kHz, SF7, CR 4/5, `rxtx`,
|
||||
with automatic scan-aware preamble. `mediamesh-range` uses 125 kHz instead.
|
||||
These presets preserve the primary profile and crossing policy; new
|
||||
configurations default to radio2 off and `radio2.cross off`.
|
||||
|
||||
## Schedule radio changes and set clocks
|
||||
|
||||
Use the normal schedule syntax, with UTC epoch seconds or `+N` whole minutes.
|
||||
|
||||
+57
-6
@@ -8,6 +8,9 @@
|
||||
settings. It is useful for listening to two networks, or keeping your usual
|
||||
channel available during a LoRa firmware update.
|
||||
|
||||
The second profile and `radio2.cross` both default to `off`. Saved settings
|
||||
survive firmware updates; selecting a profile does not change the crossing policy.
|
||||
|
||||
There are **at most two active profiles**. `tempradio` temporarily replaces
|
||||
`radio`; `tempradio2` temporarily replaces `radio2`. These are time-shared receive
|
||||
windows, not simultaneous receivers. Receiving or transmitting a packet on one
|
||||
@@ -33,6 +36,40 @@ get radio2.status
|
||||
The tuple is **frequency in MHz, bandwidth in kHz, spreading factor, coding-rate
|
||||
denominator, mode, optional preamble in symbols**. `5` means coding rate 4/5.
|
||||
|
||||
### MediaMesh presets
|
||||
|
||||
Enable the saved MediaMesh profile with one command:
|
||||
|
||||
```text
|
||||
set radio2 mediamesh
|
||||
```
|
||||
|
||||
This sets **911.150 MHz, 250 kHz, SF7, CR 4/5, `rxtx`, automatic preamble**.
|
||||
For the same profile at **125 kHz**, use:
|
||||
|
||||
```text
|
||||
set radio2 mediamesh-range
|
||||
```
|
||||
|
||||
The RF settings match the presets documented by
|
||||
[MeshCore Open TAK](https://github.com/jade-on-mesh/meshcore-open-tak/blob/main/documentation/media-mesh.md)
|
||||
and [Wadamesh TAK](https://github.com/jade-on-mesh/wadamesh-tak/blob/main/MEDIAMESH.md).
|
||||
Nodes exchanging media need matching radio settings.
|
||||
|
||||
Both leave the primary profile and `radio2.cross` unchanged. The center is
|
||||
625 kHz above a 910.525 MHz primary. Automatic preamble remains scan-aware;
|
||||
neither preset fixes it at 32 symbols. See [preamble](#preamble) for the
|
||||
switching measurement and recommendations.
|
||||
|
||||
On Companions, the default `tx.media auto` sends recognized binary photo/voice
|
||||
traffic only on an active `rxtx` MeshCore second profile, including `tempradio2`.
|
||||
Apps do not need to set `tx.media`. Ordinary traffic keeps its existing route.
|
||||
If the second profile cannot transmit MeshCore, media uses the normal
|
||||
channel/global route; `set tx.media radio2` instead requires radio2 with no
|
||||
primary fallback. See [application TX routing](#companion-media-and-ssh-tx-routing).
|
||||
|
||||
### Channel spacing
|
||||
|
||||
When setting a saved, temporary or scheduled second profile, the response warns
|
||||
if its occupied bandwidth overlaps the primary channel or comes within **6 kHz
|
||||
of either edge**. This check uses both channel bandwidths, regardless of SF or
|
||||
@@ -67,6 +104,7 @@ For transmission on both permanent profiles:
|
||||
|
||||
```text
|
||||
set radio2 910.5,500,8,5,rxtx
|
||||
set radio2.cross on
|
||||
```
|
||||
|
||||
The same encoded message is queued separately for each allowed transmit profile.
|
||||
@@ -283,17 +321,27 @@ set tx.ssh radio
|
||||
|
||||
Both accept `auto|radio|radio2|both|off` and default to `auto`. An explicit
|
||||
application route overrides the channel's TX choice for that datagram only;
|
||||
`auto` preserves the channel/global behavior. `radio2` never falls back to
|
||||
`radio`, and must be an enabled `rxtx` **MeshCore** profile. `both` uses the
|
||||
`tx.media auto` chooses only radio2 whenever its active profile permits
|
||||
`rxtx` **MeshCore** transmission, including an active `tempradio2`. This
|
||||
overrides the channel/global route for recognized media even when an app
|
||||
has never configured `tx.media`. Otherwise, media uses the normal channel/global
|
||||
route. `tx.ssh auto` always preserves that normal route.
|
||||
|
||||
The explicit `radio2` choice never falls back to `radio`, and requires an enabled
|
||||
`rxtx` **MeshCore** profile. `both` uses the
|
||||
profiles available for MeshCore transmission. `off` blocks transmission, not
|
||||
reception. Getters show the saved choice and current `active=` mask; `auto`
|
||||
shows `active=channel/global`, because the effective route depends on the
|
||||
shows `active=radio2` while media automatically selects radio2, and
|
||||
`active=channel/global` otherwise, because the effective route then depends on the
|
||||
destination channel (or global rules for raw data). Settings
|
||||
survive reboot; a failed save returns an error and keeps the previous choice.
|
||||
Already queued packets retain their original route.
|
||||
|
||||
`tx.media` recognizes Open TAK photo/voice channel data type `0x5643`, raw
|
||||
clip packets `0x56`/`0x57`, and well-formed binary fetch requests `0x72`.
|
||||
This covers `CMD_SEND_CHANNEL_DATA` and `CMD_SEND_RAW_DATA`, plus visible
|
||||
raw media in `CMD_SEND_RAW_PACKET`. Fully encoded encrypted group packets
|
||||
submitted through `CMD_SEND_RAW_PACKET` retain their existing route.
|
||||
Text announcements (`VE3:`, `VM1:`, `VI1:`), text fetch requests (`VR3:`),
|
||||
ordinary chat, contact ACKs, adverts, OTA, and unrelated raw data keep their
|
||||
normal routing. The format is defined in
|
||||
@@ -313,9 +361,11 @@ adopt the contract. Other channel data types do not acquire an SSH route
|
||||
merely because their contents start with `SSH`. Raw packets still require
|
||||
a direct path, including zero-hop; this does not enable raw flooding.
|
||||
|
||||
For a media-only side channel, configure `radio2`/`tempradio2` in `rxtx`,
|
||||
set `radio2.cross off`, then set `tx.media radio2`. Ordinary text can stay on
|
||||
the primary profile without routing every raw application onto radio2.
|
||||
For a media-only side channel, use `set radio2 mediamesh` with the default
|
||||
`radio2.cross off` and `tx.media auto`. Ordinary text can stay on the primary
|
||||
profile without routing every raw application onto radio2. Choose
|
||||
`set tx.media radio2` if media must fail rather than use the primary when
|
||||
radio2 is off, RX-only, or owned by another protocol.
|
||||
|
||||
## ESP32 Companion Ethernet over LoRa (KISS)
|
||||
|
||||
@@ -821,6 +871,7 @@ setting is preserved across firmware updates and reboots.
|
||||
|
||||
These are the global rules used by Companion targets set to `auto`; an explicit
|
||||
contact/channel choice above overrides crossing for its own outbound packets.
|
||||
Recognized media uses the separate `tx.media auto` selection described above.
|
||||
Ordinary crossing respects `rx`. The infrastructure-only `tx.reply ... force`
|
||||
override above permits locally generated replies on that profile.
|
||||
With crossing allowed, a packet received there may still be forwarded on the
|
||||
|
||||
@@ -3690,9 +3690,11 @@ bool MyMesh::handleTrafficRoutingCommand(const char* command, char* reply, size_
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const uint8_t mask = mesh::companion::trafficTransmitMask(policy, _radio->profiles());
|
||||
const uint8_t effective = mesh::companion::trafficPolicy(parsed.kind,
|
||||
_prefs.tx_media, _prefs.tx_ssh, _radio->profiles());
|
||||
const uint8_t mask = mesh::companion::trafficTransmitMask(effective, _radio->profiles());
|
||||
snprintf(reply, reply_size, "%s tx.%s %s; active=%s%s", parsed.set ? "OK -" : ">", kind,
|
||||
mesh::radioTxPolicyName(policy), policy == mesh::RADIO_TX_AUTO ? "channel/global"
|
||||
mesh::radioTxPolicyName(policy), effective == mesh::RADIO_TX_AUTO ? "channel/global"
|
||||
: mask ? mesh::radioTxPolicyName(mask) : "off",
|
||||
(policy == mesh::RADIO_TX_SECONDARY || policy == mesh::RADIO_TX_BOTH) && !(mask & 2)
|
||||
? " (radio2 unavailable for TX)" : "");
|
||||
@@ -3703,7 +3705,7 @@ bool MyMesh::sendClientGroupData(mesh::GroupChannel& channel, uint8_t* path, uin
|
||||
uint16_t data_type, const uint8_t* data, int data_len) {
|
||||
if (data_len < 0 || data_len > MAX_GROUP_DATA_LENGTH || (data_len && !data)) return false;
|
||||
const uint8_t policy = mesh::companion::trafficPolicy(mesh::companion::groupTrafficKind(data_type),
|
||||
_prefs.tx_media, _prefs.tx_ssh);
|
||||
_prefs.tx_media, _prefs.tx_ssh, _radio->profiles());
|
||||
if (policy == mesh::RADIO_TX_AUTO) return sendGroupData(channel, path, path_len, data_type, data, data_len);
|
||||
// Override only this datagram, not the saved channel, PSK, alias or text route.
|
||||
mesh::GroupChannel routed = channel;
|
||||
@@ -3716,7 +3718,7 @@ bool MyMesh::sendClientRawData(const uint8_t* data, size_t data_len, uint8_t* pa
|
||||
auto pkt = createRawData(data, data_len);
|
||||
if (!pkt) return false;
|
||||
const uint8_t policy = mesh::companion::trafficPolicy(mesh::companion::rawTrafficKind(data, data_len),
|
||||
_prefs.tx_media, _prefs.tx_ssh);
|
||||
_prefs.tx_media, _prefs.tx_ssh, _radio->profiles());
|
||||
if (policy != mesh::RADIO_TX_AUTO) pkt->tx_radio = policy;
|
||||
// sendDirect owns/releases the packet on either success or failure.
|
||||
return sendDirect(pkt, path, path_len);
|
||||
@@ -7601,6 +7603,19 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if (pkt) {
|
||||
uint8_t priority = cmd_frame[1];
|
||||
if (tryParsePacket(pkt, &cmd_frame[2], len - 2)) {
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_RAW_CUSTOM) {
|
||||
const uint8_t policy = mesh::companion::trafficPolicy(
|
||||
mesh::companion::rawTrafficKind(pkt->payload, pkt->payload_len),
|
||||
_prefs.tx_media, _prefs.tx_ssh, _radio->profiles());
|
||||
if (policy != mesh::RADIO_TX_AUTO) {
|
||||
if (!mesh::companion::trafficTransmitMask(policy, _radio->profiles())) {
|
||||
releasePacket(pkt);
|
||||
writeErrFrame(ERR_CODE_BAD_STATE);
|
||||
return;
|
||||
}
|
||||
pkt->tx_radio = policy;
|
||||
}
|
||||
}
|
||||
sendPacket(pkt, priority, 0);
|
||||
writeOKFrame();
|
||||
} else {
|
||||
|
||||
@@ -42,9 +42,17 @@ inline TrafficKind rawTrafficKind(const uint8_t* data, size_t length) {
|
||||
return TrafficKind::Other;
|
||||
}
|
||||
|
||||
inline uint8_t trafficPolicy(TrafficKind kind, uint8_t media, uint8_t ssh) {
|
||||
return kind == TrafficKind::Media ? media
|
||||
inline uint8_t trafficPolicy(TrafficKind kind, uint8_t media, uint8_t ssh,
|
||||
const RadioProfiles* profiles = nullptr) {
|
||||
const uint8_t policy = kind == TrafficKind::Media ? media
|
||||
: kind == TrafficKind::Ssh ? ssh : static_cast<uint8_t>(RADIO_TX_AUTO);
|
||||
// Keep media off the main mesh whenever the second MeshCore profile can TX.
|
||||
// An explicit radio2 choice also pins queued packets if radio2 is disabled
|
||||
// before they reach Dispatcher, rather than falling back onto the main mesh.
|
||||
return kind == TrafficKind::Media && policy == RADIO_TX_AUTO && profiles
|
||||
&& profiles->canTransmit(1)
|
||||
&& profiles->params(1).protocol == RadioProfileProtocol::MeshCore
|
||||
? static_cast<uint8_t>(RADIO_TX_SECONDARY) : policy;
|
||||
}
|
||||
|
||||
inline uint8_t trafficTransmitMask(uint8_t policy, const RadioProfiles* profiles) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "FleetCommand.h"
|
||||
#include "RadioProfilePresets.h"
|
||||
#include <SHA256.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
@@ -598,7 +599,8 @@ bool FleetCommand::commandAllowed(const char* command) {
|
||||
: (get || del) && scheduleSelector(arguments, scheduled_secondary);
|
||||
}
|
||||
if (kind == CommandKind::Radio) {
|
||||
return get ? !*arguments : set && radioTuple(arguments, key_length == 10);
|
||||
return get ? !*arguments : set && ((key_length == 6
|
||||
&& cli::radio2PresetTuple(arguments)) || radioTuple(arguments, key_length == 10));
|
||||
}
|
||||
if (kind == CommandKind::RadioStatus) return get && !*arguments;
|
||||
if (kind == CommandKind::Cross) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "CarrierWaveCLI.h"
|
||||
#endif
|
||||
#include "RadioProfileCommandUtils.h"
|
||||
#include "RadioProfilePresets.h"
|
||||
#include "FilePresence.h"
|
||||
#include "PersistentStoreFormat.h"
|
||||
#include "radiolib/RXPowerSaving.h"
|
||||
@@ -1005,6 +1006,10 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
|
||||
return true;
|
||||
}
|
||||
if (verb != Set) { cli::formatError(reply, capacity, "invalid radio2 command"); return true; }
|
||||
if (base) {
|
||||
const char* preset = cli::radio2PresetTuple(args);
|
||||
if (preset) args = preset;
|
||||
}
|
||||
char local[140];
|
||||
if (strlen(args) >= sizeof(local)) { cli::formatError(reply, capacity, "params too long"); return true; }
|
||||
strcpy(local, args);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh { namespace cli {
|
||||
|
||||
// Use the ordinary tuple path for validation, persistence and remote replies.
|
||||
inline const char* radio2PresetTuple(const char* name) {
|
||||
if (!strcmp(name, "mediamesh")) return "911.150,250,7,5,rxtx,auto";
|
||||
if (!strcmp(name, "mediamesh-range")) return "911.150,125,7,5,rxtx,auto";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} }
|
||||
+47
@@ -502,6 +502,52 @@ static void spectrumOverlap() {
|
||||
assert(roundtrips == 1000);
|
||||
}
|
||||
|
||||
static void mediaMeshPresets() {
|
||||
for (bool infrastructure : {false, true}) {
|
||||
for (bool range : {false, true}) {
|
||||
Fixture f(infrastructure);
|
||||
const auto primary = f.radio.p.primary;
|
||||
assert(!f.radio.p.enabled() && f.radio.p.cross == mesh::RadioCrossMode::Off);
|
||||
const char* command = range ? "set radio2 mediamesh-range" : "set radio2 mediamesh";
|
||||
f.cmd(command);
|
||||
assert(!f.radio.p.enabled()); // Same delayed apply as a saved tuple.
|
||||
f.advance(2000);
|
||||
const auto config = f.radio.p.secondary;
|
||||
assert(config.mode == mesh::RadioProfileMode::RxTx);
|
||||
assert(fabsf(config.params.freq - 911.150f) < 0.0001f
|
||||
&& config.params.bw == (range ? 125 : 250));
|
||||
assert(config.params.sf == 7 && config.params.cr == 5 && !config.params.preamble);
|
||||
assert(config.params.protocol == mesh::RadioProfileProtocol::MeshCore);
|
||||
assert(f.radio.p.primary == primary && f.radio.p.cross == mesh::RadioCrossMode::Off);
|
||||
assert(!f.fs.files["/radio_profiles"][14] && !f.fs.files["/radio_profiles"][15]);
|
||||
Radio reboot; mesh::RadioProfileCLI restored;
|
||||
restored.begin(&f.fs, &reboot, &f.clock, infrastructure);
|
||||
assert(reboot.p.secondary.params == config.params);
|
||||
assert(reboot.p.secondary.mode == mesh::RadioProfileMode::RxTx);
|
||||
assert(reboot.p.cross == mesh::RadioCrossMode::Off);
|
||||
// A preset replaces an old preamble override but retains operator crossover.
|
||||
f.cmd("set radio2.cross on");
|
||||
f.cmd("set radio2 912.5,500,8,5,rxtx,80"); f.advance(2000);
|
||||
f.cmd(command); f.advance(2000);
|
||||
assert(!f.radio.p.secondary.params.preamble && f.radio.p.cross == mesh::RadioCrossMode::On);
|
||||
const auto committed = f.fs.files["/radio_profiles"];
|
||||
f.fs.fail_write = true;
|
||||
f.cmd(range ? "set radio2 mediamesh" : "set radio2 mediamesh-range", false);
|
||||
assert(f.radio.p.secondary.params == config.params
|
||||
&& f.fs.files["/radio_profiles"] == committed);
|
||||
}
|
||||
}
|
||||
Fixture invalid;
|
||||
for (const char* command : {"set radio2 mediamesh-extra", "set radio2 mediamesh,32",
|
||||
"set tempradio2 mediamesh", "set radioat2 mediamesh"}) {
|
||||
invalid.cmd(command, false);
|
||||
assert(!invalid.radio.p.enabled() && invalid.fs.files.empty());
|
||||
}
|
||||
invalid.radio.minimum_frequency = 912;
|
||||
invalid.cmd("set radio2 mediamesh", false); // The hardware validator still applies.
|
||||
assert(!invalid.radio.p.enabled() && invalid.fs.files.empty());
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
static_assert(uint8_t(mesh::RadioCrossMode::Auto)==0 && uint8_t(mesh::RadioCrossMode::On)==1
|
||||
&& uint8_t(mesh::RadioCrossMode::Off)==2, "saved crossover encoding must remain compatible");
|
||||
@@ -547,6 +593,7 @@ int main(int argc, char** argv) {
|
||||
else if (!strcmp(argv[1], "storage_isolation")) storageBackoffDoesNotDelayTemporaryWindows();
|
||||
else if (!strcmp(argv[1], "relative_schedule")) relativeSchedules();
|
||||
else if (!strcmp(argv[1], "spectrum_overlap")) spectrumOverlap();
|
||||
else if (!strcmp(argv[1], "mediamesh")) mediaMeshPresets();
|
||||
else return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
+3
-1
@@ -22,6 +22,7 @@ class Inspector : public mesh::Dispatcher {
|
||||
static void secondaryReplies() {
|
||||
unsigned checks = 0;
|
||||
for (const char* command : {"set tempradio2 911.5,500,8,5,rxtx,1",
|
||||
"set radio2 mediamesh", "set radio2 mediamesh-range",
|
||||
"set radio2 912.5,500,8,5,rxtx", "set radio2 off",
|
||||
"set tempradio2 off", "del tempradioat2 all"}) {
|
||||
for (bool success : {false, true}) {
|
||||
@@ -57,7 +58,8 @@ static void secondaryReplies() {
|
||||
if (!success) {
|
||||
assert(before == f.radio.p.generation[1]);
|
||||
assert(saved == f.fs.files["/radio_profiles"]);
|
||||
} else if (strstr(command, "911.5") || strstr(command, "912.5") || !strcmp(command,"set radio2 off")) {
|
||||
} else if (strstr(command, "911.5") || strstr(command, "912.5")
|
||||
|| strstr(command, "mediamesh") || !strcmp(command,"set radio2 off")) {
|
||||
assert(before != f.radio.p.generation[1]);
|
||||
assert(!inspector.isPacketRadioCurrent(&reply));
|
||||
}
|
||||
|
||||
@@ -116,6 +116,27 @@ int main() {
|
||||
assert(trafficTransmitMask(RADIO_TX_AUTO,&profiles)==0);
|
||||
for (uint8_t policy=0;policy<=4;++policy)
|
||||
assert(trafficTransmitMask(policy,nullptr)==((policy==RADIO_TX_AUTO || policy==RADIO_TX_PRIMARY || policy==RADIO_TX_BOTH) ? 1 : 0));
|
||||
|
||||
// AUTO media selects only radio2 independently of crossover, temporary
|
||||
// isolation, and the channel route. Other traffic retains normal AUTO.
|
||||
for (auto cross : {RadioCrossMode::Off,RadioCrossMode::Auto,RadioCrossMode::On})
|
||||
for (bool primary_temporary : {false,true}) for (bool secondary_temporary : {false,true})
|
||||
for (auto mode : {RadioProfileMode::Off,RadioProfileMode::Rx,RadioProfileMode::RxTx})
|
||||
for (auto protocol : {RadioProfileProtocol::MeshCore,RadioProfileProtocol::Meshtext,RadioProfileProtocol::Ethernet}) {
|
||||
profiles=RadioProfiles();
|
||||
profiles.cross=cross; profiles.primary_temporary=primary_temporary;
|
||||
profiles.secondary_temporary=secondary_temporary;
|
||||
profiles.secondary.mode=mode; profiles.secondary.params.protocol=protocol;
|
||||
const bool media_secondary=mode==RadioProfileMode::RxTx && protocol==RadioProfileProtocol::MeshCore;
|
||||
for (uint8_t media=0;media<=4;++media) for (uint8_t ssh_policy=0;ssh_policy<=4;++ssh_policy) {
|
||||
const auto effective=trafficPolicy(TrafficKind::Media,media,ssh_policy,&profiles);
|
||||
assert(effective==(media==RADIO_TX_AUTO && media_secondary ? uint8_t(RADIO_TX_SECONDARY) : media));
|
||||
if (media==RADIO_TX_AUTO && media_secondary) assert(trafficTransmitMask(effective,&profiles)==2);
|
||||
assert(trafficPolicy(TrafficKind::Ssh,media,ssh_policy,&profiles)==ssh_policy);
|
||||
assert(trafficPolicy(TrafficKind::Other,media,ssh_policy,&profiles)==RADIO_TX_AUTO);
|
||||
assert(trafficPolicy(TrafficKind::Media,media,ssh_policy,nullptr)==media);
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
@@ -185,7 +206,7 @@ int main() {
|
||||
for (const char* name : {"media","ssh"}) {
|
||||
char text[64];
|
||||
snprintf(text,sizeof(text),"get tx.%s",name); cmd(text);
|
||||
assert(strstr(reply,"auto") && strstr(reply,"active=channel/global"));
|
||||
assert(strstr(reply,"auto") && strstr(reply,!strcmp(name,"media") ? "active=radio2" : "active=channel/global"));
|
||||
for (uint8_t policy=0;policy<=4;++policy) {
|
||||
snprintf(text,sizeof(text),"set tx.%s %s",name,radioTxPolicyName(policy)); cmd(text);
|
||||
assert(!strncmp(reply,"OK",2));
|
||||
@@ -228,13 +249,13 @@ int main() {
|
||||
for (uint8_t policy=0;policy<=4;++policy) {
|
||||
node._prefs.tx_media=policy; node._prefs.tx_ssh=RADIO_TX_BOTH;
|
||||
bool result=node.sendClientGroupData(channel,path,2,0x5643,media,sizeof(media));
|
||||
const uint8_t expected=policy==RADIO_TX_AUTO ? original.tx_radio : policy;
|
||||
const uint8_t expected=policy==RADIO_TX_AUTO ? uint8_t(RADIO_TX_SECONDARY) : policy;
|
||||
assert(result==(expected!=RADIO_TX_OFF));
|
||||
if (result) { assert(node.last_policy==expected && node.last_type==0x5643 && node.last_path_len==2); }
|
||||
assert(!memcmp(&channel,&original,sizeof(channel)));
|
||||
result=node.sendClientRawData(media,sizeof(media),path,2);
|
||||
assert(result==(policy!=RADIO_TX_OFF));
|
||||
if (result) assert(node.last_policy==policy && node.last_data==std::vector<uint8_t>(media,media+sizeof(media)));
|
||||
if (result) assert(node.last_policy==expected && node.last_data==std::vector<uint8_t>(media,media+sizeof(media)));
|
||||
}
|
||||
node._prefs.tx_media=RADIO_TX_OFF;
|
||||
for (uint8_t policy=0;policy<=4;++policy) {
|
||||
@@ -253,6 +274,40 @@ int main() {
|
||||
assert(node.last_policy==original.tx_radio && node.last_type==0x5654);
|
||||
assert(node.sendClientRawData(unknown,sizeof(unknown),path,2));
|
||||
assert(node.last_policy==RADIO_TX_AUTO);
|
||||
|
||||
// Existing apps need no new command or saved preference: AUTO media wins
|
||||
// over channel/global routing while radio2 supports MeshCore transmission.
|
||||
node._prefs.tx_media=RADIO_TX_AUTO;
|
||||
for (auto cross : {RadioCrossMode::Off,RadioCrossMode::Auto,RadioCrossMode::On})
|
||||
for (uint8_t channel_policy=0;channel_policy<=4;++channel_policy) {
|
||||
node.radio.config.cross=cross;
|
||||
channel.tx_radio=channel_policy;
|
||||
const auto before=channel;
|
||||
assert(node.sendClientGroupData(channel,path,2,0x5643,media,sizeof(media)));
|
||||
assert(node.last_policy==RADIO_TX_SECONDARY);
|
||||
assert(!memcmp(&channel,&before,sizeof(channel)));
|
||||
assert(node.sendClientRawData(media,sizeof(media),path,2));
|
||||
assert(node.last_policy==RADIO_TX_SECONDARY);
|
||||
}
|
||||
assert(node._prefs.tx_media==RADIO_TX_AUTO);
|
||||
channel=original;
|
||||
// Unavailable/receive-only/foreign profiles preserve the existing AUTO
|
||||
// fallback. Group media follows its channel; raw media follows global AUTO.
|
||||
for (auto mode : {RadioProfileMode::Off,RadioProfileMode::Rx,RadioProfileMode::RxTx})
|
||||
for (auto protocol : {RadioProfileProtocol::MeshCore,RadioProfileProtocol::Meshtext,RadioProfileProtocol::Ethernet}) {
|
||||
if (mode==RadioProfileMode::RxTx && protocol==RadioProfileProtocol::MeshCore) continue;
|
||||
node.radio.config.secondary.mode=mode;
|
||||
node.radio.config.secondary.params.protocol=protocol;
|
||||
channel.tx_radio=RADIO_TX_PRIMARY;
|
||||
cmd("get tx.media"); assert(strstr(reply,"auto") && strstr(reply,"active=channel/global"));
|
||||
assert(node.sendClientGroupData(channel,path,2,0x5643,media,sizeof(media)));
|
||||
assert(node.last_policy==RADIO_TX_PRIMARY);
|
||||
assert(node.sendClientRawData(media,sizeof(media),path,2));
|
||||
assert(node.last_policy==RADIO_TX_AUTO);
|
||||
}
|
||||
node.radio.config.secondary.mode=RadioProfileMode::RxTx;
|
||||
node.radio.config.secondary.params.protocol=RadioProfileProtocol::MeshCore;
|
||||
channel=original;
|
||||
node._prefs.tx_media=RADIO_TX_SECONDARY;
|
||||
node.radio.config.secondary.mode=RadioProfileMode::Off;
|
||||
assert(!node.sendClientRawData(media,sizeof(media),path,2));
|
||||
@@ -355,6 +410,100 @@ int main() {
|
||||
'''
|
||||
|
||||
|
||||
RAW_PACKET_HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
||||
#include <Packet.h>
|
||||
#pragma GCC diagnostic pop
|
||||
#include <helpers/CompanionTrafficRouting.h>
|
||||
@CONSTANTS@
|
||||
namespace mesh {
|
||||
@PACKET_CONSTRUCTOR@
|
||||
}
|
||||
struct TestRadio {
|
||||
mesh::RadioProfiles config;
|
||||
mesh::RadioProfiles* profiles() { return &config; }
|
||||
uint8_t receiveProfile() const { return 0; }
|
||||
uint32_t receiveProfileGeneration() const { return 1; }
|
||||
};
|
||||
class MyMesh {
|
||||
public:
|
||||
TestRadio radio;
|
||||
TestRadio* _radio=&radio;
|
||||
struct { uint8_t tx_media=mesh::RADIO_TX_AUTO,tx_ssh=mesh::RADIO_TX_AUTO; } _prefs;
|
||||
mesh::Packet packet;
|
||||
unsigned ok=0,errors=0,sends=0,releases=0;
|
||||
uint8_t error=0,last_policy=255;
|
||||
bool allocate=true;
|
||||
mesh::Packet* obtainNewPacket() { return allocate ? &packet : nullptr; }
|
||||
void releasePacket(mesh::Packet* value) { assert(value==&packet); ++releases; }
|
||||
void writeOKFrame() { ++ok; }
|
||||
void writeErrFrame(uint8_t value) { ++errors; error=value; }
|
||||
bool tryParsePacket(mesh::Packet*,const uint8_t*,int);
|
||||
bool sendPacket(mesh::Packet* value,uint8_t priority,uint32_t delay) {
|
||||
assert(value==&packet && priority==17 && !delay);
|
||||
++sends; last_policy=value->tx_radio; return true;
|
||||
}
|
||||
void onFrame(uint8_t* cmd_frame,size_t len) {
|
||||
@RAW_CLAUSE@
|
||||
else writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
|
||||
}
|
||||
};
|
||||
using namespace mesh;
|
||||
@PARSE_PACKET@
|
||||
|
||||
int main() {
|
||||
using namespace mesh;
|
||||
using namespace mesh::companion;
|
||||
const std::vector<uint8_t> payloads[] = {
|
||||
{0x56,0,1,2,3,0,99}, {0x57,0,1,2,3,0,99,42},
|
||||
{0x72,0,0,0,0,0,0,0,0,0,0,0,0}, {'S','S','H',1,42},
|
||||
{0x56}, {0x12,0x34,0x56}, {}
|
||||
};
|
||||
for (uint8_t type : {PAYLOAD_TYPE_RAW_CUSTOM,PAYLOAD_TYPE_GRP_DATA,PAYLOAD_TYPE_ACK})
|
||||
for (auto mode : {RadioProfileMode::Off,RadioProfileMode::Rx,RadioProfileMode::RxTx})
|
||||
for (auto protocol : {RadioProfileProtocol::MeshCore,RadioProfileProtocol::Meshtext,RadioProfileProtocol::Ethernet})
|
||||
for (auto cross : {RadioCrossMode::Off,RadioCrossMode::Auto,RadioCrossMode::On})
|
||||
for (uint8_t policy=RADIO_TX_AUTO;policy<=RADIO_TX_OFF;++policy)
|
||||
for (const auto& data : payloads) {
|
||||
MyMesh node;
|
||||
node.radio.config.secondary.mode=mode;
|
||||
node.radio.config.secondary.params.protocol=protocol;
|
||||
node.radio.config.cross=cross;
|
||||
node._prefs.tx_media=node._prefs.tx_ssh=policy;
|
||||
std::vector<uint8_t> frame={CMD_SEND_RAW_PACKET,17,
|
||||
static_cast<uint8_t>((type << PH_TYPE_SHIFT)|ROUTE_TYPE_DIRECT),0};
|
||||
frame.insert(frame.end(),data.begin(),data.end());
|
||||
const auto original=frame;
|
||||
node.onFrame(frame.data(),frame.size());
|
||||
const auto kind=type==PAYLOAD_TYPE_RAW_CUSTOM ? rawTrafficKind(data.data(),data.size()) : TrafficKind::Other;
|
||||
const auto effective=trafficPolicy(kind,policy,policy,&node.radio.config);
|
||||
const bool blocked=effective!=RADIO_TX_AUTO && !trafficTransmitMask(effective,&node.radio.config);
|
||||
assert(frame==original);
|
||||
if (blocked) {
|
||||
assert(node.errors==1 && node.error==ERR_CODE_BAD_STATE && !node.ok && !node.sends && node.releases==1);
|
||||
} else {
|
||||
assert(node.ok==1 && !node.errors && node.sends==1 && !node.releases);
|
||||
assert(node.last_policy==effective);
|
||||
assert(node.packet.payload_len==data.size());
|
||||
if (!data.empty()) assert(!memcmp(node.packet.payload,data.data(),data.size()));
|
||||
}
|
||||
}
|
||||
// Parsing and allocation errors still release exactly once or not at all.
|
||||
uint8_t invalid[]={CMD_SEND_RAW_PACKET,17,0xff,0};
|
||||
MyMesh bad; bad.onFrame(invalid,sizeof(invalid));
|
||||
assert(bad.error==ERR_CODE_ILLEGAL_ARG && bad.releases==1 && !bad.sends && !bad.ok);
|
||||
MyMesh full; full.allocate=false; full.onFrame(invalid,sizeof(invalid));
|
||||
assert(full.error==ERR_CODE_TABLE_FULL && !full.releases && !full.sends && !full.ok);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class CompanionTrafficRoutingTest(unittest.TestCase):
|
||||
def compile_and_run(self, source):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -414,6 +563,24 @@ class CompanionTrafficRoutingTest(unittest.TestCase):
|
||||
.replace('@PACKET_METHODS@', packet_methods)
|
||||
.replace('@RAW_CLAUSE@', clause))
|
||||
|
||||
def test_fully_encoded_raw_media_packets_use_routing(self):
|
||||
source = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text()
|
||||
packet = (ROOT / 'src/Packet.cpp').read_text()
|
||||
dispatcher = (ROOT / 'src/Dispatcher.cpp').read_text()
|
||||
clause = method(source, '} else if (cmd_frame[0] == CMD_SEND_RAW_PACKET')
|
||||
clause = clause.removeprefix('} else ')
|
||||
names = ('CMD_SEND_RAW_PACKET', 'ERR_CODE_UNSUPPORTED_CMD',
|
||||
'ERR_CODE_ILLEGAL_ARG', 'ERR_CODE_BAD_STATE', 'ERR_CODE_TABLE_FULL')
|
||||
constants = '\n'.join(re.search(
|
||||
r'^#define\s+' + name + r'\s+\d+.*$', source, re.MULTILINE).group(0)
|
||||
for name in names)
|
||||
parser = method(dispatcher, 'bool Dispatcher::tryParsePacket(')
|
||||
parser = parser.replace('Dispatcher::tryParsePacket(', 'MyMesh::tryParsePacket(')
|
||||
self.compile_and_run(RAW_PACKET_HARNESS.replace('@CONSTANTS@', constants)
|
||||
.replace('@PACKET_CONSTRUCTOR@', method(packet, 'Packet::Packet('))
|
||||
.replace('@PARSE_PACKET@', parser)
|
||||
.replace('@RAW_CLAUSE@', clause))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -109,6 +109,7 @@ TEST_F(FleetCommandTest, ExplicitRadioAndFilterFamiliesAreAllowed) {
|
||||
"get radio2", "get tempradio2", "get radio2.status", "get radio2.timing",
|
||||
"get radio2.cross", "set radio2.cross auto", "set radio2.cross on",
|
||||
"set radio2.cross off", "set radio2 off", "set tempradio2 off",
|
||||
"set radio2 mediamesh", "set radio2 mediamesh-range",
|
||||
"set radio2 910.5,500,5,5,rxtx",
|
||||
"set tempradio2 910.5,500,5,5,rx,10,auto",
|
||||
"set radio2 910.5,500,5,5,rx,32",
|
||||
@@ -256,6 +257,8 @@ TEST_F(FleetCommandTest, NoGenericAdminNamespaceOrPrefixEscapes) {
|
||||
"get flood.rule.01", "get flood.rule.256", "get flood.rule.9999999999",
|
||||
"xy|set radio2 off", "GET radio2", "get radio2", " get radio2",
|
||||
"get radio2 ", "get radio2 off", "del radio2", "set radio2",
|
||||
"set radio2 mediamesh-extra", "set radio2 mediamesh,32",
|
||||
"set tempradio2 mediamesh", "get radio2 mediamesh",
|
||||
"set radio2.cross true", "del flood.channel.data", "set flood.filter.2",
|
||||
"del flood.rule.2 extra", "get flood.rule.1 all", "set flood.max",
|
||||
"set radio2 910.5,500,5,5,rxtx,auto,99",
|
||||
|
||||
@@ -448,6 +448,17 @@ class FleetCommandCryptoTests(unittest.TestCase):
|
||||
with self.subTest(command=command):
|
||||
self.assertEqual(self.decode(envelope(command)), "reject")
|
||||
|
||||
def test_mediamesh_presets_have_cross_language_real_signatures(self):
|
||||
for preset in ["mediamesh", "mediamesh-range"]:
|
||||
command = "set radio2 " + preset
|
||||
expected = envelope(command)
|
||||
self.assertEqual(self.run_tool("encode", CHANNEL.hex(), "all", NOW, NOW + 120, command),
|
||||
expected.hex())
|
||||
self.assertEqual(self.decode(expected), f"{NOW}\n{NOW + 120}\n{command}")
|
||||
for command in ["set radio2 mediamesh-extra", "set radio2 mediamesh,32",
|
||||
"set tempradio2 mediamesh", "get radio2 mediamesh"]:
|
||||
self.assertEqual(self.decode(envelope(command)), "reject")
|
||||
|
||||
def test_scheduled_radio_commands_have_cross_language_real_signatures(self):
|
||||
for command in [
|
||||
"set radioat 910.5,500,5,5,+1,auto",
|
||||
|
||||
@@ -27,7 +27,7 @@ class RadioProfileCLITest(unittest.TestCase):
|
||||
'-o', str(exe)], check=True, capture_output=True, text=True)
|
||||
for scenario in (None, 'permanent_order', 'temporary_boundary', 'lease_creation',
|
||||
'save_failure', 'schedule_permutations', 'storage_isolation',
|
||||
'relative_schedule', 'spectrum_overlap'):
|
||||
'relative_schedule', 'spectrum_overlap', 'mediamesh'):
|
||||
with self.subTest(fleet=fleet_enabled, carrier=carrier_enabled,
|
||||
scenario=scenario or 'commands_persistence_and_timers'):
|
||||
subprocess.run([str(exe)] + ([scenario] if scenario else []), check=True)
|
||||
|
||||
Reference in new issue
Block a user