mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 22:38:19 +00:00
cad, meshamerica flasher and other polishes
This commit is contained in:
+58
-5
@@ -36,10 +36,60 @@ import sys
|
||||
# Provider identity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MAKER_KEY = "zephcore"
|
||||
MAKER_NAME = "ZephCore"
|
||||
DEVICE_CLASS = "zephcore"
|
||||
|
||||
# `maker` is the DEVICE MANUFACTURER (not the firmware provider — the ZephCore
|
||||
# provider badge comes from Mesh America's registration, independent of this).
|
||||
# Folded devices use MeshCore's exact maker key so they group under the same
|
||||
# brand; new tiles use the hardware maker. Display names for the maker map:
|
||||
MAKERS = {
|
||||
"rak": "RAK Wireless",
|
||||
"seeed": "Seeed Studio",
|
||||
"elecrow": "Elecrow",
|
||||
"lilygo": "LilyGo",
|
||||
"heltec": "Heltec",
|
||||
"promicro": "ProMicro",
|
||||
"gat-iot": "GAT-IoT",
|
||||
"uniteng": "UnitEng",
|
||||
"Ikoka": "Ikoka",
|
||||
"femtofox": "Femtofox",
|
||||
}
|
||||
|
||||
# Manufacturer per device name.
|
||||
MAKER_BY_DEVICE = {
|
||||
"RAK WisBlock / WisMesh (RAK 4631)": "rak",
|
||||
"RAK WisMesh 1W Booster (3401 + 13302)": "rak",
|
||||
"RAK WisMesh Tag": "rak",
|
||||
"Seeed Studio Wio Tracker L1 Pro": "seeed",
|
||||
"Seeed Studio SenseCAP T1000-E": "seeed",
|
||||
"Seeed Studio SenseCAP Solar": "seeed",
|
||||
"Seeed Studio Xiao nRF52 WIO": "seeed",
|
||||
"Seeed Studio Xiao C3": "seeed",
|
||||
"Seeed Studio Xiao S3 WIO": "seeed",
|
||||
"Seeed Studio Xiao ESP32-C6": "seeed",
|
||||
"Elecrow ThinkNode M1": "elecrow",
|
||||
"Elecrow ThinkNode M3": "elecrow",
|
||||
"Elecrow ThinkNode M6": "elecrow",
|
||||
"Ikoka Nano": "Ikoka",
|
||||
"LilyGo T-Echo": "lilygo",
|
||||
"LilyGo T-Beam (SX1262)": "lilygo",
|
||||
"LilyGo T-Impulse Plus": "lilygo",
|
||||
"LilyGo T-Lora C6": "lilygo",
|
||||
"ProMicro nrf52 (faketec)": "promicro",
|
||||
"Heltec T114": "heltec",
|
||||
"Heltec T096": "heltec",
|
||||
"Heltec v3": "heltec",
|
||||
"Heltec v4": "heltec",
|
||||
"Heltec v4.3": "heltec",
|
||||
"Heltec Wireless Tracker": "heltec",
|
||||
"Heltec Wireless Tracker v2": "heltec",
|
||||
"GAT-IoT GAT562 30s": "gat-iot",
|
||||
"UnitEng Station G2": "uniteng",
|
||||
"Femtofox (Luckfox Pico Mini)": "femtofox",
|
||||
"RAK6421 WisMesh (Raspberry Pi)": "rak",
|
||||
"RAK6421 WisMesh (Raspberry Pi 5)": "rak",
|
||||
}
|
||||
|
||||
DESCRIPTION = (
|
||||
"ZephCore is an independent, ground-up implementation of the MeshCore "
|
||||
"protocol on the Zephyr RTOS. It is wire-compatible with stock MeshCore "
|
||||
@@ -99,7 +149,7 @@ BOARDS = [
|
||||
|
||||
# --- nRF52: new ZephCore-only hardware (own tile) ---------------------
|
||||
dict(stem="lilygo_timpulse_plus", kind="nrf", device="LilyGo T-Impulse Plus", new=True, img="lora.svg"),
|
||||
dict(stem="heltec_t096", kind="nrf", device="Heltec T96", new=True, img="lora.svg"),
|
||||
dict(stem="heltec_t096", kind="nrf", device="Heltec T096", new=True, img="lora.svg"),
|
||||
|
||||
# --- ESP32 (sysbuild/MCUboot, -merged.bin): fold ---------------------
|
||||
dict(stem="xiao_esp32c3", kind="esp32", device="Seeed Studio Xiao C3"),
|
||||
@@ -182,6 +232,7 @@ def build(assets, url_base, version):
|
||||
devices = {} # name -> device object (first-seen order preserved)
|
||||
order = []
|
||||
used_roles = set()
|
||||
used_makers = set()
|
||||
stats = {"fold": 0, "new": 0, "skipped": []}
|
||||
|
||||
def spec_files(triples):
|
||||
@@ -208,8 +259,10 @@ def build(assets, url_base, version):
|
||||
continue
|
||||
|
||||
if name not in devices:
|
||||
maker = MAKER_BY_DEVICE[name] # KeyError if a device is unmapped
|
||||
used_makers.add(maker)
|
||||
dev = {
|
||||
"maker": MAKER_KEY,
|
||||
"maker": maker,
|
||||
"class": DEVICE_CLASS,
|
||||
"name": name,
|
||||
"type": DEVICE_TYPE[board["kind"]],
|
||||
@@ -230,7 +283,7 @@ def build(assets, url_base, version):
|
||||
|
||||
catalog = {
|
||||
"description": DESCRIPTION,
|
||||
"maker": {MAKER_KEY: {"name": MAKER_NAME}},
|
||||
"maker": {k: {"name": MAKERS[k]} for k in sorted(used_makers)},
|
||||
"device": [devices[n] for n in order],
|
||||
}
|
||||
role_map = {k: v for k, v in CUSTOM_ROLES.items() if k in used_roles}
|
||||
|
||||
+28
-12
@@ -66,19 +66,35 @@ reset completely whenever the radio parameters change (frequency, SF, BW
|
||||
`cad.auto` ships **on**, on repeaters and companions alike. A one-sided
|
||||
staircase controller acts on the probe stats:
|
||||
|
||||
- Probes concentrate on the **frontier** — one level more sensitive than
|
||||
the current operating point (3 of every 4 probes), with the remainder
|
||||
self-checking the operating level.
|
||||
- **Step down** (more sensitive) when the frontier has ≥300 samples and
|
||||
its false-positive rate is ≤1%.
|
||||
- **Step up** (less sensitive) quickly when the operating level itself
|
||||
shows a false-positive rate above 2% over ≥50 samples — false positives
|
||||
at the operating point cost real transmissions.
|
||||
- The offset is persisted to flash whenever it steps.
|
||||
The controller is **knee-seeking**. The false-positive-vs-detPeak curve
|
||||
falls as detPeak rises (less sensitive → fewer false detects) and flattens
|
||||
past a knee; the sweet spot is that knee — the most sensitive detPeak whose
|
||||
FP has already bottomed out. Probes sample the operating level and **both
|
||||
neighbours** (op−1 more sensitive, op+1 less sensitive; op weighted half, each
|
||||
neighbour a quarter) so the staircase can read the local curve *slope*:
|
||||
|
||||
At the default 15-second probe interval a step decision lands roughly
|
||||
every **1–2 hours**, so the node tracks a changing RF environment within
|
||||
that window without thrashing.
|
||||
- **Step up** (less sensitive) when the level above is markedly cleaner —
|
||||
FP drops ≥5%/level (`CAD_KNEE_SLOPE_PERMILLE`). That means we're on the
|
||||
steep part below the knee; climb toward it.
|
||||
- **Step down** (more sensitive) only on a flat plateau (frontier no worse
|
||||
than operating, slope <5%/level) that is **already clean** (FP ≤5%,
|
||||
`CAD_PLATEAU_CLEAN_PERMILLE`) — reclaim sensitivity that costs nothing.
|
||||
- Otherwise **hold** — either at the knee (steep below, flat above) or on a
|
||||
noisy flat plateau.
|
||||
|
||||
Using slopes rather than an absolute FP target makes convergence independent
|
||||
of a site's FP *floor* (which varies with traffic and with the classifier's
|
||||
residual false-positive rate). The clean-plateau guard is what stops a
|
||||
flat-but-noisy curve from walking to the sensitive rail: there, holding is the
|
||||
least-bad move, while a genuinely quiet flat-low site correctly descends to
|
||||
the floor. Each involved rung needs ≥120 samples (`CAD_STEP_MIN_PROBES`)
|
||||
before a step, so a decision lands roughly every 1–2 hours at the default
|
||||
interval.
|
||||
|
||||
A step decision requires the operating rung (and, for the direction chosen,
|
||||
its neighbour) to be warm; `get cad`'s three-rung window shows exactly those
|
||||
levels, so the slope the controller is acting on is visible directly. The
|
||||
offset is persisted to flash whenever it steps.
|
||||
|
||||
The offset is clamped to **−8…+12** levels around the family base — wide
|
||||
enough that a dense hilltop can settle much less sensitive and a quiet
|
||||
|
||||
@@ -990,48 +990,77 @@ int8_t LoRaRadioBase::pickCadProbeLevel()
|
||||
return (int8_t)(CAD_SWEEP_MIN + (_cad_probe_rr % span));
|
||||
}
|
||||
|
||||
/* Auto: concentrate samples on the frontier (one step more sensitive
|
||||
* than the operating point); every 4th probe self-checks the
|
||||
* operating level. */
|
||||
int8_t frontier = _cad_offset > CAD_LEVEL_MIN ? (int8_t)(_cad_offset - 1)
|
||||
: _cad_offset;
|
||||
|
||||
return ((_cad_probe_rr & 3) == 0) ? _cad_offset : frontier;
|
||||
/* Auto: sample the operating level AND both neighbours so the staircase
|
||||
* can read the local FP curvature (slope below vs. above) and seek the
|
||||
* knee. op is the shared term of both slopes → weight it half; each
|
||||
* neighbour a quarter. Out-of-range neighbours fall back to op. */
|
||||
int8_t lvl;
|
||||
switch (_cad_probe_rr & 3) {
|
||||
case 1: lvl = (int8_t)(_cad_offset - 1); break; /* more sensitive */
|
||||
case 3: lvl = (int8_t)(_cad_offset + 1); break; /* less sensitive */
|
||||
default: lvl = _cad_offset; break; /* operating (0, 2) */
|
||||
}
|
||||
if (lvl < CAD_LEVEL_MIN || lvl > CAD_LEVEL_MAX) {
|
||||
lvl = _cad_offset;
|
||||
}
|
||||
return lvl;
|
||||
}
|
||||
|
||||
void LoRaRadioBase::cadStaircaseStep()
|
||||
{
|
||||
/* Step down (more sensitive) when the frontier level has enough
|
||||
* samples and its suspected-FP rate is at or under target. */
|
||||
if (_cad_offset > CAD_LEVEL_MIN) {
|
||||
CadLevelStats &f = _cad_stats[(_cad_offset - 1) - CAD_LEVEL_MIN];
|
||||
/* Knee-seeking controller — see the CAD_KNEE_SLOPE / CAD_PLATEAU_CLEAN
|
||||
* notes in radio_common.h. Reads local curvature from three rungs and
|
||||
* steps toward the knee (the most sensitive detPeak whose FP has already
|
||||
* bottomed out), using slopes so the decision is site-floor-independent. */
|
||||
int oi = _cad_offset - CAD_LEVEL_MIN;
|
||||
|
||||
if (f.probes >= CAD_STEP_DOWN_MIN_PROBES &&
|
||||
(uint32_t)f.fp * 1000U <=
|
||||
(uint32_t)f.probes * CAD_FP_TARGET_PERMILLE) {
|
||||
_cad_offset--;
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
LOG_INF("cad: step down -> offset %d (frontier %up/%ufp)",
|
||||
(int)_cad_offset, f.probes, f.fp);
|
||||
return;
|
||||
/* Per-level FP rate in permille, or -1 when too few samples to trust. */
|
||||
auto rate = [&](int idx) -> int {
|
||||
if (idx < 0 || idx >= CAD_NUM_LEVELS) {
|
||||
return -1;
|
||||
}
|
||||
CadLevelStats &s = _cad_stats[idx];
|
||||
if (s.probes < CAD_STEP_MIN_PROBES) {
|
||||
return -1;
|
||||
}
|
||||
return (int)(((uint32_t)s.fp * 1000U) / s.probes);
|
||||
};
|
||||
|
||||
int r_op = rate(oi);
|
||||
if (r_op < 0) {
|
||||
return; /* operating level not warm yet — no basis to step */
|
||||
}
|
||||
int r_up = rate(oi + 1); /* one step less sensitive */
|
||||
int r_dn = rate(oi - 1); /* frontier, one step more sensitive */
|
||||
|
||||
/* Step UP (less sensitive) when the level above is markedly cleaner —
|
||||
* we're on the steep part of the curve, below the knee. */
|
||||
if (_cad_offset < CAD_LEVEL_MAX && r_up >= 0 &&
|
||||
r_op - r_up >= CAD_KNEE_SLOPE_PERMILLE) {
|
||||
_cad_offset++;
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
LOG_INF("cad: step up -> offset %d (op %d dn->up %d)",
|
||||
(int)_cad_offset, r_op, r_up);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Step up (less sensitive) when the operating level itself shows
|
||||
* FPs well above target. Lower sample bar: FPs at the operating
|
||||
* point cost real TX opportunities, react quickly. */
|
||||
if (_cad_offset < CAD_LEVEL_MAX) {
|
||||
CadLevelStats &o = _cad_stats[_cad_offset - CAD_LEVEL_MIN];
|
||||
|
||||
if (o.probes >= CAD_STEP_UP_MIN_PROBES &&
|
||||
(uint32_t)o.fp * 1000U >
|
||||
(uint32_t)o.probes * 2U * CAD_FP_TARGET_PERMILLE) {
|
||||
_cad_offset++;
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
LOG_INF("cad: step up -> offset %d (operating %up/%ufp)",
|
||||
(int)_cad_offset, o.probes, o.fp);
|
||||
}
|
||||
/* Step DOWN (more sensitive) only on a flat plateau that is already
|
||||
* clean: the frontier is no worse than operating (nothing to lose) AND
|
||||
* FP here is low enough that reclaiming sensitivity is cheap. The clean
|
||||
* guard is what keeps a flat-but-noisy curve from descending to the
|
||||
* sensitive rail — there, holding position is the least-bad move. */
|
||||
if (_cad_offset > CAD_LEVEL_MIN && r_dn >= 0 &&
|
||||
r_dn - r_op < CAD_KNEE_SLOPE_PERMILLE &&
|
||||
r_op <= CAD_PLATEAU_CLEAN_PERMILLE) {
|
||||
_cad_offset--;
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
LOG_INF("cad: step down -> offset %d (op %d dn %d)",
|
||||
(int)_cad_offset, r_op, r_dn);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Otherwise: at the knee (steep below, flat above) or a noisy flat
|
||||
* plateau — hold. */
|
||||
}
|
||||
|
||||
void LoRaRadioBase::cadMaintenance()
|
||||
@@ -1107,22 +1136,48 @@ void LoRaRadioBase::cadMaintenance()
|
||||
if (ret > 0) {
|
||||
s.busy++;
|
||||
|
||||
/* Ground-truth post-check: a real LoRa signal that tripped
|
||||
* CAD keeps transmitting — after RX restart its preamble or
|
||||
* header trips the receive path within a few symbols. Wait
|
||||
* ~8 symbols, then classify. RX is already armed, so the
|
||||
* packet itself is not at risk during this sleep. */
|
||||
/* Ground-truth post-check: was the CAD hit a REAL signal or a
|
||||
* correlator false positive? A real transmitter that tripped
|
||||
* CAD keeps radiating, so over the next preamble+header window
|
||||
* one of two things shows up:
|
||||
* (a) the restarted RX syncs on it -> isReceiving(), or
|
||||
* (b) instantaneous RSSI climbs above the noise floor.
|
||||
* (b) is the important addition: the probe tears RX down to run
|
||||
* CAD, and the STANDBY->RX restart routinely eats the preamble of
|
||||
* a real packet, so RX never re-syncs — the old isReceiving()-only
|
||||
* snapshot booked those strong-but-missed packets as false
|
||||
* positives, a ~detPeak-independent floor that flattened the FP
|
||||
* curve and drove the staircase to the ceiling. Channel energy
|
||||
* doesn't depend on winning the preamble race, so it recovers
|
||||
* them. Neither signal over the whole window => genuine FP. A
|
||||
* below-floor packet we can neither sync nor see stays ambiguous
|
||||
* and counts as FP — bias toward higher detPeak (the safe side).
|
||||
* The prefilter above guaranteed RSSI <= floor+guard pre-probe,
|
||||
* so a rise past that threshold now is a newly-arrived signal. */
|
||||
uint8_t sf = getActiveSpreadingFactor();
|
||||
uint16_t bw_x10 = getActiveBandwidthKHzX10();
|
||||
uint32_t tsym_us = bw_x10 ? (uint32_t)(((1UL << sf) * 10000UL)
|
||||
/ bw_x10) : 1024;
|
||||
uint32_t wait_ms = (8U * tsym_us) / 1000U;
|
||||
uint32_t step_ms = (3U * tsym_us) / 1000U; /* ~3 symbols/sample */
|
||||
|
||||
if (wait_ms < 20) wait_ms = 20;
|
||||
if (wait_ms > 400) wait_ms = 400;
|
||||
k_sleep(K_MSEC(wait_ms));
|
||||
if (step_ms < 5) step_ms = 5;
|
||||
if (step_ms > 100) step_ms = 100;
|
||||
|
||||
if (isReceiving()) {
|
||||
bool floor_valid = (_noise_floor != DEFAULT_NOISE_FLOOR);
|
||||
int16_t rssi_thresh = _noise_floor + CAD_PROBE_RSSI_GUARD;
|
||||
|
||||
bool real = false;
|
||||
for (int k = 0; k < 4 && !real; k++) { /* ~12 symbols total */
|
||||
k_sleep(K_MSEC(step_ms));
|
||||
if (isReceiving()) {
|
||||
real = true;
|
||||
} else if (floor_valid &&
|
||||
hwGetCurrentRSSI() > rssi_thresh) {
|
||||
real = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (real) {
|
||||
s.tp++;
|
||||
} else {
|
||||
s.fp++;
|
||||
@@ -1147,27 +1202,37 @@ int LoRaRadioBase::formatCadStatus(char *buf, int cap)
|
||||
* Header: a:on o:1 pk:22(b21/4s) iv:15s
|
||||
* a auto on/off o offset pk operating peak
|
||||
* b family base 4s symbols iv probe interval
|
||||
* Level: -3(18) 22p 18b 16f 2t 72%
|
||||
* level(peak) probes busy fp tp fp-rate%% (integer). */
|
||||
* Level: *+1(22) 22p 18b 16f 2t 72%
|
||||
* '*' = operating rung level(peak) probes busy fp tp fp-rate%%. */
|
||||
n += snprintf(buf + n, cap > n ? cap - n : 0,
|
||||
"a:%s o:%d pk:%d(b%u/4s) iv:%us",
|
||||
_cad_auto ? "on" : "off", (int)_cad_offset,
|
||||
(int)base + _cad_offset, base,
|
||||
(unsigned)_cad_probe_interval_s);
|
||||
|
||||
for (int i = 0; i < CAD_NUM_LEVELS; i++) {
|
||||
CadLevelStats &s = _cad_stats[i];
|
||||
/* Only the 3 rungs around the operating offset — the far rungs are mildly
|
||||
* irrelevant; what matters is where we sit on the ladder. The window is
|
||||
* clamped to stay inside [CAD_LEVEL_MIN, CAD_LEVEL_MAX] while still showing
|
||||
* 3 rungs, so at either end it slides inward rather than dropping a line. */
|
||||
int cur = _cad_offset;
|
||||
if (cur < CAD_LEVEL_MIN) cur = CAD_LEVEL_MIN;
|
||||
if (cur > CAD_LEVEL_MAX) cur = CAD_LEVEL_MAX;
|
||||
int lo = cur - 1, hi = cur + 1;
|
||||
if (lo < CAD_LEVEL_MIN) { lo = CAD_LEVEL_MIN; hi = lo + 2; }
|
||||
if (hi > CAD_LEVEL_MAX) { hi = CAD_LEVEL_MAX; lo = hi - 2; }
|
||||
|
||||
if (s.probes == 0) {
|
||||
continue;
|
||||
}
|
||||
/* Integer FP rate, rounded to nearest percent. */
|
||||
unsigned fp_pct = (unsigned)(((uint32_t)s.fp * 100U + s.probes / 2)
|
||||
/ s.probes);
|
||||
for (int lvl = lo; lvl <= hi; lvl++) {
|
||||
CadLevelStats &s = _cad_stats[lvl - CAD_LEVEL_MIN];
|
||||
|
||||
/* Integer FP rate, rounded to nearest percent (0 when unprobed). */
|
||||
unsigned fp_pct = s.probes
|
||||
? (unsigned)(((uint32_t)s.fp * 100U + s.probes / 2) / s.probes)
|
||||
: 0;
|
||||
|
||||
n += snprintf(buf + n, cap > n ? cap - n : 0,
|
||||
"\n%+d(%d) %up %ub %uf %ut %u%%",
|
||||
i + CAD_LEVEL_MIN, (int)base + i + CAD_LEVEL_MIN,
|
||||
"\n%c%+d(%d) %up %ub %uf %ut %u%%",
|
||||
lvl == cur ? '*' : ' ',
|
||||
lvl, (int)base + lvl,
|
||||
s.probes, s.busy, s.fp, s.tp, fp_pct);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,23 @@
|
||||
#define CAD_NUM_LEVELS (CAD_LEVEL_MAX - CAD_LEVEL_MIN + 1)
|
||||
#define CAD_SWEEP_MIN (-4) /* dry-run sweep window (get cad with auto off) */
|
||||
#define CAD_SWEEP_MAX 4
|
||||
#define CAD_FP_TARGET_PERMILLE 10 /* step-down needs FP rate <= 1% */
|
||||
#define CAD_STEP_DOWN_MIN_PROBES 300 /* samples before a down-step call */
|
||||
#define CAD_STEP_UP_MIN_PROBES 50 /* samples before an up-step call */
|
||||
/* Knee-seeking staircase (replaces the earlier absolute-FP-target band). The
|
||||
* FP-vs-detPeak curve falls as detPeak rises (less sensitive → fewer false
|
||||
* detects) and flattens past a knee; the sweet spot is the knee — the most
|
||||
* sensitive detPeak whose FP has already bottomed out. The controller reads
|
||||
* the local curve SLOPE from three rungs (frontier op-1, operating op, op+1)
|
||||
* rather than an absolute FP level, so it converges the same way regardless of
|
||||
* a site's FP floor (which varies with traffic and classifier residual).
|
||||
* - KNEE_SLOPE: the per-level FP change (permille) that counts as "steep".
|
||||
* Below the knee the curve drops fast (step up toward the knee); at/above it
|
||||
* the curve is flat (slope < KNEE_SLOPE).
|
||||
* - PLATEAU_CLEAN: on a flat plateau, only reclaim sensitivity (step down) if
|
||||
* FP is already this low — the guard that stops a flat-but-noisy curve from
|
||||
* walking to the sensitive rail (there, holding is the least-bad move; a
|
||||
* genuinely quiet flat-low site descends to the floor, which is correct). */
|
||||
#define CAD_KNEE_SLOPE_PERMILLE 50 /* >=5%/level FP change = steep */
|
||||
#define CAD_PLATEAU_CLEAN_PERMILLE 50 /* <=5% FP = clean enough to descend */
|
||||
#define CAD_STEP_MIN_PROBES 120 /* per-level samples before a step call */
|
||||
#define CAD_PROBE_RSSI_GUARD 7 /* dB above floor = channel visibly busy, skip probe */
|
||||
#define CAD_STATS_DECAY_MS (6UL * 3600UL * 1000UL) /* halve counters every 6 h */
|
||||
|
||||
|
||||
@@ -1022,6 +1022,17 @@ bool CompanionMesh::vcontactHandleFrame(const uint8_t *data, size_t len)
|
||||
bool dup = (msg_timestamp != 0 && msg_timestamp == _vcontact_last_ts);
|
||||
_vcontact_last_ts = msg_timestamp;
|
||||
|
||||
/* Emit the SENT response FIRST. It is the synchronous reply the
|
||||
* app's send request blocks on; the CLI command below runs on this
|
||||
* thread and can take a second or more (e.g. `get cad`, `advert`).
|
||||
* Deferring SENT until after the command ran let the app's response
|
||||
* timer fire and auto-retry the send, producing the duplicate
|
||||
* replies (and the lingering "sending" state) users reported. */
|
||||
uint32_t ack = 0;
|
||||
getRNG()->random((uint8_t *)&ack, 4);
|
||||
if (ack == 0) ack = 1;
|
||||
sendPacketSent(MSG_SEND_SENT_DIRECT, ack, 3000);
|
||||
|
||||
char reply[VCONTACT_CLI_REPLY_SIZE];
|
||||
reply[0] = '\0';
|
||||
if (!dup) {
|
||||
@@ -1041,12 +1052,7 @@ bool CompanionMesh::vcontactHandleFrame(const uint8_t *data, size_t len)
|
||||
LOG_DBG("vcontact CLI: dup ts=%u, re-ack only", msg_timestamp);
|
||||
}
|
||||
|
||||
/* Synthesize the normal send/ack choreography: SENT response,
|
||||
* then an immediate delivery confirmation (loopback, 0 ms). */
|
||||
uint32_t ack = 0;
|
||||
getRNG()->random((uint8_t *)&ack, 4);
|
||||
if (ack == 0) ack = 1;
|
||||
sendPacketSent(MSG_SEND_SENT_DIRECT, ack, 3000);
|
||||
/* Delivery confirmation (loopback, 0 ms trip), then the reply. */
|
||||
uint8_t ack_push[8];
|
||||
memcpy(ack_push, &ack, 4);
|
||||
memset(&ack_push[4], 0, 4); /* trip time: 0 ms */
|
||||
@@ -1092,8 +1098,34 @@ bool CompanionMesh::vcontactHandleFrame(const uint8_t *data, size_t len)
|
||||
}
|
||||
return false;
|
||||
|
||||
case CMD_SEND_TELEMETRY_REQ:
|
||||
/* Contact telemetry request: [cmd][3 reserved][32-byte pubkey].
|
||||
* The v-contact represents THIS node, so its telemetry is our own
|
||||
* self-telemetry. Synthesize the SENT ack the app blocks on, then push
|
||||
* a TELEMETRY_RESPONSE tagged with the v-contact key so the app matches
|
||||
* it to the loopback contact. (Real contacts fall through to the async
|
||||
* RF path below; the loopback key just isn't in the contacts table.) */
|
||||
if (len >= 4 + PUB_KEY_SIZE && isVContactKey(&data[4], PUB_KEY_SIZE)) {
|
||||
uint32_t tag = 0;
|
||||
getRNG()->random((uint8_t *)&tag, 4);
|
||||
if (tag == 0) tag = 1;
|
||||
sendPacketSent(MSG_SEND_SENT_DIRECT, tag, 3000);
|
||||
|
||||
uint8_t rsp[8 + 4 + 11 + 11 + (12 * POWER_MAX_CHANNELS) + 8];
|
||||
int i = 0;
|
||||
rsp[i++] = PUSH_CODE_TELEMETRY_RESPONSE;
|
||||
rsp[i++] = 0; /* reserved */
|
||||
memcpy(&rsp[i], _vcontact_pubkey, 6);
|
||||
i += 6;
|
||||
i += appendSelfTelemetry(&rsp[i],
|
||||
TELEM_PERM_BASE | TELEM_PERM_LOCATION | TELEM_PERM_ENVIRONMENT);
|
||||
sendPush(rsp[0], &rsp[1], i - 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
/* Every other pubkey-addressed opcode (login, telemetry, binary req,
|
||||
/* Every other pubkey-addressed opcode (login, binary req,
|
||||
* path discovery, export, ...) resolves the contact via
|
||||
* lookupContactByPubKey(); the v-contact is never in the table, so
|
||||
* they fail with ERR_NOT_FOUND before any packet exists. */
|
||||
|
||||
@@ -687,9 +687,13 @@ public:
|
||||
const char* getRole() override { return "companion"; }
|
||||
bool formatFileSystem() override { return data_store.formatFileSystem(); }
|
||||
|
||||
/* Advert / timer controls — mesh-internal; stub for now. */
|
||||
/* Advert — the companion can originate its own self-advert. delay_millis is
|
||||
* unused (companion sends flood at 0 ms / zero-hop immediately, matching the
|
||||
* app-triggered path); previously a no-op stub, so `advert` from the CLI
|
||||
* reported success but transmitted nothing. */
|
||||
void sendSelfAdvertisement(int delay_millis, bool flood) override {
|
||||
(void)delay_millis; (void)flood;
|
||||
(void)delay_millis;
|
||||
companion_mesh.sendSelfAdvert(flood);
|
||||
}
|
||||
void updateAdvertTimer() override {}
|
||||
void updateFloodAdvertTimer() override {}
|
||||
@@ -1378,19 +1382,15 @@ int main(void)
|
||||
}
|
||||
|
||||
#ifdef ZEPHCORE_LORA
|
||||
/* Initialize prefs with defaults */
|
||||
memset(&companion_mesh.prefs, 0, sizeof(companion_mesh.prefs));
|
||||
companion_mesh.prefs.freq = 869.618f;
|
||||
companion_mesh.prefs.bw = 62.5f;
|
||||
companion_mesh.prefs.sf = 8;
|
||||
companion_mesh.prefs.cr = 8;
|
||||
companion_mesh.prefs.tx_power_dbm = 22;
|
||||
companion_mesh.prefs.rx_delay_base = 0.0f; /* Disabled for companion */
|
||||
companion_mesh.prefs.airtime_factor = 9.0f; /* Arduino formula: 100/(af+1) → 10% (EU 868 default) */
|
||||
companion_mesh.prefs.rx_duty_cycle = 0; /* Default OFF: continuous RX */
|
||||
companion_mesh.prefs.rx_boost = 1; /* Default: boosted RX (+3dB sensitivity, +2mA) */
|
||||
companion_mesh.prefs.apc_enabled = 0; /* Default: APC off */
|
||||
companion_mesh.prefs.apc_margin = 20; /* Companions: more conservative margin (mobile) */
|
||||
/* Initialize prefs with defaults via the single source of truth
|
||||
* (initNodePrefs) so every field — including new ones added later — gets
|
||||
* its proper default. A hand-maintained subset here silently drifts: any
|
||||
* field not listed defaults to 0, and on upgrade the past-EOF read in
|
||||
* loadPrefs then keeps that 0 instead of the real default (this is what
|
||||
* zeroed cad_probe_interval / cad_auto and, earlier, the GPS settings). */
|
||||
initNodePrefs(&companion_mesh.prefs);
|
||||
/* Companion-specific overrides vs. initNodePrefs defaults: */
|
||||
companion_mesh.prefs.apc_margin = 20; /* mobile: more conservative than the 16 default */
|
||||
companion_mesh.prefs.auto_shutdown_mv = CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS; /* low-batt cutoff (0=off) */
|
||||
companion_mesh.prefs.gps_interval = CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC; /* 5-min duty cycle (0=always-on) */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user