557 lines
29 KiB
C++
557 lines
29 KiB
C++
// RAM-only HIL firmware. No Mesh, network, identity, settings, or autonomous TX.
|
|
// Explicit host-requested low-power packet probes are provided for validation.
|
|
// Uses the production CustomSX1262 + RadioLibWrapper tuneProfile implementation.
|
|
#include <Arduino.h>
|
|
#include <SPI.h>
|
|
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
|
#include <helpers/radiolib/ESP32BufferedRadioHal.h>
|
|
#include "profile_switch_experiments.h"
|
|
#include "ProfileChannelTrace.h"
|
|
#include "ProfileFrequencyOffset.h"
|
|
#ifdef HIL_HELTEC_V4
|
|
#include "ProfileHeltecV4.h"
|
|
ProfileHeltecV4 v4FrontEnd;
|
|
#endif
|
|
#ifdef HIL_INDICATOR
|
|
#include "IndicatorRadioHal.h"
|
|
#endif
|
|
|
|
class BenchBoard : public mesh::MainBoard {
|
|
public:
|
|
uint16_t getBattMilliVolts() override { return 0; }
|
|
const char* getManufacturerName() const override {
|
|
#ifdef HIL_INDICATOR
|
|
return "Indicator timing bench";
|
|
#elif defined(HIL_HELTEC_V4)
|
|
return "Heltec V4 timing bench";
|
|
#else
|
|
return "XIAO S3 WIO timing bench";
|
|
#endif
|
|
}
|
|
void reboot() override { ESP.restart(); }
|
|
uint8_t getStartupReason() const override { return 0; }
|
|
} board;
|
|
SPIClass radioSpi;
|
|
#ifdef HIL_INDICATOR
|
|
using BenchHalBase = IndicatorRadioHal;
|
|
#else
|
|
using BenchHalBase = ESP32BufferedRadioHal;
|
|
#endif
|
|
class BenchHal : public BenchHalBase {
|
|
public:
|
|
bool bulkTransfer = false;
|
|
using BenchHalBase::BenchHalBase;
|
|
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
|
|
// ESP32-only experiment: same full-duplex bytes, CS and transaction scope.
|
|
// Module still performs its original BUSY and command-status checks.
|
|
uint8_t shifted[5];
|
|
const uint8_t* wire=hilFrequencyCommand(out,len,shifted);
|
|
if (bulkTransfer) ESP32BufferedRadioHal::spiTransfer(const_cast<uint8_t*>(wire), len, in);
|
|
else ArduinoHal::spiTransfer(const_cast<uint8_t*>(wire), len, in);
|
|
channelTrace.observe(wire,len,in);
|
|
}
|
|
} radioHal(radioSpi);
|
|
ExperimentalSX1262 chip = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1,
|
|
P_LORA_RESET, P_LORA_BUSY);
|
|
int16_t benchTransmit(uint8_t* data, size_t len) {
|
|
#ifdef HIL_HELTEC_V4
|
|
v4FrontEnd.beforeTransmit();
|
|
#endif
|
|
const int16_t rc = chip.transmit(data, len);
|
|
#ifdef HIL_HELTEC_V4
|
|
v4FrontEnd.afterTransmit(); // Restore the RF path even when transmit fails.
|
|
#endif
|
|
return rc;
|
|
}
|
|
class BenchWrapper : public CustomSX1262Wrapper {
|
|
public:
|
|
bool batched = true;
|
|
bool forceModulationWrite = false;
|
|
bool warmStandby = true; // HIL rollback only; production wrapper unchanged
|
|
unsigned hopPasses = 1; // HIL-only full retune repetition
|
|
bool firstPassDetour=false;
|
|
uint32_t firstPassOffsetSteps=10; // 10 Hz request -> 10 steps; 100 Hz -> 105
|
|
uint32_t detourChecks=0,detourErrors=0,detourFirstRf=0,detourFinalRf=0;
|
|
uint32_t detourFirstMod=0,detourFinalMod=0;
|
|
uint32_t lastFirstPassUs=0,lastSecondPassUs=0,secondPassBlocked=0;
|
|
using CustomSX1262Wrapper::CustomSX1262Wrapper;
|
|
uint32_t receiveStartedUs() const {
|
|
#if !defined(ESP32_PLATFORM)
|
|
#error "profile_switch.cpp requires the ESP32 microsecond profile clock"
|
|
#endif
|
|
// ESP32 profile stamps are micros(), not the nRF52 RTC2 tick clock.
|
|
return _profile_visit_stamp;
|
|
}
|
|
mesh::RadioParamApplyResult hop(uint8_t target) {
|
|
const bool detour=firstPassDetour && hopPasses==2;
|
|
const auto desired=_profiles.params(target);
|
|
auto setTarget=[&](const mesh::RadioProfileParams& p) {
|
|
if(target==0) _profiles.setPrimary(p,true);
|
|
else { auto secondary=_profiles.secondary;secondary.params=p;_profiles.setSecondary(secondary,true); }
|
|
};
|
|
if(detour) {
|
|
// Use integer RF-word steps, not a float-MHz addition. The scoped HAL
|
|
// detour applies the selected offset at CR4/6, then restores zero.
|
|
auto temporary=desired;temporary.cr=6;
|
|
setTarget(temporary);
|
|
}
|
|
lastFirstPassUs=lastSecondPassUs=0;
|
|
const uint32_t firstStarted=micros();
|
|
chip.beginHop(isInRecvMode());
|
|
const auto result = tuneProfile(target);
|
|
chip.endHop();
|
|
if(detour) {
|
|
detourFirstRf=channelTrace.rfWord;detourFirstMod=channelTrace.modulationWord;
|
|
// Restore intended configuration even when pass one defers or fails.
|
|
// Hardware rollback and RX guards remain tuneProfile's responsibility.
|
|
setTarget(desired);
|
|
}
|
|
lastFirstPassUs=micros()-firstStarted;
|
|
if(result!=mesh::RadioParamApplyResult::APPLIED || hopPasses==1) return result;
|
|
const uint32_t busyStarted=micros();
|
|
while(chip.isChipBusy()) {
|
|
if(uint32_t(micros()-busyStarted)>=20000) return mesh::RadioParamApplyResult::FAILED;
|
|
}
|
|
lastFirstPassUs=micros()-firstStarted;
|
|
// Force an actual second transaction, not tuneProfile's same-generation
|
|
// no-op. Normal ownership/packet/BUSY guards apply independently again.
|
|
_profile_refresh_required=true;
|
|
const uint32_t secondStarted=micros();
|
|
chip.beginHop(isInRecvMode());
|
|
const auto second=tuneProfile(target);
|
|
chip.endHop();
|
|
lastSecondPassUs=micros()-secondStarted;
|
|
if(second==mesh::RadioParamApplyResult::BUSY) {
|
|
// The first pass already changed channels. Do not tell the scheduler
|
|
// "BUSY/no change" and mislabel subsequent RX as the old channel.
|
|
// Abort this diagnostic rather than override a newly acquired packet.
|
|
++secondPassBlocked;
|
|
return mesh::RadioParamApplyResult::FAILED;
|
|
}
|
|
if(detour && second==mesh::RadioParamApplyResult::APPLIED) {
|
|
detourFinalRf=channelTrace.rfWord;detourFinalMod=channelTrace.modulationWord;
|
|
++detourChecks;
|
|
// This mode is bounded to SF10/BW125, with CR4/6 then CR4/5, LDRO off.
|
|
// Verify the selected integer offset and exact nominal correction.
|
|
const uint32_t intendedRf=uint32_t(double(desired.freq)*1048576.0);
|
|
if(detourFirstRf!=intendedRf+firstPassOffsetSteps || detourFinalRf!=intendedRf
|
|
|| detourFirstMod!=0x0a040200 || detourFinalMod!=0x0a040100) {
|
|
++detourErrors;return mesh::RadioParamApplyResult::FAILED;
|
|
}
|
|
}
|
|
return second;
|
|
}
|
|
void loop() override {
|
|
#ifdef HIL_INDICATOR
|
|
radioHal.serviceInterrupt();
|
|
#endif
|
|
CustomSX1262Wrapper::loop();
|
|
}
|
|
protected:
|
|
void setProfileStandbyWarm(bool enabled) override {
|
|
CustomSX1262Wrapper::setProfileStandbyWarm(enabled && warmStandby);
|
|
}
|
|
void beginProfileRetune(bool continuousRx) override {
|
|
CustomSX1262Wrapper::beginProfileRetune(continuousRx);
|
|
if (forceModulationWrite || hopPasses==2) chip.hilInvalidateModulation();
|
|
}
|
|
bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
|
// Scoped over only this apply; rollback to CR4/5 and pass two use no
|
|
// offset, while rollback to the temporary CR4/6 tuple retains its offset.
|
|
HilFrequencyOffsetScope offset(firstPassDetour && hopPasses==2 && cr==6 ? firstPassOffsetSteps : 0);
|
|
if (batched) return CustomSX1262Wrapper::applyParams(freq, bw, sf, cr);
|
|
// Exact pre-batching baseline. Only this modulation path changes in A/B;
|
|
// transition guards, frequency, preamble, RX restart and oscillator match.
|
|
bool success = chip.setFrequency(freq) == RADIOLIB_ERR_NONE
|
|
&& chip.setSpreadingFactor(sf) == RADIOLIB_ERR_NONE
|
|
&& chip.setBandwidth(bw) == RADIOLIB_ERR_NONE
|
|
&& chip.setCodingRate(cr) == RADIOLIB_ERR_NONE
|
|
&& updatePreamble(sf, bw);
|
|
if (!success) return false;
|
|
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForParams(sf, bw));
|
|
chip.setPreambleMillis(pm.preambleMillis);
|
|
chip.setMaxPayloadMillis(pm.payloadMillis);
|
|
return true;
|
|
}
|
|
} driver(chip, board);
|
|
|
|
#include "ProfileSwitchUsb.h"
|
|
|
|
struct Timing {
|
|
uint32_t n = 0, lo = UINT32_MAX, hi = 0;
|
|
uint64_t sum = 0;
|
|
void add(uint32_t us) { ++n; lo = min(lo, us); hi = max(hi, us); sum += us; }
|
|
void print(const char* name) const {
|
|
appendBenchResult(lastRunResult,"\"%s\":{\"n\":%u,\"min_us\":%u,\"mean_us\":%.3f,\"max_us\":%u}",
|
|
name, n, n ? lo : 0, n ? double(sum) / n : 0, hi);
|
|
}
|
|
};
|
|
bool ready = false;
|
|
int readBenchRxGain() {
|
|
uint8_t gain=0;
|
|
return chip.readRegister(RADIOLIB_SX126X_REG_RX_GAIN,&gain,1)==RADIOLIB_ERR_NONE ? gain : -1;
|
|
}
|
|
|
|
bool waitBusy() {
|
|
const uint32_t start = micros();
|
|
while (chip.isChipBusy()) {
|
|
if (uint32_t(micros() - start) > 20000) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void drain() {
|
|
uint8_t packet[256];
|
|
driver.recvRaw(packet, sizeof(packet));
|
|
driver.onReceiveProcessed();
|
|
}
|
|
|
|
void run(bool warm, int sf, unsigned count, bool batched, unsigned mask, unsigned spiMHz, unsigned sequence) {
|
|
if (!ready || !waitBusy()) { Serial.println("{\"error\":\"not ready\"}"); return; }
|
|
chip.experiment = mask & 255;
|
|
chip.productionPath = chip.experiment == 0;
|
|
chip.setProfileSwitchOptimization((mask & 512) != 0);
|
|
radioHal.bulkTransfer = (mask & 256) != 0;
|
|
radioHal.spiSettings = SPISettings(spiMHz * 1000000, MSBFIRST, SPI_MODE0);
|
|
driver.batched = batched;
|
|
drain();
|
|
mesh::RadioProfileParams primary;
|
|
primary.freq = 909.5f; primary.bw = 62.5f; primary.sf = 7; primary.cr = 5;
|
|
if (driver.trySetPrimaryParams(primary, true) != mesh::RadioParamApplyResult::APPLIED) {
|
|
Serial.println("{\"error\":\"primary rejected\"}"); return;
|
|
}
|
|
mesh::RadioProfileConfig secondary;
|
|
secondary.params.freq = 910.5f; secondary.params.bw = 500;
|
|
secondary.params.sf = sf; secondary.params.cr = 5;
|
|
secondary.mode = mesh::RadioProfileMode::Rx;
|
|
driver.profiles()->setSecondary(secondary, true);
|
|
if (!waitBusy()) { Serial.println("{\"error\":\"startup busy timeout\"}"); return; }
|
|
driver.loop(); // Exercise production dual-profile entry and oscillator hook.
|
|
if (!chip.standbyXOSC) { Serial.println("{\"error\":\"warm hook not enabled\"}"); return; }
|
|
if (!waitBusy()) { Serial.println("{\"error\":\"entry busy timeout\"}"); return; }
|
|
|
|
// Only the baseline changes the policy after production mode entry. All
|
|
// retunes, modulation setters, packet guards and RX startup stay production.
|
|
chip.standbyXOSC = warm;
|
|
Timing api[2], busy[2], total[2];
|
|
uint32_t skipped = 0, failed = 0, modeErrors = 0, cacheErrors = 0, timeouts = 0, completed = 0;
|
|
const uint32_t start = millis();
|
|
const uint32_t fastStart = chip.getOptimizedProfileSwitches();
|
|
const uint32_t fsStart = chip.hilFsEntries;
|
|
const uint32_t packetStart = chip.hilPacketSkips;
|
|
const uint32_t iqStart = chip.hilIqWorkaroundSkips;
|
|
for (unsigned attempt = 0; completed < count + 8 && attempt < count * 4 + 32; ++attempt) {
|
|
if (uint32_t(millis() - start) > 30000) break;
|
|
drain();
|
|
if (!waitBusy()) { ++timeouts; break; }
|
|
// Use the production visit duration, but keep host I/O and status reads
|
|
// out of the measured hop. No cooperative loop/UI/network work in timing.
|
|
const auto active = driver.receiveProfile();
|
|
const auto dwell = driver.profiles()->listenUs(active,
|
|
driver.profilePreamble(driver.profiles()->slowerProfile()));
|
|
delay(dwell / 1000);
|
|
delayMicroseconds(dwell % 1000);
|
|
const uint8_t target = active ^ 1;
|
|
const uint32_t t0 = micros();
|
|
const auto result = driver.hop(target);
|
|
const uint32_t t1 = micros();
|
|
if (result == mesh::RadioParamApplyResult::BUSY) { ++skipped; continue; }
|
|
if (result != mesh::RadioParamApplyResult::APPLIED) { ++failed; break; }
|
|
if (!waitBusy()) { ++timeouts; break; }
|
|
const uint32_t t2 = micros();
|
|
if (sx126xReceiveMode(&chip) != 1) ++modeErrors;
|
|
const auto& expected = driver.profiles()->params(target);
|
|
if (chip.spreadingFactor != expected.sf || chip.bandwidthKhz != expected.bw
|
|
|| chip.codingRate != expected.cr - 4) ++cacheErrors;
|
|
if (completed++ < 8) continue; // first cold/warm transition is not steady-state
|
|
api[target].add(t1 - t0); busy[target].add(t2 - t1); total[target].add(t2 - t0);
|
|
}
|
|
lastRunResult.sequence=sequence;
|
|
lastRunResult.text[0]=0;
|
|
appendBenchResult(lastRunResult,"{\"result\":%u,\"warm\":%s,\"batched\":%s,\"mask\":%u,\"spi_mhz\":%u,\"sf500\":%d,\"requested\":%u,"
|
|
"\"skipped_busy\":%u,\"failures\":%u,\"busy_timeouts\":%u,\"rx_mode_errors\":%u,"
|
|
"\"cache_errors\":%u,\"tcxo_delay_us\":%u,\"optimized_rx_resumes\":%u,"
|
|
"\"fs\":%s,\"packet_cache\":%s,\"fs_entries\":%u,\"packet_skips\":%u,"
|
|
"\"iq_cache\":%s,\"iq_workaround_skips\":%u,",
|
|
sequence,warm ? "true" : "false", batched ? "true" : "false", mask, spiMHz, sf, count,
|
|
skipped, failed, timeouts, modeErrors, cacheErrors,
|
|
unsigned(chip.tcxoDelay),unsigned(chip.getOptimizedProfileSwitches()-fastStart),
|
|
chip.hilUseFsOnRetune ? "true" : "false", chip.hilSkipUnchangedPacket ? "true" : "false",
|
|
unsigned(chip.hilFsEntries-fsStart),unsigned(chip.hilPacketSkips-packetStart),
|
|
chip.hilSkipUnchangedIq ? "true" : "false",unsigned(chip.hilIqWorkaroundSkips-iqStart));
|
|
#ifdef HIL_INDICATOR
|
|
appendBenchResult(lastRunResult,"\"port0\":%s,",radioHal.hilUsePort0Only ? "true" : "false");
|
|
#endif
|
|
appendBenchResult(lastRunResult,"\"directions\":[");
|
|
for (int i = 0; i < 2; ++i) {
|
|
if (i) appendBenchResult(lastRunResult,",");
|
|
appendBenchResult(lastRunResult,"{\"to_profile\":%d,", i);
|
|
api[i].print("api"); appendBenchResult(lastRunResult,","); busy[i].print("busy_tail"); appendBenchResult(lastRunResult,",");
|
|
total[i].print("total"); appendBenchResult(lastRunResult,"}");
|
|
}
|
|
// Restore the production flag before exercising its normal off transition.
|
|
chip.standbyXOSC = true;
|
|
secondary.mode = mesh::RadioProfileMode::Off;
|
|
driver.profiles()->setSecondary(secondary, true);
|
|
const uint32_t exitStart = millis();
|
|
while (chip.standbyXOSC && uint32_t(millis() - exitStart) < 1000) {
|
|
drain();
|
|
if (!waitBusy()) break;
|
|
driver.loop();
|
|
delay(1);
|
|
}
|
|
appendBenchResult(lastRunResult,"],\"off_restored_rc\":%s}\n", chip.standbyXOSC ? "false" : "true");
|
|
if (lastRunResult.sequence) emitBenchResult(lastRunResult);
|
|
else Serial.println("{\"error\":\"result overflow\"}");
|
|
}
|
|
|
|
#include "profile_switch_packets.h"
|
|
#include "profile_switch_lifecycle.h"
|
|
#include "profile_switch_channels.h"
|
|
#include "profile_preamble_diagnostic.h"
|
|
#include "profile_stationary_baseline.h"
|
|
|
|
// HIL-only, same-image controls for the existing production state machine.
|
|
// They do not relax ownership, BUSY polling, or command-status checks, and
|
|
// CustomSX1262 resets all three fast policies to false on benchmark reboot.
|
|
bool selectProfileFastPolicy(const char* line) {
|
|
unsigned value;
|
|
char trailing;
|
|
unsigned policy;
|
|
if (sscanf(line, "fs %u %c", &value, &trailing) == 1 && value <= 1) {
|
|
policy = 0;
|
|
} else if (sscanf(line, "packetcache %u %c", &value, &trailing) == 1
|
|
&& value <= 1) {
|
|
policy = 1;
|
|
} else if (sscanf(line, "iqcache %u %c", &value, &trailing) == 1 && value <= 1) {
|
|
policy = 3;
|
|
#ifdef HIL_INDICATOR
|
|
} else if (sscanf(line, "port0 %u %c", &value, &trailing) == 1 && value <= 1) {
|
|
policy = 2;
|
|
#endif
|
|
} else {
|
|
return false;
|
|
}
|
|
if (packetListening || stationaryBaseline.active || preambleDiagnostic.active
|
|
|| channelSweep.active) {
|
|
Serial.println("{\"error\":\"stop receiver before changing fast policy\"}");
|
|
} else {
|
|
if (policy == 0) chip.hilUseFsOnRetune = value;
|
|
else if (policy == 1) chip.hilSkipUnchangedPacket = value;
|
|
else if (policy == 3) chip.hilSkipUnchangedIq = value;
|
|
#ifdef HIL_INDICATOR
|
|
else radioHal.hilUsePort0Only = value;
|
|
#endif
|
|
Serial.printf("{\"%s\":%s}\n", policy == 0 ? "fs" : policy == 1 ? "packet_cache" : policy == 3 ? "iq_cache" : "port0",
|
|
value ? "true" : "false");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void setup() {
|
|
#ifndef HIL_INDICATOR
|
|
Serial.setTxBufferSize(4096);
|
|
Serial.setTxTimeoutMs(250);
|
|
#endif
|
|
Serial.begin(115200);
|
|
delay(1500);
|
|
#ifdef HIL_HELTEC_V4
|
|
v4FrontEnd.begin(); // Power/select FEM before the radio's initial calibration.
|
|
#endif
|
|
#ifdef HIL_INDICATOR
|
|
if (!radioHal.beginExpander()) { Serial.println("{\"error\":\"expander unavailable\"}"); return; }
|
|
#endif
|
|
ready = chip.std_init(&radioSpi);
|
|
if (ready) {
|
|
driver.begin();
|
|
ready = driver.setRxPowerSaving(false, 65625, 60000);
|
|
drain();
|
|
ready = ready && waitBusy();
|
|
}
|
|
Serial.printf("{\"ready\":%s,\"bench\":\"production-profile-switch-v8\",\"autonomous_tx\":false}\n",
|
|
ready ? "true" : "false");
|
|
}
|
|
void loop() {
|
|
static char line[80];
|
|
static size_t used = 0;
|
|
while (Serial.available()) {
|
|
const char c = Serial.read();
|
|
if (c == '\r' || c == '\n') {
|
|
if (!used) continue;
|
|
line[used] = 0; used = 0;
|
|
int warm, sf, batched, power; unsigned count, mask, spiMHz, target, seq, len, preamble, bulk, dwell, trace, scanSf, scanBw, freqKhz;
|
|
char resultKind[12];
|
|
if (!strcmp(line,"pairinfo")) {
|
|
Serial.println(pairInfo);
|
|
} else if (sscanf(line,"basepair %u %u",&target,&seq)==2 && target<2 && seq) {
|
|
channelStepKhz=1000;
|
|
startStationaryBaseline(target,pairProfiles[target].sf,seq,pairProfiles[target].bw);
|
|
} else if (sscanf(line,"pairstart %u %u %u",&seq,&count,&dwell)==3 && seq
|
|
&& pairPlanSupported(count,dwell)) {
|
|
if(stationaryBaseline.active || channelSweep.active || preambleDiagnostic.active) {
|
|
Serial.println("{\"error\":\"reboot before pair scan\"}");
|
|
} else {
|
|
channelPair=true;pairLoopUs=count;pairSlowExtraUs=dwell;channelStepKhz=1000;
|
|
startChannelSweep(2,32,seq,pairListenUs(0,pairLoopUs,pairSlowExtraUs),false,8,500);
|
|
}
|
|
} else if (!strcmp(line,"mixinfo")) {
|
|
Serial.println(mixedChannelInfo);
|
|
} else if (!strcmp(line,"retuneinfo")) {
|
|
Serial.println("{\"full_retune_repeat\":1,\"max_passes\":2,\"guarded\":true}");
|
|
} else if (!strcmp(line,"continueinfo")) {
|
|
Serial.printf("{\"fixed_sample_scan\":1,\"timeout_ms\":10000,\"retune_on_miss\":false}\n");
|
|
} else if (!strcmp(line,"detourinfo")) {
|
|
// Trailing whitespace avoids exact 64/128-byte CDC reply boundaries.
|
|
Serial.printf("{\"first_pass_detour\":1,\"requested_offset_khz\":%.2f,\"rf_offset_steps\":%u,\"offset_hz\":%.14f,\"first_cr\":6,\"final_cr\":5} \n",
|
|
driver.firstPassOffsetSteps==105?0.10:0.01,unsigned(driver.firstPassOffsetSteps),
|
|
double(driver.firstPassOffsetSteps)*32000000.0/33554432.0);
|
|
} else if (sscanf(line,"basemixed %u %u",&target,&seq)==2 && target<4 && seq) {
|
|
const auto& p=mixedChannelProfiles[target];
|
|
startStationaryBaseline(target,p.sf,seq,p.bwKhz);
|
|
} else if (sscanf(line,"basestart %u %u %u",&target,&scanSf,&seq)==3 && target<4 && scanSf>=5 && scanSf<=10 && seq) {
|
|
startStationaryBaseline(target,scanSf,seq);
|
|
} else if (sscanf(line,"baseexpectch %u %u",&target,&seq)==2 && target<4 && seq) {
|
|
expectStationaryPacket(seq,target);
|
|
} else if (sscanf(line,"baseexpect %u",&seq)==1 && seq) {
|
|
expectStationaryPacket(seq);
|
|
} else if (sscanf(line,"basestatus %u",&seq)==1 && seq) {
|
|
stationaryBaselineStatus(seq);
|
|
} else if (!strcmp(line,"basestop")) {
|
|
stopStationaryBaseline();
|
|
} else if (stationaryBaseline.active && strcmp(line,"info") && strncmp(line,"result ",7) && strcmp(line,"reboot")) {
|
|
Serial.println("{\"error\":\"stop stationary baseline before other operations\"}");
|
|
} else if (sscanf(line,"diaglisten %u %u %u %u",&scanSf,&freqKhz,&seq,&count)==4) {
|
|
if((scanSf==6 || scanSf==8) && freqKhz>=909000 && freqKhz<=911000 && seq && count>=200 && count<=1000)
|
|
diagnosticListen(scanSf,freqKhz,seq,count);
|
|
else Serial.println("{\"error\":\"invalid diagnostic receiver parameters\"}");
|
|
} else if (sscanf(line,"diagtxsetup %u %u %u",&scanSf,&freqKhz,&seq)==3) {
|
|
if((scanSf==6 || scanSf==8) && freqKhz>=909000 && freqKhz<=911000 && seq)
|
|
diagnosticTxSetup(scanSf,freqKhz,seq);
|
|
else Serial.println("{\"error\":\"invalid diagnostic transmitter parameters\"}");
|
|
} else if (sscanf(line,"diagtx %u",&seq)==1 && seq) {
|
|
diagnosticTransmit(seq);
|
|
} else if (!strcmp(line,"diagstop")) {
|
|
stopPreambleDiagnostic();
|
|
} else if (preambleDiagnostic.active && strcmp(line,"info") && strncmp(line,"result ",7) && strcmp(line,"reboot")) {
|
|
Serial.println("{\"error\":\"stop diagnostic receiver before another operation\"}");
|
|
} else if (sscanf(line,"scanfirstdetour %u",&count)==1 && count<=1) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before detour policy\"}");
|
|
else { driver.firstPassDetour=count;Serial.printf("{\"first_pass_detour\":%s}\n",count?"true":"false"); }
|
|
} else if (sscanf(line,"scandetouroffset %u",&count)==1 && (count==10 || count==100)) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before detour offset\"}");
|
|
else { driver.firstPassOffsetSteps=count==100?105:10;Serial.printf("{\"requested_offset_hz\":%u}\n",count); }
|
|
} else if (sscanf(line,"scanretunepasses %u",&count)==1 && (count==1 || count==2)) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before full retune policy\"}");
|
|
else { driver.hopPasses=count;Serial.printf("{\"retune_passes\":%u}\n",count); }
|
|
} else if (sscanf(line,"scanmixed %u",&count)==1 && count<=1) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before modulation plan\"}");
|
|
else { channelMixed=count;Serial.printf("{\"mixed_profiles\":%s}\n",count?"true":"false"); }
|
|
} else if (sscanf(line,"scanacceptoffchannel %u",&count)==1 && count<=1) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before acceptance policy\"}");
|
|
else { channelAcceptOffChannel=count;Serial.printf("{\"accept_offchannel\":%s}\n",count?"true":"false"); }
|
|
} else if (sscanf(line,"scancontinue %u",&count)==1 && count<=1) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before continuation policy\"}");
|
|
else { channelContinueOnMiss=count;Serial.printf("{\"continue_on_miss\":%s}\n",count?"true":"false"); }
|
|
} else if (sscanf(line,"scanstep %u",&count)==1 && (count==250 || count==1000)) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before spacing policy\"}");
|
|
else { channelStepKhz=count;Serial.printf("{\"channel_step_khz\":%u}\n",count); }
|
|
} else if (sscanf(line,"scantcxo %u",&count)==1 && (count==1600 || count==6000)) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before TCXO policy\"}");
|
|
else { channelTcxoUs=count;Serial.printf("{\"tcxo_us\":%u}\n",count); }
|
|
} else if (sscanf(line,"scanrollback %u",&count)==1 && count<=15) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before rollback policy\"}");
|
|
else { channelRollback=count;Serial.printf("{\"rollback\":%u}\n",count); }
|
|
} else if (sscanf(line,"scansettle %u",&count)==1 && count<=24000) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before changing settling delay\"}");
|
|
else { channelSettleUs=count;Serial.printf("{\"settle_us\":%u}\n",count); }
|
|
} else if (sscanf(line,"scanmodcache %u",&count)==1 && count<=1) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before changing cache policy\"}");
|
|
else { driver.forceModulationWrite=!count;Serial.printf("{\"modulation_cache\":%s}\n",count?"true":"false"); }
|
|
} else if (sscanf(line,"scanfreqrepeat %u",&count)==1 && count<=1) {
|
|
if(channelSweep.active) Serial.println("{\"error\":\"stop scanner before changing frequency policy\"}");
|
|
else { chip.repeatFrequency=count;Serial.printf("{\"frequency_repeat\":%s}\n",count?"true":"false"); }
|
|
} else if (sscanf(line,"scanstart %u %u %u %u %u %u %u",&count,&preamble,&seq,&dwell,&trace,&scanSf,&scanBw)==7) {
|
|
if(count>=4 && count<=64 && preamble>=12 && preamble<=256 && seq && trace<=1
|
|
&& (scanSf>=5 && scanSf<=10) && (scanBw==125 || scanBw==250)
|
|
&& dwell>=4*(1u<<scanSf)*1000/scanBw && dwell<=32*(1u<<scanSf)*1000/scanBw)
|
|
startChannelSweep(count,preamble,seq,dwell,trace,scanSf,scanBw);
|
|
else Serial.println("{\"error\":\"invalid scan SF/BW parameters\"}");
|
|
} else if (sscanf(line,"scanstart %u %u %u %u %u %u",&count,&preamble,&seq,&dwell,&trace,&scanSf)==6) {
|
|
if(count>=4 && count<=64 && preamble>=12 && preamble<=256 && seq
|
|
&& (scanSf==6 || scanSf==8) && dwell>=2048 && dwell<=65536 && trace<=1)
|
|
startChannelSweep(count,preamble,seq,dwell,trace,scanSf);
|
|
else Serial.println("{\"error\":\"invalid scan parameters\"}");
|
|
} else if (sscanf(line,"scanstart %u %u %u %u %u",&count,&preamble,&seq,&dwell,&trace)==5
|
|
&& count>=4 && count<=64 && preamble>=12 && preamble<=256 && seq
|
|
&& dwell>=2048 && dwell<=16384 && trace<=1) {
|
|
startChannelSweep(count,preamble,seq,dwell,trace);
|
|
} else if (sscanf(line,"scanstart %u %u %u",&count,&preamble,&seq)==3
|
|
&& count>=4 && count<=64 && preamble>=12 && preamble<=256 && seq) {
|
|
startChannelSweep(count,preamble,seq);
|
|
} else if (sscanf(line,"scanexpect %u %u",&target,&seq)==2 && seq) {
|
|
expectChannelPacket(target,seq);
|
|
} else if (sscanf(line,"scantx %u %u %u %u %u %u",&target,&seq,&len,&preamble,&scanSf,&scanBw)==6) {
|
|
if(target<64 && seq && len>=12 && len<=255 && preamble>=12 && preamble<=256
|
|
&& (scanSf>=5 && scanSf<=10) && (scanBw==125 || scanBw==250))
|
|
sendChannelPacket(target,seq,len,preamble,scanSf,scanBw);
|
|
else Serial.println("{\"error\":\"invalid scan TX SF/BW parameters\"}");
|
|
} else if (sscanf(line,"scantx %u %u %u %u %u",&target,&seq,&len,&preamble,&scanSf)==5) {
|
|
if(target<64 && seq && len>=12 && len<=255 && preamble>=12 && preamble<=256
|
|
&& (scanSf==6 || scanSf==8)) sendChannelPacket(target,seq,len,preamble,scanSf);
|
|
else Serial.println("{\"error\":\"invalid scan TX parameters\"}");
|
|
} else if (sscanf(line,"scantx %u %u %u %u",&target,&seq,&len,&preamble)==4
|
|
&& target<64 && seq && len>=12 && len<=255 && preamble>=12 && preamble<=256) {
|
|
sendChannelPacket(target,seq,len,preamble);
|
|
} else if (sscanf(line,"scanstatus %u",&seq)==1 && seq) {
|
|
channelSweepStatus(seq);
|
|
} else if (sscanf(line,"scantrace %u %u",&seq,&target)==2 && seq) {
|
|
channelSweepTrace(seq,target);
|
|
} else if (!strcmp(line,"scanstop")) {
|
|
stopChannelSweep();
|
|
} else if (channelSweep.active && strcmp(line,"info") && strncmp(line,"result ",7) && strcmp(line,"reboot")) {
|
|
Serial.println("{\"error\":\"stop channel sweep before other experiments\"}");
|
|
} else if (selectProfileFastPolicy(line)) {
|
|
// The helper also rejects an active single-packet probe.
|
|
} else if (sscanf(line, "run %d %d %u %d %u %u %u", &warm, &sf, &count, &batched, &mask, &spiMHz, &seq) == 7 && seq
|
|
&& (warm == 0 || warm == 1) && (batched == 0 || batched == 1)
|
|
&& mask <= 1023 && (spiMHz == 2 || spiMHz == 4 || spiMHz == 8)
|
|
&& sf >= 7 && sf <= 9 && count >= 16 && count <= 1000) {
|
|
packetListening=false;
|
|
run(warm, sf, count, batched, mask, spiMHz, seq);
|
|
} else if (sscanf(line,"listen %d %u %u %u %u",&sf,&target,&seq,&mask,&spiMHz)==5
|
|
&& sf>=7 && sf<=9 && target<=1 && mask<=1023 && (spiMHz==2 || spiMHz==4 || spiMHz==8)) {
|
|
listenPacket(sf,target,seq,mask,spiMHz);
|
|
} else if (sscanf(line,"tx %d %u %u %u %u %d %u %u",&sf,&target,&seq,&len,&preamble,&power,&spiMHz,&bulk)==8
|
|
&& sf>=7 && sf<=9 && target<=1 && len>=12 && len<=255 && preamble>=8 && preamble<=256 && power>=-9 && power<=0
|
|
&& (spiMHz==2 || spiMHz==4 || spiMHz==8) && bulk<=1) {
|
|
sendPacket(sf,target,seq,len,preamble,power,spiMHz,bulk);
|
|
} else if (!strcmp(line, "lifecycle")) {
|
|
runLifecycle();
|
|
} else if (!strcmp(line, "reboot")) {
|
|
ESP.restart();
|
|
} else if (!strcmp(line, "info")) {
|
|
#ifdef HIL_HELTEC_V4
|
|
Serial.printf("{\"ready\":%s,\"bench\":\"production-profile-switch-v8\",\"channel_sweep\":1,\"channel_trace\":2,\"channel_sf_select\":1,\"channel_bw_select\":1,\"channel_step_select\":1,\"payload_delivery_ab\":1,\"preamble_diagnostic\":1,\"modulation_cache_ab\":1,\"frequency_repeat_ab\":1,\"stationary_baseline\":1,\"stationary_offset\":1,\"rollback_ab\":1,\"rx_gain_reg\":%d,\"board\":\"Heltec V4\",\"fem\":\"%s\",\"tx_fem_mode\":\"%s\"}\n", ready ? "true" : "false",readBenchRxGain(),v4FrontEnd.type(),v4FrontEnd.txMode());
|
|
#else
|
|
Serial.printf("{\"ready\":%s,\"bench\":\"production-profile-switch-v8\",\"channel_sweep\":1,\"channel_trace\":2,\"channel_sf_select\":1,\"channel_bw_select\":1,\"channel_step_select\":1,\"payload_delivery_ab\":1,\"preamble_diagnostic\":1,\"modulation_cache_ab\":1,\"frequency_repeat_ab\":1,\"stationary_baseline\":1,\"stationary_offset\":1,\"rollback_ab\":1,\"rx_gain_reg\":%d}\n", ready ? "true" : "false",readBenchRxGain());
|
|
#endif
|
|
} else if (sscanf(line,"result %11s %u",resultKind,&seq)==2) {
|
|
replayBenchResult(resultKind,seq);
|
|
} else Serial.println("{\"error\":\"use run <warm> <SF> <count> <batched> <mask> <spi MHz> <sequence>\"}");
|
|
} else if (used + 1 < sizeof(line)) line[used++] = c;
|
|
}
|
|
if (stationaryBaseline.active) {
|
|
serviceStationaryBaseline();delay(0);
|
|
} else if (preambleDiagnostic.active) {
|
|
servicePreambleDiagnostic();delay(0);
|
|
} else if (channelSweep.active) {
|
|
serviceChannelSweep();
|
|
// No millisecond sleep: it would quantize a 2.458 ms receive visit.
|
|
// FreeRTOS still preempts normally and USB is serviced every iteration.
|
|
delay(0);
|
|
} else {
|
|
servicePacketProbe();
|
|
delay(1);
|
|
}
|
|
}
|