Add receive-only same-image display, SPI, modem/IQ-cache and scoped-priority experiments with checkpointed collectors. Keep rejected clock-phase correction and elevated-priority experiments out of production. Publish sanitized timing data, Full Companion/Bluetooth/USB checks and redraw measurements with their limits. Wire the new behavioral checks into CI.
545 lines
25 KiB
C++
545 lines
25 KiB
C++
// Receive-only, RAM-only, same-image A/B harness for the production T-Deck
|
|
// display and production radio2 scheduler. There is deliberately no TX path.
|
|
#include <Arduino.h>
|
|
#include <SPI.h>
|
|
#include <stdarg.h>
|
|
#include <atomic>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/task.h>
|
|
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
|
#include <helpers/radiolib/ESP32BufferedRadioHal.h>
|
|
#include "TDeckRadioHal.h"
|
|
#include <helpers/radiolib/ESP32RetunePriority.h>
|
|
#include <helpers/ui/ST7789LCDDisplay.h>
|
|
|
|
namespace {
|
|
// Exact min/mean/max plus bounded histogram percentiles. The percentile field
|
|
// is the upper edge of a bucket, not an interpolated/exact percentile.
|
|
const uint32_t timingEdges[] = {20, 50, 100, 200, 500, 750, 1000, 1500,
|
|
2000, 3000, 5000, 7500, 10000, 20000, 40000, 80000, 160000, UINT32_MAX};
|
|
struct Timing {
|
|
uint32_t n = 0, lo = UINT32_MAX, hi = 0;
|
|
uint64_t sum = 0;
|
|
uint32_t buckets[sizeof(timingEdges) / sizeof(timingEdges[0])] = {};
|
|
void add(uint32_t us) {
|
|
++n;
|
|
if (us < lo) lo = us;
|
|
if (us > hi) hi = us;
|
|
sum += us;
|
|
for (unsigned i = 0; i < sizeof(buckets) / sizeof(buckets[0]); ++i) {
|
|
if (us <= timingEdges[i]) { ++buckets[i]; break; }
|
|
}
|
|
}
|
|
uint32_t percentileUpper(unsigned percent) const {
|
|
if (!n) return 0;
|
|
const uint32_t target = (uint64_t(n) * percent + 99) / 100;
|
|
uint32_t cumulative = 0;
|
|
for (unsigned i = 0; i < sizeof(buckets) / sizeof(buckets[0]); ++i) {
|
|
cumulative += buckets[i];
|
|
if (cumulative >= target) return timingEdges[i];
|
|
}
|
|
return hi;
|
|
}
|
|
};
|
|
|
|
class BenchBoard : public mesh::MainBoard {
|
|
public:
|
|
uint16_t getBattMilliVolts() override { return 0; }
|
|
const char* getManufacturerName() const override { return "T-Deck display timing HIL"; }
|
|
void reboot() override { ESP.restart(); }
|
|
uint8_t getStartupReason() const override { return 0; }
|
|
} board;
|
|
|
|
// Default construction matches production's HSPI selection. The display has
|
|
// its own SPIClass(HSPI) handle and the same physical SCLK/MOSI pins.
|
|
SPIClass radioSpi;
|
|
class BenchHal : public TDeckRadioHal {
|
|
public:
|
|
bool bulkTransfer = false;
|
|
uint32_t transfers = 0, bytes = 0, modemQueries = 0, actualHz = 0;
|
|
explicit BenchHal(SPIClass& bus) : TDeckRadioHal(bus, 2000000) {}
|
|
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
|
|
++transfers; bytes += len;
|
|
actualHz = spiClockDivToFrequency(spi->getClockDivider());
|
|
if (len && out[0] == RADIOLIB_SX126X_CMD_GET_PACKET_TYPE) ++modemQueries;
|
|
if (bulkTransfer) ESP32BufferedRadioHal::spiTransfer(out, len, in);
|
|
else ArduinoHal::spiTransfer(out, len, in);
|
|
}
|
|
} radioHal(radioSpi);
|
|
CustomSX1262 chip = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1,
|
|
P_LORA_RESET, P_LORA_BUSY);
|
|
|
|
class BenchWrapper : public CustomSX1262Wrapper {
|
|
uint32_t retuneStarted = 0, visitStarted = 0, dueAfterUs = 0;
|
|
uint8_t retuneFrom = 0;
|
|
public:
|
|
ESP32RetunePriority retunePriority;
|
|
using CustomSX1262Wrapper::CustomSX1262Wrapper;
|
|
bool measuring = false;
|
|
Timing retune[2], visit[2], late[2];
|
|
uint32_t successfulHops = 0, failedRetunes = 0;
|
|
unsigned actualRetunePriority = 0;
|
|
void resetMeasurements() {
|
|
for (unsigned i = 0; i < 2; ++i) { retune[i] = {}; visit[i] = {}; late[i] = {}; }
|
|
successfulHops = failedRetunes = 0;
|
|
actualRetunePriority = 0;
|
|
}
|
|
bool floorCalibrationPending() const { return _nf_refresh_requested; }
|
|
uint32_t nominalVisitUs(uint8_t profile) const {
|
|
return _profiles.listenUs(profile, profilePreamble(_profiles.slowerProfile()));
|
|
}
|
|
protected:
|
|
void beginProfileRetune(bool continuousRx) override {
|
|
retuneStarted = micros();
|
|
retunePriority.begin(continuousRx);
|
|
if (unsigned(uxTaskPriorityGet(nullptr)) > actualRetunePriority)
|
|
actualRetunePriority = uxTaskPriorityGet(nullptr);
|
|
retuneFrom = _active_profile;
|
|
visitStarted = _profile_visit_stamp;
|
|
dueAfterUs = nominalVisitUs(retuneFrom);
|
|
// Match serviceProfileScan's temporary noise-floor sample extension.
|
|
// A packet can legitimately extend an RX visit further; report received
|
|
// packets alongside lateness so that a busy-channel run is identifiable.
|
|
if (_nf_refresh_requested && profileFloorEstimator(retuneFrom).ready(uint32_t(millis()))
|
|
&& dueAfterUs < NoiseFloorSettleMillis * 1000UL)
|
|
dueAfterUs = NoiseFloorSettleMillis * 1000UL;
|
|
CustomSX1262Wrapper::beginProfileRetune(continuousRx);
|
|
}
|
|
void endProfileRetune(bool success) override {
|
|
CustomSX1262Wrapper::endProfileRetune(success);
|
|
retunePriority.end();
|
|
const uint32_t done = micros();
|
|
if (!measuring) return;
|
|
if (!success) { ++failedRetunes; return; }
|
|
if (retuneFrom == _active_profile) return;
|
|
++successfulHops;
|
|
retune[_active_profile].add(done - retuneStarted);
|
|
const uint32_t dwell = retuneStarted - visitStarted;
|
|
visit[retuneFrom].add(dwell);
|
|
late[retuneFrom].add(dwell > dueAfterUs ? dwell - dueAfterUs : 0);
|
|
}
|
|
} driver(chip, board);
|
|
|
|
ST7789LCDDisplay display;
|
|
bool ready = false, measuringService = false;
|
|
uint32_t lastServiceUs = 0, receivePackets = 0;
|
|
Timing serviceGap, serviceDuration, frameDuration;
|
|
char savedResult[6144] = {};
|
|
unsigned savedSequence = 0;
|
|
size_t resultUsed = 0;
|
|
std::atomic<bool> stressEnabled{false};
|
|
std::atomic<uint32_t> stressRounds{0};
|
|
bool stressTaskReady = false;
|
|
unsigned setupStage = 0;
|
|
int setupApplyResult = -1;
|
|
bool exactSpiClock = false;
|
|
uint32_t clockSettingHz = 8000000;
|
|
void configureRadioClock(unsigned spiMHz) {
|
|
clockSettingHz = spiMHz * 1000000UL;
|
|
if (exactSpiClock)
|
|
clockSettingHz = spiClockDivToFrequency(spiFrequencyToClockDiv(clockSettingHz));
|
|
radioHal.spiSettings = SPISettings(clockSettingHz, MSBFIRST, SPI_MODE0);
|
|
}
|
|
// A bounded, same-core priority-1 CPU competitor. It never accesses the radio,
|
|
// display, or SPI and sleeps between 250-us bursts. Report it separately from
|
|
// the unloaded measurements; it is not a simulation of Bluetooth's stack.
|
|
void stressTask(void*) {
|
|
for (;;) {
|
|
if (stressEnabled.load(std::memory_order_relaxed)) {
|
|
const uint32_t started = micros();
|
|
while (uint32_t(micros() - started) < 250) {}
|
|
stressRounds.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(2));
|
|
}
|
|
}
|
|
|
|
bool appendResult(const char* format, ...) {
|
|
if (resultUsed >= sizeof(savedResult)) return false;
|
|
va_list args;
|
|
va_start(args, format);
|
|
const int count = vsnprintf(savedResult + resultUsed, sizeof(savedResult) - resultUsed,
|
|
format, args);
|
|
va_end(args);
|
|
if (count < 0 || size_t(count) >= sizeof(savedResult) - resultUsed) {
|
|
resultUsed = sizeof(savedResult);
|
|
return false;
|
|
}
|
|
resultUsed += count;
|
|
return true;
|
|
}
|
|
|
|
void appendTiming(const char* name, const Timing& timing) {
|
|
appendResult("\"%s\":{\"n\":%u,\"min_us\":%u,\"mean_us\":%.3f,\"max_us\":%u,"
|
|
"\"p95_upper_us\":%u,\"p99_upper_us\":%u}", name, timing.n,
|
|
timing.n ? timing.lo : 0, timing.n ? double(timing.sum) / timing.n : 0,
|
|
timing.hi, timing.percentileUpper(95), timing.percentileUpper(99));
|
|
}
|
|
|
|
bool waitBusy() {
|
|
const uint32_t started = micros();
|
|
while (chip.isChipBusy()) {
|
|
if (uint32_t(micros() - started) > 20000) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void serviceRadio() {
|
|
const uint32_t started = micros();
|
|
if (measuringService) {
|
|
if (lastServiceUs) serviceGap.add(started - lastServiceUs);
|
|
lastServiceUs = started;
|
|
}
|
|
// Keep the complete production scheduling/calibration/receive guards. The
|
|
// display callback is called only after its SPI transaction has ended.
|
|
driver.loop();
|
|
uint8_t packet[256];
|
|
const int received = driver.recvRaw(packet, sizeof(packet));
|
|
if (received > 0) {
|
|
++receivePackets;
|
|
driver.onReceiveProcessed();
|
|
}
|
|
if (measuringService) serviceDuration.add(micros() - started);
|
|
}
|
|
|
|
// Repeatable representative full UI redraw. Content and geometry are exactly
|
|
// the same for direct and buffered paths. Only the counter/bar change after
|
|
// the initial frame. Both paths redraw the full screen; the optimized path
|
|
// permits radio service between short, completed display transactions.
|
|
void drawFrame(unsigned mode, uint32_t frame) {
|
|
const uint32_t started = micros();
|
|
display.startFrame(UIColor::window_bkg);
|
|
display.setColor(UIColor::title_bkg);
|
|
display.fillRect(0, 0, display.width(), 28);
|
|
display.setColor(UIColor::title_txt);
|
|
display.setTextSize(2);
|
|
display.setCursor(10, 6);
|
|
display.print("T-Deck radio2 HIL");
|
|
display.setTextSize(1);
|
|
display.setColor(UIColor::primary_txt);
|
|
display.setCursor(12, 46);
|
|
display.print("Receive-only production profile scheduler");
|
|
display.setCursor(12, 68);
|
|
display.print("Primary: 909.5 MHz SF7 / BW62.5");
|
|
display.setCursor(12, 88);
|
|
display.print("Radio2: 910.5 MHz SF8 / BW500");
|
|
display.setColor(UIColor::corp_blue);
|
|
display.drawRect(12, 116, display.width() - 24, 28);
|
|
display.fillRect(16, 120, (frame % 20) * (display.width() - 32) / 20, 20);
|
|
char text[72];
|
|
snprintf(text, sizeof(text), "Frame %06lu render %s", (unsigned long)frame,
|
|
mode == 1 ? "direct" : "buffered");
|
|
display.setColor(UIColor::primary_txt);
|
|
display.setCursor(12, 168);
|
|
display.print(text);
|
|
display.setCursor(12, 200);
|
|
display.print("Native USB commands; no autonomous TX");
|
|
display.endFrame();
|
|
if (measuringService) frameDuration.add(micros() - started);
|
|
}
|
|
|
|
bool setProfiles(unsigned secondaryPreamble = 32) {
|
|
setupStage = 1; setupApplyResult = -1;
|
|
mesh::RadioProfileConfig secondary;
|
|
secondary.mode = mesh::RadioProfileMode::Off;
|
|
driver.profiles()->setSecondary(secondary, true);
|
|
for (unsigned i = 0; i < 4; ++i) { serviceRadio(); if (!waitBusy()) return false; }
|
|
mesh::RadioProfileParams primary;
|
|
primary.freq = 909.5f; primary.bw = 62.5f; primary.sf = 7;
|
|
primary.cr = 5; primary.preamble = 32;
|
|
setupStage = 2;
|
|
setupApplyResult = int(driver.trySetPrimaryParams(primary, true));
|
|
if (setupApplyResult != int(mesh::RadioParamApplyResult::APPLIED))
|
|
return false;
|
|
setupStage = 3;
|
|
secondary.params.freq = 910.5f; secondary.params.bw = 500;
|
|
secondary.params.sf = 8; secondary.params.cr = 5;
|
|
secondary.params.preamble = secondaryPreamble;
|
|
secondary.mode = mesh::RadioProfileMode::Rx;
|
|
driver.profiles()->setSecondary(secondary, true);
|
|
const uint32_t started = millis();
|
|
while (uint32_t(millis() - started) < 300) {
|
|
serviceRadio();
|
|
delay(0);
|
|
}
|
|
setupStage = 4;
|
|
return driver.profiles()->switchTestReady() && driver.isInRecvMode() && waitBusy();
|
|
}
|
|
|
|
void printInfo() {
|
|
Serial.printf("{\"bench\":\"tdeck-display-radio2-v2\",\"ready\":%s,"
|
|
"\"autonomous_tx\":false,\"psram_bytes\":%u,\"free_psram_bytes\":%u,"
|
|
"\"free_heap_bytes\":%u,\"buffered\":%s,\"width\":%d,\"height\":%d,"
|
|
"\"stress_task_ready\":%s,\"loop_priority\":%u,\"core\":%u,"
|
|
"\"clock_fix_supported\":%s,"
|
|
"\"command\":\"trial <display:0,1,2> <MHz:8..16> <modemcache:0,1> "
|
|
"<iqcache:0,1> <priority:0,3> <load:0,1> <preamble:32,40> "
|
|
"<seconds:1..10> <fps:1..30> <seq>\"}\n",
|
|
ready ? "true" : "false", ESP.getPsramSize(), ESP.getFreePsram(),
|
|
ESP.getFreeHeap(), display.isBufferedRendering() ? "true" : "false",
|
|
display.width(), display.height(), stressTaskReady ? "true" : "false",
|
|
unsigned(uxTaskPriorityGet(nullptr)), unsigned(xPortGetCoreID()),
|
|
TDeckRadioHal::supportsClockTimingCorrection() ? "true" : "false");
|
|
}
|
|
|
|
// Read-only link diagnostic, outside timed trials. Put the chip in standby
|
|
// at the known 8 MHz clock before changing the transfer policy; recover at
|
|
// 8 MHz afterwards. No identity/settings writes or RF transmission.
|
|
void inspectSpi(unsigned spiMHz, unsigned bulk, unsigned count = 100) {
|
|
radioHal.spiSettings = SPISettings(8000000, MSBFIRST, SPI_MODE0);
|
|
radioHal.bulkTransfer = true;
|
|
const int16_t standby = chip.standby();
|
|
uint8_t reference[2] = {};
|
|
const int16_t referenceRc = chip.readRegister(0x0740, reference, 2);
|
|
configureRadioClock(spiMHz);
|
|
radioHal.bulkTransfer = bulk;
|
|
unsigned correct = 0, bad = 0, firstBad = 0, syncBad = 0, readErrors = 0;
|
|
int firstReadError = 0;
|
|
for (unsigned i = 0; i < count; ++i) {
|
|
uint8_t modem = 0xff, sync[2] = {};
|
|
const int16_t modemRc = chip.getMod()->SPIreadStream(
|
|
RADIOLIB_SX126X_CMD_GET_PACKET_TYPE, &modem, 1);
|
|
const int16_t syncRc = chip.readRegister(0x0740, sync, 2);
|
|
if (modemRc != RADIOLIB_ERR_NONE || syncRc != RADIOLIB_ERR_NONE) {
|
|
if (!readErrors) firstReadError = modemRc ? modemRc : syncRc;
|
|
++readErrors;
|
|
}
|
|
if (modemRc == RADIOLIB_ERR_NONE && modem == RADIOLIB_SX126X_PACKET_TYPE_LORA) ++correct;
|
|
else { if (!bad) firstBad = modem; ++bad; }
|
|
if (syncRc != RADIOLIB_ERR_NONE || memcmp(sync, reference, sizeof(sync))) ++syncBad;
|
|
}
|
|
const uint32_t actualHz = radioHal.actualHz;
|
|
const uint32_t originalClock = radioHal.getOriginalClockTiming();
|
|
const uint32_t correctedClock = radioHal.getCorrectedClockTiming();
|
|
radioHal.spiSettings = SPISettings(8000000, MSBFIRST, SPI_MODE0);
|
|
radioHal.bulkTransfer = true;
|
|
const unsigned recoveryModem = chip.getPacketType();
|
|
const unsigned errors = chip.getDeviceErrors();
|
|
const int16_t resumed = chip.startReceive();
|
|
Serial.printf("{\"spi_inspection\":true,\"spi_mhz\":%u,\"bulk\":%s,"
|
|
"\"actual_spi_hz\":%u,\"read_pairs\":%u,\"exact_clock\":%s,"
|
|
"\"clock_setting_hz\":%u,\"clock_timing_corrected\":%s,"
|
|
"\"spi_clock_original\":%u,\"spi_clock_corrected\":%u,"
|
|
"\"standby_result\":%d,\"reference_result\":%d,"
|
|
"\"reference_sync\":[%u,%u],\"correct_modem_reads\":%u,"
|
|
"\"bad_modem_reads\":%u,\"first_bad_modem\":%u,"
|
|
"\"bad_sync_reads\":%u,\"read_errors\":%u,\"first_read_error\":%d,"
|
|
"\"recovery_modem\":%u,\"chip_errors_at_8mhz\":%u,"
|
|
"\"rx_resume_at_8mhz\":%d}\n", spiMHz, bulk ? "true" : "false",
|
|
actualHz, count, exactSpiClock ? "true" : "false", clockSettingHz,
|
|
radioHal.isClockTimingCorrectionActive() ? "true" : "false",
|
|
originalClock, correctedClock,
|
|
standby, referenceRc, reference[0], reference[1], correct,
|
|
bad, firstBad, syncBad, readErrors, firstReadError, recoveryModem, errors, resumed);
|
|
}
|
|
|
|
void run(unsigned mode, unsigned spiMHz, unsigned bulk, unsigned seconds,
|
|
unsigned fps, unsigned sequence, unsigned modemCache = 0,
|
|
unsigned iqCache = 0, unsigned priority = 0, unsigned load = 0,
|
|
unsigned secondaryPreamble = 32) {
|
|
if (!ready || !waitBusy()) { Serial.println("{\"error\":\"radio not ready\"}"); return; }
|
|
measuringService = driver.measuring = false;
|
|
radioHal.bulkTransfer = bulk;
|
|
configureRadioClock(spiMHz);
|
|
chip.hilUseFsOnRetune = true;
|
|
chip.hilSkipUnchangedPacket = true;
|
|
chip.hilSkipConfirmedModemQuery = modemCache;
|
|
chip.hilSkipUnchangedIq = iqCache;
|
|
driver.retunePriority.setPriority(priority);
|
|
display.setBufferedRendering(mode == 2);
|
|
display.setRenderYieldCallback(mode == 2 ? serviceRadio : nullptr);
|
|
if (mode == 2 && !display.isBufferedRendering()) {
|
|
Serial.println("{\"error\":\"display buffer unavailable\"}"); return;
|
|
}
|
|
if (!setProfiles(secondaryPreamble)) {
|
|
Serial.printf("{\"error\":\"profile setup failed\",\"setup_stage\":%u,"
|
|
"\"primary_apply_result\":%d,\"switch_failures\":%u,"
|
|
"\"spi_mhz\":%u,\"actual_spi_hz\":%u,\"chip_errors\":%u,"
|
|
"\"rx_mode\":%d}\n", setupStage, setupApplyResult,
|
|
driver.profiles()->switch_failures, spiMHz, radioHal.actualHz,
|
|
chip.getDeviceErrors(), sx126xReceiveMode(&chip));
|
|
return;
|
|
}
|
|
// Prime the frame buffer before measuring. Both redraw paths start from
|
|
// an already visible frame; no-display leaves that static screen in place.
|
|
if (mode) drawFrame(mode, 0);
|
|
if (!waitBusy()) { Serial.println("{\"error\":\"warmup busy timeout\"}"); return; }
|
|
const uint16_t chipErrorsBefore = chip.getDeviceErrors();
|
|
const uint32_t switchesBefore = driver.profiles()->switches;
|
|
const uint32_t failuresBefore = driver.profiles()->switch_failures;
|
|
const uint32_t optimizedBefore = chip.getOptimizedProfileSwitches();
|
|
const uint32_t modemSkipsBefore = chip.hilModemQuerySkips;
|
|
const uint32_t iqSkipsBefore = chip.hilIqWorkaroundSkips;
|
|
const uint32_t transfersBefore = radioHal.transfers, bytesBefore = radioHal.bytes;
|
|
const uint32_t modemQueriesBefore = radioHal.modemQueries;
|
|
const uint32_t stressBefore = stressRounds.load(std::memory_order_relaxed);
|
|
const unsigned originalPriority = uxTaskPriorityGet(nullptr);
|
|
stressEnabled.store(load != 0, std::memory_order_relaxed);
|
|
driver.resetMeasurements();
|
|
serviceGap = {}; serviceDuration = {}; frameDuration = {};
|
|
receivePackets = lastServiceUs = 0;
|
|
measuringService = driver.measuring = true;
|
|
const uint32_t started = micros();
|
|
uint32_t nextFrame = started, frames = 0;
|
|
const uint32_t intervalUs = 1000000UL / fps;
|
|
while (uint32_t(micros() - started) < seconds * 1000000UL) {
|
|
serviceRadio();
|
|
const uint32_t now = micros();
|
|
if (mode && int32_t(now - nextFrame) >= 0) {
|
|
drawFrame(mode, ++frames);
|
|
// Do not pile up overdue frames and starve the rest of the application.
|
|
nextFrame = now + intervalUs;
|
|
}
|
|
delay(0);
|
|
}
|
|
const uint32_t elapsedUs = micros() - started;
|
|
stressEnabled.store(false, std::memory_order_relaxed);
|
|
measuringService = driver.measuring = false;
|
|
display.setRenderYieldCallback(nullptr);
|
|
const bool idle = waitBusy();
|
|
const uint16_t chipErrorsAfter = chip.getDeviceErrors();
|
|
const int receiveMode = sx126xReceiveMode(&chip);
|
|
const unsigned active = driver.receiveProfile();
|
|
const auto& expected = driver.profiles()->params(active);
|
|
const bool cacheCorrect = chip.spreadingFactor == expected.sf
|
|
&& chip.bandwidthKhz == expected.bw && chip.codingRate == expected.cr - 4;
|
|
resultUsed = 0;
|
|
savedSequence = sequence;
|
|
savedResult[0] = 0;
|
|
appendResult("{\"result\":%u,\"bench\":\"tdeck-display-radio2-v2\",\"display_mode\":%u,"
|
|
"\"spi_mhz\":%u,\"bulk\":%s,\"seconds\":%u,\"requested_fps\":%u,"
|
|
"\"elapsed_us\":%u,\"frames\":%u,\"switches\":%u,\"switch_failures\":%u,"
|
|
"\"hook_hops\":%u,\"failed_retunes\":%u,\"optimized_rx_resumes\":%u,"
|
|
"\"received_packets\":%u,\"chip_errors_before\":%u,\"chip_errors_after\":%u,"
|
|
"\"busy_timeout\":%s,\"rx_mode\":%d,\"cache_correct\":%s,"
|
|
"\"floor_calibration_pending\":%s,\"nominal_visit_us\":[%u,%u],",
|
|
sequence, mode, spiMHz, bulk ? "true" : "false", seconds, fps,
|
|
elapsedUs, frames, driver.profiles()->switches - switchesBefore,
|
|
driver.profiles()->switch_failures - failuresBefore, driver.successfulHops,
|
|
driver.failedRetunes, chip.getOptimizedProfileSwitches() - optimizedBefore,
|
|
receivePackets, chipErrorsBefore, chipErrorsAfter, idle ? "false" : "true",
|
|
receiveMode, cacheCorrect ? "true" : "false",
|
|
driver.floorCalibrationPending() ? "true" : "false",
|
|
driver.nominalVisitUs(0), driver.nominalVisitUs(1));
|
|
appendResult("\"modem_cache\":%s,\"iq_cache\":%s,\"retune_priority\":%u,"
|
|
"\"synthetic_load\":%s,\"secondary_preamble\":%u,"
|
|
"\"modem_query_skips\":%u,\"iq_workaround_skips\":%u,\"actual_spi_hz\":%u,"
|
|
"\"spi_transfers\":%u,\"spi_bytes\":%u,\"modem_queries\":%u,"
|
|
"\"stress_rounds\":%u,\"original_priority\":%u,\"final_priority\":%u,"
|
|
"\"actual_retune_priority\":%u,\"core\":%u,\"exact_clock\":%s,"
|
|
"\"clock_setting_hz\":%u,\"clock_timing_corrected\":%s,"
|
|
"\"spi_clock_original\":%u,\"spi_clock_corrected\":%u,",
|
|
modemCache ? "true" : "false", iqCache ? "true" : "false", priority,
|
|
load ? "true" : "false", secondaryPreamble,
|
|
chip.hilModemQuerySkips - modemSkipsBefore,
|
|
chip.hilIqWorkaroundSkips - iqSkipsBefore,
|
|
radioHal.actualHz,
|
|
radioHal.transfers - transfersBefore, radioHal.bytes - bytesBefore,
|
|
radioHal.modemQueries - modemQueriesBefore,
|
|
stressRounds.load(std::memory_order_relaxed) - stressBefore,
|
|
originalPriority, unsigned(uxTaskPriorityGet(nullptr)),
|
|
driver.actualRetunePriority, unsigned(xPortGetCoreID()),
|
|
exactSpiClock ? "true" : "false", clockSettingHz,
|
|
radioHal.isClockTimingCorrectionActive() ? "true" : "false",
|
|
radioHal.getOriginalClockTiming(), radioHal.getCorrectedClockTiming());
|
|
appendTiming("service_gap", serviceGap); appendResult(",");
|
|
appendTiming("service_duration", serviceDuration); appendResult(",");
|
|
appendTiming("frame", frameDuration); appendResult(",\"directions\":[");
|
|
for (unsigned i = 0; i < 2; ++i) {
|
|
if (i) appendResult(",");
|
|
appendResult("{\"profile\":%u,", i);
|
|
appendTiming("retune_to", driver.retune[i]); appendResult(",");
|
|
appendTiming("visit_before_departure", driver.visit[i]); appendResult(",");
|
|
appendTiming("deadline_lateness", driver.late[i]); appendResult("}");
|
|
}
|
|
// ESP-IDF reports the high-water mark in bytes. This includes synchronous
|
|
// nesting of the display's stack strip buffer and radio-service callback.
|
|
appendResult("],\"free_heap_bytes\":%u,\"free_psram_bytes\":%u,"
|
|
"\"loop_stack_min_free_bytes\":%u}",
|
|
ESP.getFreeHeap(), ESP.getFreePsram(),
|
|
unsigned(uxTaskGetStackHighWaterMark(nullptr)));
|
|
if (resultUsed >= sizeof(savedResult)) {
|
|
savedSequence = 0;
|
|
Serial.println("{\"error\":\"result overflow\"}");
|
|
} else Serial.println(savedResult);
|
|
}
|
|
} // namespace
|
|
|
|
void setup() {
|
|
Serial.setTxBufferSize(8192);
|
|
Serial.setTxTimeoutMs(250);
|
|
Serial.begin(115200);
|
|
// Shared peripheral rail, TFT and SD deselected before touching the bus.
|
|
pinMode(PIN_PERF_POWERON, OUTPUT); digitalWrite(PIN_PERF_POWERON, HIGH);
|
|
pinMode(PIN_TFT_CS, OUTPUT); digitalWrite(PIN_TFT_CS, HIGH);
|
|
pinMode(39, OUTPUT); digitalWrite(39, HIGH);
|
|
pinMode(P_LORA_NSS, OUTPUT); digitalWrite(P_LORA_NSS, HIGH);
|
|
delay(100);
|
|
ready = chip.std_init(&radioSpi);
|
|
if (ready) {
|
|
driver.begin();
|
|
ready = driver.setRxPowerSaving(false, 65625, 60000);
|
|
}
|
|
const bool displayReady = display.begin();
|
|
display.setBufferedRendering(false);
|
|
ready = ready && displayReady && waitBusy();
|
|
stressTaskReady = xTaskCreatePinnedToCore(stressTask, "hilCpuLoad", 2048,
|
|
nullptr, 1, nullptr, xPortGetCoreID()) == pdPASS;
|
|
printInfo();
|
|
}
|
|
|
|
void loop() {
|
|
static char line[112];
|
|
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;
|
|
unsigned mode, spiMHz, bulk, seconds, fps, sequence;
|
|
unsigned modemCache, iqCache, priority, load, preamble, count;
|
|
char trailing;
|
|
if (!strcmp(line, "info")) printInfo();
|
|
else if (!strcmp(line, "reboot")) ESP.restart();
|
|
else if (sscanf(line, "clockfix %u %c", &bulk, &trailing) == 1 && bulk <= 1) {
|
|
const bool supported = radioHal.setClockTimingCorrection(bulk);
|
|
Serial.printf("{\"clock_fix\":%s,\"supported\":%s}\n",
|
|
radioHal.isClockTimingCorrectionActive() ? "true" : "false",
|
|
supported ? "true" : "false");
|
|
}
|
|
else if (sscanf(line, "exactclock %u %c", &bulk, &trailing) == 1 && bulk <= 1) {
|
|
exactSpiClock = bulk;
|
|
Serial.printf("{\"exact_spi_clock\":%s}\n", exactSpiClock ? "true" : "false");
|
|
} else if (sscanf(line, "inspect %u %u %u %c", &spiMHz, &bulk, &count, &trailing) == 3
|
|
&& spiMHz >= 8 && spiMHz <= 16 && bulk <= 1 && count >= 1 && count <= 10000) {
|
|
inspectSpi(spiMHz, bulk, count);
|
|
}
|
|
else if (sscanf(line, "inspect %u %u %c", &spiMHz, &bulk, &trailing) == 2
|
|
&& spiMHz >= 8 && spiMHz <= 16 && bulk <= 1) {
|
|
inspectSpi(spiMHz, bulk);
|
|
}
|
|
else if (sscanf(line, "result %u %c", &sequence, &trailing) == 1) {
|
|
if (sequence == savedSequence && savedSequence) Serial.println(savedResult);
|
|
else Serial.println("{\"error\":\"result not retained\"}");
|
|
} else if (sscanf(line, "trial %u %u %u %u %u %u %u %u %u %u %c",
|
|
&mode, &spiMHz, &modemCache, &iqCache, &priority, &load,
|
|
&preamble, &seconds, &fps, &sequence, &trailing) == 10
|
|
&& mode <= 2 && spiMHz >= 8 && spiMHz <= 16
|
|
&& modemCache <= 1 && iqCache <= 1 && (priority == 0 || priority == 3)
|
|
&& load <= 1 && (!load || stressTaskReady)
|
|
&& (preamble == 32 || preamble == 40)
|
|
&& seconds >= 1 && seconds <= 10 && fps >= 1 && fps <= 30 && sequence) {
|
|
run(mode, spiMHz, 1, seconds, fps, sequence, modemCache, iqCache,
|
|
priority, load, preamble);
|
|
} else if (sscanf(line, "run %u %u %u %u %u %u %c", &mode, &spiMHz, &bulk,
|
|
&seconds, &fps, &sequence, &trailing) == 6
|
|
&& mode <= 2 && (spiMHz == 2 || spiMHz == 8) && bulk <= 1
|
|
&& seconds >= 1 && seconds <= 10 && fps >= 1 && fps <= 30 && sequence) {
|
|
run(mode, spiMHz, bulk, seconds, fps, sequence);
|
|
} else Serial.println("{\"error\":\"invalid command; use info\"}");
|
|
} else if (used + 1 < sizeof(line)) line[used++] = c;
|
|
}
|
|
if (ready) serviceRadio();
|
|
delay(0);
|
|
}
|