Skip to content

Rosfly/coap_server_sed_nfc_alarm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Easy way to integrate low-power devices running Zephyr RTOS into Home Assistant

Proposed approach does not compete with Matter protocol, it should show how to add ultra low-power devices into available Thread PHY-layer below Matter and with similar UX, so that the user commissions new device to deployed Thread network safely using NFC in his smartphone and the device appears in Home Assistant UI automatically. So it reuses available hardware/software and is much simpler to program than Matter stack. The blue blocks below show the application code blocks written for Zephyr stack and for the Home Assistant stack.

Target board for CoAP Server: Xiao Seeed nRF54L15
nRF Connect SDK: v3.2.1
Zephyr Version: v4.3, zephyr-sdk-0.17.4
Home Assistant: Core 2026.2.1 (all add-ons as of 2/26 updated)
Status: ⚠️ HOBBYIST PROOF-OF-CONCEPT

Motivation: CoAP-over-Thread connectivity is simpler than Matter-over-Thread

Thread protocol is extremely power efficient compared to WiFi, it builds a solid PHY with IPv6-based mesh networking Layer for devices that don't need high bandwidth and work for many months or even 1-2 years on battery.

Matter protocol on top of Thread creates a safe application layer with a straightforward service discovery and UI on all available home automation platforms. Programming Matter is complicated for DIY, even the installation of the toolchain is a challenge. Nordic provides a few Matter examples, but the Matter itself is not yet even planned for mainstream Zephyr RTOS integration.

CoAP (RFC 7252 Constrained Application Protocol) was invented before Matter, it is UDP-based and stateless, has no pre-defined device classes (light, binary sensor) and no commissioning concept to integrate the new gadget into existing Thread network. But it is simple and has both Python and Zephyr support, what makes CoAP over Thread more attractive for DIY development. The application layer should be built on top of CoAP and it exists: Home Assistant has automatic service discovery for many types of devices, so one should push the proper messages to MQTT Add-on to register the new devices.

The proposed architecture consists of two parts:

  • CoAP Client with Data Model as a Bridge between existing OpenThread Add-on and MQTT Add-on, it is a container one can download from GitHub and install on HA OS in 1 minute. It is not a simple relay between Thread and MQTT, it manages a small database for device commissioning and decommissioning

  • CoAP Server on the device side, in this example a cheap siren with integrated Xiao Seeed nRF54L15 board, the board is flashed using nRF Connect (Zephyr-based, but not native Zephyr). Vanilla Zephyr does not yet support Nordic's NFC libraries needed for device commissioning using smartphone, that's why nRF SDK. If you can live with copying credentials over Zephyr shell, then use the Debug chapter, it doesn't need nRF Connect at all.

The shown architecture integrates both CoAP-over-Thread and Matter-over-Thread on one Home Assistant host, all devices run on the same Thread PHY and over same border router.

Automatic Service Discovery after Commissioning

After easy commissioning described in chapter below the device appears with all its functions in Home Assistant UI:

  • buttons
  • leds (in this application used for alarm on/off)
  • battery level in voltage
  • battery level in percent
  • uptime since last boot (very useful function for tracking devices)


Now you can use these services in HA Automation routines, e.g. trigger condition like motion sensor on the picture and siren turns on.

CoAP-over-Thread Interaction for Dummies: Push vs. Poll

CoAP has two established mechanisms:

  1. "Polling Mechanism" CoAP Client (on HA) requests CoAP Server (target board) with GET/PUT for sensor/actor actions, the Server's Thread radio should be ON at that time, otherwise there is no chance of communication. There is NO mechanism of waking up a sleeping Thread device!
    1.1 Thread Sleepy End Device (SED) wakes up every N seconds/hours and polls the Thread Leader to provide him the stored requests (Thread leader has his cash to store incoming requests)
    1.2 CoAP client gets all the requested information during that wake-up period, so the Thread device can now go back to power saving sleep
    1.3 Example: LED turn-on command takes worst-case N seconds/hours, this time is defined by Thread device itself, nobody else can influence that, there is no negotiations on that

  2. "Observe/Push Mechanism" CoAP Client can register the resource of interest
    2.1 The CoAP server responds with the current state of the resource and includes an Observe option with a sequence number. This establishes the observation relationship
    2.2 Whenever the resource changes, the CoAP server sends a notification (a new response). But the layer below CoAP - Thread - knows nothing about that relationship.
    2.3 Zephyr Code in this example wakes up the Thread radio on Button (sw0 and sw1) event interrupt and the CoAP server pushes the notification to client

