Files
HaloKeymind/tools/hil/profile_switch_t1000_lr1110.cpp
T

442 lines
21 KiB
C++

// Application-only T1000-E LR1110 production-wrapper RX retune timing bench.
// RAM-only; no Mesh identity, saved settings, autonomous TX or TX command.
// RX-only test profiles are 909.5 and 909.75 MHz; never 910.525 MHz.
#include <Arduino.h>
#include <Adafruit_TinyUSB.h>
#include <SPI.h>
#include <helpers/radiolib/CustomLR1110Wrapper.h>
#include "ProfileSwitchUsb.h"
class BenchBoard : public mesh::MainBoard {
public:
uint16_t getBattMilliVolts() override { return 0; }
const char* getManufacturerName() const override { return "Seeed T1000-E LR1110 timing bench"; }
void reboot() override { NVIC_SystemReset(); }
uint8_t getStartupReason() const override { return 0; }
} board;
class CountingHal : public ArduinoHal {
public:
using ArduinoHal::ArduinoHal;
bool bulkTransfer = true;
uint32_t spiHz = 16000000;
void setSpiHz(uint32_t hz) {
spiHz = hz;
spiSettings = SPISettings(hz, MSBFIRST, SPI_MODE0);
}
uint32_t rfWrites = 0, modulationWrites = 0, packetWrites = 0;
uint32_t standbyWrites = 0, rxWrites = 0, dioWrites = 0, packetTypeReads = 0;
uint32_t clearWrites = 0, fsWrites = 0;
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
if (len >= 2 && out[0] == 2 && out[1] == 0x0B) ++rfWrites;
if (len >= 2 && out[0] == 2 && out[1] == 0x0F) ++modulationWrites;
if (len >= 2 && out[0] == 2 && out[1] == 0x10) ++packetWrites;
if (len >= 2 && out[0] == 1 && out[1] == 0x1C) ++standbyWrites;
if (len >= 2 && out[0] == 1 && out[1] == 0x14) ++clearWrites;
if (len >= 2 && out[0] == 1 && out[1] == 0x1D) ++fsWrites;
if (len >= 2 && out[0] == 2 && out[1] == 0x09) ++rxWrites;
if (len >= 2 && out[0] == 1 && out[1] == 0x13) ++dioWrites;
if (len >= 2 && out[0] == 2 && out[1] == 0x02) ++packetTypeReads;
if (bulkTransfer) spi->transfer(out, in, len);
else ArduinoHal::spiTransfer(out, len, in);
}
} radioHal(SPI, SPISettings(16000000, MSBFIRST, SPI_MODE0));
class BenchLR1110 : public CustomLR1110 {
public:
using CustomLR1110::CustomLR1110;
uint16_t preambleSymbols() const { return preambleLengthLoRa; }
uint64_t standbyCycles = 0, startRxCycles = 0;
uint32_t standbyCount = 0, startRxCount = 0;
int16_t standby() override {
const uint32_t started = DWT->CYCCNT;
const int16_t rc = CustomLR1110::standby();
standbyCycles += DWT->CYCCNT - started;
++standbyCount;
return rc;
}
int16_t startReceive() override {
const uint32_t started = DWT->CYCCNT;
const int16_t rc = CustomLR1110::startReceive();
startRxCycles += DWT->CYCCNT - started;
++startRxCount;
return rc;
}
} chip = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1,
P_LORA_RESET, P_LORA_BUSY);
class BenchWrapper : public CustomLR1110Wrapper {
public:
using CustomLR1110Wrapper::CustomLR1110Wrapper;
uint64_t applyCycles = 0, receivingCycles = 0, startModeCycles = 0;
uint32_t applyCount = 0, receivingCount = 0, startModeCount = 0;
uint32_t busyHighChecks = 0, receivingTrueChecks = 0;
mesh::RadioParamApplyResult hop(uint8_t profile) { return tuneProfile(profile); }
void startRx() { startRecv(); }
void stopRx() { idle(); }
void enableWarmStandby() { setProfileStandbyWarm(true); }
bool isReceivingPacket() override {
const uint32_t started = DWT->CYCCNT;
const bool receiving = CustomLR1110Wrapper::isReceivingPacket();
if (receiving) ++receivingTrueChecks;
receivingCycles += DWT->CYCCNT - started;
++receivingCount;
return receiving;
}
bool isChipBusy() override {
const bool busy = CustomLR1110Wrapper::isChipBusy();
if (busy) ++busyHighChecks;
return busy;
}
protected:
bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
const uint32_t started = DWT->CYCCNT;
const bool rc = CustomLR1110Wrapper::applyParams(freq, bw, sf, cr);
applyCycles += DWT->CYCCNT - started;
++applyCount;
return rc;
}
int startReceiveMode() override {
const uint32_t started = DWT->CYCCNT;
const int rc = CustomLR1110Wrapper::startReceiveMode();
startModeCycles += DWT->CYCCNT - started;
++startModeCount;
return rc;
}
} driver(chip, board);
static const uint32_t switchPins[Module::RFSWITCH_MAX_PINS] = {
RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6,
RADIOLIB_LR11X0_DIO7, RADIOLIB_LR11X0_DIO8, RADIOLIB_NC,
};
static const Module::RfSwitchMode_t switchModes[] = {
{LR11x0::MODE_STBY, {LOW, LOW, LOW, LOW}},
{LR11x0::MODE_RX, {HIGH, LOW, LOW, HIGH}},
{LR11x0::MODE_TX, {HIGH, HIGH, LOW, HIGH}},
{LR11x0::MODE_TX_HP, {LOW, HIGH, LOW, HIGH}},
{LR11x0::MODE_TX_HF, {LOW, LOW, LOW, LOW}},
{LR11x0::MODE_GNSS, {LOW, LOW, HIGH, LOW}},
{LR11x0::MODE_WIFI, {LOW, LOW, LOW, LOW}},
END_OF_MODE_TABLE,
};
struct Timing {
uint32_t n = 0, minCycles = UINT32_MAX, maxCycles = 0;
uint64_t sumCycles = 0;
void add(uint32_t cycles) {
++n;
if (cycles < minCycles) minCycles = cycles;
if (cycles > maxCycles) maxCycles = cycles;
sumCycles += cycles;
}
double toUs(uint32_t cycles) const { return double(cycles) * 1000000.0 / SystemCoreClock; }
double minUs() const { return n ? toUs(minCycles) : 0; }
double maxUs() const { return n ? toUs(maxCycles) : 0; }
double meanUs() const { return n ? double(sumCycles) * 1000000.0 / n / SystemCoreClock : 0; }
};
bool ready = false;
int initError = 0;
uint32_t nextSequence = 0;
uint32_t configuredTcxoUs = MC_TCXO_DELAY_US;
bool injectTimeoutNextRun = false;
bool waitBusy(uint32_t limitUs = 20000) {
const uint32_t start = micros();
while (chip.isChipBusy()) {
if (uint32_t(micros() - start) >= limitUs) return false;
}
return true;
}
void run(const char* mode, unsigned count, uint32_t sequence) {
const bool modulation = !strcmp(mode, "mod") || !strcmp(mode, "both");
const bool frequency = !strcmp(mode, "freq") || !strcmp(mode, "both");
const bool preambleChange = !strcmp(mode, "preamble");
if (!ready || !sequence || sequence <= nextSequence || count < 16 || count > 1000) {
Serial.println("{\"error\":\"invalid or repeated run\"}");
return;
}
if (!modulation && !frequency && !preambleChange) {
Serial.println("{\"error\":\"mode must be mod, freq, both, or preamble\"}");
return;
}
nextSequence = sequence;
if (!waitBusy()) { Serial.println("{\"error\":\"initial BUSY timeout\"}"); return; }
mesh::RadioProfileParams primary;
primary.freq = 909.5f; primary.bw = 125; primary.sf = 7; primary.cr = 5;
primary.preamble = 32;
mesh::RadioProfileConfig secondary;
secondary.params = primary;
if (modulation) secondary.params.sf = 8;
if (frequency) secondary.params.freq = 909.75f;
if (preambleChange) secondary.params.preamble = 48;
secondary.mode = mesh::RadioProfileMode::Rx;
uint32_t primaryBusyDeferrals = 0;
uint32_t drainedPackets = 0, drainCalls = 0;
auto drainPendingRx = [&]() {
uint8_t packet[RADIOLIB_LR11X0_MAX_PACKET_LENGTH] = {};
const int length = driver.recvRaw(packet, sizeof(packet));
++drainCalls;
if (length > 0) ++drainedPackets;
};
auto primaryResult = driver.trySetPrimaryParams(primary, true);
while (primaryResult == mesh::RadioParamApplyResult::BUSY && primaryBusyDeferrals < 20) {
++primaryBusyDeferrals;
drainPendingRx();
delay(20); // An unrelated packet can briefly own RX at setup.
primaryResult = driver.trySetPrimaryParams(primary, true);
}
if (primaryResult != mesh::RadioParamApplyResult::APPLIED) {
Serial.printf("{\"error\":\"primary rejected\",\"result\":%u,"
"\"busy_deferrals\":%lu}\n",
unsigned(primaryResult), (unsigned long)primaryBusyDeferrals);
return;
}
driver.profiles()->setSecondary(secondary, true);
driver.startRx();
if (!driver.isInRecvMode() || !waitBusy()) {
Serial.println("{\"error\":\"RX did not start\"}"); return;
}
chip.hilInjectRxTimeout = injectTimeoutNextRun;
injectTimeoutNextRun = false;
Timing spot[2], total[2];
uint32_t failures = 0, busyDeferrals = 0, rxErrors = 0, cacheErrors = 0;
uint32_t busyTimeouts = 0, rssiErrors = 0;
const uint32_t rfBefore = radioHal.rfWrites;
const uint32_t modBefore = radioHal.modulationWrites;
const uint32_t packetBefore = radioHal.packetWrites;
const uint32_t standbyBefore = radioHal.standbyWrites;
const uint32_t rxBefore = radioHal.rxWrites;
const uint32_t dioBefore = radioHal.dioWrites;
const uint32_t packetTypeBefore = radioHal.packetTypeReads;
const uint32_t clearBefore = radioHal.clearWrites;
const uint32_t fsBefore = radioHal.fsWrites;
const uint32_t pendingBefore = chip.hilPendingIrqSnapshots;
const uint32_t fastBefore = chip.getOptimizedProfileSwitches();
const uint64_t standbyCyclesBefore = chip.standbyCycles, startRxCyclesBefore = chip.startRxCycles;
const uint64_t applyCyclesBefore = driver.applyCycles, receivingCyclesBefore = driver.receivingCycles;
const uint64_t startModeCyclesBefore = driver.startModeCycles;
const uint32_t standbyCountBefore = chip.standbyCount, startRxCountBefore = chip.startRxCount;
const uint32_t applyCountBefore = driver.applyCount, receivingCountBefore = driver.receivingCount;
const uint32_t startModeCountBefore = driver.startModeCount;
const uint32_t busyHighBefore = driver.busyHighChecks;
const uint32_t receivingTrueBefore = driver.receivingTrueChecks;
const uint64_t clearCyclesBefore = chip.fastClearCycles;
const uint64_t packetCyclesBefore = chip.fastPacketCycles;
const uint64_t setRxCyclesBefore = chip.fastSetRxCycles;
unsigned completed = 0;
uint32_t spotBudgetUs = 0;
for (unsigned attempt = 0; completed < count + 8 && attempt < (count + 8) * 4; ++attempt) {
if (!waitBusy()) { ++busyTimeouts; break; }
if (completed >= 8) delay(10); // The first eight hops match the rapid startup self-test.
const uint8_t target = driver.receiveProfile() ^ 1;
const uint32_t started = DWT->CYCCNT;
const auto result = driver.hop(target);
if (result == mesh::RadioParamApplyResult::BUSY) {
++busyDeferrals;
drainPendingRx();
continue;
}
if (result != mesh::RadioParamApplyResult::APPLIED) { ++failures; break; }
if (!waitBusy()) { ++busyTimeouts; break; }
const uint32_t elapsed = DWT->CYCCNT - started;
if (!driver.isInRecvMode()) ++rxErrors;
if (chip.getSpreadingFactor() != (target && modulation ? 8 : 7)
|| chip.getFreqMHz() != (target && frequency ? 909.75f : 909.5f)
|| chip.preambleSymbols() != (target && preambleChange ? 48 : 32)) ++cacheErrors;
if (completed < 8) spot[target].add(elapsed);
else {
delay(7); // RSSI needs a settled receiver; this is outside switch timing.
float rssi = 0;
if (chip.getRssiInst(&rssi) != RADIOLIB_ERR_NONE || !isfinite(rssi)
|| rssi < -180.0f || rssi > 20.0f) ++rssiErrors;
total[target].add(elapsed);
}
++completed;
if (completed == 8) spotBudgetUs = driver.profiles()->switchBudgetUs();
}
const uint32_t longBudgetUs = driver.profiles()->switchBudgetUs();
driver.profiles()->setSecondary({}, true);
auto restored = driver.hop(0);
for (unsigned retry = 0; restored == mesh::RadioParamApplyResult::BUSY && retry < 20; ++retry) {
drainPendingRx();
delay(20);
restored = driver.hop(0);
}
const bool safe = restored == mesh::RadioParamApplyResult::APPLIED
&& chip.getFreqMHz() == 909.5f && driver.isInRecvMode();
recordBenchResult(lastRunResult, sequence,
"{\"result\":%lu,\"board\":\"Seeed T1000-E LR1110\",\"mode\":\"%s\","
"\"primary\":\"909.5,125,7,5\",\"secondary_freq_mhz\":%.2f,\"secondary_sf\":%u,"
"\"secondary_preamble\":%u,"
"\"tcxo_us\":%lu,\"spi_hz\":%lu,\"bulk_spi\":%s,"
"\"requested\":%u,\"completed\":%u,\"spot_budget_us\":%lu,\"long_budget_us\":%lu,"
"\"primary_busy_deferrals\":%lu,"
"\"busy_deferrals\":%lu,\"drain_calls\":%lu,\"drained_packets\":%lu,"
"\"busy_high_checks\":%lu,"
"\"receiving_true_checks\":%lu,\"failures\":%lu,"
"\"busy_timeouts\":%lu,\"rx_errors\":%lu,\"cache_errors\":%lu,\"rssi_errors\":%lu,"
"\"rf_commands\":%lu,\"modulation_commands\":%lu,\"packet_commands\":%lu,"
"\"standby_commands\":%lu,\"rx_commands\":%lu,\"dio_commands\":%lu,"
"\"packet_type_reads\":%lu,\"clear_commands\":%lu,\"fs_commands\":%lu,"
"\"pending_irq_snapshots\":%lu,\"fast_resumes\":%lu,\"restored\":%s,"
"\"standby_mean_us\":%.3f,\"apply_mean_us\":%.3f,\"receiving_check_mean_us\":%.3f,"
"\"start_mode_mean_us\":%.3f,\"start_rx_mean_us\":%.3f,"
"\"clear_mean_us\":%.3f,\"packet_mean_us\":%.3f,\"set_rx_mean_us\":%.3f,"
"\"spot_to_primary\":{\"n\":%lu,\"min_us\":%.3f,\"mean_us\":%.3f,\"max_us\":%.3f},"
"\"spot_to_secondary\":{\"n\":%lu,\"min_us\":%.3f,\"mean_us\":%.3f,\"max_us\":%.3f},"
"\"to_primary\":{\"n\":%lu,\"min_us\":%.3f,\"mean_us\":%.3f,\"max_us\":%.3f},"
"\"to_secondary\":{\"n\":%lu,\"min_us\":%.3f,\"mean_us\":%.3f,\"max_us\":%.3f}}\n",
(unsigned long)sequence, mode, double(secondary.params.freq), unsigned(secondary.params.sf),
unsigned(secondary.params.preamble),
(unsigned long)configuredTcxoUs, (unsigned long)radioHal.spiHz,
radioHal.bulkTransfer ? "true" : "false",
count, completed, (unsigned long)spotBudgetUs, (unsigned long)longBudgetUs,
(unsigned long)primaryBusyDeferrals, (unsigned long)busyDeferrals,
(unsigned long)drainCalls, (unsigned long)drainedPackets,
(unsigned long)(driver.busyHighChecks - busyHighBefore),
(unsigned long)(driver.receivingTrueChecks - receivingTrueBefore),
(unsigned long)failures, (unsigned long)busyTimeouts, (unsigned long)rxErrors,
(unsigned long)cacheErrors, (unsigned long)rssiErrors,
(unsigned long)(radioHal.rfWrites - rfBefore),
(unsigned long)(radioHal.modulationWrites - modBefore),
(unsigned long)(radioHal.packetWrites - packetBefore),
(unsigned long)(radioHal.standbyWrites - standbyBefore),
(unsigned long)(radioHal.rxWrites - rxBefore),
(unsigned long)(radioHal.dioWrites - dioBefore),
(unsigned long)(radioHal.packetTypeReads - packetTypeBefore),
(unsigned long)(radioHal.clearWrites - clearBefore),
(unsigned long)(radioHal.fsWrites - fsBefore),
(unsigned long)(chip.hilPendingIrqSnapshots - pendingBefore),
(unsigned long)(chip.getOptimizedProfileSwitches() - fastBefore), safe ? "true" : "false",
chip.standbyCount == standbyCountBefore ? 0.0 : double(chip.standbyCycles - standbyCyclesBefore)
* 1000000.0 / (chip.standbyCount - standbyCountBefore) / SystemCoreClock,
driver.applyCount == applyCountBefore ? 0.0 : double(driver.applyCycles - applyCyclesBefore)
* 1000000.0 / (driver.applyCount - applyCountBefore) / SystemCoreClock,
driver.receivingCount == receivingCountBefore ? 0.0 : double(driver.receivingCycles - receivingCyclesBefore)
* 1000000.0 / (driver.receivingCount - receivingCountBefore) / SystemCoreClock,
driver.startModeCount == startModeCountBefore ? 0.0 : double(driver.startModeCycles - startModeCyclesBefore)
* 1000000.0 / (driver.startModeCount - startModeCountBefore) / SystemCoreClock,
chip.startRxCount == startRxCountBefore ? 0.0 : double(chip.startRxCycles - startRxCyclesBefore)
* 1000000.0 / (chip.startRxCount - startRxCountBefore) / SystemCoreClock,
chip.getOptimizedProfileSwitches() == fastBefore ? 0.0
: double(chip.fastClearCycles - clearCyclesBefore) * 1000000.0
/ (chip.getOptimizedProfileSwitches() - fastBefore) / SystemCoreClock,
chip.getOptimizedProfileSwitches() == fastBefore ? 0.0
: double(chip.fastPacketCycles - packetCyclesBefore) * 1000000.0
/ (chip.getOptimizedProfileSwitches() - fastBefore) / SystemCoreClock,
chip.getOptimizedProfileSwitches() == fastBefore ? 0.0
: double(chip.fastSetRxCycles - setRxCyclesBefore) * 1000000.0
/ (chip.getOptimizedProfileSwitches() - fastBefore) / SystemCoreClock,
(unsigned long)spot[0].n, spot[0].minUs(), spot[0].meanUs(), spot[0].maxUs(),
(unsigned long)spot[1].n, spot[1].minUs(), spot[1].meanUs(), spot[1].maxUs(),
(unsigned long)total[0].n, total[0].minUs(), total[0].meanUs(), total[0].maxUs(),
(unsigned long)total[1].n, total[1].minUs(), total[1].meanUs(), total[1].maxUs());
}
void setup() {
Serial.begin(115200);
delay(1500);
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
NRF_POWER->DCDCEN = 1;
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
SPI.begin();
initError = chip.begin(909.5f, 125, 7, 5,
RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, 0, 32, 1.6f);
if (!initError) initError = chip.setCRC(2);
if (!initError) initError = chip.explicitHeader();
if (!initError) chip.setRfSwitchTable(switchPins, switchModes);
if (!initError) initError = chip.setRxBoostedGainMode(true);
const uint32_t cycles = DWT->CYCCNT;
delayMicroseconds(100);
ready = initError == RADIOLIB_ERR_NONE && DWT->CYCCNT != cycles;
if (ready) { driver.begin(); driver.startRx(); driver.enableWarmStandby(); }
}
void loop() {
if (NRF_WDT->RUNSTATUS)
for (unsigned i = 0; i < 8; ++i) NRF_WDT->RR[i] = 0x6E524635;
static char line[96];
static size_t used = 0;
static bool overflow = false;
while (Serial.available()) {
const char c = Serial.read();
if (c == '\r' || c == '\n') {
if (overflow) { used = 0; overflow = false; Serial.println("{\"error\":\"command too long\"}"); continue; }
if (!used) continue;
line[used] = 0; used = 0;
unsigned count; unsigned long seq; char trailing; char mode[16];
if (!strcmp(line, "reboot")) {
Serial.println("{\"rebooting\":true}");
Serial.flush();
delay(20);
NVIC_SystemReset();
} else if (!strcmp(line, "info")) {
char info[384];
snprintf(info, sizeof(info),
"{\"bench\":\"t1000-lr1110-production-retune-v1\",\"ready\":%s,"
"\"init_rc\":%d,\"board\":\"Seeed T1000-E LR1110\","
"\"rx_frequencies_mhz\":[909.5,909.75],\"autonomous_tx\":false,\"tx_command\":false,"
"\"sd_fwid\":%u,\"app_base\":%u,\"cycle_hz\":%lu,\"tcxo_us\":%lu,"
"\"spi_hz\":%lu,\"bulk_spi\":%s,\"last_sequence\":%lu}\n",
ready ? "true" : "false", initError,
unsigned(*reinterpret_cast<volatile uint16_t*>(0x300c)),
unsigned(*reinterpret_cast<volatile uint32_t*>(0x3008)),
(unsigned long)SystemCoreClock, (unsigned long)configuredTcxoUs,
(unsigned long)radioHal.spiHz, radioHal.bulkTransfer ? "true" : "false",
(unsigned long)nextSequence);
Serial.print(info);
} else if (sscanf(line, "fs %u %c", &count, &trailing) == 1 && count <= 1) {
chip.hilUseFsOnRetune = count;
Serial.printf("{\"fs_retune\":%s}\n", count ? "true" : "false");
} else if (sscanf(line, "clear %u %c", &count, &trailing) == 1 && count <= 2) {
chip.hilFastClearMode = count;
Serial.printf("{\"fast_clear_mode\":%u}\n", count);
} else if (sscanf(line, "inject %u %c", &count, &trailing) == 1 && count <= 1) {
injectTimeoutNextRun = count;
Serial.printf("{\"inject_timeout_next_run\":%s}\n", count ? "true" : "false");
} else if (sscanf(line, "bulk %u %c", &count, &trailing) == 1 && count <= 1) {
radioHal.bulkTransfer = count;
Serial.printf("{\"bulk_spi\":%s}\n", radioHal.bulkTransfer ? "true" : "false");
} else if (sscanf(line, "spihz %u %c", &count, &trailing) == 1
&& (count == 2000000 || count == 8000000 || count == 16000000)) {
radioHal.setSpiHz(count);
Serial.printf("{\"spi_hz\":%lu}\n", (unsigned long)radioHal.spiHz);
} else if (sscanf(line, "tcxo %u %c", &count, &trailing) == 1) {
if ((!ready && count != 1600) || (count != 0 && count != 50 && count != 100
&& (count < 150 || count > 200 || count % 10 != 0)
&& count != 400 && count != 800 && count != 1600)) {
Serial.println("{\"error\":\"unsupported tcxo delay\"}");
} else {
driver.stopRx();
int rc = chip.setTCXO(1.6f, count);
if (!rc) rc = chip.calibrate(0x3F);
delay(50);
driver.startRx();
if (!rc && driver.isInRecvMode() && waitBusy()) {
configuredTcxoUs = count;
driver.enableWarmStandby();
Serial.printf("{\"tcxo_us\":%lu,\"ready\":true}\n", (unsigned long)configuredTcxoUs);
} else {
ready = false;
Serial.printf("{\"error\":\"tcxo reconfigure failed\",\"rc\":%d}\n", rc);
}
}
} else if (sscanf(line, "run %15s %u %lu %c", mode, &count, &seq, &trailing) == 3)
run(mode, count, seq);
else if (sscanf(line, "result result %lu %c", &seq, &trailing) == 1)
replayBenchResult("result", seq);
else Serial.println("{\"error\":\"unsupported command\"}");
} else if (!overflow) {
if (used < sizeof(line) - 1) line[used++] = c;
else overflow = true;
}
}
delay(1);
}