Public Access
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
162 lines
6.4 KiB
C++
162 lines
6.4 KiB
C++
#pragma once
|
|
|
|
#include <Print.h>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/semphr.h>
|
|
#include <freertos/task.h>
|
|
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
|
|
#include "loratap.h"
|
|
#include "meshtastic_presets.h"
|
|
#include "service.h"
|
|
|
|
class SX1262;
|
|
class Module;
|
|
class ArduinoHal;
|
|
|
|
namespace roro {
|
|
|
|
// One received packet, as the Radio Service keeps it.
|
|
struct RadioPacket {
|
|
uint32_t seq = 0; // 1, 2, 3… since boot
|
|
uint32_t ms = 0; // millis() when it was read (the Clock is read on the main loop, not here)
|
|
lora::RxInfo rx;
|
|
float frequencyError = 0; // Hz
|
|
bool crcOk = true;
|
|
uint8_t len = 0;
|
|
uint8_t data[255];
|
|
};
|
|
|
|
// One Sweep pass (M3, Q98): the strongest of three instantaneous RSSI readings at each step.
|
|
struct SweepFrame {
|
|
static constexpr size_t kMaxSteps = 160;
|
|
uint32_t seq = 0; // 1, 2, 3… per pass
|
|
uint32_t fromHz = 0, stepHz = 0;
|
|
uint16_t steps = 0;
|
|
uint16_t passMs = 0; // how long the pass took
|
|
int8_t dbm[kMaxSteps];
|
|
};
|
|
|
|
// The LoRa radio on the Cap (CONTEXT.md: Radio Service; docs/milestones/M3.md). One task owns the
|
|
// SX1262 and does all its SPI traffic, behind the bus lock the SD card uses (Q93); the main loop
|
|
// only posts requests, and DIO1's interrupt only wakes the task. **Receive only (Q94): there is
|
|
// no way to transmit.** The radio listens while someone asks it to (Q100) and sleeps otherwise.
|
|
class RadioService : public Service {
|
|
public:
|
|
enum Client : uint8_t { App = 1, Capture = 2, Console = 4 };
|
|
static constexpr size_t kRing = 32; // Q103
|
|
static constexpr uint32_t kNoiseEveryMs = 500; // instantaneous RSSI while listening
|
|
|
|
struct Config {
|
|
uint32_t frequencyHz;
|
|
float bandwidthKHz;
|
|
uint8_t spreadingFactor, codingRate, syncWord;
|
|
uint16_t preamble;
|
|
const char* name; // the preset's, or "Custom"
|
|
};
|
|
struct Counters {
|
|
uint32_t packets, crcErrors, radioErrors, restarts;
|
|
// Seen in the IRQ status at each noise sample (every 500 ms). Preamble detections include
|
|
// false alarms in noise (M3: more on an empty frequency than on LongFast); headers don't.
|
|
uint32_t preambles, headers, headerErrors;
|
|
};
|
|
|
|
RadioService();
|
|
const char* name() const override { return "radio"; }
|
|
uint32_t tickIntervalMs() const override { return 100; }
|
|
void start() override;
|
|
void tick(uint32_t nowMs) override;
|
|
|
|
bool present() const { return present_; }
|
|
bool listening() const { return listening_; }
|
|
void listen(Client client, bool on);
|
|
Config config() const;
|
|
void setPreset(const meshtastic::Preset& preset);
|
|
void setConfig(const Config& config); // custom settings (Q95), named "Custom"
|
|
|
|
uint32_t received() const { return seq_; } // seq of the newest packet
|
|
uint32_t lastPacketMs() const { return lastPacketMs_; } // millis(), 0 before the first
|
|
bool packet(uint32_t seq, RadioPacket& out) const; // false once it has left the ring
|
|
uint32_t available() const; // packets in the ring: seqs received()-available()+1 to received()
|
|
float noiseFloor() const { return noise_; }
|
|
Counters counters() const {
|
|
return {packets_, crcErrors_, radioErrors_, restarts_, preambles_, headers_, headerErrors_};
|
|
}
|
|
|
|
// Sweep (Q98, Q99): takes the radio across a band, pausing the Sniffer (its packets are kept)
|
|
// until stopped. EU868 by default: 863 to 870 MHz in 100 kHz steps, measured at 125 kHz.
|
|
void sweep(bool on, uint32_t fromHz = 863000000, uint32_t toHz = 870000000, uint32_t stepHz = 100000);
|
|
bool sweeping() const { return sweeping_; }
|
|
bool sweepFrame(SweepFrame& out) const; // the latest pass; false before the first
|
|
uint32_t sweeps() const { return sweepSeq_; }
|
|
void setSweepEcho(bool echo) { sweepEcho_ = echo; } // a summary on the console every 2 s
|
|
void printSweep(Print& out) const; // `lora sweep dump`: the latest pass
|
|
|
|
// Debug aids (Q102).
|
|
void printStatus(Print& out) const;
|
|
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);
|
|
#endif
|
|
|
|
private:
|
|
enum Notify : uint32_t { kIrq = 1, kRequest = 2 };
|
|
static void taskEntry(void* self);
|
|
static void onDio1();
|
|
void run();
|
|
bool beginRadio(Print* report);
|
|
void applyConfig();
|
|
void startListening();
|
|
void stopListening();
|
|
int16_t receive();
|
|
void readPacket();
|
|
void store(RadioPacket& p);
|
|
void sampleNoise();
|
|
void runProbe();
|
|
void sweepPass();
|
|
void endSweep();
|
|
void wake(Notify why);
|
|
|
|
std::unique_ptr<ArduinoHal> hal_;
|
|
std::unique_ptr<Module> module_;
|
|
std::unique_ptr<SX1262> radio_;
|
|
TaskHandle_t task_ = nullptr;
|
|
mutable SemaphoreHandle_t lock_; // the ring and the config
|
|
std::unique_ptr<RadioPacket[]> ring_;
|
|
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};
|
|
std::atomic<uint32_t> preambles_{0}, headers_{0}, headerErrors_{0};
|
|
std::atomic<uint32_t> lastPacketMs_{0};
|
|
std::atomic<uint32_t> ringFirst_{1}; // the first seq the current ring can hold
|
|
std::atomic<bool> sweepWanted_{false}, sweeping_{false};
|
|
std::atomic<uint32_t> sweepFrom_{863000000}, sweepTo_{870000000}, sweepStep_{100000}, sweepSeq_{0};
|
|
SweepFrame frame_; // under lock_
|
|
std::atomic<float> noise_{0};
|
|
uint32_t lastNoiseMs_ = 0, echoed_ = 0, sweepEchoMs_ = 0;
|
|
bool sweepEcho_ = false;
|
|
Print* probeOut_ = nullptr;
|
|
static RadioService* instance_;
|
|
};
|
|
|
|
} // namespace roro
|