That solves a common misunderstanding: how can a sleeping device with long downtime be fast in case of emergency, e.g. in case of alarm/motion/occupancy sensor should push its notification immediately on event. This is a nice example of how CoAP-over-Thread saves the battery power.

Physical Setup

The target embedded device is the Xiao Seeed nRF54L15 board with soldered JST battery connector and NFC Antenna attached to NFC pads. This board has Thread-capable radio in MCU and on-board 2.4GHz Antenna.

The board is integrated into a cheap siren system with PIR sensor, so nRF54L15 can only trigger alarm, it does not change any functions.

The system is mounted together into alarm box

NFC Commissioning Process

The firmware is flashed using OpenOCD over USB, no additional hardware needed. The board starts at boot the NFC commissioning mode, so it waits for Thread dataset to be pushed from smartphone.

Prerequisites:

  • running Home Assistant (HAOS) with 3 Add-ons installed and running (MQTT, OpenThread and CoAP Bridge), Terminal Add-on recommended for debugging
  • available Thread network, so an OpenThread border router (OTBR) like ZBT-2 on the diagram or even another nRF54L15 board with flashed OTBR image from Nordic samples (it was successfully tested as part of this project)
  • App like NFC Tools for initial commissioning over NFC-capable smartphone
  • HomeAssistant Companion App installed on same smartphone

Step 1: copy the working Thread TLV dataset from HAOS Settings/Devices/Thread

Step 2: Paste the dataset into NFC Tools and save it
Write/Add Dataset/Text

Step 3: Place smartphone near NFC antenna and press "Write"

Now the credentials are in non-volatile memory and you can verify the device joining the Thread network using Terminal Add-on

~ docker exec -it addon_core_openthread_border_router /bin/bash
root@homeassistant:/usr/src# ot-ctl state
leader
Done
root@homeassistant:/usr/src# ot-ctl neighbor table
| Role | RLOC16 | Age | Avg RSSI | Last RSSI |R|D|N| Extended MAC     | Version |
+------+--------+-----+----------+-----------+-+-+-+------------------+---------+
|   C  | 0x3401 |   1 |      -68 |       -68 |0|0|0| c2d7101ffc8a974b |       4 |
|   C  | 0x3402 |  11 |      -71 |       -71 |0|0|0| 9e57ff5e2bfafda3 |       4 |
|   C  | 0x340a |   1 |      -79 |       -79 |0|0|0| 3ad296c17cef650a |       5 |

The firmware appears as Minimal Thread Device in child role (C) and gives CoAP client the grace period to discover the new device: it does not sleep during first 2 minutes after boot, so the CoAP client polling every minute can commission the new CoAP devices. You can track the RDN state 100 in the table above, that means MTD. After the timer expiration the device goes into SED (Sleepy End Device) mode, RDN=000 like on picture above. It wakes up every 5 seconds, polls to thread leader that he is ready for instructions, Thread leader signals to CoAP application it can accept instructions and the CoAP Client can send him UDP requests.

Tech Details on Thread CoAP Server for Home Assistant Integration

A Zephyr-based Thread CoAP server designed for integration with Home Assistant via the Thread CoAP Bridge add-on. Supports LED control, button input, battery monitoring with real ADC measurements, NFC-based commissioning, and automatic network reconnection.

https://github.com/Rosfly/thread-coap-bridge-addon

Features

  • NFC Commissioning: Provision Thread credentials via Android phone NFC - no cables needed
  • Thread SED Mode: Runs as Sleepy End Device for ultra-low power consumption
  • SED Discovery Grace Period: Stays awake for 2 minutes after boot for bridge discovery
  • CoAP Server: Exposes /led, /sw (button), /uptime, /battery, and /voltage resources
  • CoAP Observe: Push notifications for LED and button state changes (RFC 7641)
  • Battery Monitoring: Real ADC measurements with LiPo discharge curve lookup table
  • Power Optimization: TPS22916C load switch enables voltage divider only during measurement to save power
  • Automatic Reconnection: Network monitor thread handles disconnection recovery
  • NVS Storage: Thread credentials persist across reboots

