cleishm/idfxx_radio

1.0.0

Latest
uploaded 15 hours ago
Abstract LoRa radio transceiver interface and shared types

Readme

# idfxx_radio

Abstract LoRa radio transceiver interface and shared types.

## Features

- Abstract `idfxx::radio::lora_transceiver` base class — drivers for any LoRa chip (SX126x,
  SX127x, LR1110, ...) implement this single interface so application code can
  target any of them.
- Modulation-agnostic core: frequency, output power, sync word, transmit,
  receive, and RSSI are set through the base class; LoRa modulation and packet
  framing arrive via `set_modulation` and `set_packet_params`.
- Semantic LoRa types (spreading factor, bandwidth, coding rate, ramp time,
  packet parameters) that map to each chip's own register encoding.
- Chip-agnostic event base for `tx_done`, `rx_done`, `crc_error`,
  `cad_done`, and `preamble_detected`.
- Three coordinated API styles for every concrete driver: blocking calls,
  future-based one-shot operations, and event-loop dispatch for packet
  streams.
- One-shot async operations (`start_transmit`, single-shot
  `start_receive(buffer)`, `start_channel_scan`) return an `idfxx::future` —
  completion is awaitable without configuring an event loop.
- Low-power operation: duty-cycled listening (`start_listening(rx, sleep)`) and
  channel-activity detection (`scan_channel` / `start_channel_scan`) for
  listen-before-talk.
- `constexpr` `time_on_air` helper (`<idfxx/radio/airtime>`) for sizing timeouts
  and scheduling without touching hardware.
- `constexpr` `rx_duty_cycle_for` helper (`<idfxx/radio/duty_cycle>`) that
  computes listen/sleep windows guaranteed to catch packets for a given
  modulation and sender preamble length, with automatic fallback to
  continuous receive.

## Requirements

- ESP-IDF 5.5 or later
- C++23 compiler
- `idfxx_core` (error handling)
- `idfxx_event` (event loop and event base)

## Installation

### ESP-IDF Component Manager

Add to your project's `idf_component.yml`:

```yaml
dependencies:
  idfxx_radio:
    version: "^1.0.0"
```

Or add `idfxx_radio` to the `REQUIRES` list in your component's
`CMakeLists.txt`.

This component is rarely used on its own — depend on a concrete driver such as
`idfxx_radio_sx126x`, which pulls in `idfxx_radio` automatically.

## Usage

`idfxx_radio` defines no concrete radio; it's an interface. Application code
that talks to *any* LoRa radio receives an `idfxx::radio::lora_transceiver&`:

```cpp
#include <idfxx/radio/lora_transceiver>
#include <idfxx/radio/events>

void send_one(idfxx::radio::lora_transceiver& radio) {
    radio.set_modulation({
        .sf = idfxx::radio::spreading_factor::sf9,
        .bw = idfxx::radio::bandwidth::bw_125,
        .cr = idfxx::radio::coding_rate::cr_4_5,
    });

    std::array<uint8_t, 4> payload = {'p', 'i', 'n', 'g'};
    radio.transmit(payload);  // timeout self-sized from the packet's air-time
}
```

Construct a concrete driver (e.g. `idfxx::radio::sx126x`) and pass it where a
`lora_transceiver&` is expected. See `idfxx_radio_sx126x` for a working example.

### Future-based async operations

One-shot operations return an `idfxx::future` that completes with the
operation's result — no event loop required:

```cpp
auto tx = radio.start_transmit(payload);
// ... other work while the packet is on the air ...
tx.wait_for(std::chrono::seconds{2});  // throws errc::timeout if not done

std::array<uint8_t, 256> buf;
auto rx = radio.start_receive(buf);    // single-shot: one packet into buf
auto info = rx.wait();                 // rx_info once it arrives
```

The buffer passed to single-shot `start_receive` must stay valid until the
future completes (or the operation is cancelled with `standby()`/`sleep()`).
Dropping a future without waiting is safe — the operation continues and its
completion event still posts when an event loop is configured.

### Event-driven receive

