idfxx 1.0.0
Modern C++23 components for ESP-IDF
Loading...
Searching...
No Matches
lora_transceiver.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Chris Leishman
3
4#pragma once
5
28#include <idfxx/error>
29#include <idfxx/future>
32#include <idfxx/radio/types.hpp>
33
34#include <chrono>
35#include <cstdint>
36#include <electro/decibel>
37#include <frequency/frequency>
38#include <optional>
39#include <span>
40
41namespace idfxx::radio {
42
62public:
63 virtual ~lora_transceiver() = default;
64
67
68 // =========================================================================
69 // Accessors
70 // =========================================================================
71
77 [[nodiscard]] chip_mode current_mode() const noexcept { return do_current_mode(); }
78
89 [[nodiscard]] lora_modulation modulation() const noexcept { return _modulation; }
90
100 [[nodiscard]] lora_packet_params packet_params() const noexcept { return _packet_params; }
101
102 // =========================================================================
103 // Link calculations
104 // =========================================================================
105
120 [[nodiscard]] std::chrono::microseconds time_on_air(size_t payload_length) const noexcept {
121 return idfxx::radio::time_on_air(_modulation, _packet_params, payload_length);
122 }
123
146 [[nodiscard]] std::optional<rx_duty_cycle> rx_duty_cycle_for(uint16_t min_symbols = 8) const noexcept {
148 _modulation, _packet_params.preamble_length, min_symbols, do_rx_duty_cycle_min_sleep()
149 );
150 }
151
152 // =========================================================================
153 // Mode control
154 // =========================================================================
155
156#ifdef CONFIG_COMPILER_CXX_EXCEPTIONS
167
177 void sleep() { unwrap(try_sleep()); }
178
191
207 void start_listening(std::chrono::microseconds rx_period, std::chrono::microseconds sleep_period) {
208 unwrap(try_start_listening(rx_period, sleep_period));
209 }
210
224
241 void start_listening(const std::optional<rx_duty_cycle>& cycle) { unwrap(try_start_listening(cycle)); }
242
261
275 [[nodiscard]] cad_info scan_channel() { return unwrap(try_scan_channel()); }
276
294 [[nodiscard]] bool channel_busy() { return unwrap(try_channel_busy()); }
295#endif
296
306
316
327
336 [[nodiscard]] result<void>
337 try_start_listening(std::chrono::microseconds rx_period, std::chrono::microseconds sleep_period) {
338 if (rx_period <= std::chrono::microseconds::zero() || sleep_period <= std::chrono::microseconds::zero()) {
339 return error(errc::invalid_arg);
340 }
341 return do_start_listening(rx_period, sleep_period);
342 }
343
355 return try_start_listening(cycle.rx_period, cycle.sleep_period);
356 }
357
368 [[nodiscard]] result<void> try_start_listening(const std::optional<rx_duty_cycle>& cycle) {
369 return cycle ? try_start_listening(*cycle) : try_start_listening();
370 }
371
387
401 auto f = try_start_channel_scan();
402 if (!f) {
403 return error(f.error());
404 }
405 return _await(*f, scan_guard_window);
406 }
407
419 auto r = try_scan_channel();
420 if (!r) {
421 return error(r.error());
422 }
423 return r->detected;
424 }
425
426 // =========================================================================
427 // Configuration
428 // =========================================================================
429
430#ifdef CONFIG_COMPILER_CXX_EXCEPTIONS
452 void configure(const lora_link& link) { unwrap(try_configure(link)); }
453
460 void set_frequency(freq::hertz hz) { unwrap(try_set_frequency(hz)); }
461
471 void set_output_power(electro::dbm power, ramp_time ramp = ramp_time::us_200) {
472 unwrap(try_set_output_power(power, ramp));
473 }
474
482
490
504#endif
505
519 [[nodiscard]] result<void> try_configure(const lora_link& link) {
520 if (link.frequency.count() == 0) {
521 return error(errc::invalid_arg);
522 }
523 if (auto r = try_set_frequency(link.frequency); !r) {
524 return r;
525 }
526 if (auto r = try_set_output_power(link.output_power, link.ramp); !r) {
527 return r;
528 }
529 if (auto r = try_set_modulation(link.modulation); !r) {
530 return r;
531 }
532 if (auto r = try_set_packet_params(link.packet_params); !r) {
533 return r;
534 }
535 return try_set_sync_word(link.network);
536 }
537
543 [[nodiscard]] result<void> try_set_frequency(freq::hertz hz) { return do_set_frequency(hz); }
544
552 [[nodiscard]] result<void> try_set_output_power(electro::dbm power, ramp_time ramp = ramp_time::us_200) {
553 return do_set_output_power(power, ramp);
554 }
555
562 if (auto r = do_set_modulation(mod); !r) {
563 return r;
564 }
565 _modulation = mod;
566 return {};
567 }
568
575 if (auto r = do_set_packet_params(params); !r) {
576 return r;
577 }
578 _packet_params = params;
579 return {};
580 }
581
591 [[nodiscard]] result<void> try_set_sync_word(lora_network network) { return do_set_sync_word(network); }
592
593 // =========================================================================
594 // Data path: blocking
595 // =========================================================================
596
597#ifdef CONFIG_COMPILER_CXX_EXCEPTIONS
608 void transmit(std::span<const uint8_t> data) { unwrap(try_transmit(data)); }
609
617 template<typename Rep, typename Period>
618 void transmit(std::span<const uint8_t> data, const std::chrono::duration<Rep, Period>& timeout) {
620 }
621
631 template<typename Rep, typename Period>
632 [[nodiscard]] rx_info receive(std::span<uint8_t> buffer, const std::chrono::duration<Rep, Period>& timeout) {
633 return unwrap(try_receive(buffer, timeout));
634 }
635#endif
636
649 [[nodiscard]] result<void> try_transmit(std::span<const uint8_t> data) {
650 return try_transmit(data, time_on_air(data.size()) + transmit_timeout_margin);
651 }
652
667 template<typename Rep, typename Period>
668 [[nodiscard]] result<void>
669 try_transmit(std::span<const uint8_t> data, const std::chrono::duration<Rep, Period>& timeout) {
670 auto f = try_start_transmit(data);
671 if (!f) {
672 return error(f.error());
673 }
674 return _await(*f, std::chrono::ceil<std::chrono::milliseconds>(timeout));
675 }
676
691 template<typename Rep, typename Period>
692 [[nodiscard]] result<rx_info>
693 try_receive(std::span<uint8_t> buffer, const std::chrono::duration<Rep, Period>& timeout) {
694 auto f = try_start_receive(buffer);
695 if (!f) {
696 return error(f.error());
697 }
698 return _await(*f, std::chrono::ceil<std::chrono::milliseconds>(timeout));
699 }
700
701 // =========================================================================
702 // Data path: async
703 // =========================================================================
704
705#ifdef CONFIG_COMPILER_CXX_EXCEPTIONS
731 [[nodiscard]] idfxx::future<void> start_transmit(std::span<const uint8_t> data) {
732 return unwrap(try_start_transmit(data));
733 }
734
756 [[nodiscard]] idfxx::future<rx_info> start_receive(std::span<uint8_t> buffer) {
757 return unwrap(try_start_receive(buffer));
758 }
759
774 [[nodiscard]] rx_info read_received(std::span<uint8_t> buffer) { return unwrap(try_read_received(buffer)); }
775#endif
776
797 [[nodiscard]] result<idfxx::future<void>> try_start_transmit(std::span<const uint8_t> data) {
798 if (data.empty() || data.size() > max_payload_length) {
799 return error(errc::invalid_arg);
800 }
801 return do_start_transmit(data);
802 }
803
826 [[nodiscard]] result<idfxx::future<rx_info>> try_start_receive(std::span<uint8_t> buffer) {
827 return do_start_receive(buffer);
828 }
829
841 [[nodiscard]] result<rx_info> try_read_received(std::span<uint8_t> buffer) { return do_read_received(buffer); }
842
843 // =========================================================================
844 // Status
845 // =========================================================================
846
847#ifdef CONFIG_COMPILER_CXX_EXCEPTIONS
855
862 [[nodiscard]] electro::centi_dbm current_rssi() { return unwrap(try_current_rssi()); }
863#endif
864
870
876
878 static constexpr size_t max_payload_length = 255;
879
883 static constexpr std::chrono::milliseconds transmit_timeout_margin{250};
884
890 static constexpr std::chrono::milliseconds scan_guard_window{1000};
891
892protected:
893 lora_transceiver() = default;
894 lora_transceiver(lora_transceiver&&) noexcept = default;
895 lora_transceiver& operator=(lora_transceiver&&) noexcept = default;
896
897 // =========================================================================
898 // Customization hooks
899 //
900 // Concrete drivers override these (typically privately). Each hook
901 // implements the correspondingly named public method; argument validation
902 // common to all chips (payload length, positive durations) has already
903 // been performed by the public wrappers.
904 // =========================================================================
905
907 [[nodiscard]] virtual chip_mode do_current_mode() const noexcept = 0;
908
910 [[nodiscard]] virtual result<void> do_standby() = 0;
912 [[nodiscard]] virtual result<void> do_sleep() = 0;
914 [[nodiscard]] virtual result<void> do_start_listening() = 0;
919 [[nodiscard]] virtual result<void>
920 do_start_listening(std::chrono::microseconds rx_period, std::chrono::microseconds sleep_period) {
921 (void)rx_period;
922 (void)sleep_period;
924 }
929
931 [[nodiscard]] virtual result<void> do_set_frequency(freq::hertz hz) = 0;
933 [[nodiscard]] virtual result<void> do_set_output_power(electro::dbm power, ramp_time ramp) = 0;
935 [[nodiscard]] virtual result<void> do_set_modulation(lora_modulation mod) = 0;
937 [[nodiscard]] virtual result<void> do_set_packet_params(lora_packet_params params) = 0;
939 [[nodiscard]] virtual result<void> do_set_sync_word(lora_network network) = 0;
940
947 [[nodiscard]] virtual result<idfxx::future<void>> do_start_transmit(std::span<const uint8_t> data) = 0;
954 [[nodiscard]] virtual result<idfxx::future<rx_info>> do_start_receive(std::span<uint8_t> buffer) = 0;
956 [[nodiscard]] virtual result<rx_info> do_read_received(std::span<uint8_t> buffer) = 0;
957
959 [[nodiscard]] virtual result<packet_status> do_last_packet_status() = 0;
961 [[nodiscard]] virtual result<electro::centi_dbm> do_current_rssi() = 0;
962
967 [[nodiscard]] virtual std::chrono::microseconds do_rx_duty_cycle_min_sleep() const noexcept {
969 }
970
971private:
972 // Link parameters applied by the last successful try_set_modulation /
973 // try_set_packet_params; served back by modulation()/packet_params()
974 // and consumed by the time_on_air/rx_duty_cycle_for members.
975 lora_modulation _modulation{};
976 lora_packet_params _packet_params{};
977
978 // Shared tail of the blocking compositions (try_transmit / try_receive /
979 // try_scan_channel): waits for the operation's future, cancelling the
980 // operation via try_standby if it does not complete in time and masking
981 // the resulting cancellation error back to `errc::timeout`.
982 template<typename T>
983 [[nodiscard]] result<T> _await(const idfxx::future<T>& f, std::chrono::milliseconds timeout) {
984 auto r = f.try_wait_for(timeout);
985 if (r || r.error() != errc::timeout) {
986 return r;
987 }
988 // The completion IRQ may land between the wait timing out and the
989 // cancel below; only cancel an operation that is still pending.
990 if (f.done()) {
991 return f.try_wait();
992 }
993 (void)try_standby();
994 auto late = f.try_wait_for(std::chrono::milliseconds{0});
995 if (late || late.error() != errc::not_finished) {
996 return late;
997 }
998 return error(errc::timeout); // our own cancellation, reported as a timeout
999 }
1000};
1001
1002} // namespace idfxx::radio
1003
// end of idfxx_radio
LoRa time-on-air (air-time) calculation.
Async completion token.
Definition future.hpp:62
result< T > try_wait_for(const std::chrono::duration< Rep, Period > &timeout) const
Blocks until the operation completes or the timeout expires.
Definition future.hpp:233
result< T > try_wait() const
Blocks until the operation completes.
Definition future.hpp:220
bool done() const noexcept
Non-blocking check for whether the operation has completed.
Definition future.hpp:208
Abstract base class for LoRa radio transceivers.
static constexpr std::chrono::milliseconds scan_guard_window
Guard window for the blocking scan_channel.
rx_info read_received(std::span< uint8_t > buffer)
Reads the most recently received packet into the caller's buffer.
virtual result< void > do_standby()=0
Hook for try_standby.
result< void > try_set_frequency(freq::hertz hz)
Sets the RF carrier frequency.
result< cad_info > try_scan_channel()
Scans the channel for LoRa activity, blocking until the result is known.
void start_listening()
Starts continuous-receive mode.
idfxx::future< rx_info > start_receive(std::span< uint8_t > buffer)
Starts a single-shot receive into the caller's buffer.
result< electro::centi_dbm > try_current_rssi()
Returns the instantaneous RSSI on the configured channel.
static constexpr size_t max_payload_length
Maximum LoRa payload length in bytes.
result< bool > try_channel_busy()
Returns whether LoRa activity is currently detected on the channel.
chip_mode current_mode() const noexcept
Returns the radio's current high-level mode.
void start_listening(std::chrono::microseconds rx_period, std::chrono::microseconds sleep_period)
Starts duty-cycled (periodic) receive for low-power listening.
result< idfxx::future< rx_info > > try_start_receive(std::span< uint8_t > buffer)
Starts a single-shot receive into the caller's buffer.
virtual result< idfxx::future< void > > do_start_transmit(std::span< const uint8_t > data)=0
Hook for try_start_transmit.
result< void > try_sleep()
Puts the radio in its lowest-power sleep mode.
void sleep()
Puts the radio in its lowest-power sleep mode.
result< void > try_start_listening()
Starts continuous-receive mode.
virtual result< electro::centi_dbm > do_current_rssi()=0
Hook for try_current_rssi.
void set_packet_params(lora_packet_params params)
Configures the LoRa packet framing.
virtual std::chrono::microseconds do_rx_duty_cycle_min_sleep() const noexcept
Hook for rx_duty_cycle_for: the shortest sleep window worth duty-cycling for on this chip.
void transmit(std::span< const uint8_t > data)
Transmits a packet and blocks until completion, sizing the timeout automatically.
void start_listening(rx_duty_cycle cycle)
Starts duty-cycled (periodic) receive from precomputed windows.
result< void > try_set_packet_params(lora_packet_params params)
Configures the LoRa packet framing.
virtual result< void > do_set_frequency(freq::hertz hz)=0
Hook for try_set_frequency.
lora_packet_params packet_params() const noexcept
Returns the configured LoRa packet framing parameters.
result< void > try_start_listening(std::chrono::microseconds rx_period, std::chrono::microseconds sleep_period)
Starts duty-cycled (periodic) receive for low-power listening.
virtual result< void > do_set_modulation(lora_modulation mod)=0
Hook for try_set_modulation.
void set_modulation(lora_modulation mod)
Configures the LoRa modulation parameters.
void standby()
Puts the radio in standby mode.
result< void > try_start_listening(rx_duty_cycle cycle)
Starts duty-cycled (periodic) receive from precomputed windows.
std::chrono::microseconds time_on_air(size_t payload_length) const noexcept
Computes the time-on-air of a packet under the configured link parameters.
result< void > try_standby()
Puts the radio in standby mode.
result< idfxx::future< cad_info > > try_start_channel_scan()
Starts a one-shot channel-activity scan and returns a future.
result< void > try_transmit(std::span< const uint8_t > data, const std::chrono::duration< Rep, Period > &timeout)
Transmits a packet and blocks until completion.
electro::centi_dbm current_rssi()
Returns the instantaneous RSSI on the configured channel.
lora_transceiver & operator=(const lora_transceiver &)=delete
void start_listening(const std::optional< rx_duty_cycle > &cycle)
Starts duty-cycled receive, falling back to continuous receive.
idfxx::future< cad_info > start_channel_scan()
Starts a one-shot channel-activity scan and returns a future.
rx_info receive(std::span< uint8_t > buffer, const std::chrono::duration< Rep, Period > &timeout)
Receives a single packet, blocking until one arrives or the timeout expires.
void set_sync_word(lora_network network)
Selects the LoRa network by setting the sync word.
result< void > try_set_sync_word(lora_network network)
Selects the LoRa network by setting the sync word.
static constexpr std::chrono::milliseconds transmit_timeout_margin
Margin added to the packet's time-on-air when the blocking transmit overload without a timeout sizes ...
virtual result< rx_info > do_read_received(std::span< uint8_t > buffer)=0
Hook for try_read_received.
virtual result< idfxx::future< cad_info > > do_start_channel_scan()=0
Hook for try_start_channel_scan.
virtual result< void > do_set_packet_params(lora_packet_params params)=0
Hook for try_set_packet_params.
packet_status last_packet_status()
Returns detailed status for the most recent packet.
virtual result< void > do_set_output_power(electro::dbm power, ramp_time ramp)=0
Hook for try_set_output_power.
lora_transceiver(lora_transceiver &&) noexcept=default
result< void > try_transmit(std::span< const uint8_t > data)
Transmits a packet and blocks until completion, sizing the timeout automatically.
void transmit(std::span< const uint8_t > data, const std::chrono::duration< Rep, Period > &timeout)
Transmits a packet and blocks until completion.
virtual result< idfxx::future< rx_info > > do_start_receive(std::span< uint8_t > buffer)=0
Hook for try_start_receive(std::span<uint8_t>) (single-shot receive).
result< void > try_start_listening(const std::optional< rx_duty_cycle > &cycle)
Starts duty-cycled receive, falling back to continuous receive.
result< void > try_set_output_power(electro::dbm power, ramp_time ramp=ramp_time::us_200)
Sets the transmit output power.
virtual result< void > do_set_sync_word(lora_network network)=0
Hook for try_set_sync_word.
std::optional< rx_duty_cycle > rx_duty_cycle_for(uint16_t min_symbols=8) const noexcept
Computes duty-cycle receive windows for the configured link parameters.
result< void > try_configure(const lora_link &link)
Applies a complete link configuration.
cad_info scan_channel()
Scans the channel for LoRa activity, blocking until the result is known.
virtual result< void > do_sleep()=0
Hook for try_sleep.
result< idfxx::future< void > > try_start_transmit(std::span< const uint8_t > data)
Starts a transmit and returns a future tracking its completion.
virtual result< void > do_start_listening()=0
Hook for try_start_listening() (continuous receive).
bool channel_busy()
Returns whether LoRa activity is currently detected on the channel.
void set_frequency(freq::hertz hz)
Sets the RF carrier frequency.
virtual ~lora_transceiver()=default
idfxx::future< void > start_transmit(std::span< const uint8_t > data)
Starts a transmit and returns a future tracking its completion.
void set_output_power(electro::dbm power, ramp_time ramp=ramp_time::us_200)
Sets the transmit output power.
virtual result< packet_status > do_last_packet_status()=0
Hook for try_last_packet_status.
result< rx_info > try_receive(std::span< uint8_t > buffer, const std::chrono::duration< Rep, Period > &timeout)
Receives a single packet, blocking until one arrives or the timeout expires.
virtual chip_mode do_current_mode() const noexcept=0
Hook for current_mode.
void configure(const lora_link &link)
Applies a complete link configuration.
result< packet_status > try_last_packet_status()
Returns detailed status for the most recent packet.
lora_modulation modulation() const noexcept
Returns the configured LoRa modulation parameters.
lora_transceiver(const lora_transceiver &)=delete
result< rx_info > try_read_received(std::span< uint8_t > buffer)
Reads the most recently received packet into the caller's buffer.
result< void > try_set_modulation(lora_modulation mod)
Configures the LoRa modulation parameters.
Duty-cycled receive window calculation.
Chip-agnostic LoRa modulation and packet types.
LoRa radio types and driver classes.
Definition airtime.hpp:26
constexpr std::chrono::microseconds time_on_air(const lora_modulation &mod, const lora_packet_params &pkt, size_t payload_length) noexcept
Computes the time-on-air of a LoRa packet.
Definition airtime.hpp:135
lora_network
LoRa network selection.
Definition types.hpp:100
constexpr std::chrono::microseconds default_min_rx_sleep
Default shortest sleep window worth duty-cycling for.
ramp_time
Power-amplifier ramp-time bucket.
Definition types.hpp:120
@ us_200
~200 µs ramp-up.
chip_mode
Top-level radio operating mode.
Definition types.hpp:32
constexpr std::optional< rx_duty_cycle > rx_duty_cycle_for(const lora_modulation &mod, uint16_t sender_preamble, uint16_t min_symbols=8, std::chrono::microseconds min_sleep=default_min_rx_sleep) noexcept
Computes duty-cycle receive windows that cannot miss a packet.
constexpr std::unexpected< std::error_code > error(E e) noexcept
Creates an unexpected error from an error code enum.
Definition error.hpp:187
T unwrap(result< T > result)
Throws a std::system_error if the result is an error.
Definition error.hpp:307
@ timeout
Operation timed out.
@ not_supported
Operation or feature not supported.
@ invalid_arg
Invalid argument.
@ not_finished
Operation has not fully completed.
std::expected< T, std::error_code > result
result type wrapping a value or error code.
Definition error.hpp:120
Result of a channel-activity-detection operation.
Definition types.hpp:223
LoRa modulation parameters.
Definition types.hpp:132
LoRa packet framing parameters.
Definition types.hpp:149
uint16_t preamble_length
Preamble length in symbols.
Definition types.hpp:150
Detailed status for the most recent packet.
Definition types.hpp:236
Listen/sleep windows for duty-cycled receive.
Definition types.hpp:194
std::chrono::microseconds sleep_period
Time to sleep in each cycle.
Definition types.hpp:196
std::chrono::microseconds rx_period
Time to listen in each cycle.
Definition types.hpp:195
Information about a received packet.
Definition types.hpp:208