Hardware Support

  • Seeed XIAO nRF54L15 (primary target)
  • NFC antenna connected to NFC1/NFC2 pads
  • Other nRF52/nRF53/nRF54 boards with Thread support

Battery Voltage Measurement Circuit

VBAT ─── TPS22916C ─── R5(10K) ───┬─── P1.14 (AIN7)
         (load switch)            │
         P1.15 (EN)          R6(10K)
                                  │
                                 GND
  • Voltage divider ratio: 2.0 (20K/10K)
  • ADC: 12-bit, 4x oversampling, 1/4 gain
  • Power saving: TPS22916C disables divider when not measuring

External IPEX Antenna

The Seeed XIAO nRF54L15 board includes an on-board ceramic 2.4 GHz antenna and an IPEX connector for an external antenna. An on-board RF switch (controlled via P2.03 and P2.05) routes the radio signal to one of the two paths.

Hardware

The RF switch is controlled by two GPIO pins on Port 2:

Signal Pin Logic Meaning
rfsw_pwr P2.03 HIGH = switch powered Must be HIGH for the switch to operate
rfsw_ctl P2.05 HIGH = external IPEX LOW = ceramic on-board antenna

Both pins are defined in the board DTS as regulator-fixed nodes with regulator-boot-on. Without CONFIG_REGULATOR (which is not set in this project — enabling it would brick the nRF54L15 by re-initializing internal CPU voltage regulators), neither pin is driven at reset and the radio falls back to the ceramic antenna.

To use the external IPEX antenna both pins must be explicitly driven from application code. The overlay disables both regulator nodes so the application owns the pins:

/* boards/xiao_nrf54l15_nrf54l15_cpuapp.overlay */
&rfsw_pwr { status = "disabled"; };  /* release P2.03 */
&rfsw_ctl { status = "disabled"; };  /* release P2.05 */

Configuration

Enable external IPEX antenna as the boot default in prj.conf:

CONFIG_DEFAULT_ANTENNA_EXTERNAL=y

When enabled, main.c drives both pins immediately after coap_init():

gpio_pin_configure(gpio2, 3, GPIO_OUTPUT | GPIO_OUTPUT_INIT_HIGH); /* rfsw_pwr: switch on */
gpio_pin_configure(gpio2, 5, GPIO_OUTPUT | GPIO_OUTPUT_INIT_HIGH); /* rfsw_ctl: external */

Set CONFIG_DEFAULT_ANTENNA_EXTERNAL=n (or remove the line) to revert to the ceramic antenna — no other changes needed.

Antenna Placement

Connect an IPEX-to-SMA pigtail to the IPEX connector on the board and mount the external antenna away from the PCB. For a device installed inside a plastic enclosure this can significantly improve Thread RSSI and reduce packet loss, especially if the ceramic antenna is shielded by the enclosure wall or a metal chassis.

Building

Important: This project requires nRF Connect SDK, not mainline Zephyr. The NFC libraries are only available in nRF Connect SDK.

Production Build (Battery-Optimized) and Flash

Default build disables UART/logging for battery-only boot:

cd <ncs_sdk_path>

# you can build in nRF Connect directly
west build -p always -b xiao_nrf54l15/nrf54l15/cpuapp \
    --shield seeed_xiao_expansion_board \
    -s <project_path>

# this step is needed always
cd <project_path>/build && west flash

Or use nRF side bar in VSCode

Debug Build (USB/UART + Shell) and Flash

For development with USB serial console and OpenThread shell.

Note: This build will NOT boot from battery alone - USB connection required. Add prj_uart to configuration:

 alt text for screen readers

cd <ncs_sdk_path>

west build -p always -b xiao_nrf54l15/nrf54l15/cpuapp \
    --shield seeed_xiao_expansion_board \
    -s <project_path> \
    -- -DOVERLAY_CONFIG="prj_uart.conf"
