Public Access
The radio's noise: the GNSS receiver costs 8 dB; a setting pauses it (#20)
Debug Builds: `lora noise test` changes one thing at a time, Sweeps the band, and reports the floor under each condition; it runs on the device by itself, since one condition pauses Wi-Fi (not saved, so a restart brings it back). Result, at 125 kHz: -117 dBm with the antenna switched off, -106 with the GNSS receiver in standby, -98 with it running. The receiver's serial line isn't it (one sentence a second changes nothing), and neither are the main loop, the CPU frequency, Wi-Fi, the screen or the radio's own regulator, all within 1 dB. Settings > "Pause GNSS for LoRa", off by default: the receiver waits in standby while the radio listens or sweeps, except during a Track, and has a Fix again about 7 s after. The GNSS App says it's paused. 11 dB remain between the antenna with GNSS quiet and the chip alone, untouched by anything that can be switched from the firmware. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -66,7 +66,7 @@ void GnssService::close() {
|
||||
|
||||
void GnssService::tick(uint32_t nowMs) {
|
||||
if (!open_) open(nowMs);
|
||||
bool wanted = settings_.getBool(Setting::GnssEnabled);
|
||||
bool wanted = settings_.getBool(Setting::GnssEnabled) && !held_;
|
||||
if (wanted && !active_) wake(nowMs);
|
||||
if (!wanted && (active_ || !standbyMs_ || nowMs - standbyMs_ >= kStandbyRenewMs)) standby(nowMs);
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ class GnssService : public Service {
|
||||
void tick(uint32_t nowMs) override;
|
||||
|
||||
bool on() const { return active_; }
|
||||
// Standby while the LoRa radio listens, when the user chose that (issue #20): the receiver
|
||||
// raises the radio's noise floor by 8 dB while it runs. Never asked for during a Track.
|
||||
void holdForLora(bool hold) { held_ = hold; }
|
||||
bool heldForLora() const { return held_ && !active_; }
|
||||
bool receiving(uint32_t nowMs) const { return active_ && lastLineMs_ && nowMs - lastLineMs_ < kSilentAfterMs; }
|
||||
const gnss::GnssState& state() const { return parser_.state(); }
|
||||
uint32_t onSinceMs() const { return openedMs_; } // when the receiver was last woken
|
||||
@@ -71,7 +75,7 @@ class GnssService : public Service {
|
||||
StorageService& storage_;
|
||||
EventBus& bus_;
|
||||
gnss::NmeaParser parser_;
|
||||
bool open_ = false, active_ = false, echo_ = false;
|
||||
bool open_ = false, active_ = false, echo_ = false, held_ = false;
|
||||
uint32_t bytes_ = 0, standbyMs_ = 0;
|
||||
uint32_t openedMs_ = 0, lastLineMs_ = 0, firstFixMs_ = 0, clockSetMs_ = 0;
|
||||
gnss::FixType lastFix_ = gnss::FixType::None;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#ifdef RORO_DEBUG
|
||||
|
||||
#include "services/noise_test.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
#include "platform/console.h"
|
||||
#include "sweep_view.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
void NoiseTest::start(std::vector<Condition> conditions, int passes) {
|
||||
if (running()) return;
|
||||
conditions_ = std::move(conditions);
|
||||
results_.clear();
|
||||
report_.clear();
|
||||
passes_ = passes;
|
||||
index_ = 0;
|
||||
console.printf("noise test: %u conditions, %d passes each\n", (unsigned)conditions_.size(), passes_);
|
||||
begin(millis());
|
||||
}
|
||||
|
||||
void NoiseTest::begin(uint32_t nowMs) {
|
||||
const Condition& c = conditions_[index_];
|
||||
if (c.apply) c.apply();
|
||||
radio_.sweep(true); // sets the radio up again, with whatever the condition changed
|
||||
std::fill(std::begin(lowest_), std::end(lowest_), 0);
|
||||
std::fill(std::begin(counts_), std::end(counts_), 0);
|
||||
seen_ = 0;
|
||||
phase_ = Phase::Settle;
|
||||
phaseMs_ = nowMs;
|
||||
}
|
||||
|
||||
void NoiseTest::step(uint32_t nowMs) {
|
||||
if (phase_ == Phase::Idle) return;
|
||||
const Condition& c = conditions_[index_];
|
||||
if (phase_ == Phase::Settle) {
|
||||
if (nowMs - phaseMs_ < c.settleMs) return;
|
||||
lastSeq_ = radio_.sweeps();
|
||||
phase_ = Phase::Sweep;
|
||||
phaseMs_ = nowMs;
|
||||
return;
|
||||
}
|
||||
SweepFrame f;
|
||||
if (radio_.sweeps() != lastSeq_ && radio_.sweepFrame(f)) {
|
||||
lastSeq_ = f.seq;
|
||||
fromHz_ = f.fromHz, stepHz_ = f.stepHz, steps_ = f.steps;
|
||||
for (uint16_t i = 0; i < f.steps; i++) {
|
||||
lowest_[i] = seen_ ? std::min(lowest_[i], f.dbm[i]) : f.dbm[i];
|
||||
counts_[std::clamp<int>(-f.dbm[i], 0, 127)]++;
|
||||
}
|
||||
seen_++;
|
||||
}
|
||||
bool stuck = nowMs - phaseMs_ > 20000; // the radio never swept: don't hang the test
|
||||
if (seen_ < passes_ && !stuck) return;
|
||||
|
||||
Result r;
|
||||
r.name = c.name;
|
||||
if (seen_) {
|
||||
// The floor: the median of every reading. The top and the peaks: from the lowest reading
|
||||
// at each step, so a burst in one pass doesn't count and a steady carrier does.
|
||||
uint32_t total = 0, half = static_cast<uint32_t>(seen_) * steps_ / 2;
|
||||
for (int level = 127; level >= 0; level--) {
|
||||
total += counts_[level];
|
||||
if (total > half) {
|
||||
r.floor = -level;
|
||||
break;
|
||||
}
|
||||
}
|
||||
lora::SweepStats st = lora::summarize(lowest_, steps_, fromHz_, stepHz_);
|
||||
r.top = st.top;
|
||||
char p[32];
|
||||
for (auto& peak : st.peaks) {
|
||||
std::snprintf(p, sizeof p, "%s%.1f %d", r.peaks.empty() ? "" : ", ", peak.hz / 1e6, peak.dbm);
|
||||
r.peaks += p;
|
||||
}
|
||||
} else {
|
||||
r.name += " (no sweep)";
|
||||
}
|
||||
results_.push_back(r);
|
||||
radio_.sweep(false);
|
||||
if (c.restore) c.restore();
|
||||
if (++index_ < conditions_.size()) begin(nowMs);
|
||||
else finish();
|
||||
}
|
||||
|
||||
void NoiseTest::finish() {
|
||||
phase_ = Phase::Idle;
|
||||
char line[160];
|
||||
report_.push_back("noise test: floor = median of every reading; top and peaks = steady (lowest of the passes), dBm at 125 kHz");
|
||||
for (auto& r : results_) {
|
||||
std::snprintf(line, sizeof line, "noise test: %-26s floor %4d top %4d %s", r.name.c_str(), r.floor, r.top,
|
||||
r.peaks.empty() ? "no steady peak" : r.peaks.c_str());
|
||||
report_.push_back(line);
|
||||
}
|
||||
report_.push_back("noise test: done");
|
||||
printReport(console);
|
||||
}
|
||||
|
||||
void NoiseTest::printReport(Print& out) const {
|
||||
if (report_.empty()) return (void)out.println(running() ? "noise test: still running" : "noise test: none run yet");
|
||||
for (auto& l : report_) out.println(l.c_str());
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
|
||||
#endif // RORO_DEBUG
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef RORO_DEBUG
|
||||
|
||||
#include <Print.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "services/radio_service.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// `lora noise test` (Debug Builds, issue #20): which part of the Cardputer raises the radio's
|
||||
// noise floor? Applies one condition at a time, Sweeps the band a few passes, records the floor
|
||||
// and the steady peaks, and puts things back. It runs from the main loop on its own, because one
|
||||
// condition switches Wi-Fi off, and prints its report once the console can be reached again.
|
||||
class NoiseTest {
|
||||
public:
|
||||
struct Condition {
|
||||
std::string name;
|
||||
std::function<void()> apply, restore; // either may be empty
|
||||
uint32_t settleMs = 1500;
|
||||
};
|
||||
|
||||
explicit NoiseTest(RadioService& radio) : radio_(radio) {}
|
||||
void start(std::vector<Condition> conditions, int passes = 8);
|
||||
void step(uint32_t nowMs); // from the main loop
|
||||
bool running() const { return phase_ != Phase::Idle; }
|
||||
void printReport(Print& out) const;
|
||||
|
||||
private:
|
||||
enum class Phase { Idle, Settle, Sweep };
|
||||
struct Result {
|
||||
std::string name;
|
||||
int floor = 0, top = 0;
|
||||
std::string peaks; // steady ones: the lowest reading at each step over the passes
|
||||
};
|
||||
void begin(uint32_t nowMs);
|
||||
void finish();
|
||||
|
||||
RadioService& radio_;
|
||||
std::vector<Condition> conditions_;
|
||||
std::vector<Result> results_;
|
||||
size_t index_ = 0;
|
||||
int passes_ = 8, seen_ = 0;
|
||||
Phase phase_ = Phase::Idle;
|
||||
uint32_t phaseMs_ = 0, lastSeq_ = 0;
|
||||
int8_t lowest_[SweepFrame::kMaxSteps];
|
||||
uint16_t counts_[128]; // how often each level (in -dBm) was read: for the median
|
||||
uint32_t fromHz_ = 0, stepHz_ = 0;
|
||||
uint16_t steps_ = 0;
|
||||
std::vector<std::string> report_;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
|
||||
#endif // RORO_DEBUG
|
||||
@@ -71,19 +71,19 @@ bool RadioService::beginRadio(Print* report) {
|
||||
Config c = config();
|
||||
float mhz = c.frequencyHz / 1e6f;
|
||||
int16_t state = radio_->begin(mhz, c.bandwidthKHz, c.spreadingFactor, c.codingRate, c.syncWord, 0, c.preamble,
|
||||
tcxo_, false);
|
||||
tcxo_, ldo_);
|
||||
if (state != RADIOLIB_ERR_NONE && tcxo_ > 0) { // Meshtastic's TCXO_OPTIONAL: fall back to the crystal
|
||||
if (report) report->printf("lora probe: begin with TCXO %.1f V: error %d, trying the crystal\n", tcxo_, state);
|
||||
tcxo_ = 0;
|
||||
state = radio_->begin(mhz, c.bandwidthKHz, c.spreadingFactor, c.codingRate, c.syncWord, 0, c.preamble, tcxo_,
|
||||
false);
|
||||
ldo_);
|
||||
}
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
if (report) report->printf("lora probe: error %d, no SX1262 found\n", state);
|
||||
return false;
|
||||
}
|
||||
radio_->setDio2AsRfSwitch(true); // DIO2 selects TX or RX in the switch, as in Meshtastic
|
||||
radio_->setRxBoostedGainMode(true); // about 2 dB more sensitivity for about 2 mA
|
||||
radio_->setRxBoostedGainMode(boostedGain_); // about 2 dB more sensitivity for about 2 mA
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -191,6 +191,12 @@ void RadioService::store(RadioPacket& p) {
|
||||
}
|
||||
|
||||
#ifdef RORO_DEBUG
|
||||
void RadioService::debugAntenna(bool on) {
|
||||
auto& i2c = M5.In_I2C;
|
||||
uint8_t out = i2c.readRegister8(kExpander, kOutput, kI2cFreq);
|
||||
i2c.writeRegister8(kExpander, kOutput, on ? (out | 1) : (out & ~1), kI2cFreq);
|
||||
}
|
||||
|
||||
void RadioService::inject(const uint8_t* data, size_t len, float rssi, float snr) {
|
||||
if (!listening_) return (void)console.println("lora inject: not listening");
|
||||
RadioPacket p;
|
||||
|
||||
@@ -100,6 +100,15 @@ class RadioService : public Service {
|
||||
void setEcho(bool echo);
|
||||
void probe(Print& out); // `lora probe`: runs on the radio task
|
||||
#ifdef RORO_DEBUG
|
||||
// For the noise self-test (issue #20): the chip's regulator as an LDO instead of its DC-DC
|
||||
// converter, and receive gain boosted or not. Used the next time the radio is set up.
|
||||
void debugOptions(bool ldo, bool boostedGain) {
|
||||
ldo_ = ldo;
|
||||
boostedGain_ = boostedGain;
|
||||
}
|
||||
// The antenna switch (the Cap's expander, P0), for the reference "what the chip hears alone".
|
||||
// From the main loop only, which owns the I2C bus.
|
||||
void debugAntenna(bool on);
|
||||
// `lora inject`: a packet into the ring as if received, to test the App and Captures with no
|
||||
// transmitter in range. Nothing goes on air.
|
||||
void inject(const uint8_t* data, size_t len, float rssi, float snr);
|
||||
@@ -132,6 +141,7 @@ class RadioService : public Service {
|
||||
Config config_;
|
||||
bool present_ = false, expander_ = false;
|
||||
float tcxo_ = 1.8f;
|
||||
std::atomic<bool> ldo_{false}, boostedGain_{true};
|
||||
std::atomic<uint8_t> clients_{0};
|
||||
std::atomic<bool> listening_{false}, configChanged_{false}, probeWanted_{false}, echo_{false};
|
||||
std::atomic<uint32_t> seq_{0}, packets_{0}, crcErrors_{0}, radioErrors_{0}, restarts_{0};
|
||||
|
||||
@@ -247,7 +247,7 @@ void WifiService::tick(uint32_t nowMs) {
|
||||
apply(controller_.disconnected(nowMs));
|
||||
}
|
||||
|
||||
apply(controller_.update(nowMs, settings_.getBool(Setting::WifiEnabled)));
|
||||
apply(controller_.update(nowMs, settings_.getBool(Setting::WifiEnabled) && !paused_));
|
||||
|
||||
if (controller_.state() == State::Connected && nowMs - serversCheckedMs_ >= kServersEveryMs) applyServers(Why::Check);
|
||||
|
||||
|
||||
@@ -56,6 +56,11 @@ class WifiService : public Service {
|
||||
void ipSettingChanged(const std::string& ssid);
|
||||
// The DNS or NTP settings changed: use them now.
|
||||
void serversChanged() { applyServers(Why::SettingsChanged); }
|
||||
#ifdef RORO_DEBUG
|
||||
// The noise self-test switches the radio off for a few seconds. Not saved anywhere: a restart
|
||||
// during the test brings Wi-Fi back, which a changed setting wouldn't.
|
||||
void debugPause(bool paused) { paused_ = paused; }
|
||||
#endif
|
||||
|
||||
// A Saved Network was added: try it now rather than after the retry delay.
|
||||
void savedNetworksChanged() { controller_.retryNow(millis()); }
|
||||
@@ -82,6 +87,7 @@ class WifiService : public Service {
|
||||
uint32_t listScanSeq_ = 0;
|
||||
bool ntpWaiting_ = false;
|
||||
bool radioInitialised_ = false;
|
||||
bool paused_ = false; // Debug Builds: off for a moment, whatever the setting says
|
||||
bool fixed_ = false; // the network in use has a Fixed address
|
||||
bool dnsFromSettings_ = false;
|
||||
std::string ntpNames_[2]; // lwIP keeps the pointers, so the names live here
|
||||
|
||||
Reference in New Issue
Block a user