diff --git a/.gitmodules b/.gitmodules
index a9a9dac..80837aa 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "third_party/opendroneid-core-c"]
path = third_party/opendroneid-core-c
url = https://github.com/opendroneid/opendroneid-core-c.git
+[submodule "third_party/mavlink_c_library_v2"]
+ path = third_party/mavlink_c_library_v2
+ url = https://github.com/mavlink/c_library_v2.git
diff --git a/README.md b/README.md
index 542b921..4aeed82 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ and navigate to **ESP Remote ID**. The sections below explain what each option m
| **UAS ID** | Unique identifier for the aircraft, broadcast in the Basic ID message. Max 20 characters. |
| **Operator registration ID** | Your national pilot/operator registration number (e.g. an EASA number like `FIN87ASTRDGE12K8`), broadcast in the Operator ID message. Max 20 characters. |
-Set both to your actual registration values before any flight. The placeholder defaults (`CHANGE_ME_*`) are not valid for operation.
+Set both to your actual registration values before any flight. The placeholder defaults (`CHANGE_ME_*`) are not valid for operation and intentionally block all broadcasts until replaced or overridden by valid MAVLink OpenDroneID input.
### UAS ID type
@@ -106,15 +106,102 @@ BLE is enabled by default. Wi-Fi Beacon and Wi-Fi NAN are available but disabled
| Option | Default | Description |
|--------|---------|-------------|
| `Enable Bluetooth LE legacy advertisements` | Enabled | Broadcasts Android-compatible BLE service-data advertisements on UUID `0xFFFA`. |
+| `BLE advertisement rotation interval` | `250 ms` | BLE payload rotation cadence. Default keeps Location refreshed around 1 Hz while also rotating Basic ID, System, and Operator ID. |
| `Enable Wi-Fi Beacon advertisements` | Disabled | Broadcasts OpenDroneID Message Pack payloads in Wi-Fi Beacon vendor IEs. |
| `Enable Wi-Fi NAN advertisements` | Disabled | Broadcasts OpenDroneID Message Pack payloads in Wi-Fi NAN sync/action frames. |
| `Wi-Fi Remote ID channel` | `6` | 2.4 GHz channel used by the ESP32-S3 Wi-Fi transport. |
-| `Wi-Fi Remote ID transmit interval` | `1000 ms` | Remote ID Wi-Fi cadence. |
| `Wi-Fi TX power` | `20 dBm` | Converted internally to ESP-IDF quarter-dBm units. |
| `Wi-Fi Beacon SSID` | `OpenDroneID` | SSID embedded only in Beacon frames. Receivers should parse the vendor IE, not rely on this SSID. |
-| `TX status LED GPIO` | Disabled (`-1`) | Optional simple GPIO LED that pulses after each successful BLE, Wi-Fi Beacon, or Wi-Fi NAN transmission. |
-| `TX status LED is active high` | Enabled | Disable for active-low LEDs. |
-| `TX status LED pulse duration` | `40 ms` | Duration of each transmission pulse. |
+| `Wi-Fi Beacon TX interval` | `1000 ms` | Wi-Fi Beacon transmission cadence. |
+| `Wi-Fi NAN TX interval` | `1000 ms` | Wi-Fi NAN sync/action transmission cadence. |
+| `Enable onboard RGB status indicator` | Disabled | Drives a board-mounted addressable RGB LED as a local status indicator. |
+| `Onboard RGB LED data GPIO` | `48` | Physical ESP32-S3 GPIO connected to the onboard WS2812/SK6812-style RGB LED data input. The UICPAL ESP32-S3-N16R8 board variant used by this firmware uses GPIO48; other variants may use GPIO33 or another pin. |
+| `Onboard RGB LED brightness` | `16%` | Scales the local indicator LED brightness. |
+| `Onboard RGB operational flash pattern` | `Drone beacon 1 Hz short flash` | Pattern used after transports have started and valid Remote ID identity is available. |
+| `Enable external GPIO lighting outputs` | Disabled | Enables up to five simple GPIO outputs for external light trigger circuits. |
+| `Startup delay before transmissions` | `10000 ms` | Development delay before starting BLE/Wi-Fi so there is time to attach the serial monitor. |
+
+### Onboard RGB Indicator
+
+The onboard RGB indicator is intended for local module status, not for driving external aviation lights. It uses the ESP32-S3 RMT peripheral to drive one addressable RGB LED, typically a WS2812/SK6812 device on the devboard. The UICPAL ESP32-S3-N16R8 board variant used by this firmware uses GPIO48 for the onboard RGB LED.
+
+Status behavior:
+
+| Firmware state | Onboard RGB behavior |
+|----------------|----------------------|
+| Waiting for Remote ID readiness or transport startup | Amber slow blink |
+| Operational | Configured green flash pattern |
+| Error state | Red fast blink |
+
+Operational means the firmware has started its enabled transports and the state store has a valid Basic ID/UAS ID plus Operator ID. This prevents the indicator from showing the operational pattern while placeholder identity values are still blocking broadcasts.
+
+The RGB data pin is not a suitable transistor trigger for external lights because addressable LEDs use a high-speed encoded data waveform. Use the external GPIO lighting outputs for that purpose.
+
+### External GPIO Lighting
+
+The external GPIO lighting module provides five independently configurable GPIO outputs. These outputs are logic-level triggers intended for transistor, MOSFET, relay-driver, or opto-isolator inputs. Do not power aircraft lights directly from ESP32 GPIO pins.
+
+Each output has its own configuration:
+
+| Setting | Description |
+|---------|-------------|
+| `Enable output N` | Enables this lighting output. |
+| `Output N GPIO` | Physical ESP32-S3 GPIO used as the trigger signal. |
+| `Output N active high` | Controls whether logical ON drives the pin high or low. |
+| `Output N open drain` | Uses open-drain output mode for compatible external circuits. |
+| `Output N operational pattern` | Pattern used when Remote ID transports are started and identity is ready. |
+| `Output N pattern phase offset` | Staggers matching patterns across multiple outputs. |
+
+Outputs remain off until valid Basic ID/UAS ID and Operator ID are available and enabled transports have started.
+
+Available operational patterns:
+
+- `Solid on`
+- `Beacon 1 Hz short flash`
+- `Beacon 1 Hz 50% duty`
+- `Single strobe`
+- `Double strobe`
+- `Triple strobe`
+- `Fast strobe`
+
+Example setup:
+
+```text
+Output 0: GPIO 4, Double strobe, phase 0 ms
+Output 1: GPIO 5, Double strobe, phase 500 ms
+Output 2: GPIO 6, Beacon 1 Hz short flash
+Output 3: disabled
+Output 4: disabled
+```
+
+### MAVLink OpenDroneID Input
+
+MAVLink input is available but disabled by default. When enabled, the firmware listens on a configured UART and accepts these MAVLink OpenDroneID messages:
+
+- `OPEN_DRONE_ID_BASIC_ID`
+- `OPEN_DRONE_ID_OPERATOR_ID`
+- `OPEN_DRONE_ID_LOCATION`
+
+Incoming Basic ID and Operator ID updates feed the same readiness gate as Kconfig values. This allows a flight controller or companion computer to provide identity at runtime while keeping the placeholder Kconfig values in flash. Broadcasts still do not start until a valid Basic ID/UAS ID and Operator ID are present.
+
+| Option | Default | Description |
+|--------|---------|-------------|
+| `Enable MAVLink OpenDroneID UART input` | Disabled | Enables the UART MAVLink parser and state producer. |
+| `MAVLink UART number` | `1` | ESP32-S3 UART peripheral used for input. This does not imply physical pins. |
+| `MAVLink UART baud rate` | `57600` | UART baud rate. Common values are `57600`, `115200`, and `921600`. |
+| `MAVLink UART RX GPIO` | `-1` | Physical ESP32-S3 pin routed to the selected UART RX signal. Connect this to the flight controller MAVLink TX pin. Must be configured when MAVLink input is enabled. |
+| `MAVLink UART TX GPIO` | `-1` | Optional physical ESP32-S3 pin routed to the selected UART TX signal. Connect this to the flight controller MAVLink RX pin only for bidirectional wiring. Leave as `-1` for receive-only. |
+| `Accepted MAVLink target system` | `0` | `0` accepts all systems; non-zero accepts that system or broadcast target `0`. |
+| `Accepted MAVLink target component` | `0` | `0` accepts all components; non-zero accepts that component or broadcast target `0`. |
+
+On ESP32-S3, the UART number selects the hardware UART block (`UART0`, `UART1`, or `UART2`). It does not uniquely select board pins. UART signals are routed through the ESP32 GPIO matrix, and many dev boards label only the programming/debug serial pins as `RX`/`TX`. Choose GPIOs that are exposed on your board and not already used by USB serial, flash/PSRAM, buttons, LEDs, or strapping functions.
+
+Receive-only wiring is enough for the current MAVLink producer:
+
+```text
+Flight controller TX -> ESP32-S3 MAVLink UART RX GPIO
+Flight controller GND -> ESP32-S3 GND
+```
For Android app testing, use one transport at a time because many receiver apps do not display whether a detected aircraft came from BLE or Wi-Fi.
@@ -130,6 +217,8 @@ Before flashing for any actual operation:
- [ ] Set **EU operation category** to **Open** (or your authorised category)
- [ ] Optionally enable **Broadcast a known takeoff position** and enter your takeoff coordinates
+Note: BLE and Wi-Fi transports wait for the store readiness gate before transmitting. The gate requires a usable Basic ID/UAS ID and Operator ID, either from Kconfig or from runtime producers such as MAVLink.
+
## Prerequisites
- ESP32-S3 board
@@ -164,21 +253,6 @@ The default ESP serial device inside the container is `/dev/ttyESP32`. Override
make flash ESPPORT=/dev/ttyACM0
```
-## Architecture
-
-The ESP-IDF component is split so transports can share the same Remote ID state and OpenDroneID encoding:
-
-- `src/remoteid/main.c`: application entrypoint and top-level startup.
-- `src/remoteid/remoteid/config.h`: compile-time configuration constants from Kconfig.
-- `src/remoteid/remoteid/types.h`: internal Remote ID state and message bundle types.
-- `src/remoteid/remoteid/model.c`: builds the current aircraft/operator/location state from configuration.
-- `src/remoteid/remoteid/encode.c`: converts internal state into official OpenDroneID messages.
-- `src/remoteid/remoteid/led.c`: optional TX status LED pulse handling.
-- `src/remoteid/remoteid/ble.c`: NimBLE lifecycle, TX power, and BLE legacy advertisement rotation.
-- `src/remoteid/remoteid/wifi.c`: ESP-IDF Wi-Fi setup and OpenDroneID Wi-Fi Beacon/NAN transmission.
-
-Both transports use the same `model` and `encode` modules instead of rebuilding message payloads independently.
-
## Transmit Power
OpenDroneID BLE reception range is not guaranteed by a fixed distance in the EU documents; practical range depends on transmitter power, receiver hardware, antenna design, enclosure, orientation, and RF environment. This firmware therefore makes BLE advertising TX power explicit and configurable.
@@ -191,9 +265,9 @@ CONFIG_REMOTEID_BLE_TX_POWER_P9=y
Change it with `make menuconfig` under `ESP Remote ID -> BLE advertising TX power`, or by editing `sdkconfig.defaults` before regenerating `sdkconfig`. Keep the configured level within the limits of your board, antenna, enclosure, and local RF rules.
-## macOS Serial Bridge
+## (macOS) Serial Bridge
-Docker Desktop on macOS does not expose `/dev/cu.*` serial devices directly to Linux containers. Use `socat` to bridge the ESP32-S3 USB serial device from the host into the devcontainer.
+Docker on macOS does not expose `/dev/cu.*` serial devices directly to Linux containers. Use `socat` to bridge the ESP32-S3 USB serial device from the host into the devcontainer.
On the macOS host:
@@ -244,7 +318,7 @@ Use `make menuconfig` under **ESP Remote ID** within the `Component config` menu
For Wi-Fi-only testing, flash the firmware and use an Android receiver that supports the specific Wi-Fi transport being tested. If the app does not show a source transport, enable only one Wi-Fi transport at a time so any detection has a known source.
-The screenshot below shows the OpenDroneID Android app receiving live advertisements from the module. The placeholder identifiers (`CHANGE_ME_UAS_ID`, `CHANGE_ME_OP_ID`) are visible, replace these with your registration details before any flight.
+The screenshot below shows the OpenDroneID Android app receiving live advertisements from the module. Current firmware blocks placeholder identifiers (`CHANGE_ME_UAS_ID`, `CHANGE_ME_OP_ID`), so replace these with your registration details or provide valid MAVLink OpenDroneID identity before testing reception.
diff --git a/src/remoteid/CMakeLists.txt b/src/remoteid/CMakeLists.txt
index b130de8..3281ddc 100644
--- a/src/remoteid/CMakeLists.txt
+++ b/src/remoteid/CMakeLists.txt
@@ -3,8 +3,12 @@ idf_component_register(
"main.c"
"remoteid/ble.c"
"remoteid/encode.c"
- "remoteid/led.c"
+ "remoteid/indicator.c"
+ "remoteid/lighting.c"
+ "remoteid/mavlink.c"
"remoteid/model.c"
+ "remoteid/pattern.c"
+ "remoteid/store.c"
"remoteid/wifi.c"
INCLUDE_DIRS
"."
@@ -12,6 +16,8 @@ idf_component_register(
REQUIRES
bt
esp_driver_gpio
+ esp_driver_rmt
+ esp_driver_uart
esp_event
esp_netif
esp_timer
@@ -19,3 +25,7 @@ idf_component_register(
nvs_flash
opendroneid_core
)
+
+target_include_directories(${COMPONENT_LIB} PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/../../third_party/mavlink_c_library_v2"
+)
diff --git a/src/remoteid/Kconfig b/src/remoteid/Kconfig
index e34fab7..72f3586 100644
--- a/src/remoteid/Kconfig
+++ b/src/remoteid/Kconfig
@@ -43,6 +43,89 @@ config REMOTEID_OPERATOR_ID
is broadcast in the Operator ID message. Must be 20 characters or
fewer.
+config REMOTEID_STARTUP_DELAY_MS
+ int "Startup delay before transmissions (ms)"
+ default 10000
+ range 0 60000
+ help
+ Delay after boot before BLE or Wi-Fi Remote ID transmissions start.
+ This is useful during development because it leaves time to attach the
+ serial monitor after flashing or reset.
+
+config REMOTEID_MAVLINK_INPUT
+ bool "Enable MAVLink OpenDroneID UART input"
+ default n
+ help
+ Parse incoming MAVLink v2 OpenDroneID messages from a flight controller
+ or companion computer and feed them into the Remote ID state store.
+ The first supported messages are OPEN_DRONE_ID_BASIC_ID,
+ OPEN_DRONE_ID_OPERATOR_ID, and OPEN_DRONE_ID_LOCATION.
+
+if REMOTEID_MAVLINK_INPUT
+
+config REMOTEID_MAVLINK_UART_NUM
+ int "MAVLink UART number"
+ default 1
+ range 0 2
+ help
+ ESP32-S3 UART peripheral used for MAVLink input. This selects the
+ hardware UART block, not the physical pins. The ESP32-S3 GPIO matrix
+ lets UART signals be routed to many different pins, so configure the RX
+ and optional TX GPIOs below to match your board wiring.
+
+config REMOTEID_MAVLINK_BAUD
+ int "MAVLink UART baud rate"
+ default 57600
+ range 1200 921600
+ help
+ Baud rate for the MAVLink input UART. Common flight-controller values
+ are 57600, 115200, and 921600.
+
+config REMOTEID_MAVLINK_RX_GPIO
+ int "MAVLink UART RX GPIO"
+ default -1
+ range -1 48
+ help
+ Physical ESP32-S3 GPIO used as the selected UART's RX signal. Connect
+ this pin to the flight controller or companion computer MAVLink TX pin.
+ This must be set to a valid GPIO when MAVLink input is enabled. Do not
+ assume the UART number automatically selects a usable board pin.
+
+config REMOTEID_MAVLINK_TX_GPIO
+ int "MAVLink UART TX GPIO"
+ default -1
+ range -1 48
+ help
+ Optional physical ESP32-S3 GPIO used as the selected UART's TX signal.
+ Connect this pin to the flight controller or companion computer MAVLink
+ RX pin only if bidirectional UART wiring is needed. Current firmware
+ only requires receive-only MAVLink input, so leave this at -1 unless
+ you intentionally want the TX signal routed.
+
+config REMOTEID_MAVLINK_TASK_STACK
+ int "MAVLink input task stack size"
+ default 4096
+ range 3072 8192
+
+config REMOTEID_MAVLINK_TARGET_SYSTEM
+ int "Accepted MAVLink target system"
+ default 0
+ range 0 255
+ help
+ If non-zero, only OpenDroneID messages addressed to this target system
+ or broadcast target 0 are accepted. Leave as 0 to accept all systems.
+
+config REMOTEID_MAVLINK_TARGET_COMPONENT
+ int "Accepted MAVLink target component"
+ default 0
+ range 0 255
+ help
+ If non-zero, only OpenDroneID messages addressed to this target
+ component or broadcast target 0 are accepted. Leave as 0 to accept all
+ components.
+
+endif # REMOTEID_MAVLINK_INPUT
+
choice REMOTEID_ID_TYPE
prompt "UAS ID type"
default REMOTEID_ID_TYPE_CAA_REG
@@ -217,31 +300,82 @@ config REMOTEID_BLE_TX_POWER_P9
endchoice
-config REMOTEID_TX_LED_GPIO
- int "TX status LED GPIO"
- default -1
- range -1 48
+config REMOTEID_BLE_TX_INTERVAL_MS
+ int "BLE advertisement rotation interval (ms)"
+ default 250
+ range 100 5000
+ depends on REMOTEID_TRANSPORT_BLE
help
- GPIO for a simple status LED that pulses on each successful BLE or
- Wi-Fi Remote ID transmission. Set to -1 to disable. For boards with
- an addressable RGB LED, this option only works if the LED can be driven
- as a simple GPIO.
+ Interval between BLE advertisement payload rotations. The default
+ rotates through the message schedule fast enough to keep Location at
+ approximately 1 Hz while still periodically advertising Basic ID,
+ System, and Operator ID.
-config REMOTEID_TX_LED_ACTIVE_HIGH
- bool "TX status LED is active high"
- default y
- depends on REMOTEID_TX_LED_GPIO >= 0
+config REMOTEID_INDICATOR_RGB_ENABLE
+ bool "Enable onboard RGB status indicator"
+ default n
help
- Disable this if the configured LED turns on when the GPIO is driven
- low.
+ Drive a board-mounted addressable RGB LED, typically WS2812/SK6812, as
+ a local firmware status indicator. Before Remote ID is operational it
+ shows status/error indications. After transports have started and valid
+ identity is present, it switches to the configured operational flash
+ pattern. This is separate from the TX status LED pulse output.
+
+if REMOTEID_INDICATOR_RGB_ENABLE
-config REMOTEID_TX_LED_PULSE_MS
- int "TX status LED pulse duration (ms)"
- default 40
- range 1 1000
- depends on REMOTEID_TX_LED_GPIO >= 0
+config REMOTEID_INDICATOR_RGB_GPIO
+ int "Onboard RGB LED data GPIO"
+ default 48
+ range -1 48
+ help
+ Physical ESP32-S3 GPIO connected to the onboard addressable RGB LED data
+ input. The UICPAL ESP32-S3-N16R8 board variant used by this firmware
+ uses GPIO48 for its WS2812 RGB LED. Other ESP32-S3 dev boards may use
+ GPIO33 or another pin, so verify your board schematic before enabling
+ this option on non-UICPAL hardware. Set to -1 to leave the indicator
+ disabled even when this menu section is enabled.
+
+config REMOTEID_INDICATOR_RGB_BRIGHTNESS_PERCENT
+ int "Onboard RGB LED brightness (%)"
+ default 16
+ range 1 100
+ help
+ Scales the local indicator LED brightness. This only affects the
+ onboard RGB status LED, not any future external light trigger GPIOs.
+
+choice REMOTEID_INDICATOR_OPERATIONAL_PATTERN
+ prompt "Onboard RGB operational flash pattern"
+ default REMOTEID_INDICATOR_PATTERN_DRONE_BEACON_1HZ_SHORT
help
- Duration of the LED pulse after each successful transmission.
+ Pattern used by the onboard RGB LED after Remote ID is operational.
+ External aviation lighting outputs will be configured separately later.
+
+config REMOTEID_INDICATOR_PATTERN_SOLID
+ bool "Solid on"
+
+config REMOTEID_INDICATOR_PATTERN_DRONE_BEACON_1HZ_SHORT
+ bool "Drone beacon 1 Hz short flash"
+
+config REMOTEID_INDICATOR_PATTERN_DRONE_BEACON_1HZ_50
+ bool "Drone beacon 1 Hz 50% duty"
+
+config REMOTEID_INDICATOR_PATTERN_STROBE_SINGLE
+ bool "Single strobe"
+
+config REMOTEID_INDICATOR_PATTERN_STROBE_DOUBLE
+ bool "Double strobe"
+
+config REMOTEID_INDICATOR_PATTERN_STROBE_TRIPLE
+ bool "Triple strobe"
+
+config REMOTEID_INDICATOR_PATTERN_FAST_STROBE
+ bool "Fast strobe"
+
+endchoice
+
+endif # REMOTEID_INDICATOR_RGB_ENABLE
+
+rsource "Kconfig.lighting"
if REMOTEID_TRANSPORT_WIFI_BEACON || REMOTEID_TRANSPORT_WIFI_NAN
@@ -254,14 +388,6 @@ config REMOTEID_WIFI_CHANNEL
6 is the standard/default channel for ASTM F3411 Wi-Fi Beacon/NAN on
2.4 GHz. Select a different channel only when permitted and required.
-config REMOTEID_WIFI_INTERVAL_MS
- int "Wi-Fi Remote ID transmit interval (ms)"
- default 1000
- range 100 5000
- help
- Interval between OpenDroneID Wi-Fi transmissions. Use 1000 ms for a
- 1 Hz Remote ID cadence.
-
config REMOTEID_WIFI_TX_POWER_DBM
int "Wi-Fi TX power (dBm)"
default 20
@@ -283,6 +409,26 @@ config REMOTEID_WIFI_BEACON_SSID
bytes or fewer. Remote ID receivers should parse the vendor IE, not
rely on this SSID.
+config REMOTEID_WIFI_BEACON_TX_INTERVAL_MS
+ int "Wi-Fi Beacon TX interval (ms)"
+ default 1000
+ range 100 5000
+ help
+ Interval between OpenDroneID Wi-Fi Beacon transmissions. The default
+ 1000 ms gives a 1 Hz Remote ID Beacon cadence.
+
endif # REMOTEID_TRANSPORT_WIFI_BEACON
+if REMOTEID_TRANSPORT_WIFI_NAN
+
+config REMOTEID_WIFI_NAN_TX_INTERVAL_MS
+ int "Wi-Fi NAN TX interval (ms)"
+ default 1000
+ range 100 5000
+ help
+ Interval between OpenDroneID Wi-Fi NAN sync/action transmissions. The
+ default 1000 ms gives a 1 Hz Remote ID NAN cadence.
+
+endif # REMOTEID_TRANSPORT_WIFI_NAN
+
endmenu
diff --git a/src/remoteid/Kconfig.lighting b/src/remoteid/Kconfig.lighting
new file mode 100644
index 0000000..3b5ef18
--- /dev/null
+++ b/src/remoteid/Kconfig.lighting
@@ -0,0 +1,306 @@
+menu "External GPIO lighting outputs"
+
+config REMOTEID_LIGHTING_ENABLE
+ bool "Enable external GPIO lighting outputs"
+ default n
+ help
+ Enables up to five simple GPIO outputs for external lighting triggers.
+ These outputs are intended to drive transistor, MOSFET, relay driver,
+ or opto-isolator inputs. Do not drive aircraft lights directly from an
+ ESP32 GPIO pin.
+
+if REMOTEID_LIGHTING_ENABLE
+
+menu "Lighting output 0"
+
+config REMOTEID_LIGHTING_OUTPUT0_ENABLE
+ bool "Enable output 0"
+ default n
+
+config REMOTEID_LIGHTING_OUTPUT0_GPIO
+ int "Output 0 GPIO"
+ default -1
+ range -1 48
+ depends on REMOTEID_LIGHTING_OUTPUT0_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT0_ACTIVE_HIGH
+ bool "Output 0 active high"
+ default y
+ depends on REMOTEID_LIGHTING_OUTPUT0_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT0_OPEN_DRAIN
+ bool "Output 0 open drain"
+ default n
+ depends on REMOTEID_LIGHTING_OUTPUT0_ENABLE
+
+choice REMOTEID_LIGHTING_OUTPUT0_PATTERN
+ prompt "Output 0 operational pattern"
+ default REMOTEID_LIGHTING_OUTPUT0_PATTERN_BEACON_1HZ_SHORT
+ depends on REMOTEID_LIGHTING_OUTPUT0_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT0_PATTERN_SOLID
+ bool "Solid on"
+
+config REMOTEID_LIGHTING_OUTPUT0_PATTERN_BEACON_1HZ_SHORT
+ bool "Beacon 1 Hz short flash"
+
+config REMOTEID_LIGHTING_OUTPUT0_PATTERN_BEACON_1HZ_50
+ bool "Beacon 1 Hz 50% duty"
+
+config REMOTEID_LIGHTING_OUTPUT0_PATTERN_STROBE_SINGLE
+ bool "Single strobe"
+
+config REMOTEID_LIGHTING_OUTPUT0_PATTERN_STROBE_DOUBLE
+ bool "Double strobe"
+
+config REMOTEID_LIGHTING_OUTPUT0_PATTERN_STROBE_TRIPLE
+ bool "Triple strobe"
+
+config REMOTEID_LIGHTING_OUTPUT0_PATTERN_FAST_STROBE
+ bool "Fast strobe"
+
+endchoice
+
+config REMOTEID_LIGHTING_OUTPUT0_PHASE_MS
+ int "Output 0 pattern phase offset (ms)"
+ default 0
+ range 0 60000
+ depends on REMOTEID_LIGHTING_OUTPUT0_ENABLE
+
+endmenu
+
+menu "Lighting output 1"
+
+config REMOTEID_LIGHTING_OUTPUT1_ENABLE
+ bool "Enable output 1"
+ default n
+
+config REMOTEID_LIGHTING_OUTPUT1_GPIO
+ int "Output 1 GPIO"
+ default -1
+ range -1 48
+ depends on REMOTEID_LIGHTING_OUTPUT1_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT1_ACTIVE_HIGH
+ bool "Output 1 active high"
+ default y
+ depends on REMOTEID_LIGHTING_OUTPUT1_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT1_OPEN_DRAIN
+ bool "Output 1 open drain"
+ default n
+ depends on REMOTEID_LIGHTING_OUTPUT1_ENABLE
+
+choice REMOTEID_LIGHTING_OUTPUT1_PATTERN
+ prompt "Output 1 operational pattern"
+ default REMOTEID_LIGHTING_OUTPUT1_PATTERN_BEACON_1HZ_SHORT
+ depends on REMOTEID_LIGHTING_OUTPUT1_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT1_PATTERN_SOLID
+ bool "Solid on"
+
+config REMOTEID_LIGHTING_OUTPUT1_PATTERN_BEACON_1HZ_SHORT
+ bool "Beacon 1 Hz short flash"
+
+config REMOTEID_LIGHTING_OUTPUT1_PATTERN_BEACON_1HZ_50
+ bool "Beacon 1 Hz 50% duty"
+
+config REMOTEID_LIGHTING_OUTPUT1_PATTERN_STROBE_SINGLE
+ bool "Single strobe"
+
+config REMOTEID_LIGHTING_OUTPUT1_PATTERN_STROBE_DOUBLE
+ bool "Double strobe"
+
+config REMOTEID_LIGHTING_OUTPUT1_PATTERN_STROBE_TRIPLE
+ bool "Triple strobe"
+
+config REMOTEID_LIGHTING_OUTPUT1_PATTERN_FAST_STROBE
+ bool "Fast strobe"
+
+endchoice
+
+config REMOTEID_LIGHTING_OUTPUT1_PHASE_MS
+ int "Output 1 pattern phase offset (ms)"
+ default 0
+ range 0 60000
+ depends on REMOTEID_LIGHTING_OUTPUT1_ENABLE
+
+endmenu
+
+menu "Lighting output 2"
+
+config REMOTEID_LIGHTING_OUTPUT2_ENABLE
+ bool "Enable output 2"
+ default n
+
+config REMOTEID_LIGHTING_OUTPUT2_GPIO
+ int "Output 2 GPIO"
+ default -1
+ range -1 48
+ depends on REMOTEID_LIGHTING_OUTPUT2_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT2_ACTIVE_HIGH
+ bool "Output 2 active high"
+ default y
+ depends on REMOTEID_LIGHTING_OUTPUT2_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT2_OPEN_DRAIN
+ bool "Output 2 open drain"
+ default n
+ depends on REMOTEID_LIGHTING_OUTPUT2_ENABLE
+
+choice REMOTEID_LIGHTING_OUTPUT2_PATTERN
+ prompt "Output 2 operational pattern"
+ default REMOTEID_LIGHTING_OUTPUT2_PATTERN_BEACON_1HZ_SHORT
+ depends on REMOTEID_LIGHTING_OUTPUT2_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT2_PATTERN_SOLID
+ bool "Solid on"
+
+config REMOTEID_LIGHTING_OUTPUT2_PATTERN_BEACON_1HZ_SHORT
+ bool "Beacon 1 Hz short flash"
+
+config REMOTEID_LIGHTING_OUTPUT2_PATTERN_BEACON_1HZ_50
+ bool "Beacon 1 Hz 50% duty"
+
+config REMOTEID_LIGHTING_OUTPUT2_PATTERN_STROBE_SINGLE
+ bool "Single strobe"
+
+config REMOTEID_LIGHTING_OUTPUT2_PATTERN_STROBE_DOUBLE
+ bool "Double strobe"
+
+config REMOTEID_LIGHTING_OUTPUT2_PATTERN_STROBE_TRIPLE
+ bool "Triple strobe"
+
+config REMOTEID_LIGHTING_OUTPUT2_PATTERN_FAST_STROBE
+ bool "Fast strobe"
+
+endchoice
+
+config REMOTEID_LIGHTING_OUTPUT2_PHASE_MS
+ int "Output 2 pattern phase offset (ms)"
+ default 0
+ range 0 60000
+ depends on REMOTEID_LIGHTING_OUTPUT2_ENABLE
+
+endmenu
+
+menu "Lighting output 3"
+
+config REMOTEID_LIGHTING_OUTPUT3_ENABLE
+ bool "Enable output 3"
+ default n
+
+config REMOTEID_LIGHTING_OUTPUT3_GPIO
+ int "Output 3 GPIO"
+ default -1
+ range -1 48
+ depends on REMOTEID_LIGHTING_OUTPUT3_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT3_ACTIVE_HIGH
+ bool "Output 3 active high"
+ default y
+ depends on REMOTEID_LIGHTING_OUTPUT3_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT3_OPEN_DRAIN
+ bool "Output 3 open drain"
+ default n
+ depends on REMOTEID_LIGHTING_OUTPUT3_ENABLE
+
+choice REMOTEID_LIGHTING_OUTPUT3_PATTERN
+ prompt "Output 3 operational pattern"
+ default REMOTEID_LIGHTING_OUTPUT3_PATTERN_BEACON_1HZ_SHORT
+ depends on REMOTEID_LIGHTING_OUTPUT3_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT3_PATTERN_SOLID
+ bool "Solid on"
+
+config REMOTEID_LIGHTING_OUTPUT3_PATTERN_BEACON_1HZ_SHORT
+ bool "Beacon 1 Hz short flash"
+
+config REMOTEID_LIGHTING_OUTPUT3_PATTERN_BEACON_1HZ_50
+ bool "Beacon 1 Hz 50% duty"
+
+config REMOTEID_LIGHTING_OUTPUT3_PATTERN_STROBE_SINGLE
+ bool "Single strobe"
+
+config REMOTEID_LIGHTING_OUTPUT3_PATTERN_STROBE_DOUBLE
+ bool "Double strobe"
+
+config REMOTEID_LIGHTING_OUTPUT3_PATTERN_STROBE_TRIPLE
+ bool "Triple strobe"
+
+config REMOTEID_LIGHTING_OUTPUT3_PATTERN_FAST_STROBE
+ bool "Fast strobe"
+
+endchoice
+
+config REMOTEID_LIGHTING_OUTPUT3_PHASE_MS
+ int "Output 3 pattern phase offset (ms)"
+ default 0
+ range 0 60000
+ depends on REMOTEID_LIGHTING_OUTPUT3_ENABLE
+
+endmenu
+
+menu "Lighting output 4"
+
+config REMOTEID_LIGHTING_OUTPUT4_ENABLE
+ bool "Enable output 4"
+ default n
+
+config REMOTEID_LIGHTING_OUTPUT4_GPIO
+ int "Output 4 GPIO"
+ default -1
+ range -1 48
+ depends on REMOTEID_LIGHTING_OUTPUT4_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT4_ACTIVE_HIGH
+ bool "Output 4 active high"
+ default y
+ depends on REMOTEID_LIGHTING_OUTPUT4_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT4_OPEN_DRAIN
+ bool "Output 4 open drain"
+ default n
+ depends on REMOTEID_LIGHTING_OUTPUT4_ENABLE
+
+choice REMOTEID_LIGHTING_OUTPUT4_PATTERN
+ prompt "Output 4 operational pattern"
+ default REMOTEID_LIGHTING_OUTPUT4_PATTERN_BEACON_1HZ_SHORT
+ depends on REMOTEID_LIGHTING_OUTPUT4_ENABLE
+
+config REMOTEID_LIGHTING_OUTPUT4_PATTERN_SOLID
+ bool "Solid on"
+
+config REMOTEID_LIGHTING_OUTPUT4_PATTERN_BEACON_1HZ_SHORT
+ bool "Beacon 1 Hz short flash"
+
+config REMOTEID_LIGHTING_OUTPUT4_PATTERN_BEACON_1HZ_50
+ bool "Beacon 1 Hz 50% duty"
+
+config REMOTEID_LIGHTING_OUTPUT4_PATTERN_STROBE_SINGLE
+ bool "Single strobe"
+
+config REMOTEID_LIGHTING_OUTPUT4_PATTERN_STROBE_DOUBLE
+ bool "Double strobe"
+
+config REMOTEID_LIGHTING_OUTPUT4_PATTERN_STROBE_TRIPLE
+ bool "Triple strobe"
+
+config REMOTEID_LIGHTING_OUTPUT4_PATTERN_FAST_STROBE
+ bool "Fast strobe"
+
+endchoice
+
+config REMOTEID_LIGHTING_OUTPUT4_PHASE_MS
+ int "Output 4 pattern phase offset (ms)"
+ default 0
+ range 0 60000
+ depends on REMOTEID_LIGHTING_OUTPUT4_ENABLE
+
+endmenu
+
+endif # REMOTEID_LIGHTING_ENABLE
+
+endmenu
diff --git a/src/remoteid/main.c b/src/remoteid/main.c
index 752dc6e..df24725 100644
--- a/src/remoteid/main.c
+++ b/src/remoteid/main.c
@@ -1,29 +1,52 @@
#include "esp_bt.h"
#include "esp_err.h"
+#include "esp_log.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
#include "nvs_flash.h"
#include "remoteid/ble.h"
-#include "remoteid/led.h"
+#include "remoteid/indicator.h"
+#include "remoteid/lighting.h"
+#include "remoteid/mavlink.h"
#include "remoteid/model.h"
+#include "remoteid/store.h"
#include "remoteid/wifi.h"
#include "sdkconfig.h"
+static const char *TAG = "remoteid_main";
+
+#ifndef CONFIG_REMOTEID_STARTUP_DELAY_MS
+#define CONFIG_REMOTEID_STARTUP_DELAY_MS 10000
+#endif
+
void app_main(void)
{
static remoteid_state_t state;
- // TODO DO NOT FORGET When remoteid_state_t is no longer static (e.g., when fed through MAVLlink or other channels)
- // ensure access is thread-safe of check how freertos handles it.
-
+ ESP_ERROR_CHECK(remoteid_indicator_init());
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
ESP_ERROR_CHECK(remoteid_model_init(&state));
- ESP_ERROR_CHECK(remoteid_led_init());
+ ESP_ERROR_CHECK(remoteid_store_start(&state));
+ ESP_ERROR_CHECK(remoteid_lighting_init());
+
+#if CONFIG_REMOTEID_MAVLINK_INPUT
+ ESP_ERROR_CHECK(remoteid_mavlink_start());
+#endif
+
+ if (CONFIG_REMOTEID_STARTUP_DELAY_MS > 0) {
+ ESP_LOGI(TAG, "delaying Remote ID transport startup for %d ms", CONFIG_REMOTEID_STARTUP_DELAY_MS);
+ vTaskDelay(pdMS_TO_TICKS(CONFIG_REMOTEID_STARTUP_DELAY_MS));
+ }
#if CONFIG_REMOTEID_TRANSPORT_BLE
- ESP_ERROR_CHECK(remoteid_ble_start(&state));
+ ESP_ERROR_CHECK(remoteid_ble_start());
#endif
#if CONFIG_REMOTEID_TRANSPORT_WIFI_BEACON || CONFIG_REMOTEID_TRANSPORT_WIFI_NAN
- ESP_ERROR_CHECK(remoteid_wifi_start(&state));
+ ESP_ERROR_CHECK(remoteid_wifi_start());
#endif
+
+ remoteid_indicator_mark_transports_started();
+ remoteid_lighting_mark_transports_started();
}
diff --git a/src/remoteid/remoteid/ble.c b/src/remoteid/remoteid/ble.c
index f404a93..1cc9efe 100644
--- a/src/remoteid/remoteid/ble.c
+++ b/src/remoteid/remoteid/ble.c
@@ -11,16 +11,19 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "host/ble_hs.h"
-#include "led.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "opendroneid.h"
#include "sdkconfig.h"
+#include "store.h"
#define REMOTEID_SERVICE_DATA_LEN (2 + ODID_MESSAGE_SIZE)
#define REMOTEID_LEGACY_ADV_DATA_LEN (1 + 1 + 2 + REMOTEID_SERVICE_DATA_LEN)
#define REMOTEID_ADV_INTERVAL_100_MS 0x00a0
-#define REMOTEID_ADV_ROTATION_MS 250
+
+#ifndef CONFIG_REMOTEID_BLE_TX_INTERVAL_MS
+#define CONFIG_REMOTEID_BLE_TX_INTERVAL_MS 250
+#endif
static const char *TAG = "remoteid_ble";
@@ -35,7 +38,6 @@ static const remoteid_message_index_t s_message_schedule[] = {
REMOTEID_MESSAGE_LOCATION,
};
-static const remoteid_state_t *s_state;
static remoteid_message_bundle_t s_bundle;
static uint8_t s_message_counters[ODID_MSG_COUNTER_AMOUNT];
static size_t s_schedule_index;
@@ -109,17 +111,35 @@ static esp_err_t advertise_message(const uint8_t message[ODID_MESSAGE_SIZE])
}
s_message_counters[counter_index]++;
- remoteid_led_pulse();
return ESP_OK;
}
static void remoteid_ble_task(void *arg)
{
(void)arg;
+ remoteid_state_t snapshot;
+
+ ESP_LOGI(TAG, "BLE transport waiting for Remote ID Basic ID and Operator ID");
+ ESP_ERROR_CHECK(remoteid_store_wait_ready(portMAX_DELAY));
+ ESP_LOGI(TAG, "BLE transport identity ready, starting advertisements");
while (true) {
+ if (!remoteid_store_is_ready()) {
+ ble_gap_adv_stop();
+ ESP_LOGW(TAG, "BLE transport paused until Remote ID identity is ready again");
+ ESP_ERROR_CHECK(remoteid_store_wait_ready(portMAX_DELAY));
+ }
+
+ ESP_ERROR_CHECK(remoteid_store_get_snapshot(&snapshot));
+ esp_err_t static_rc = remoteid_encode_static_messages(&snapshot, &s_bundle);
+ if (static_rc != ESP_OK) {
+ ESP_LOGE(TAG, "failed to encode static messages");
+ vTaskDelay(pdMS_TO_TICKS(CONFIG_REMOTEID_BLE_TX_INTERVAL_MS));
+ continue;
+ }
+
remoteid_message_index_t message_index = s_message_schedule[s_schedule_index];
- esp_err_t rc = remoteid_encode_dynamic_message(s_state, &s_bundle, message_index);
+ esp_err_t rc = remoteid_encode_dynamic_message(&snapshot, &s_bundle, message_index);
if (rc != ESP_OK) {
ESP_LOGE(TAG, "failed to encode dynamic message %u, broadcasting stale message", message_index);
}
@@ -130,7 +150,7 @@ static void remoteid_ble_task(void *arg)
}
s_schedule_index = (s_schedule_index + 1) % (sizeof(s_message_schedule) / sizeof(s_message_schedule[0]));
- vTaskDelay(pdMS_TO_TICKS(REMOTEID_ADV_ROTATION_MS));
+ vTaskDelay(pdMS_TO_TICKS(CONFIG_REMOTEID_BLE_TX_INTERVAL_MS));
}
}
@@ -155,11 +175,8 @@ static void nimble_host_task(void *param)
nimble_port_freertos_deinit();
}
-esp_err_t remoteid_ble_start(const remoteid_state_t *state)
+esp_err_t remoteid_ble_start(void)
{
- s_state = state;
-
- ESP_RETURN_ON_ERROR(remoteid_encode_static_messages(s_state, &s_bundle), TAG, "build static ODID messages");
ESP_RETURN_ON_ERROR(nimble_port_init(), TAG, "initialize NimBLE");
ESP_RETURN_ON_ERROR(configure_ble_tx_power(), TAG, "configure BLE TX power");
diff --git a/src/remoteid/remoteid/ble.h b/src/remoteid/remoteid/ble.h
index e068879..128ba62 100644
--- a/src/remoteid/remoteid/ble.h
+++ b/src/remoteid/remoteid/ble.h
@@ -1,6 +1,4 @@
#pragma once
#include "esp_err.h"
-#include "types.h"
-
-esp_err_t remoteid_ble_start(const remoteid_state_t *state);
+esp_err_t remoteid_ble_start(void);
diff --git a/src/remoteid/remoteid/encode.c b/src/remoteid/remoteid/encode.c
index 29d8b5f..b086655 100644
--- a/src/remoteid/remoteid/encode.c
+++ b/src/remoteid/remoteid/encode.c
@@ -33,57 +33,42 @@ static void fill_basic_id_data(const remoteid_state_t *state, ODID_BasicID_data
static void fill_location_data(const remoteid_state_t *state, ODID_Location_data *data)
{
- if (state->has_position) {
- data->Status = ODID_STATUS_AIRBORNE;
- data->Direction = 0.0f;
- data->SpeedHorizontal = 0.0f;
- data->SpeedVertical = 0.0f;
- data->Latitude = state->latitude;
- data->Longitude = state->longitude;
- data->AltitudeBaro = state->altitude_m;
- data->AltitudeGeo = state->altitude_m;
- data->HeightType = ODID_HEIGHT_REF_OVER_TAKEOFF;
- data->Height = 0.0f;
- data->HorizAccuracy = ODID_HOR_ACC_UNKNOWN;
- data->VertAccuracy = ODID_VER_ACC_UNKNOWN;
- data->BaroAccuracy = ODID_VER_ACC_UNKNOWN;
- data->SpeedAccuracy = ODID_SPEED_ACC_UNKNOWN;
- data->TSAccuracy = ODID_TIME_ACC_UNKNOWN;
- data->TimeStamp = (float)(uptime_seconds() % MAX_TIMESTAMP);
- } else {
- data->Status = ODID_STATUS_UNDECLARED;
- data->Direction = (float)INV_DIR;
- data->SpeedHorizontal = (float)INV_SPEED_H;
- data->SpeedVertical = (float)INV_SPEED_V;
- data->Latitude = 0.0;
- data->Longitude = 0.0;
- data->AltitudeBaro = (float)INV_ALT;
- data->AltitudeGeo = (float)INV_ALT;
- data->HeightType = ODID_HEIGHT_REF_OVER_TAKEOFF;
- data->Height = (float)INV_ALT;
- data->HorizAccuracy = ODID_HOR_ACC_UNKNOWN;
- data->VertAccuracy = ODID_VER_ACC_UNKNOWN;
- data->BaroAccuracy = ODID_VER_ACC_UNKNOWN;
- data->SpeedAccuracy = ODID_SPEED_ACC_UNKNOWN;
- data->TSAccuracy = ODID_TIME_ACC_UNKNOWN;
- data->TimeStamp = (float)INV_TIMESTAMP;
- }
+ data->Status = state->status;
+ data->Direction = state->direction;
+ data->SpeedHorizontal = state->speed_horizontal;
+ data->SpeedVertical = state->speed_vertical;
+ data->Latitude = state->latitude;
+ data->Longitude = state->longitude;
+ data->AltitudeBaro = state->altitude_baro_m;
+ data->AltitudeGeo = state->altitude_geo_m;
+ data->HeightType = state->height_type;
+ data->Height = state->height;
+ data->HorizAccuracy = state->horiz_acc;
+ data->VertAccuracy = state->vert_acc;
+ data->BaroAccuracy = state->baro_acc;
+ data->SpeedAccuracy = state->speed_acc;
+ data->TSAccuracy = state->ts_acc;
+ data->TimeStamp = state->timestamp >= 0.0f
+ ? state->timestamp
+ : (float)(uptime_seconds() % MAX_TIMESTAMP);
}
static void fill_system_data(const remoteid_state_t *state, ODID_System_data *data)
{
- data->OperatorLocationType = ODID_OPERATOR_LOCATION_TYPE_TAKEOFF;
- data->ClassificationType = ODID_CLASSIFICATION_TYPE_EU;
- data->OperatorLatitude = state->has_position ? state->latitude : 0.0;
- data->OperatorLongitude = state->has_position ? state->longitude : 0.0;
- data->AreaCount = 1;
- data->AreaRadius = 0;
- data->AreaCeiling = (float)INV_ALT;
- data->AreaFloor = (float)INV_ALT;
- data->CategoryEU = state->eu_category;
- data->ClassEU = state->eu_class;
- data->OperatorAltitudeGeo = state->has_position ? state->altitude_m : (float)INV_ALT;
- data->Timestamp = uptime_seconds();
+ data->OperatorLocationType = state->operator_location_type;
+ data->ClassificationType = state->classification_type;
+ data->OperatorLatitude = state->has_operator_position ? state->operator_latitude : 0.0;
+ data->OperatorLongitude = state->has_operator_position ? state->operator_longitude : 0.0;
+ data->AreaCount = state->area_count;
+ data->AreaRadius = state->area_radius;
+ data->AreaCeiling = state->area_ceiling_m;
+ data->AreaFloor = state->area_floor_m;
+ data->CategoryEU = state->eu_category;
+ data->ClassEU = state->eu_class;
+ data->OperatorAltitudeGeo = state->has_operator_position
+ ? state->operator_altitude_geo_m
+ : (float)INV_ALT;
+ data->Timestamp = uptime_seconds();
}
static void fill_operator_id_data(const remoteid_state_t *state, ODID_OperatorID_data *data)
diff --git a/src/remoteid/remoteid/indicator.c b/src/remoteid/remoteid/indicator.c
new file mode 100644
index 0000000..91f04b2
--- /dev/null
+++ b/src/remoteid/remoteid/indicator.c
@@ -0,0 +1,234 @@
+#include "indicator.h"
+
+#include "sdkconfig.h"
+
+#ifndef CONFIG_REMOTEID_INDICATOR_RGB_ENABLE
+#define CONFIG_REMOTEID_INDICATOR_RGB_ENABLE 0
+#endif
+
+#if CONFIG_REMOTEID_INDICATOR_RGB_ENABLE
+
+#include
+#include
+#include
+
+#include "driver/rmt_encoder.h"
+#include "driver/rmt_tx.h"
+#include "esp_check.h"
+#include "esp_log.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "pattern.h"
+#include "store.h"
+
+#define REMOTEID_INDICATOR_RMT_RESOLUTION_HZ 10000000
+#define REMOTEID_INDICATOR_TASK_STACK 3072
+#define REMOTEID_INDICATOR_TASK_PRIORITY 4
+#define REMOTEID_INDICATOR_LED_BYTES 3
+
+typedef struct remoteid_indicator_color {
+ uint8_t red;
+ uint8_t green;
+ uint8_t blue;
+} remoteid_indicator_color_t;
+
+typedef enum remoteid_indicator_mode {
+ REMOTEID_INDICATOR_MODE_WAITING,
+ REMOTEID_INDICATOR_MODE_OPERATIONAL,
+ REMOTEID_INDICATOR_MODE_ERROR,
+} remoteid_indicator_mode_t;
+
+static const char *TAG = "remoteid_indicator";
+
+static rmt_channel_handle_t s_rmt_channel;
+static rmt_encoder_handle_t s_rmt_encoder;
+static _Atomic bool s_transports_started;
+static _Atomic bool s_error;
+
+static const remoteid_pattern_step_t s_status_waiting[] = {
+ { true, 250 },
+ { false, 1750 },
+};
+
+static const remoteid_pattern_step_t s_status_error[] = {
+ { true, 100 },
+ { false, 100 },
+};
+
+static uint8_t scale_channel(uint8_t value)
+{
+ return (uint8_t)(((uint16_t)value * CONFIG_REMOTEID_INDICATOR_RGB_BRIGHTNESS_PERCENT) / 100U);
+}
+
+static esp_err_t set_rgb(remoteid_indicator_color_t color)
+{
+ uint8_t payload[REMOTEID_INDICATOR_LED_BYTES] = {
+ scale_channel(color.green),
+ scale_channel(color.red),
+ scale_channel(color.blue),
+ };
+ const rmt_transmit_config_t tx_config = {
+ .loop_count = 0,
+ .flags.eot_level = 0,
+ };
+
+ ESP_RETURN_ON_ERROR(rmt_transmit(s_rmt_channel, s_rmt_encoder, payload, sizeof(payload), &tx_config),
+ TAG, "transmit onboard RGB LED data");
+ ESP_RETURN_ON_ERROR(rmt_tx_wait_all_done(s_rmt_channel, 100), TAG, "wait for onboard RGB LED update");
+ return ESP_OK;
+}
+
+static void set_rgb_logged(remoteid_indicator_color_t color)
+{
+ esp_err_t rc = set_rgb(color);
+ if (rc != ESP_OK) {
+ ESP_LOGW(TAG, "failed to update onboard RGB LED: %s", esp_err_to_name(rc));
+ }
+}
+
+static const remoteid_pattern_step_t *operational_pattern(size_t *count)
+{
+#if CONFIG_REMOTEID_INDICATOR_PATTERN_SOLID
+ return remoteid_pattern_steps(REMOTEID_PATTERN_SOLID, count);
+#elif CONFIG_REMOTEID_INDICATOR_PATTERN_DRONE_BEACON_1HZ_50
+ return remoteid_pattern_steps(REMOTEID_PATTERN_BEACON_1HZ_50, count);
+#elif CONFIG_REMOTEID_INDICATOR_PATTERN_STROBE_SINGLE
+ return remoteid_pattern_steps(REMOTEID_PATTERN_STROBE_SINGLE, count);
+#elif CONFIG_REMOTEID_INDICATOR_PATTERN_STROBE_DOUBLE
+ return remoteid_pattern_steps(REMOTEID_PATTERN_STROBE_DOUBLE, count);
+#elif CONFIG_REMOTEID_INDICATOR_PATTERN_STROBE_TRIPLE
+ return remoteid_pattern_steps(REMOTEID_PATTERN_STROBE_TRIPLE, count);
+#elif CONFIG_REMOTEID_INDICATOR_PATTERN_FAST_STROBE
+ return remoteid_pattern_steps(REMOTEID_PATTERN_FAST_STROBE, count);
+#else
+ return remoteid_pattern_steps(REMOTEID_PATTERN_BEACON_1HZ_SHORT, count);
+#endif
+}
+
+static remoteid_indicator_mode_t current_mode(void)
+{
+ if (atomic_load_explicit(&s_error, memory_order_acquire)) {
+ return REMOTEID_INDICATOR_MODE_ERROR;
+ }
+ if (atomic_load_explicit(&s_transports_started, memory_order_acquire) && remoteid_store_is_ready()) {
+ return REMOTEID_INDICATOR_MODE_OPERATIONAL;
+ }
+ return REMOTEID_INDICATOR_MODE_WAITING;
+}
+
+static const remoteid_pattern_step_t *pattern_for_mode(remoteid_indicator_mode_t mode,
+ size_t *count,
+ remoteid_indicator_color_t *color)
+{
+ if (mode == REMOTEID_INDICATOR_MODE_ERROR) {
+ *count = sizeof(s_status_error) / sizeof(s_status_error[0]);
+ *color = (remoteid_indicator_color_t){ .red = 255, .green = 0, .blue = 0 };
+ return s_status_error;
+ }
+
+ if (mode == REMOTEID_INDICATOR_MODE_OPERATIONAL) {
+ *color = (remoteid_indicator_color_t){ .red = 0, .green = 255, .blue = 0 };
+ return operational_pattern(count);
+ }
+
+ *count = sizeof(s_status_waiting) / sizeof(s_status_waiting[0]);
+ *color = (remoteid_indicator_color_t){ .red = 255, .green = 128, .blue = 0 };
+ return s_status_waiting;
+}
+
+static void remoteid_indicator_task(void *arg)
+{
+ (void)arg;
+ set_rgb_logged((remoteid_indicator_color_t){ 0 });
+
+ while (true) {
+ size_t step_count = 0;
+ remoteid_indicator_color_t color = { 0 };
+ remoteid_indicator_mode_t mode = current_mode();
+ const remoteid_pattern_step_t *pattern = pattern_for_mode(mode, &step_count, &color);
+
+ for (size_t i = 0; i < step_count; i++) {
+ set_rgb_logged(pattern[i].on ? color : (remoteid_indicator_color_t){ 0 });
+ vTaskDelay(pdMS_TO_TICKS(pattern[i].duration_ms));
+
+ if (current_mode() != mode) {
+ break;
+ }
+ }
+ }
+}
+
+esp_err_t remoteid_indicator_init(void)
+{
+ if (CONFIG_REMOTEID_INDICATOR_RGB_GPIO < 0) {
+ ESP_LOGW(TAG, "onboard RGB indicator enabled but no GPIO configured; indicator disabled");
+ return ESP_OK;
+ }
+
+ const rmt_tx_channel_config_t channel_config = {
+ .gpio_num = CONFIG_REMOTEID_INDICATOR_RGB_GPIO,
+ .clk_src = RMT_CLK_SRC_DEFAULT,
+ .resolution_hz = REMOTEID_INDICATOR_RMT_RESOLUTION_HZ,
+ .mem_block_symbols = 64,
+ .trans_queue_depth = 1,
+ .flags.init_level = 0,
+ };
+ ESP_RETURN_ON_ERROR(rmt_new_tx_channel(&channel_config, &s_rmt_channel), TAG,
+ "create onboard RGB LED RMT channel");
+
+ const rmt_bytes_encoder_config_t encoder_config = {
+ .bit0 = {
+ .level0 = 1,
+ .duration0 = 3,
+ .level1 = 0,
+ .duration1 = 9,
+ },
+ .bit1 = {
+ .level0 = 1,
+ .duration0 = 9,
+ .level1 = 0,
+ .duration1 = 3,
+ },
+ .flags.msb_first = 1,
+ };
+ ESP_RETURN_ON_ERROR(rmt_new_bytes_encoder(&encoder_config, &s_rmt_encoder), TAG,
+ "create onboard RGB LED RMT encoder");
+ ESP_RETURN_ON_ERROR(rmt_enable(s_rmt_channel), TAG, "enable onboard RGB LED RMT channel");
+
+ if (xTaskCreate(remoteid_indicator_task, "remoteid_indicator", REMOTEID_INDICATOR_TASK_STACK,
+ NULL, REMOTEID_INDICATOR_TASK_PRIORITY, NULL) != pdPASS) {
+ ESP_LOGE(TAG, "failed to create onboard RGB indicator task");
+ return ESP_ERR_NO_MEM;
+ }
+
+ ESP_LOGI(TAG, "onboard RGB indicator enabled on GPIO %d, brightness %d%%",
+ CONFIG_REMOTEID_INDICATOR_RGB_GPIO, CONFIG_REMOTEID_INDICATOR_RGB_BRIGHTNESS_PERCENT);
+ return ESP_OK;
+}
+
+void remoteid_indicator_mark_transports_started(void)
+{
+ atomic_store_explicit(&s_transports_started, true, memory_order_release);
+}
+
+void remoteid_indicator_set_error(void)
+{
+ atomic_store_explicit(&s_error, true, memory_order_release);
+}
+
+#else
+
+esp_err_t remoteid_indicator_init(void)
+{
+ return ESP_OK;
+}
+
+void remoteid_indicator_mark_transports_started(void)
+{
+}
+
+void remoteid_indicator_set_error(void)
+{
+}
+
+#endif
diff --git a/src/remoteid/remoteid/indicator.h b/src/remoteid/remoteid/indicator.h
new file mode 100644
index 0000000..1271ed3
--- /dev/null
+++ b/src/remoteid/remoteid/indicator.h
@@ -0,0 +1,7 @@
+#pragma once
+
+#include "esp_err.h"
+
+esp_err_t remoteid_indicator_init(void);
+void remoteid_indicator_mark_transports_started(void);
+void remoteid_indicator_set_error(void);
diff --git a/src/remoteid/remoteid/led.c b/src/remoteid/remoteid/led.c
deleted file mode 100644
index a4dc088..0000000
--- a/src/remoteid/remoteid/led.c
+++ /dev/null
@@ -1,87 +0,0 @@
-#include "led.h"
-
-#include "driver/gpio.h"
-#include "esp_check.h"
-#include "esp_log.h"
-#include "esp_timer.h"
-#include "sdkconfig.h"
-
-#ifndef CONFIG_REMOTEID_TX_LED_GPIO
-#define CONFIG_REMOTEID_TX_LED_GPIO -1
-#endif
-
-#ifndef CONFIG_REMOTEID_TX_LED_ACTIVE_HIGH
-#define CONFIG_REMOTEID_TX_LED_ACTIVE_HIGH 1
-#endif
-
-#ifndef CONFIG_REMOTEID_TX_LED_PULSE_MS
-#define CONFIG_REMOTEID_TX_LED_PULSE_MS 40
-#endif
-
-static const char *TAG = "remoteid_led";
-
-#if CONFIG_REMOTEID_TX_LED_GPIO >= 0
-static esp_timer_handle_t s_led_timer;
-
-static int led_on_level(void)
-{
- return CONFIG_REMOTEID_TX_LED_ACTIVE_HIGH ? 1 : 0;
-}
-
-static int led_off_level(void)
-{
- return CONFIG_REMOTEID_TX_LED_ACTIVE_HIGH ? 0 : 1;
-}
-
-static void turn_led_off(void *arg)
-{
- (void)arg;
- gpio_set_level(CONFIG_REMOTEID_TX_LED_GPIO, led_off_level());
-}
-
-esp_err_t remoteid_led_init(void)
-{
- gpio_config_t config = {
- .pin_bit_mask = 1ULL << CONFIG_REMOTEID_TX_LED_GPIO,
- .mode = GPIO_MODE_OUTPUT,
- .pull_up_en = GPIO_PULLUP_DISABLE,
- .pull_down_en = GPIO_PULLDOWN_DISABLE,
- .intr_type = GPIO_INTR_DISABLE,
- };
- ESP_RETURN_ON_ERROR(gpio_config(&config), TAG, "configure TX status LED GPIO");
- ESP_RETURN_ON_ERROR(gpio_set_level(CONFIG_REMOTEID_TX_LED_GPIO, led_off_level()), TAG, "turn TX status LED off");
-
- const esp_timer_create_args_t timer_args = {
- .callback = turn_led_off,
- .name = "remoteid_tx_led",
- };
- ESP_RETURN_ON_ERROR(esp_timer_create(&timer_args, &s_led_timer), TAG, "create TX status LED timer");
-
- ESP_LOGI(TAG, "TX status LED enabled on GPIO %d, active %s, pulse %d ms", CONFIG_REMOTEID_TX_LED_GPIO,
- CONFIG_REMOTEID_TX_LED_ACTIVE_HIGH ? "high" : "low", CONFIG_REMOTEID_TX_LED_PULSE_MS);
-
- return ESP_OK;
-}
-
-void remoteid_led_pulse(void)
-{
- gpio_set_level(CONFIG_REMOTEID_TX_LED_GPIO, led_on_level());
- esp_timer_stop(s_led_timer);
- esp_err_t rc = esp_timer_start_once(s_led_timer, CONFIG_REMOTEID_TX_LED_PULSE_MS * 1000ULL);
- if (rc != ESP_OK) {
- ESP_LOGW(TAG, "failed to start TX status LED pulse timer: %s", esp_err_to_name(rc));
- gpio_set_level(CONFIG_REMOTEID_TX_LED_GPIO, led_off_level());
- }
-}
-#else
-esp_err_t remoteid_led_init(void)
-{
- ESP_LOGI(TAG, "TX status LED disabled; set ESP Remote ID -> TX status LED GPIO to pulse an LED");
- return ESP_OK;
-}
-
-void remoteid_led_pulse(void)
-{
- // TODO Implement
-}
-#endif
diff --git a/src/remoteid/remoteid/led.h b/src/remoteid/remoteid/led.h
deleted file mode 100644
index 27582cf..0000000
--- a/src/remoteid/remoteid/led.h
+++ /dev/null
@@ -1,6 +0,0 @@
-#pragma once
-
-#include "esp_err.h"
-
-esp_err_t remoteid_led_init(void);
-void remoteid_led_pulse(void);
diff --git a/src/remoteid/remoteid/lighting.c b/src/remoteid/remoteid/lighting.c
new file mode 100644
index 0000000..11f4a5a
--- /dev/null
+++ b/src/remoteid/remoteid/lighting.c
@@ -0,0 +1,365 @@
+#include "lighting.h"
+
+#include
+#include
+#include
+
+#include "pattern.h"
+
+#include "driver/gpio.h"
+#include "esp_check.h"
+#include "esp_log.h"
+#include "esp_timer.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "sdkconfig.h"
+#include "store.h"
+
+#ifndef CONFIG_REMOTEID_LIGHTING_ENABLE
+#define CONFIG_REMOTEID_LIGHTING_ENABLE 0
+#endif
+
+#define REMOTEID_LIGHTING_OUTPUT_COUNT 5
+#define REMOTEID_LIGHTING_TASK_STACK 3072
+#define REMOTEID_LIGHTING_TASK_PRIORITY 4
+#define REMOTEID_LIGHTING_TICK_MS 20
+
+#if CONFIG_REMOTEID_LIGHTING_ENABLE
+
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT0_ENABLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT0_ENABLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT0_GPIO
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT0_GPIO -1
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT0_ACTIVE_HIGH
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT0_ACTIVE_HIGH 1
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT0_OPEN_DRAIN
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT0_OPEN_DRAIN 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_SOLID
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_SOLID 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_BEACON_1HZ_50
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_BEACON_1HZ_50 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_STROBE_SINGLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_STROBE_SINGLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_STROBE_DOUBLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_STROBE_DOUBLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_STROBE_TRIPLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_STROBE_TRIPLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_FAST_STROBE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT0_PATTERN_FAST_STROBE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT0_PHASE_MS
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT0_PHASE_MS 0
+#endif
+
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT1_ENABLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT1_ENABLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT1_GPIO
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT1_GPIO -1
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT1_ACTIVE_HIGH
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT1_ACTIVE_HIGH 1
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT1_OPEN_DRAIN
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT1_OPEN_DRAIN 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_SOLID
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_SOLID 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_BEACON_1HZ_50
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_BEACON_1HZ_50 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_STROBE_SINGLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_STROBE_SINGLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_STROBE_DOUBLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_STROBE_DOUBLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_STROBE_TRIPLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_STROBE_TRIPLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_FAST_STROBE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT1_PATTERN_FAST_STROBE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT1_PHASE_MS
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT1_PHASE_MS 0
+#endif
+
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT2_ENABLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT2_ENABLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT2_GPIO
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT2_GPIO -1
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT2_ACTIVE_HIGH
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT2_ACTIVE_HIGH 1
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT2_OPEN_DRAIN
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT2_OPEN_DRAIN 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_SOLID
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_SOLID 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_BEACON_1HZ_50
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_BEACON_1HZ_50 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_STROBE_SINGLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_STROBE_SINGLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_STROBE_DOUBLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_STROBE_DOUBLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_STROBE_TRIPLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_STROBE_TRIPLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_FAST_STROBE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT2_PATTERN_FAST_STROBE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT2_PHASE_MS
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT2_PHASE_MS 0
+#endif
+
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT3_ENABLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT3_ENABLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT3_GPIO
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT3_GPIO -1
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT3_ACTIVE_HIGH
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT3_ACTIVE_HIGH 1
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT3_OPEN_DRAIN
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT3_OPEN_DRAIN 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_SOLID
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_SOLID 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_BEACON_1HZ_50
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_BEACON_1HZ_50 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_STROBE_SINGLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_STROBE_SINGLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_STROBE_DOUBLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_STROBE_DOUBLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_STROBE_TRIPLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_STROBE_TRIPLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_FAST_STROBE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT3_PATTERN_FAST_STROBE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT3_PHASE_MS
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT3_PHASE_MS 0
+#endif
+
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT4_ENABLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT4_ENABLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT4_GPIO
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT4_GPIO -1
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT4_ACTIVE_HIGH
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT4_ACTIVE_HIGH 1
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT4_OPEN_DRAIN
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT4_OPEN_DRAIN 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_SOLID
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_SOLID 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_BEACON_1HZ_50
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_BEACON_1HZ_50 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_STROBE_SINGLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_STROBE_SINGLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_STROBE_DOUBLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_STROBE_DOUBLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_STROBE_TRIPLE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_STROBE_TRIPLE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_FAST_STROBE
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT4_PATTERN_FAST_STROBE 0
+#endif
+#ifndef CONFIG_REMOTEID_LIGHTING_OUTPUT4_PHASE_MS
+#define CONFIG_REMOTEID_LIGHTING_OUTPUT4_PHASE_MS 0
+#endif
+
+typedef struct remoteid_lighting_output_config {
+ bool enabled;
+ int gpio;
+ bool active_high;
+ bool open_drain;
+ remoteid_pattern_t pattern;
+ uint32_t phase_ms;
+} remoteid_lighting_output_config_t;
+
+typedef struct remoteid_lighting_output_state {
+ bool configured;
+} remoteid_lighting_output_state_t;
+
+static const char *TAG = "remoteid_lighting";
+
+static _Atomic bool s_transports_started;
+static int64_t s_start_us;
+static remoteid_lighting_output_state_t s_outputs[REMOTEID_LIGHTING_OUTPUT_COUNT];
+
+#define OUTPUT_PATTERN(idx) \
+ (CONFIG_REMOTEID_LIGHTING_OUTPUT##idx##_PATTERN_SOLID ? REMOTEID_PATTERN_SOLID : \
+ CONFIG_REMOTEID_LIGHTING_OUTPUT##idx##_PATTERN_BEACON_1HZ_50 ? REMOTEID_PATTERN_BEACON_1HZ_50 : \
+ CONFIG_REMOTEID_LIGHTING_OUTPUT##idx##_PATTERN_STROBE_SINGLE ? REMOTEID_PATTERN_STROBE_SINGLE : \
+ CONFIG_REMOTEID_LIGHTING_OUTPUT##idx##_PATTERN_STROBE_DOUBLE ? REMOTEID_PATTERN_STROBE_DOUBLE : \
+ CONFIG_REMOTEID_LIGHTING_OUTPUT##idx##_PATTERN_STROBE_TRIPLE ? REMOTEID_PATTERN_STROBE_TRIPLE : \
+ CONFIG_REMOTEID_LIGHTING_OUTPUT##idx##_PATTERN_FAST_STROBE ? REMOTEID_PATTERN_FAST_STROBE : \
+ REMOTEID_PATTERN_BEACON_1HZ_SHORT)
+
+#define OUTPUT_CONFIG(idx) \
+ { \
+ .enabled = CONFIG_REMOTEID_LIGHTING_OUTPUT##idx##_ENABLE, \
+ .gpio = CONFIG_REMOTEID_LIGHTING_OUTPUT##idx##_GPIO, \
+ .active_high = CONFIG_REMOTEID_LIGHTING_OUTPUT##idx##_ACTIVE_HIGH, \
+ .open_drain = CONFIG_REMOTEID_LIGHTING_OUTPUT##idx##_OPEN_DRAIN, \
+ .pattern = OUTPUT_PATTERN(idx), \
+ .phase_ms = CONFIG_REMOTEID_LIGHTING_OUTPUT##idx##_PHASE_MS, \
+ }
+
+static const remoteid_lighting_output_config_t s_config[REMOTEID_LIGHTING_OUTPUT_COUNT] = {
+ OUTPUT_CONFIG(0),
+ OUTPUT_CONFIG(1),
+ OUTPUT_CONFIG(2),
+ OUTPUT_CONFIG(3),
+ OUTPUT_CONFIG(4),
+};
+
+static int output_level(const remoteid_lighting_output_config_t *config, bool on)
+{
+ bool active = config->active_high ? on : !on;
+ return active ? 1 : 0;
+}
+
+static void set_output(size_t index, bool on)
+{
+ if (!s_outputs[index].configured) {
+ return;
+ }
+
+ gpio_set_level(s_config[index].gpio, output_level(&s_config[index], on));
+}
+
+
+static bool output_should_be_on(size_t index, int64_t now_us)
+{
+ const remoteid_lighting_output_config_t *config = &s_config[index];
+
+ if (!s_outputs[index].configured) {
+ return false;
+ }
+
+ if (!atomic_load_explicit(&s_transports_started, memory_order_acquire) || !remoteid_store_is_ready()) {
+ return false;
+ }
+
+ uint32_t elapsed_ms = (uint32_t)((now_us - s_start_us) / 1000LL);
+ return remoteid_pattern_is_on(config->pattern, elapsed_ms, config->phase_ms);
+}
+
+static void lighting_task(void *arg)
+{
+ (void)arg;
+
+ while (true) {
+ int64_t now_us = esp_timer_get_time();
+ for (size_t i = 0; i < REMOTEID_LIGHTING_OUTPUT_COUNT; i++) {
+ set_output(i, output_should_be_on(i, now_us));
+ }
+ vTaskDelay(pdMS_TO_TICKS(REMOTEID_LIGHTING_TICK_MS));
+ }
+}
+
+static bool gpio_already_used(size_t current_index)
+{
+ for (size_t i = 0; i < current_index; i++) {
+ if (s_outputs[i].configured && s_config[i].gpio == s_config[current_index].gpio) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static esp_err_t configure_output(size_t index)
+{
+ const remoteid_lighting_output_config_t *config = &s_config[index];
+
+ if (!config->enabled) {
+ return ESP_OK;
+ }
+ if (config->gpio < 0) {
+ ESP_LOGW(TAG, "lighting output %u enabled without GPIO; output disabled", index);
+ return ESP_OK;
+ }
+ if (gpio_already_used(index)) {
+ ESP_LOGW(TAG, "lighting output %u duplicates GPIO %d; output disabled", index, config->gpio);
+ return ESP_OK;
+ }
+
+ gpio_config_t pin_config = {
+ .pin_bit_mask = 1ULL << config->gpio,
+ .mode = config->open_drain ? GPIO_MODE_OUTPUT_OD : GPIO_MODE_OUTPUT,
+ .pull_up_en = GPIO_PULLUP_DISABLE,
+ .pull_down_en = GPIO_PULLDOWN_DISABLE,
+ .intr_type = GPIO_INTR_DISABLE,
+ };
+ ESP_RETURN_ON_ERROR(gpio_config(&pin_config), TAG, "configure lighting output GPIO");
+
+ s_outputs[index].configured = true;
+ set_output(index, false);
+ ESP_LOGI(TAG, "lighting output %u enabled on GPIO %d (pattern=%d phase=%lu ms)",
+ index, config->gpio, config->pattern, config->phase_ms);
+ return ESP_OK;
+}
+
+esp_err_t remoteid_lighting_init(void)
+{
+ s_start_us = esp_timer_get_time();
+
+ for (size_t i = 0; i < REMOTEID_LIGHTING_OUTPUT_COUNT; i++) {
+ ESP_RETURN_ON_ERROR(configure_output(i), TAG, "configure lighting output");
+ }
+
+ if (xTaskCreate(lighting_task, "remoteid_lighting", REMOTEID_LIGHTING_TASK_STACK,
+ NULL, REMOTEID_LIGHTING_TASK_PRIORITY, NULL) != pdPASS) {
+ ESP_LOGE(TAG, "failed to create lighting task");
+ return ESP_ERR_NO_MEM;
+ }
+
+ return ESP_OK;
+}
+
+void remoteid_lighting_mark_transports_started(void)
+{
+ atomic_store_explicit(&s_transports_started, true, memory_order_release);
+}
+
+#else
+
+esp_err_t remoteid_lighting_init(void)
+{
+ return ESP_OK;
+}
+
+void remoteid_lighting_mark_transports_started(void)
+{
+}
+
+#endif
diff --git a/src/remoteid/remoteid/lighting.h b/src/remoteid/remoteid/lighting.h
new file mode 100644
index 0000000..0802ede
--- /dev/null
+++ b/src/remoteid/remoteid/lighting.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include "esp_err.h"
+
+esp_err_t remoteid_lighting_init(void);
+void remoteid_lighting_mark_transports_started(void);
diff --git a/src/remoteid/remoteid/mavlink.c b/src/remoteid/remoteid/mavlink.c
new file mode 100644
index 0000000..b6b604e
--- /dev/null
+++ b/src/remoteid/remoteid/mavlink.c
@@ -0,0 +1,274 @@
+#include "mavlink.h"
+
+#include "sdkconfig.h"
+
+#if CONFIG_REMOTEID_MAVLINK_INPUT
+
+#include
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Waddress-of-packed-member"
+#include "common/mavlink.h"
+#pragma GCC diagnostic pop
+#include "driver/uart.h"
+#include "esp_check.h"
+#include "esp_log.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "store.h"
+
+#define REMOTEID_MAVLINK_BUF_SIZE 256
+#define REMOTEID_MAVLINK_QUEUE_SIZE 0
+#define REMOTEID_MAVLINK_TASK_PRIORITY 5
+
+static const char *TAG = "remoteid_mavlink";
+
+static bool target_matches(uint8_t target_system, uint8_t target_component)
+{
+ if (CONFIG_REMOTEID_MAVLINK_TARGET_SYSTEM != 0 &&
+ target_system != 0 &&
+ target_system != CONFIG_REMOTEID_MAVLINK_TARGET_SYSTEM) {
+ return false;
+ }
+
+ if (CONFIG_REMOTEID_MAVLINK_TARGET_COMPONENT != 0 &&
+ target_component != 0 &&
+ target_component != CONFIG_REMOTEID_MAVLINK_TARGET_COMPONENT) {
+ return false;
+ }
+
+ return true;
+}
+
+static void copy_mavlink_text(char *dst, size_t dst_len, const void *src, size_t src_len)
+{
+ size_t len = 0;
+ const char *text = (const char *)src;
+
+ while (len < src_len && text[len] != '\0') {
+ len++;
+ }
+
+ if (len >= dst_len) {
+ len = dst_len - 1;
+ }
+
+ memcpy(dst, text, len);
+ dst[len] = '\0';
+}
+
+static void submit_basic_id(const mavlink_message_t *message)
+{
+ mavlink_open_drone_id_basic_id_t msg = { 0 };
+ mavlink_msg_open_drone_id_basic_id_decode(message, &msg);
+
+ if (!target_matches(msg.target_system, msg.target_component)) {
+ ESP_LOGD(TAG, "ignored Basic ID for target system=%u component=%u", msg.target_system, msg.target_component);
+ return;
+ }
+
+ remoteid_store_update_t update = {
+ .type = REMOTEID_STORE_UPDATE_BASIC_ID,
+ .data.basic_id.id_type = (ODID_idtype_t)msg.id_type,
+ .data.basic_id.ua_type = (ODID_uatype_t)msg.ua_type,
+ };
+ copy_mavlink_text(update.data.basic_id.uas_id, sizeof(update.data.basic_id.uas_id),
+ msg.uas_id, sizeof(msg.uas_id));
+
+ if (remoteid_store_submit(&update, pdMS_TO_TICKS(100)) == ESP_OK) {
+ ESP_LOGI(TAG, "accepted MAVLink Basic ID");
+ } else {
+ ESP_LOGW(TAG, "dropped MAVLink Basic ID update: state queue full");
+ }
+}
+
+static void submit_operator_id(const mavlink_message_t *message)
+{
+ mavlink_open_drone_id_operator_id_t msg = { 0 };
+ mavlink_msg_open_drone_id_operator_id_decode(message, &msg);
+
+ if (!target_matches(msg.target_system, msg.target_component)) {
+ ESP_LOGD(TAG, "ignored Operator ID for target system=%u component=%u", msg.target_system, msg.target_component);
+ return;
+ }
+
+ remoteid_store_update_t update = {
+ .type = REMOTEID_STORE_UPDATE_OPERATOR_ID,
+ };
+ copy_mavlink_text(update.data.operator_id.operator_id, sizeof(update.data.operator_id.operator_id),
+ msg.operator_id, sizeof(msg.operator_id));
+
+ if (remoteid_store_submit(&update, pdMS_TO_TICKS(100)) == ESP_OK) {
+ ESP_LOGI(TAG, "accepted MAVLink Operator ID");
+ } else {
+ ESP_LOGW(TAG, "dropped MAVLink Operator ID update: state queue full");
+ }
+}
+
+static void submit_location(const mavlink_message_t *message)
+{
+ mavlink_open_drone_id_location_t msg = { 0 };
+ mavlink_msg_open_drone_id_location_decode(message, &msg);
+
+ if (!target_matches(msg.target_system, msg.target_component)) {
+ ESP_LOGD(TAG, "ignored Location for target system=%u component=%u", msg.target_system, msg.target_component);
+ return;
+ }
+
+ bool has_position = msg.latitude != 0 || msg.longitude != 0;
+
+ remoteid_store_update_t update = {
+ .type = REMOTEID_STORE_UPDATE_LOCATION,
+ .data.location.has_position = has_position,
+ .data.location.latitude = (double)msg.latitude / 10000000.0,
+ .data.location.longitude = (double)msg.longitude / 10000000.0,
+ .data.location.altitude_geo_m = msg.altitude_geodetic <= (float)INV_ALT ? (float)INV_ALT : msg.altitude_geodetic,
+ .data.location.altitude_baro_m = msg.altitude_barometric <= (float)INV_ALT ? (float)INV_ALT : msg.altitude_barometric,
+ .data.location.status = (ODID_status_t)msg.status,
+ .data.location.speed_horizontal = msg.speed_horizontal >= (float)INV_SPEED_H ? (float)INV_SPEED_H : msg.speed_horizontal,
+ .data.location.speed_vertical = msg.speed_vertical >= (float)INV_SPEED_V ? (float)INV_SPEED_V : msg.speed_vertical,
+ .data.location.direction = msg.direction >= 36100
+ ? (float)INV_DIR
+ : msg.direction / 100.0f,
+ .data.location.height = msg.height <= (float)INV_ALT ? (float)INV_ALT : msg.height,
+ .data.location.height_type = (ODID_Height_reference_t)msg.height_reference,
+ .data.location.horiz_acc = (ODID_Horizontal_accuracy_t)msg.horizontal_accuracy,
+ .data.location.vert_acc = (ODID_Vertical_accuracy_t)msg.vertical_accuracy,
+ .data.location.baro_acc = (ODID_Vertical_accuracy_t)msg.barometer_accuracy,
+ .data.location.speed_acc = (ODID_Speed_accuracy_t)msg.speed_accuracy,
+ .data.location.ts_acc = (ODID_Timestamp_accuracy_t)msg.timestamp_accuracy,
+ .data.location.timestamp = msg.timestamp >= (float)MAX_TIMESTAMP ? -1.0f : msg.timestamp,
+ };
+
+ if (remoteid_store_submit(&update, pdMS_TO_TICKS(100)) == ESP_OK) {
+ ESP_LOGD(TAG, "accepted MAVLink Location (has_position=%d status=%u)", has_position, msg.status);
+ } else {
+ ESP_LOGW(TAG, "dropped MAVLink Location update: state queue full");
+ }
+}
+
+static void submit_system(const mavlink_message_t *message)
+{
+ mavlink_open_drone_id_system_t msg = { 0 };
+ mavlink_msg_open_drone_id_system_decode(message, &msg);
+
+ if (!target_matches(msg.target_system, msg.target_component)) {
+ ESP_LOGD(TAG, "ignored System for target system=%u component=%u", msg.target_system, msg.target_component);
+ return;
+ }
+
+ bool has_operator_position = msg.operator_latitude != 0 || msg.operator_longitude != 0;
+
+ remoteid_store_update_t update = {
+ .type = REMOTEID_STORE_UPDATE_SYSTEM,
+ .data.system.has_operator_position = has_operator_position,
+ .data.system.operator_latitude = (double)msg.operator_latitude / 10000000.0,
+ .data.system.operator_longitude = (double)msg.operator_longitude / 10000000.0,
+ .data.system.operator_altitude_geo_m = msg.operator_altitude_geo <= (float)INV_ALT ? (float)INV_ALT : msg.operator_altitude_geo,
+ .data.system.operator_location_type = (ODID_operator_location_type_t)msg.operator_location_type,
+ .data.system.area_count = msg.area_count,
+ .data.system.area_radius = msg.area_radius,
+ .data.system.area_ceiling_m = msg.area_ceiling,
+ .data.system.area_floor_m = msg.area_floor,
+ .data.system.classification_type = (ODID_classification_type_t)msg.classification_type,
+ .data.system.eu_category = (ODID_category_EU_t)msg.category_eu,
+ .data.system.eu_class = (ODID_class_EU_t)msg.class_eu,
+ };
+
+ if (remoteid_store_submit(&update, pdMS_TO_TICKS(100)) == ESP_OK) {
+ ESP_LOGD(TAG, "accepted MAVLink System (has_operator_position=%d)", has_operator_position);
+ } else {
+ ESP_LOGW(TAG, "dropped MAVLink System update: state queue full");
+ }
+}
+
+static void handle_message(const mavlink_message_t *message)
+{
+ switch (message->msgid) {
+ case MAVLINK_MSG_ID_OPEN_DRONE_ID_BASIC_ID:
+ submit_basic_id(message);
+ break;
+ case MAVLINK_MSG_ID_OPEN_DRONE_ID_OPERATOR_ID:
+ submit_operator_id(message);
+ break;
+ case MAVLINK_MSG_ID_OPEN_DRONE_ID_LOCATION:
+ submit_location(message);
+ break;
+ case MAVLINK_MSG_ID_OPEN_DRONE_ID_SYSTEM:
+ submit_system(message);
+ break;
+ default:
+ break;
+ }
+}
+
+static void remoteid_mavlink_task(void *arg)
+{
+ (void)arg;
+ uint8_t buf[REMOTEID_MAVLINK_BUF_SIZE];
+ mavlink_message_t message;
+ mavlink_status_t status;
+
+ memset(&message, 0, sizeof(message));
+ memset(&status, 0, sizeof(status));
+
+ while (true) {
+ int len = uart_read_bytes(CONFIG_REMOTEID_MAVLINK_UART_NUM, buf, sizeof(buf), pdMS_TO_TICKS(1000));
+ for (int i = 0; i < len; i++) {
+ if (mavlink_parse_char(MAVLINK_COMM_0, buf[i], &message, &status)) {
+ handle_message(&message);
+ }
+ }
+ }
+}
+
+esp_err_t remoteid_mavlink_start(void)
+{
+ ESP_RETURN_ON_FALSE(CONFIG_REMOTEID_MAVLINK_RX_GPIO >= 0, ESP_ERR_INVALID_ARG, TAG,
+ "MAVLink RX GPIO must be configured when MAVLink input is enabled");
+
+ uart_config_t uart_config = {
+ .baud_rate = CONFIG_REMOTEID_MAVLINK_BAUD,
+ .data_bits = UART_DATA_8_BITS,
+ .parity = UART_PARITY_DISABLE,
+ .stop_bits = UART_STOP_BITS_1,
+ .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
+ .source_clk = UART_SCLK_DEFAULT,
+ };
+
+ int tx_pin = CONFIG_REMOTEID_MAVLINK_TX_GPIO >= 0 ? CONFIG_REMOTEID_MAVLINK_TX_GPIO : UART_PIN_NO_CHANGE;
+ ESP_RETURN_ON_ERROR(uart_driver_install(CONFIG_REMOTEID_MAVLINK_UART_NUM,
+ REMOTEID_MAVLINK_BUF_SIZE * 2,
+ 0,
+ REMOTEID_MAVLINK_QUEUE_SIZE,
+ NULL,
+ 0),
+ TAG, "install MAVLink UART driver");
+ ESP_RETURN_ON_ERROR(uart_param_config(CONFIG_REMOTEID_MAVLINK_UART_NUM, &uart_config),
+ TAG, "configure MAVLink UART");
+ ESP_RETURN_ON_ERROR(uart_set_pin(CONFIG_REMOTEID_MAVLINK_UART_NUM,
+ tx_pin,
+ CONFIG_REMOTEID_MAVLINK_RX_GPIO,
+ UART_PIN_NO_CHANGE,
+ UART_PIN_NO_CHANGE),
+ TAG, "configure MAVLink UART pins");
+
+ if (xTaskCreate(remoteid_mavlink_task, "remoteid_mavlink", CONFIG_REMOTEID_MAVLINK_TASK_STACK,
+ NULL, REMOTEID_MAVLINK_TASK_PRIORITY, NULL) != pdPASS) {
+ ESP_LOGE(TAG, "failed to create MAVLink input task");
+ return ESP_ERR_NO_MEM;
+ }
+
+ ESP_LOGI(TAG, "MAVLink OpenDroneID input started on UART%d at %d baud",
+ CONFIG_REMOTEID_MAVLINK_UART_NUM, CONFIG_REMOTEID_MAVLINK_BAUD);
+ return ESP_OK;
+}
+
+#else
+
+esp_err_t remoteid_mavlink_start(void)
+{
+ return ESP_OK;
+}
+
+#endif
diff --git a/src/remoteid/remoteid/mavlink.h b/src/remoteid/remoteid/mavlink.h
new file mode 100644
index 0000000..cf0b83f
--- /dev/null
+++ b/src/remoteid/remoteid/mavlink.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include "esp_err.h"
+
+esp_err_t remoteid_mavlink_start(void);
diff --git a/src/remoteid/remoteid/model.c b/src/remoteid/remoteid/model.c
index 7919afe..3a1d909 100644
--- a/src/remoteid/remoteid/model.c
+++ b/src/remoteid/remoteid/model.c
@@ -68,19 +68,50 @@ esp_err_t remoteid_model_init(remoteid_state_t *state)
strncpy(state->operator_id, REMOTEID_OPERATOR_ID, sizeof(state->operator_id) - 1);
state->id_type = configured_id_type();
state->ua_type = configured_ua_type();
- state->eu_category = configured_eu_category();
- state->eu_class = configured_eu_class();
+
+ state->speed_horizontal = (float)INV_SPEED_H;
+ state->speed_vertical = (float)INV_SPEED_V;
+ state->direction = (float)INV_DIR;
+ state->height = (float)INV_ALT;
+ state->height_type = ODID_HEIGHT_REF_OVER_TAKEOFF;
+ state->horiz_acc = ODID_HOR_ACC_UNKNOWN;
+ state->vert_acc = ODID_VER_ACC_UNKNOWN;
+ state->baro_acc = ODID_VER_ACC_UNKNOWN;
+ state->speed_acc = ODID_SPEED_ACC_UNKNOWN;
+ state->ts_acc = ODID_TIME_ACC_UNKNOWN;
+ state->timestamp = -1.0f;
+
+ state->operator_location_type = ODID_OPERATOR_LOCATION_TYPE_TAKEOFF;
+ state->area_count = 1;
+ state->area_radius = 0;
+ state->area_ceiling_m = (float)INV_ALT;
+ state->area_floor_m = (float)INV_ALT;
+ state->classification_type = ODID_CLASSIFICATION_TYPE_EU;
+ state->eu_category = configured_eu_category();
+ state->eu_class = configured_eu_class();
#if CONFIG_REMOTEID_HAS_POSITION
- state->has_position = true;
- state->latitude = REMOTEID_TAKEOFF_LATITUDE;
- state->longitude = REMOTEID_TAKEOFF_LONGITUDE;
- state->altitude_m = REMOTEID_TAKEOFF_ALTITUDE_M;
+ state->has_position = true;
+ state->latitude = REMOTEID_TAKEOFF_LATITUDE;
+ state->longitude = REMOTEID_TAKEOFF_LONGITUDE;
+ state->altitude_geo_m = REMOTEID_TAKEOFF_ALTITUDE_M;
+ state->altitude_baro_m = REMOTEID_TAKEOFF_ALTITUDE_M;
+ state->status = ODID_STATUS_AIRBORNE;
+ state->has_operator_position = true;
+ state->operator_latitude = REMOTEID_TAKEOFF_LATITUDE;
+ state->operator_longitude = REMOTEID_TAKEOFF_LONGITUDE;
+ state->operator_altitude_geo_m = REMOTEID_TAKEOFF_ALTITUDE_M;
#else
- state->has_position = false;
- state->latitude = 0.0;
- state->longitude = 0.0;
- state->altitude_m = (float)INV_ALT;
+ state->has_position = false;
+ state->latitude = 0.0;
+ state->longitude = 0.0;
+ state->altitude_geo_m = (float)INV_ALT;
+ state->altitude_baro_m = (float)INV_ALT;
+ state->status = ODID_STATUS_UNDECLARED;
+ state->has_operator_position = false;
+ state->operator_latitude = 0.0;
+ state->operator_longitude = 0.0;
+ state->operator_altitude_geo_m = (float)INV_ALT;
#endif
return ESP_OK;
diff --git a/src/remoteid/remoteid/pattern.c b/src/remoteid/remoteid/pattern.c
new file mode 100644
index 0000000..90d634c
--- /dev/null
+++ b/src/remoteid/remoteid/pattern.c
@@ -0,0 +1,107 @@
+#include "pattern.h"
+
+static const remoteid_pattern_step_t s_solid[] = {
+ { true, 1000 },
+};
+
+static const remoteid_pattern_step_t s_beacon_1hz_short[] = {
+ { true, 100 },
+ { false, 900 },
+};
+
+static const remoteid_pattern_step_t s_beacon_1hz_50[] = {
+ { true, 500 },
+ { false, 500 },
+};
+
+static const remoteid_pattern_step_t s_strobe_single[] = {
+ { true, 60 },
+ { false, 1940 },
+};
+
+static const remoteid_pattern_step_t s_strobe_double[] = {
+ { true, 60 },
+ { false, 80 },
+ { true, 60 },
+ { false, 1800 },
+};
+
+static const remoteid_pattern_step_t s_strobe_triple[] = {
+ { true, 60 },
+ { false, 80 },
+ { true, 60 },
+ { false, 80 },
+ { true, 60 },
+ { false, 1660 },
+};
+
+static const remoteid_pattern_step_t s_fast_strobe[] = {
+ { true, 60 },
+ { false, 440 },
+};
+
+const remoteid_pattern_step_t *remoteid_pattern_steps(remoteid_pattern_t pattern, size_t *count)
+{
+ switch (pattern) {
+ case REMOTEID_PATTERN_SOLID:
+ *count = sizeof(s_solid) / sizeof(s_solid[0]);
+ return s_solid;
+ case REMOTEID_PATTERN_BEACON_1HZ_50:
+ *count = sizeof(s_beacon_1hz_50) / sizeof(s_beacon_1hz_50[0]);
+ return s_beacon_1hz_50;
+ case REMOTEID_PATTERN_STROBE_SINGLE:
+ *count = sizeof(s_strobe_single) / sizeof(s_strobe_single[0]);
+ return s_strobe_single;
+ case REMOTEID_PATTERN_STROBE_DOUBLE:
+ *count = sizeof(s_strobe_double) / sizeof(s_strobe_double[0]);
+ return s_strobe_double;
+ case REMOTEID_PATTERN_STROBE_TRIPLE:
+ *count = sizeof(s_strobe_triple) / sizeof(s_strobe_triple[0]);
+ return s_strobe_triple;
+ case REMOTEID_PATTERN_FAST_STROBE:
+ *count = sizeof(s_fast_strobe) / sizeof(s_fast_strobe[0]);
+ return s_fast_strobe;
+ case REMOTEID_PATTERN_BEACON_1HZ_SHORT:
+ default:
+ *count = sizeof(s_beacon_1hz_short) / sizeof(s_beacon_1hz_short[0]);
+ return s_beacon_1hz_short;
+ }
+}
+
+uint32_t remoteid_pattern_period_ms(remoteid_pattern_t pattern)
+{
+ size_t count = 0;
+ const remoteid_pattern_step_t *steps = remoteid_pattern_steps(pattern, &count);
+ uint32_t period_ms = 0;
+
+ for (size_t i = 0; i < count; i++) {
+ period_ms += steps[i].duration_ms;
+ }
+
+ return period_ms;
+}
+
+bool remoteid_pattern_is_on(remoteid_pattern_t pattern, uint32_t elapsed_ms, uint32_t phase_ms)
+{
+ size_t count = 0;
+ const remoteid_pattern_step_t *steps = remoteid_pattern_steps(pattern, &count);
+ uint32_t period_ms = 0;
+
+ for (size_t i = 0; i < count; i++) {
+ period_ms += steps[i].duration_ms;
+ }
+
+ if (period_ms == 0) {
+ return false;
+ }
+
+ uint32_t position_ms = (elapsed_ms % period_ms + phase_ms % period_ms) % period_ms;
+ for (size_t i = 0; i < count; i++) {
+ if (position_ms < steps[i].duration_ms) {
+ return steps[i].on;
+ }
+ position_ms -= steps[i].duration_ms;
+ }
+
+ return false;
+}
diff --git a/src/remoteid/remoteid/pattern.h b/src/remoteid/remoteid/pattern.h
new file mode 100644
index 0000000..f9c1aff
--- /dev/null
+++ b/src/remoteid/remoteid/pattern.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include
+#include
+#include
+
+typedef enum remoteid_pattern {
+ REMOTEID_PATTERN_SOLID,
+ REMOTEID_PATTERN_BEACON_1HZ_SHORT,
+ REMOTEID_PATTERN_BEACON_1HZ_50,
+ REMOTEID_PATTERN_STROBE_SINGLE,
+ REMOTEID_PATTERN_STROBE_DOUBLE,
+ REMOTEID_PATTERN_STROBE_TRIPLE,
+ REMOTEID_PATTERN_FAST_STROBE,
+} remoteid_pattern_t;
+
+typedef struct remoteid_pattern_step {
+ bool on;
+ uint16_t duration_ms;
+} remoteid_pattern_step_t;
+
+const remoteid_pattern_step_t *remoteid_pattern_steps(remoteid_pattern_t pattern, size_t *count);
+uint32_t remoteid_pattern_period_ms(remoteid_pattern_t pattern);
+bool remoteid_pattern_is_on(remoteid_pattern_t pattern, uint32_t elapsed_ms, uint32_t phase_ms);
diff --git a/src/remoteid/remoteid/store.c b/src/remoteid/remoteid/store.c
new file mode 100644
index 0000000..78e1e65
--- /dev/null
+++ b/src/remoteid/remoteid/store.c
@@ -0,0 +1,207 @@
+#include "store.h"
+
+#include
+
+#include "esp_check.h"
+#include "esp_log.h"
+#include "freertos/event_groups.h"
+#include "freertos/queue.h"
+#include "freertos/semphr.h"
+#include "freertos/task.h"
+
+#define REMOTEID_STORE_READY_BIT BIT0
+#define REMOTEID_STORE_QUEUE_LEN 8
+
+static const char *TAG = "remoteid_store";
+
+static QueueHandle_t s_update_queue;
+static SemaphoreHandle_t s_state_mutex;
+static EventGroupHandle_t s_events;
+static remoteid_state_t s_state;
+static bool s_basic_id_valid;
+static bool s_operator_id_valid;
+
+static bool usable_text_field(const char *value, const char *placeholder)
+{
+ return value[0] != '\0' && strncmp(value, placeholder, ODID_ID_SIZE) != 0;
+}
+
+static bool basic_id_is_usable(const remoteid_state_t *state)
+{
+ return usable_text_field(state->uas_id, "CHANGE_ME_UAS_ID");
+}
+
+static bool operator_id_is_usable(const remoteid_state_t *state)
+{
+ return usable_text_field(state->operator_id, "CHANGE_ME_OP_ID");
+}
+
+static void update_ready_state_locked(void)
+{
+ bool ready = s_basic_id_valid && s_operator_id_valid;
+
+ if (ready) {
+ xEventGroupSetBits(s_events, REMOTEID_STORE_READY_BIT);
+ } else {
+ xEventGroupClearBits(s_events, REMOTEID_STORE_READY_BIT);
+ }
+}
+
+static void log_missing_identity_locked(void)
+{
+ if (!s_basic_id_valid) {
+ ESP_LOGW(TAG, "Remote ID broadcast waiting for valid Basic ID/UAS ID");
+ }
+ if (!s_operator_id_valid) {
+ ESP_LOGW(TAG, "Remote ID broadcast waiting for valid Operator ID");
+ }
+}
+
+static void apply_update_locked(const remoteid_store_update_t *update)
+{
+ switch (update->type) {
+ case REMOTEID_STORE_UPDATE_BASIC_ID:
+ strlcpy(s_state.uas_id, update->data.basic_id.uas_id, sizeof(s_state.uas_id));
+ s_state.id_type = update->data.basic_id.id_type;
+ s_state.ua_type = update->data.basic_id.ua_type;
+ s_basic_id_valid = basic_id_is_usable(&s_state);
+ ESP_LOGI(TAG, "updated Basic ID from input source (valid=%d)", s_basic_id_valid);
+ break;
+ case REMOTEID_STORE_UPDATE_OPERATOR_ID:
+ strlcpy(s_state.operator_id, update->data.operator_id.operator_id, sizeof(s_state.operator_id));
+ s_operator_id_valid = operator_id_is_usable(&s_state);
+ ESP_LOGI(TAG, "updated Operator ID from input source (valid=%d)", s_operator_id_valid);
+ break;
+ case REMOTEID_STORE_UPDATE_LOCATION:
+ s_state.has_position = update->data.location.has_position;
+ s_state.latitude = update->data.location.latitude;
+ s_state.longitude = update->data.location.longitude;
+ s_state.altitude_geo_m = update->data.location.altitude_geo_m;
+ s_state.altitude_baro_m = update->data.location.altitude_baro_m;
+ s_state.status = update->data.location.status;
+ s_state.speed_horizontal = update->data.location.speed_horizontal;
+ s_state.speed_vertical = update->data.location.speed_vertical;
+ s_state.direction = update->data.location.direction;
+ s_state.height = update->data.location.height;
+ s_state.height_type = update->data.location.height_type;
+ s_state.horiz_acc = update->data.location.horiz_acc;
+ s_state.vert_acc = update->data.location.vert_acc;
+ s_state.baro_acc = update->data.location.baro_acc;
+ s_state.speed_acc = update->data.location.speed_acc;
+ s_state.ts_acc = update->data.location.ts_acc;
+ s_state.timestamp = update->data.location.timestamp;
+ ESP_LOGD(TAG, "updated Location from input source (has_position=%d status=%d)",
+ s_state.has_position, s_state.status);
+ break;
+ case REMOTEID_STORE_UPDATE_SYSTEM:
+ s_state.has_operator_position = update->data.system.has_operator_position;
+ s_state.operator_latitude = update->data.system.operator_latitude;
+ s_state.operator_longitude = update->data.system.operator_longitude;
+ s_state.operator_altitude_geo_m = update->data.system.operator_altitude_geo_m;
+ s_state.operator_location_type = update->data.system.operator_location_type;
+ s_state.area_count = update->data.system.area_count;
+ s_state.area_radius = update->data.system.area_radius;
+ s_state.area_ceiling_m = update->data.system.area_ceiling_m;
+ s_state.area_floor_m = update->data.system.area_floor_m;
+ s_state.classification_type = update->data.system.classification_type;
+ s_state.eu_category = update->data.system.eu_category;
+ s_state.eu_class = update->data.system.eu_class;
+ ESP_LOGD(TAG, "updated System from input source");
+ break;
+ }
+
+ bool was_ready = (xEventGroupGetBits(s_events) & REMOTEID_STORE_READY_BIT) != 0;
+ update_ready_state_locked();
+ bool is_ready = s_basic_id_valid && s_operator_id_valid;
+ if (!was_ready && is_ready) {
+ ESP_LOGI(TAG, "Remote ID identity complete, broadcasts may start");
+ }
+}
+
+static void remoteid_store_task(void *arg)
+{
+ (void)arg;
+ remoteid_store_update_t update;
+
+ while (true) {
+ if (xQueueReceive(s_update_queue, &update, portMAX_DELAY) != pdTRUE) {
+ continue;
+ }
+
+ xSemaphoreTake(s_state_mutex, portMAX_DELAY);
+ apply_update_locked(&update);
+ xSemaphoreGive(s_state_mutex);
+ }
+}
+
+esp_err_t remoteid_store_start(const remoteid_state_t *initial_state)
+{
+ ESP_RETURN_ON_FALSE(initial_state, ESP_ERR_INVALID_ARG, TAG, "initial state is required");
+
+ s_update_queue = xQueueCreate(REMOTEID_STORE_QUEUE_LEN, sizeof(remoteid_store_update_t));
+ ESP_RETURN_ON_FALSE(s_update_queue, ESP_ERR_NO_MEM, TAG, "create state update queue");
+
+ s_state_mutex = xSemaphoreCreateMutex();
+ ESP_RETURN_ON_FALSE(s_state_mutex, ESP_ERR_NO_MEM, TAG, "create state mutex");
+
+ s_events = xEventGroupCreate();
+ ESP_RETURN_ON_FALSE(s_events, ESP_ERR_NO_MEM, TAG, "create state event group");
+
+ xSemaphoreTake(s_state_mutex, portMAX_DELAY);
+ s_state = *initial_state;
+ s_basic_id_valid = basic_id_is_usable(&s_state);
+ s_operator_id_valid = operator_id_is_usable(&s_state);
+ update_ready_state_locked();
+ if (s_basic_id_valid && s_operator_id_valid) {
+ ESP_LOGI(TAG, "Remote ID identity loaded from Kconfig, broadcasts may start");
+ } else {
+ log_missing_identity_locked();
+ }
+ xSemaphoreGive(s_state_mutex);
+
+ if (xTaskCreate(remoteid_store_task, "remoteid_store", 4096, NULL, 6, NULL) != pdPASS) {
+ ESP_LOGE(TAG, "failed to create remoteid_store task");
+ return ESP_ERR_NO_MEM;
+ }
+
+ return ESP_OK;
+}
+
+esp_err_t remoteid_store_submit(const remoteid_store_update_t *update, TickType_t timeout)
+{
+ ESP_RETURN_ON_FALSE(update, ESP_ERR_INVALID_ARG, TAG, "update is required");
+ ESP_RETURN_ON_FALSE(s_update_queue, ESP_ERR_INVALID_STATE, TAG, "state store is not started");
+
+ if (xQueueSend(s_update_queue, update, timeout) != pdTRUE) {
+ return ESP_ERR_TIMEOUT;
+ }
+ return ESP_OK;
+}
+
+esp_err_t remoteid_store_get_snapshot(remoteid_state_t *out)
+{
+ ESP_RETURN_ON_FALSE(out, ESP_ERR_INVALID_ARG, TAG, "snapshot destination is required");
+ ESP_RETURN_ON_FALSE(s_state_mutex, ESP_ERR_INVALID_STATE, TAG, "state store is not started");
+
+ xSemaphoreTake(s_state_mutex, portMAX_DELAY);
+ *out = s_state;
+ xSemaphoreGive(s_state_mutex);
+
+ return ESP_OK;
+}
+
+esp_err_t remoteid_store_wait_ready(TickType_t timeout)
+{
+ ESP_RETURN_ON_FALSE(s_events, ESP_ERR_INVALID_STATE, TAG, "state store is not started");
+
+ EventBits_t bits = xEventGroupWaitBits(s_events, REMOTEID_STORE_READY_BIT, pdFALSE, pdTRUE, timeout);
+ return (bits & REMOTEID_STORE_READY_BIT) ? ESP_OK : ESP_ERR_TIMEOUT;
+}
+
+bool remoteid_store_is_ready(void)
+{
+ if (!s_events) {
+ return false;
+ }
+ return (xEventGroupGetBits(s_events) & REMOTEID_STORE_READY_BIT) != 0;
+}
diff --git a/src/remoteid/remoteid/store.h b/src/remoteid/remoteid/store.h
new file mode 100644
index 0000000..7dfb220
--- /dev/null
+++ b/src/remoteid/remoteid/store.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include
+
+#include "esp_err.h"
+#include "freertos/FreeRTOS.h"
+#include "types.h"
+
+typedef enum remoteid_store_update_type {
+ REMOTEID_STORE_UPDATE_BASIC_ID,
+ REMOTEID_STORE_UPDATE_OPERATOR_ID,
+ REMOTEID_STORE_UPDATE_LOCATION,
+ REMOTEID_STORE_UPDATE_SYSTEM,
+} remoteid_store_update_type_t;
+
+typedef struct remoteid_store_update {
+ remoteid_store_update_type_t type;
+ union {
+ struct {
+ char uas_id[ODID_ID_SIZE + 1];
+ ODID_idtype_t id_type;
+ ODID_uatype_t ua_type;
+ } basic_id;
+ struct {
+ char operator_id[ODID_ID_SIZE + 1];
+ } operator_id;
+ struct {
+ bool has_position;
+ double latitude;
+ double longitude;
+ float altitude_geo_m;
+ float altitude_baro_m;
+ ODID_status_t status;
+ float speed_horizontal;
+ float speed_vertical;
+ float direction;
+ float height;
+ ODID_Height_reference_t height_type;
+ ODID_Horizontal_accuracy_t horiz_acc;
+ ODID_Vertical_accuracy_t vert_acc;
+ ODID_Vertical_accuracy_t baro_acc;
+ ODID_Speed_accuracy_t speed_acc;
+ ODID_Timestamp_accuracy_t ts_acc;
+ float timestamp;
+ } location;
+ struct {
+ bool has_operator_position;
+ double operator_latitude;
+ double operator_longitude;
+ float operator_altitude_geo_m;
+ ODID_operator_location_type_t operator_location_type;
+ uint16_t area_count;
+ uint16_t area_radius;
+ float area_ceiling_m;
+ float area_floor_m;
+ ODID_classification_type_t classification_type;
+ ODID_category_EU_t eu_category;
+ ODID_class_EU_t eu_class;
+ } system;
+ } data;
+} remoteid_store_update_t;
+
+esp_err_t remoteid_store_start(const remoteid_state_t *initial_state);
+esp_err_t remoteid_store_submit(const remoteid_store_update_t *update, TickType_t timeout);
+esp_err_t remoteid_store_get_snapshot(remoteid_state_t *out);
+esp_err_t remoteid_store_wait_ready(TickType_t timeout);
+bool remoteid_store_is_ready(void);
diff --git a/src/remoteid/remoteid/types.h b/src/remoteid/remoteid/types.h
index 53beaaf..81f1145 100644
--- a/src/remoteid/remoteid/types.h
+++ b/src/remoteid/remoteid/types.h
@@ -14,16 +14,44 @@ typedef enum remoteid_message_index {
} remoteid_message_index_t;
typedef struct remoteid_state {
+ // Basic ID
char uas_id[ODID_ID_SIZE + 1];
char operator_id[ODID_ID_SIZE + 1];
ODID_idtype_t id_type;
ODID_uatype_t ua_type;
- ODID_category_EU_t eu_category;
- ODID_class_EU_t eu_class;
+
+ // Location
bool has_position;
double latitude;
double longitude;
- float altitude_m;
+ float altitude_geo_m;
+ float altitude_baro_m;
+ ODID_status_t status;
+ float speed_horizontal;
+ float speed_vertical;
+ float direction;
+ float height;
+ ODID_Height_reference_t height_type;
+ ODID_Horizontal_accuracy_t horiz_acc;
+ ODID_Vertical_accuracy_t vert_acc;
+ ODID_Vertical_accuracy_t baro_acc;
+ ODID_Speed_accuracy_t speed_acc;
+ ODID_Timestamp_accuracy_t ts_acc;
+ float timestamp; // seconds after the UTC hour, or < 0 if unknown
+
+ // System / operator
+ bool has_operator_position;
+ double operator_latitude;
+ double operator_longitude;
+ float operator_altitude_geo_m;
+ ODID_operator_location_type_t operator_location_type;
+ uint16_t area_count;
+ uint16_t area_radius;
+ float area_ceiling_m;
+ float area_floor_m;
+ ODID_classification_type_t classification_type;
+ ODID_category_EU_t eu_category;
+ ODID_class_EU_t eu_class;
} remoteid_state_t;
typedef struct remoteid_message_bundle {
diff --git a/src/remoteid/remoteid/wifi.c b/src/remoteid/remoteid/wifi.c
index 1a8a071..b8c1dca 100644
--- a/src/remoteid/remoteid/wifi.c
+++ b/src/remoteid/remoteid/wifi.c
@@ -11,12 +11,11 @@
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
-#include "led.h"
#include "opendroneid.h"
#include "sdkconfig.h"
+#include "store.h"
#define REMOTEID_WIFI_FRAME_BUF_SIZE 512
-#define REMOTEID_WIFI_TX_INTERVAL_MS 1000
#ifndef CONFIG_REMOTEID_WIFI_BEACON_SSID
#define CONFIG_REMOTEID_WIFI_BEACON_SSID "OpenDroneID"
@@ -34,6 +33,14 @@
#define CONFIG_REMOTEID_WIFI_TX_POWER_DBM 20
#endif
+#ifndef CONFIG_REMOTEID_WIFI_BEACON_TX_INTERVAL_MS
+#define CONFIG_REMOTEID_WIFI_BEACON_TX_INTERVAL_MS 1000
+#endif
+
+#ifndef CONFIG_REMOTEID_WIFI_NAN_TX_INTERVAL_MS
+#define CONFIG_REMOTEID_WIFI_NAN_TX_INTERVAL_MS 1000
+#endif
+
#ifndef CONFIG_REMOTEID_TRANSPORT_WIFI_BEACON
#define CONFIG_REMOTEID_TRANSPORT_WIFI_BEACON 0
#endif
@@ -44,13 +51,17 @@
static const char *TAG = "remoteid_wifi";
-static const remoteid_state_t *s_state;
-static uint8_t s_send_counter;
+#if CONFIG_REMOTEID_TRANSPORT_WIFI_BEACON
+static uint8_t s_beacon_send_counter;
+#endif
+#if CONFIG_REMOTEID_TRANSPORT_WIFI_NAN
+static uint8_t s_nan_send_counter;
+#endif
#if CONFIG_REMOTEID_TRANSPORT_WIFI_BEACON
static uint16_t beacon_interval_tu(void)
{
- uint32_t interval_us = REMOTEID_WIFI_TX_INTERVAL_MS * 1000U;
+ uint32_t interval_us = CONFIG_REMOTEID_WIFI_BEACON_TX_INTERVAL_MS * 1000U;
uint32_t interval_tu = interval_us / 1024U;
return (uint16_t)(interval_tu > 0 ? interval_tu : 1);
@@ -124,7 +135,7 @@ static esp_err_t init_wifi_driver(void)
ap_config.ap.channel = CONFIG_REMOTEID_WIFI_CHANNEL;
ap_config.ap.authmode = WIFI_AUTH_OPEN;
ap_config.ap.max_connection = 0;
- ap_config.ap.beacon_interval = REMOTEID_WIFI_TX_INTERVAL_MS;
+ ap_config.ap.beacon_interval = CONFIG_REMOTEID_WIFI_BEACON_TX_INTERVAL_MS;
ESP_RETURN_ON_ERROR(esp_wifi_set_config(WIFI_IF_AP, &ap_config), TAG, "configure Wi-Fi AP");
#endif
@@ -135,34 +146,37 @@ static esp_err_t init_wifi_driver(void)
#endif
ESP_RETURN_ON_ERROR(esp_wifi_set_max_tx_power(CONFIG_REMOTEID_WIFI_TX_POWER_DBM * 4), TAG, "set Wi-Fi TX power");
- ESP_LOGI(TAG, "Wi-Fi Remote ID enabled on channel %d at %d dBm, interval %d ms (beacon=%d, nan=%d)",
- CONFIG_REMOTEID_WIFI_CHANNEL, CONFIG_REMOTEID_WIFI_TX_POWER_DBM, REMOTEID_WIFI_TX_INTERVAL_MS,
- CONFIG_REMOTEID_TRANSPORT_WIFI_BEACON, CONFIG_REMOTEID_TRANSPORT_WIFI_NAN);
+ ESP_LOGI(TAG,
+ "Wi-Fi Remote ID enabled on channel %d at %d dBm (beacon=%d interval=%d ms, nan=%d interval=%d ms)",
+ CONFIG_REMOTEID_WIFI_CHANNEL, CONFIG_REMOTEID_WIFI_TX_POWER_DBM, CONFIG_REMOTEID_TRANSPORT_WIFI_BEACON,
+ CONFIG_REMOTEID_WIFI_BEACON_TX_INTERVAL_MS, CONFIG_REMOTEID_TRANSPORT_WIFI_NAN,
+ CONFIG_REMOTEID_WIFI_NAN_TX_INTERVAL_MS);
return ESP_OK;
}
#if CONFIG_REMOTEID_TRANSPORT_WIFI_BEACON
static esp_err_t send_remoteid_beacon(void)
{
+ remoteid_state_t snapshot;
ODID_UAS_Data uas_data;
uint8_t frame[REMOTEID_WIFI_FRAME_BUF_SIZE];
uint8_t mac[6];
- ESP_RETURN_ON_ERROR(remoteid_encode_uas_data(s_state, &uas_data), TAG, "build OpenDroneID UAS data");
+ ESP_RETURN_ON_ERROR(remoteid_store_get_snapshot(&snapshot), TAG, "get Remote ID snapshot");
+ ESP_RETURN_ON_ERROR(remoteid_encode_uas_data(&snapshot, &uas_data), TAG, "build OpenDroneID UAS data");
ESP_RETURN_ON_ERROR(esp_wifi_get_mac(WIFI_IF_AP, mac), TAG, "get Wi-Fi AP MAC");
int len = odid_wifi_build_message_pack_beacon_frame(
&uas_data, (const char *)mac, CONFIG_REMOTEID_WIFI_BEACON_SSID, strlen(CONFIG_REMOTEID_WIFI_BEACON_SSID),
- beacon_interval_tu(), s_send_counter, frame, sizeof(frame));
+ beacon_interval_tu(), s_beacon_send_counter, frame, sizeof(frame));
if (len < 0) {
ESP_LOGE(TAG, "failed to build OpenDroneID Wi-Fi Beacon frame: %d", len);
return ESP_FAIL;
}
ESP_RETURN_ON_ERROR(esp_wifi_80211_tx(WIFI_IF_AP, frame, len, true), TAG, "transmit Wi-Fi Beacon frame");
- s_send_counter++;
+ s_beacon_send_counter++;
log_transmitted_message_types("Wi-Fi Beacon", &uas_data);
- remoteid_led_pulse();
return ESP_OK;
}
@@ -171,12 +185,14 @@ static esp_err_t send_remoteid_beacon(void)
#if CONFIG_REMOTEID_TRANSPORT_WIFI_NAN
static esp_err_t send_remoteid_nan(void)
{
+ remoteid_state_t snapshot;
ODID_UAS_Data uas_data;
uint8_t frame[REMOTEID_WIFI_FRAME_BUF_SIZE];
uint8_t mac[6];
int len;
- ESP_RETURN_ON_ERROR(remoteid_encode_uas_data(s_state, &uas_data), TAG, "build OpenDroneID UAS data");
+ ESP_RETURN_ON_ERROR(remoteid_store_get_snapshot(&snapshot), TAG, "get Remote ID snapshot");
+ ESP_RETURN_ON_ERROR(remoteid_encode_uas_data(&snapshot, &uas_data), TAG, "build OpenDroneID UAS data");
ESP_RETURN_ON_ERROR(esp_wifi_get_mac(WIFI_IF_STA, mac), TAG, "get Wi-Fi STA MAC");
len = odid_wifi_build_nan_sync_beacon_frame((const char *)mac, frame, sizeof(frame));
@@ -186,49 +202,83 @@ static esp_err_t send_remoteid_nan(void)
}
ESP_RETURN_ON_ERROR(esp_wifi_80211_tx(WIFI_IF_STA, frame, len, true), TAG, "transmit Wi-Fi NAN sync frame");
- len = odid_wifi_build_message_pack_nan_action_frame(&uas_data, (const char *)mac, s_send_counter, frame,
+ len = odid_wifi_build_message_pack_nan_action_frame(&uas_data, (const char *)mac, s_nan_send_counter, frame,
sizeof(frame));
if (len < 0) {
ESP_LOGE(TAG, "failed to build OpenDroneID Wi-Fi NAN action frame: %d", len);
return ESP_FAIL;
}
ESP_RETURN_ON_ERROR(esp_wifi_80211_tx(WIFI_IF_STA, frame, len, true), TAG, "transmit Wi-Fi NAN action frame");
- s_send_counter++;
+ s_nan_send_counter++;
log_transmitted_message_types("Wi-Fi NAN", &uas_data);
- remoteid_led_pulse();
return ESP_OK;
}
#endif
-static void remoteid_wifi_task(void *arg)
+#if CONFIG_REMOTEID_TRANSPORT_WIFI_BEACON
+static void remoteid_wifi_beacon_task(void *arg)
{
(void)arg;
+ ESP_LOGI(TAG, "Wi-Fi Beacon transport waiting for Remote ID Basic ID and Operator ID");
+ ESP_ERROR_CHECK(remoteid_store_wait_ready(portMAX_DELAY));
+ ESP_LOGI(TAG, "Wi-Fi Beacon transport identity ready, starting transmissions");
+
while (true) {
-#if CONFIG_REMOTEID_TRANSPORT_WIFI_BEACON
+ if (!remoteid_store_is_ready()) {
+ ESP_LOGW(TAG, "Wi-Fi Beacon transport paused until Remote ID identity is ready again");
+ ESP_ERROR_CHECK(remoteid_store_wait_ready(portMAX_DELAY));
+ }
+
if (send_remoteid_beacon() == ESP_OK) {
ESP_LOGD(TAG, "transmitted OpenDroneID Wi-Fi Beacon");
}
+ vTaskDelay(pdMS_TO_TICKS(CONFIG_REMOTEID_WIFI_BEACON_TX_INTERVAL_MS));
+ }
+}
#endif
+
#if CONFIG_REMOTEID_TRANSPORT_WIFI_NAN
+static void remoteid_wifi_nan_task(void *arg)
+{
+ (void)arg;
+
+ ESP_LOGI(TAG, "Wi-Fi NAN transport waiting for Remote ID Basic ID and Operator ID");
+ ESP_ERROR_CHECK(remoteid_store_wait_ready(portMAX_DELAY));
+ ESP_LOGI(TAG, "Wi-Fi NAN transport identity ready, starting transmissions");
+
+ while (true) {
+ if (!remoteid_store_is_ready()) {
+ ESP_LOGW(TAG, "Wi-Fi NAN transport paused until Remote ID identity is ready again");
+ ESP_ERROR_CHECK(remoteid_store_wait_ready(portMAX_DELAY));
+ }
+
if (send_remoteid_nan() == ESP_OK) {
ESP_LOGD(TAG, "transmitted OpenDroneID Wi-Fi NAN");
}
-#endif
- vTaskDelay(pdMS_TO_TICKS(REMOTEID_WIFI_TX_INTERVAL_MS));
+ vTaskDelay(pdMS_TO_TICKS(CONFIG_REMOTEID_WIFI_NAN_TX_INTERVAL_MS));
}
}
+#endif
-esp_err_t remoteid_wifi_start(const remoteid_state_t *state)
+esp_err_t remoteid_wifi_start(void)
{
- s_state = state;
+ ESP_RETURN_ON_ERROR(init_wifi_driver(), TAG, "initialize Wi-Fi Remote ID transport");
+
+#if CONFIG_REMOTEID_TRANSPORT_WIFI_BEACON
+ if (xTaskCreate(remoteid_wifi_beacon_task, "remoteid_wifi_bcn", 4096, NULL, 5, NULL) != pdPASS) {
+ ESP_LOGE(TAG, "failed to create remoteid_wifi_bcn task");
+ return ESP_ERR_NO_MEM;
+ }
+#endif
- ESP_RETURN_ON_ERROR(init_wifi_driver(), TAG, "initialize Wi-Fi Beacon transport");
- if (xTaskCreate(remoteid_wifi_task, "remoteid_wifi", 4096, NULL, 5, NULL) != pdPASS) {
- ESP_LOGE(TAG, "failed to create remoteid_wifi task");
+#if CONFIG_REMOTEID_TRANSPORT_WIFI_NAN
+ if (xTaskCreate(remoteid_wifi_nan_task, "remoteid_wifi_nan", 4096, NULL, 5, NULL) != pdPASS) {
+ ESP_LOGE(TAG, "failed to create remoteid_wifi_nan task");
return ESP_ERR_NO_MEM;
}
+#endif
return ESP_OK;
}
diff --git a/src/remoteid/remoteid/wifi.h b/src/remoteid/remoteid/wifi.h
index b04caf6..6edde56 100644
--- a/src/remoteid/remoteid/wifi.h
+++ b/src/remoteid/remoteid/wifi.h
@@ -1,6 +1,4 @@
#pragma once
#include "esp_err.h"
-#include "types.h"
-
-esp_err_t remoteid_wifi_start(const remoteid_state_t *state);
+esp_err_t remoteid_wifi_start(void);
diff --git a/third_party/mavlink_c_library_v2 b/third_party/mavlink_c_library_v2
new file mode 160000
index 0000000..59ae0eb
--- /dev/null
+++ b/third_party/mavlink_c_library_v2
@@ -0,0 +1 @@
+Subproject commit 59ae0ebc759d10e8beb6bd55cadaa45c0915db5b