# this step is needed always
cd <project_path>/build && west flash

# Monitor serial output (115200 baud)
# Use VSCode Serial Monitor or: screen /dev/ttyACM0 115200

The debug build enables:

  • UART console logging
  • OpenThread shell (ot commands)
  • Useful for troubleshooting and manual commissioning

Commissioning

The device needs to be commissioned to your Thread network once. After commissioning, credentials are stored in NVS and the device auto-joins on subsequent boots.

Method 1: NFC Commissioning (Recommended)

No cables needed - just tap your phone to provision the device.

Step 1: Get Thread Dataset

From Home Assistant:

  1. Go to SettingsDevices & ServicesThread
  2. Click on your Thread network
  3. Find the Active Operational Dataset (TLV hex string)

Or export (-x) from OpenThread Border Router:

# SSH into OTBR container
docker exec -it addon_core_openthread_border_router ot-ctl dataset active -x

Example output:

0e080000000000010000000300000f35060004001fffe00208dead...

Step 2: Install NFC Tools App

Download NFC Tools (free) on your Android phone.

Step 3: Write Dataset via NFC

  1. Open NFC Tools
  2. Tap WriteAdd a recordText
  3. Paste the hex string from Step 1
  4. Tap Write
  5. Hold phone to device's NFC antenna

LED Feedback

State LED Pattern Meaning
Slow blink (500ms) Waiting for NFC Ready to receive dataset
Rapid blink (100ms) Phone detected Processing data
Solid 1 second Success Dataset applied, joining network
3 rapid blinks Error Invalid data, retry

Method 2: Shell Commissioning (Debug Build Only)

Use this method when debugging or if NFC isn't available.

# Connect to serial console (115200 baud)

# Set the Thread dataset
ot dataset set active 0e080000000000010000000300000f35060004001fffe00208...

# Enable IPv6 interface
ot ifconfig up

# Start Thread
ot thread start

# Verify connection (wait 10-30 seconds)
ot state
# Should show: child

Note: After shell commissioning with debug build, flash the production build for battery operation.

Boot Flow

BOOT
  │
  ▼
Check sw0 button
  │
  ├─── sw0 held ───────► Clear stored dataset
  │                         │
  ▼                         ▼
Check NVS for dataset     NFC mode (LED blinks)
  │                         │
  ├─── Dataset found ──► Start Thread ──► Join network ──► SED mode
  │                         │
  └─── No dataset ────► NFC mode (LED blinks)
                            │
                            ▼
                        Phone taps NFC
                            │
                            ▼
                        Parse & apply dataset
                            │
                            ▼
                        Start Thread ──► Join network ──► SED mode

Re-commissioning

To switch to a different Thread network, hold the sw0 button while pressing reset switch. The device clears its stored dataset and enters NFC commissioning mode. Then provision the new network dataset via NFC as usual.

Network Recovery

The firmware includes a network monitor that handles disconnection:

  1. Detection: Monitor thread detects DETACHED state
  2. Wait: Allows OpenThread's internal reattachment (up to 1 minute)
  3. Force Restart: If still detached after 1 minute, forces Thread stack restart
  4. Rejoin: Device rejoins network with fresh state

CoAP Resources

LED Resource (/led)

GET - Returns current LED state:

{"device_id": "f4ce3616e67c7a1c", "leds": [{"led_id": 0, "state": 1}]}

PUT - Control LED:

{"led_id": 0, "state": 1}   // 0=OFF, 1=ON, 2=TOGGLE

Observe - Subscribe for push notifications on state changes.

Button Resource (/sw)

GET - Returns button state:

{"device_id": "f4ce3616e67c7a1c", "btns": [{"btn_id": 0, "state": 0}]}

Observe - Subscribe for push notifications on button press/release.

Battery Resource (/battery)

GET - Returns battery percentage:

{"device_id": "f4ce3616e67c7a1c", "value": 75}

Voltage Resource (/voltage)

GET - Returns battery voltage in millivolts:

{"device_id": "f4ce3616e67c7a1c", "value": 3850}