Events are posted only when the concrete driver is given an event loop (see the
driver's `config`). Subscribe on that loop, then start continuous receive:

```cpp
loop.listener_add(idfxx::radio::rx_done,
    [&radio](const idfxx::radio::rx_info& info) {
        std::array<uint8_t, 256> buf;
        auto rx = radio.read_received(buf);
        // ... process rx.length bytes ...
    });

radio.start_listening();
```

### Low-power duty-cycled receive

Instead of listening continuously, the radio can alternate between short
listen windows and sleep. `rx_duty_cycle_for` computes windows that cannot
miss a packet for the configured link; when the link's parameters make
duty-cycling infeasible it returns `std::nullopt`, which `start_listening`
accepts as "listen continuously":

```cpp
radio.set_modulation({.sf = idfxx::radio::spreading_factor::sf9});
radio.set_packet_params({.preamble_length = 100});
radio.start_listening(radio.rx_duty_cycle_for());
```

A `constexpr` free-function form (`rx_duty_cycle_for(mod, sender_preamble,
min_symbols, min_sleep)` in `<idfxx/radio/duty_cycle>`) computes the same
windows at compile time for a fixed link configuration.

## API Overview

### `idfxx::radio::lora_transceiver` (abstract)

**Mode control:** `standby`, `sleep`, `current_mode`.

**Configuration:** `configure(lora_link)` applies the frequency, output
power, modulation, packet framing, and network in one call; `set_frequency`,
`set_output_power`, `set_modulation`, `set_packet_params`, and
`set_sync_word` change them individually. `modulation()` / `packet_params()`
read back the configured link parameters.

**Data path (blocking):** `transmit(span)` (timeout self-sized from the
packet's air-time), `transmit(span, timeout)`, `receive(span, timeout)`,
`scan_channel()`, `channel_busy()`.

**Data path (async, future-based):** `start_transmit(span)` →
`idfxx::future<void>`, `start_receive(buffer)` (single-shot) →
`idfxx::future<rx_info>`, `start_channel_scan()` → `idfxx::future<cad_info>`.

**Data path (event-loop streams):** `start_listening()` (continuous),
`start_listening(rx, sleep)` / `start_listening(rx_duty_cycle)` (duty-cycled, also
accepting `std::optional<rx_duty_cycle>` with continuous fallback), paired
with `read_received`.

**Status:** `last_packet_status`, `current_rssi`.

**Air-time:** `radio.time_on_air(payload_length)` — the packet's on-air
duration under the configured link parameters — or the `constexpr` free
function `time_on_air(mod, pkt, payload_length)` in `<idfxx/radio/airtime>`
for compile-time use.

**Duty-cycle windows:** `radio.rx_duty_cycle_for()` — listen/sleep windows
for the configured link parameters, with the driver's per-wake oscillator
cost (e.g. TCXO start-up) folded into the minimum worthwhile sleep — or the
`constexpr` free function `rx_duty_cycle_for(mod, sender_preamble,
min_symbols, min_sleep)` in `<idfxx/radio/duty_cycle>` for compile-time use.
Both return `std::optional<rx_duty_cycle>`, `std::nullopt` when continuous
receive is required.

Every method has a result-returning `try_*` form; the exception-throwing forms
above are inline wrappers guarded by `CONFIG_COMPILER_CXX_EXCEPTIONS`.

### Types

- `spreading_factor` — `sf5` through `sf12`, encoded as the LoRa spec values.
- `bandwidth` — semantic enum (`bw_7_8` through `bw_500`) named by kHz.
- `coding_rate` — `cr_4_5` through `cr_4_8`.
- `header_type` — `variable` or `fixed`.
- `ramp_time` — semantic buckets (`us_10`, `us_40`, `us_200`, `us_800`,
  `us_3400`). Drivers map to the closest supported value.
- `chip_mode` — `sleep`, `stdby`, `fs`, `tx`, `rx`, `cad`.
- `lora_modulation`, `lora_packet_params` — packet/modulation config structs.
- `rx_duty_cycle` — listen/sleep windows for duty-cycled receive.
- `rx_info`, `cad_info`, `packet_status` — result payloads.

### Events

Defined in `<idfxx/radio/events>`:

- `tx_done`
- `rx_done` (carries `rx_info`)
- `crc_error`
- `cad_done` (carries `cad_info`)
- `preamble_detected`

## Error Handling

The interface uses `idfxx::result<T>` and `idfxx::errc` from `idfxx_core`. CRC
errors surface as `errc::invalid_crc`; timed-out blocking operations return
`errc::timeout`. Out-of-range parameters (e.g. an unsupported output power for
a particular chip variant) return `errc::invalid_arg`. A future whose
operation is cancelled by a mode change (`standby`/`sleep`) before completing
reports `errc::not_finished`. Using a moved-from driver object is undefined
behavior.

## Important Notes

- `idfxx_radio` itself ships no register-level details: enum values are
  semantic, not chip-register encodings. Each driver translates internally.
- Single-shot `start_receive(buffer)`: the buffer must remain valid until the
  returned future completes or the operation is cancelled (`standby`/`sleep`);
  the radio returns to standby after the packet. Transmit buffers are staged
  before `start_transmit` returns and need not outlive the call.
- Dropping a future mid-flight is safe: the operation continues, and its
  completion is still observable through events when an event loop is
  configured.
- The base interface intentionally omits chip-specific concerns such as TCXO
  configuration, IRQ-register bitfields, and CAD detection thresholds. Those
  live on concrete drivers like `idfxx::radio::sx126x`.
- The sync word is selected semantically (`lora_network::public_network` /
  `lora_network::private_network`); each driver maps the selection to its
  chip's native encoding. Raw chip-specific values are available through
  driver-specific overloads (e.g. `sx126x::set_sync_word(uint16_t)`).

## License

Apache License 2.0 - see [LICENSE](LICENSE) for details.

Links

Supports all targets

To add this component to your project, run:

idf.py add-dependency "cleishm/idfxx_radio^1.0.0"

download archive

Stats

  • Archive size
    Archive size ~ 30.10 KB
  • Downloaded in total
    Downloaded in total 0 times
  • Downloaded this version
    This version: 0 times

Badge

cleishm/idfxx_radio version: 1.0.0
|