Uptime Resource (/uptime)

GET - Returns milliseconds since boot:

{"device_id": "f4ce3616e67c7a1c", "value": 123456}

Used by the bridge to detect device reboots and re-register CoAP Observe subscriptions.

Discovery Resource (/.well-known/core)

GET - Returns CoRE Link Format:

</led>;rt="led",</sw>;rt="button",</battery>;rt="battery",</voltage>;rt="voltage",</uptime>;rt="uptime"

SED (Sleepy End Device) Mode

The device operates as a Sleepy End Device for optimal battery life:

  • Radio OFF most of the time
  • Wakes every 5 seconds to poll parent for queued messages (CONFIG_OPENTHREAD_POLL_PERIOD=5000 in prj.conf:70)
  • Wakes immediately on button press (GPIO interrupt)

Discovery Grace Period

After joining the network, the device stays awake for 2 minutes to allow the bridge to discover it via multicast. After the grace period, it switches to full SED sleep mode.

Boot → Join Thread → Grace Period (2 min, awake) → SED Mode (sleeping)
                      ↑
                Bridge discovers device here

Latency Tradeoffs

Operation Latency Notes
Button notification ~100-200ms GPIO wakes device immediately
GET /battery Up to 5s Request queued until next poll
PUT /led Up to 5s Command queued until next poll

Configuration

Key Kconfig Options (prj.conf)

# Thread SED mode
CONFIG_OPENTHREAD_MTD=y
CONFIG_OPENTHREAD_MTD_SED=y
CONFIG_OPENTHREAD_POLL_PERIOD=5000

# NFC Commissioning
CONFIG_OT_COAP_NFC_COMMISSION=y
CONFIG_NFC_T4T_NRFXLIB=y
CONFIG_NFC_NDEF=y
CONFIG_NFC_NDEF_MSG=y
CONFIG_NFC_NDEF_PARSER=y

# System workqueue stack (required for NFC processing)
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096

# TX power for range
CONFIG_OPENTHREAD_DEFAULT_TX_POWER=8

Board-specific Options (boards/*.conf)

CONFIG_OT_COAP_SAMPLE_SERVER=y
CONFIG_OT_COAP_SAMPLE_LED=y
CONFIG_OT_COAP_SAMPLE_SW=y
CONFIG_OT_COAP_SAMPLE_BATTERY=y

Shell Commands (Debug Build)

Available when built with prj_uart.conf:

# OpenThread commands
ot state                    # Show current role (child/detached/disabled)
ot ipaddr                   # Show IPv6 addresses
ot dataset active           # Show active dataset
ot dataset active -x        # Show dataset as hex
ot ping <ipv6>              # Ping another device

# Check SED mode
ot mode                     # Should show "-" (no 'r' = SED)
ot pollperiod               # Should show 15000 (ms)

Troubleshooting

NFC: Device doesn't detect phone

  • Ensure NFC antenna is properly connected to NFC1/NFC2 pads
  • Check that phone NFC is enabled
  • Try different phone positioning (center of antenna)

NFC: "Invalid data" error (3 blinks)

  • Verify hex string is valid (even number of characters, 0-9 and a-f only)
  • Ensure you're using a Text record, not URI or other types
  • Check dataset length (max 254 bytes = 508 hex chars)

NFC: Want to re-provision

  • Hold sw0 while pressing reset to clear the stored dataset and enter NFC mode
  • Then tap your phone with the new Thread dataset as usual

Device Not Joining Network

  1. Verify dataset is correct: ot dataset active (debug build)
  2. Check channel matches your network
  3. Verify Thread is enabled: ot state should not be "disabled"

Device Keeps Detaching

  1. Check signal strength - move closer to border router
  2. Check for interference on Thread channel
  3. TX power is set to maximum (+8 dBm)

Build Fails with Undefined NFC Symbols

You're building with mainline Zephyr instead of nRF Connect SDK. NFC libraries are only in nRF Connect SDK.

Integration with Thread CoAP Bridge

This firmware works with the Thread CoAP Bridge Home Assistant add-on:

  1. Install the Thread CoAP Bridge add-on
  2. Flash this firmware and commission via NFC
  3. Within 2 minutes, the bridge discovers the device via multicast
  4. Device appears in Home Assistant with LED control, button sensor, battery monitoring
  5. After 2 minutes, device enters SED sleep mode

The bridge fully supports SED devices with 75-second timeouts for all CoAP operations.

Implementation Notes

NFC Processing Architecture

The NFC implementation uses a two-phase approach to avoid crashes:

  1. Work Queue Phase: NDEF parsing runs in system workqueue (handles NFC callback safely)
  2. Main Thread Phase: OpenThread API calls (otDatasetSetActiveTlvs, otIp6SetEnabled, otThreadSetEnabled) run in main thread

This separation is necessary because OpenThread APIs require specific thread context and sufficient stack space.

OpenThread API (nRF Connect SDK 2.x)

Use the current non-deprecated API:

// Get instance directly
otInstance *ot = openthread_get_default_instance();

// Lock/unlock without context parameter
openthread_mutex_lock();
// ... OpenThread API calls ...
openthread_mutex_unlock();

Alarm Output Pulse (P1.06)

The /led resource drives an additional digital output on pin P1.06 (XIAO connector D2), intended for triggering an external alarm or relay circuit.

Behavior:

LED command LED (P2.0) Alarm pin (P1.06)
OFF (state=0) OFF High-impedance (floating)
ON (state=1) ON Driven HIGH (3.3V) for ~1 second, then high-impedance
TOGGLE (state=2) Toggles Follows resulting ON/OFF state

Implementation details:

  • Pin is defined as alarm_pin in the devicetree overlay (boards/xiao_nrf54l15_nrf54l15_cpuapp.overlay)
  • Default state is GPIO_INPUT (high-impedance / disconnected)
  • On LED ON: pin is reconfigured to GPIO_OUTPUT_HIGH (standard push-pull drive to VDD)
  • A k_timer fires after 1 second and reconfigures the pin back to GPIO_INPUT (high-impedance)
  • On LED OFF: any pending pulse timer is cancelled and pin returns to high-impedance immediately
  • Only led_id == 0 triggers the alarm pulse
  • All alarm pin code is guarded by #if DT_NODE_EXISTS(DT_NODELABEL(alarm_pin)) so it compiles out cleanly if the overlay node is removed

NVS Persistence

Thread dataset persistence is handled automatically:

  • CONFIG_SETTINGS is auto-enabled by OpenThread when CONFIG_FLASH=y
  • otDatasetSetActiveTlvs() saves to NVS through the Settings subsystem
  • On boot, OpenThread loads the dataset from NVS automatically

Software re-flash does not delete available dataset in NVS, so erase the MCU completely in case of debugging.

File Structure

File Purpose
src/main.c Entry point, NFC commissioning check
src/nfc_commission.c NFC T4T handling, NDEF parsing
src/nfc_commission.h NFC public API
src/coap_utils.c CoAP server initialization
src/led.c LED resource
src/button.c Button resource with observe
src/battery.c Battery/voltage ADC measurement
src/network_monitor.c Connection recovery, SED grace period
prj.conf Production config (no UART)
prj_uart.conf Debug overlay (UART + shell)

Future Enhancements

  • Hold button on boot to clear dataset and re-enter NFC mode

  • Encryption on CoAP layer (now only Thread encrypted)

  • Add IPEX Antenna to target board to improve radio link

  • BLE commissioning like Matter as alternative method

  • Home Assistant add-on to generate NFC tags with dataset

Code Review issues

One medium issue — race condition in `src/coap_observe.c: The observer list (resource->observers[], resource->observer_count) is accessed from both CoAP request callbacks and button/LED notification paths without synchronization. In practice this is low-risk because OpenThread serializes most of these callbacks, but adding a k_mutex to struct coap_observe_resource would be the clean fix.

Minor: src/coap_discovery.c` builds the .well-known/core response into a 128-byte buffer without checking snprintf return for truncation. Currently fits, but fragile if more resources are added.

References

License

MIT License - see LICENSE file.

About

Low-power Zephyr CoAP-over-Thread Server on nRF54L15 MCU talking to Home Assistant

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors