feat(uwb): replace ESP-NOW with DW1000 data frames + BLE GPS/IMU input
- Tag: strip all ESP-NOW/WiFi code
- Tag: add BLE GPS characteristic (def3) for iPhone binary GPS writes
- Tag: add BLE Phone IMU characteristic (def4) for iPhone IMU writes
- Tag: transmit GPS/IMU/heartbeat via DW1000 data frames (v2 protocol)
- Tag: update OLED display with GPS speed/heading/fix indicators
- Tag: e-stop now sent via UWB data frames (3x for reliability)
- Anchor: strip all ESP-NOW/WiFi code
- Anchor: receive DW1000 data frames, forward to serial as +GPS/+PIMU/+TIMU/+ESTOP
- Protocol v2: magic {0x5B, 0x02}, msg types 0x60-0x64
This commit is contained in:
parent
30a5212ba2
commit
5e591ab466
176
TASK.md
Normal file
176
TASK.md
Normal file
@ -0,0 +1,176 @@
|
||||
# Task: UWB Tag + Anchor Firmware Rewrite — GPS Breadcrumb Trail
|
||||
|
||||
## Context
|
||||
SaltyBot is a self-balancing robot that follows a rider on an EUC (electric unicycle). The rider wears a UWB tag (ESP32 UWB Pro with Display). The robot has 2 UWB anchors. The rider's iPhone connects to the tag via BLE and streams GPS+IMU data. The tag relays this over UWB data frames to the anchors on the robot, which forward to the Jetson Orin via serial.
|
||||
|
||||
When the rider moves out of UWB range, the iPhone falls back to sending GPS over Tailscale/MQTT directly to Orin. The robot replays the exact breadcrumb path.
|
||||
|
||||
## Files to Modify
|
||||
- `esp32/uwb_tag/src/main.cpp` — Tag firmware (wearable, ESP32 UWB Pro with Display)
|
||||
- `esp32/uwb_anchor/src/main.cpp` — Anchor firmware (on robot, ESP32 UWB Pro, no display)
|
||||
|
||||
## Changes Required
|
||||
|
||||
### Both Files
|
||||
1. **Remove ALL ESP-NOW code** — no more WiFi, esp_now, broadcast MAC, ESP-NOW packets, etc.
|
||||
2. Replace ESP-NOW transport with **DW1000 data frames** for tag→anchor communication
|
||||
|
||||
### Tag Firmware (uwb_tag/src/main.cpp)
|
||||
|
||||
#### Strip
|
||||
- All ESP-NOW includes, init, send, receive, packet structs
|
||||
- WiFi mode/channel/disconnect calls
|
||||
- `espnow_send()`, `espnow_send_imu()`, `espnow_rx_cb()`
|
||||
|
||||
#### Add: BLE GPS Input Service
|
||||
New GATT characteristic for iPhone to write GPS data:
|
||||
- **GPS Characteristic UUID:** `12345678-1234-5678-1234-56789abcdef3` (WRITE, WRITE_NR)
|
||||
- iPhone writes a packed binary struct (not JSON — too slow at 5Hz):
|
||||
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct GpsInput {
|
||||
int32_t lat_e7; // latitude * 1e7 (nanodegrees)
|
||||
int32_t lon_e7; // longitude * 1e7
|
||||
int16_t alt_dm; // altitude in decimeters (±3276.7m range)
|
||||
uint16_t speed_cmps; // ground speed in cm/s (0-655 m/s)
|
||||
uint16_t heading_cd; // heading in centidegrees (0-35999)
|
||||
uint8_t accuracy_dm; // horizontal accuracy in decimeters (0-25.5m)
|
||||
uint8_t fix_type; // 0=none, 1=2D, 2=3D
|
||||
uint32_t timestamp_ms; // iPhone monotonic timestamp
|
||||
}; // 20 bytes total
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
- On BLE write → store in `g_gps` global, set `g_gps_fresh = true`
|
||||
- Also add an **iPhone IMU Characteristic UUID:** `12345678-1234-5678-1234-56789abcdef4` (WRITE, WRITE_NR)
|
||||
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct PhoneImuInput {
|
||||
int16_t ax_mg; // acceleration X in milli-g
|
||||
int16_t ay_mg; // Y
|
||||
int16_t az_mg; // Z
|
||||
int16_t gx_cdps; // gyro X in centi-degrees/sec
|
||||
int16_t gy_cdps; // Y
|
||||
int16_t gz_cdps; // Z
|
||||
int16_t mx_ut; // magnetometer X in micro-Tesla (for heading)
|
||||
int16_t my_ut; // Y
|
||||
int16_t mz_ut; // Z
|
||||
uint32_t timestamp_ms; // iPhone monotonic timestamp
|
||||
}; // 22 bytes
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
#### Add: DW1000 Data Frame TX
|
||||
After each GPS or IMU BLE write, transmit a DW1000 data frame to the anchors:
|
||||
|
||||
```c
|
||||
#define UWB_MSG_GPS 0x60
|
||||
#define UWB_MSG_PIMU 0x61 // Phone IMU
|
||||
#define UWB_MSG_TIMU 0x62 // Tag onboard IMU (MPU6050)
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct UwbDataFrame {
|
||||
uint8_t magic[2]; // {0x5B, 0x02} — v2 protocol
|
||||
uint8_t tag_id;
|
||||
uint8_t msg_type; // UWB_MSG_GPS, UWB_MSG_PIMU, UWB_MSG_TIMU
|
||||
uint8_t seq;
|
||||
uint8_t payload[0]; // variable: GpsInput (20B) or ImuPayload (22B) or TagImu (14B)
|
||||
};
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
Use `DW1000.setData()` + `DW1000.startTransmit()` to send raw data frames between ranging cycles. The DW1000Ranging library handles TWR, but we can interleave data frames in between. Use a simple approach:
|
||||
- After each successful TWR cycle in `newRange()`, if there's fresh GPS/IMU data, transmit a data frame
|
||||
- Also send GPS at 5Hz and tag IMU at 10Hz on a timer if no ranging is happening
|
||||
|
||||
For the tag's own MPU6050 IMU, keep sending that too:
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct TagImuPayload {
|
||||
int16_t ax_mg;
|
||||
int16_t ay_mg;
|
||||
int16_t az_mg;
|
||||
int16_t gx_dps10;
|
||||
int16_t gy_dps10;
|
||||
int16_t gz_dps10;
|
||||
uint8_t accel_mag_10;
|
||||
uint8_t flags; // bit0: fall detected
|
||||
}; // 14 bytes
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
#### Update OLED Display
|
||||
- Add GPS info line: lat/lon or speed+heading
|
||||
- Show BLE data status (receiving from phone or not)
|
||||
- Show "GPS: 3D" or "GPS: ---" indicator
|
||||
|
||||
#### Keep Everything Else
|
||||
- UWB ranging (DW1000Ranging as tag/initiator)
|
||||
- BLE config service (existing UUIDs ...def0, ...def1, ...def2)
|
||||
- MPU6050 IMU reads + fall detection
|
||||
- Power management (display sleep, DW1000 sleep, deep sleep)
|
||||
- E-stop on fall detection (send via UWB data frame instead of ESP-NOW)
|
||||
|
||||
### Anchor Firmware (uwb_anchor/src/main.cpp)
|
||||
|
||||
#### Strip
|
||||
- All ESP-NOW code (includes, init, rx callback, send, packet structs, ring buffer)
|
||||
- WiFi mode/channel/disconnect
|
||||
- `espnow_send_range()`, `espnow_rx_cb()`, `espnow_process()`, `g_other_*` tracking
|
||||
|
||||
#### Add: DW1000 Data Frame RX
|
||||
- Register a DW1000 receive handler that catches data frames from the tag
|
||||
- Parse UwbDataFrame header, then forward payload to Orin via serial:
|
||||
|
||||
```
|
||||
+GPS:<tag_id>,<lat_e7>,<lon_e7>,<alt_dm>,<speed_cmps>,<heading_cd>,<accuracy_dm>,<fix>,<ts>\r\n
|
||||
+PIMU:<tag_id>,<ax>,<ay>,<az>,<gx>,<gy>,<gz>,<mx>,<my>,<mz>,<ts>\r\n
|
||||
+TIMU:<tag_id>,<ax>,<ay>,<az>,<gx>,<gy>,<gz>,<mag>,<flags>\r\n
|
||||
+ESTOP:<tag_id>\r\n
|
||||
```
|
||||
|
||||
#### Keep
|
||||
- UWB ranging (DW1000Ranging as anchor/responder)
|
||||
- AT command handler
|
||||
- TDM slot system
|
||||
- Serial output format for +RANGE lines
|
||||
|
||||
## DW1000 Data Frame Implementation Notes
|
||||
|
||||
The DW1000Ranging library uses the DW1000 for TWR (two-way ranging). To send data frames alongside ranging:
|
||||
|
||||
**Option A (simpler):** Use the DW1000's transmit-after-ranging capability. After a TWR exchange completes, the tag can immediately send a data frame. The DW1000 library exposes `DW1000.setData()` and `DW1000.startTransmit()`.
|
||||
|
||||
**Option B (more robust):** Use ESP-NOW on a separate WiFi channel just for data (not ranging). But we're removing ESP-NOW, so this is not an option.
|
||||
|
||||
**Option C (recommended):** Since DW1000Ranging uses SPI and manages the transceiver state, the safest approach is to **interleave data TX between ranging cycles**. After `newRange()` fires and a TWR cycle completes, queue one data frame for TX. The tag can check `DW1000Ranging.loop()` return or use a flag to know when a cycle is done.
|
||||
|
||||
Actually, the simplest reliable approach: **Use DW1000's user data field in ranging frames**. The DW1000Ranging library sends ranging frames that have a data payload area. We can embed our GPS/IMU data in the ranging frame itself. Check if `DW1000Ranging` exposes `setUserData()` or similar. If not, we can modify the ranging frame assembly.
|
||||
|
||||
If none of that works cleanly, fall back to **periodic standalone TX** using a timer. Between ranging cycles, send raw DW1000 frames. The anchors listen for both ranging responses and raw data frames (differentiated by frame type or header byte).
|
||||
|
||||
Pick whichever approach compiles cleanly with the existing DW1000/DW1000Ranging library in `../../lib/DW1000/`.
|
||||
|
||||
## Important Constraints
|
||||
- ESP32 (not ESP32-S3) — standard Arduino + ESP-IDF
|
||||
- DW1000 library is in `../../lib/DW1000/` (Makerfabs fork from mf_DW1000.zip)
|
||||
- PlatformIO build, `pio run -e tag` / `pio run -e anchor0` / `pio run -e anchor1`
|
||||
- Keep both files self-contained (no shared header files needed — duplicate structs is fine)
|
||||
- Tag CS pin = 21 (display board), Anchor CS pin = 4 (non-display board)
|
||||
- OLED is 128x64 SSD1306 on I2C (tag only)
|
||||
- BLE + DW1000 SPI coexist fine on ESP32 (different peripherals)
|
||||
- Max DW1000 frame payload is ~127 bytes — our packets are well under that
|
||||
|
||||
## Build Verification
|
||||
After changes, verify the code compiles:
|
||||
```bash
|
||||
cd esp32/uwb_tag && pio run -e tag
|
||||
cd esp32/uwb_anchor && pio run -e anchor0
|
||||
```
|
||||
(anchor0/anchor1 envs should exist in platformio.ini, or just `pio run`)
|
||||
|
||||
## Branch
|
||||
Working on branch: `salty/uwb-tag-display-wireless`
|
||||
Commit changes with a descriptive message when done.
|
||||
@ -7,10 +7,13 @@
|
||||
*
|
||||
* Serial output to Jetson (115200):
|
||||
* +RANGE:<anchor_id>,<tag_short_addr>,<range_mm>,<rssi_dbm>\r\n
|
||||
* +GPS:<tag_id>,<lat_e7>,<lon_e7>,<alt_dm>,<speed_cmps>,<heading_cd>,<accuracy_dm>,<fix>,<ts>\r\n
|
||||
* +PIMU:<tag_id>,<ax>,<ay>,<az>,<gx>,<gy>,<gz>,<mx>,<my>,<mz>,<ts>\r\n
|
||||
* +TIMU:<tag_id>,<ax>,<ay>,<az>,<gx>,<gy>,<gz>,<mag>,<flags>\r\n
|
||||
* +ESTOP:<tag_id>,<flags>\r\n
|
||||
*
|
||||
* Also receives ESP-NOW packets from tag and forwards:
|
||||
* +ESPNOW:<tag_id>,<msg_type>,<range_mm>,<rssi>,<batt>,<flags>,<seq>\r\n
|
||||
* +ESTOP:<tag_id>\r\n (priority)
|
||||
* Receives DW1000 data frames from tag (v2 protocol, magic 0x5B 0x02).
|
||||
* Ranging via DW1000Ranging library (TWR responder).
|
||||
*
|
||||
* AT commands (host → anchor):
|
||||
* AT+ID? → +ID:<anchor_id>
|
||||
@ -23,10 +26,8 @@
|
||||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
#include <string.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_now.h>
|
||||
#include <esp_wifi.h>
|
||||
#include "DW1000Ranging.h"
|
||||
#include "DW1000.h"
|
||||
|
||||
/* ── Config ─────────────────────────────────────────────────────── */
|
||||
|
||||
@ -36,7 +37,6 @@
|
||||
|
||||
#define SERIAL_BAUD 115200
|
||||
|
||||
/* Unique DW1000 address per anchor (last 2 bytes differ) */
|
||||
#if ANCHOR_ID == 0
|
||||
static char ANCHOR_ADDR[] = "86:17:5B:D5:A9:9A:E2:00";
|
||||
#else
|
||||
@ -52,72 +52,83 @@ static char ANCHOR_ADDR[] = "86:17:5B:D5:A9:9A:E2:01";
|
||||
#define PIN_RST 27
|
||||
#define PIN_IRQ 34
|
||||
|
||||
/* ── ESP-NOW packet format (shared with tag) ───────────────────── */
|
||||
/* ── UWB Data Frame Protocol (shared with tag) ──────────────────── */
|
||||
|
||||
#define ESPNOW_MAGIC_0 0x5B
|
||||
#define ESPNOW_MAGIC_1 0x01
|
||||
#define MSG_RANGE 0x10
|
||||
#define MSG_ESTOP 0x20
|
||||
#define MSG_HEARTBEAT 0x30
|
||||
#define MSG_IMU 0x40
|
||||
#define MSG_FALL 0x50
|
||||
#define UWB_MAGIC_0 0x5B
|
||||
#define UWB_MAGIC_1 0x02
|
||||
|
||||
#define UWB_MSG_GPS 0x60
|
||||
#define UWB_MSG_PIMU 0x61
|
||||
#define UWB_MSG_TIMU 0x62
|
||||
#define UWB_MSG_ESTOP 0x63
|
||||
#define UWB_MSG_HB 0x64
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct EspNowPacket {
|
||||
struct UwbFrameHeader {
|
||||
uint8_t magic[2];
|
||||
uint8_t tag_id;
|
||||
uint8_t msg_type;
|
||||
uint8_t anchor_id;
|
||||
int32_t range_mm;
|
||||
float rssi_dbm;
|
||||
uint8_t seq;
|
||||
};
|
||||
|
||||
struct GpsInput {
|
||||
int32_t lat_e7;
|
||||
int32_t lon_e7;
|
||||
int16_t alt_dm;
|
||||
uint16_t speed_cmps;
|
||||
uint16_t heading_cd;
|
||||
uint8_t accuracy_dm;
|
||||
uint8_t fix_type;
|
||||
uint32_t timestamp_ms;
|
||||
uint8_t battery_pct;
|
||||
};
|
||||
|
||||
struct PhoneImuInput {
|
||||
int16_t ax_mg;
|
||||
int16_t ay_mg;
|
||||
int16_t az_mg;
|
||||
int16_t gx_cdps;
|
||||
int16_t gy_cdps;
|
||||
int16_t gz_cdps;
|
||||
int16_t mx_ut;
|
||||
int16_t my_ut;
|
||||
int16_t mz_ut;
|
||||
uint32_t timestamp_ms;
|
||||
};
|
||||
|
||||
struct TagImuPayload {
|
||||
int16_t ax_mg;
|
||||
int16_t ay_mg;
|
||||
int16_t az_mg;
|
||||
int16_t gx_dps10;
|
||||
int16_t gy_dps10;
|
||||
int16_t gz_dps10;
|
||||
uint8_t accel_mag_10;
|
||||
uint8_t flags;
|
||||
uint8_t seq_num;
|
||||
uint8_t _pad;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
/* ISR ring buffer for ESP-NOW */
|
||||
#define ESPNOW_Q_SIZE 8
|
||||
static EspNowPacket g_enq[ESPNOW_Q_SIZE];
|
||||
static volatile int g_en_head = 0, g_en_tail = 0;
|
||||
/* ── DW1000 data frame RX buffer ────────────────────────────────── */
|
||||
|
||||
static void IRAM_ATTR espnow_rx_cb(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
|
||||
if (len < (int)sizeof(EspNowPacket)) return;
|
||||
const EspNowPacket *p = (const EspNowPacket *)data;
|
||||
if (p->magic[0] != ESPNOW_MAGIC_0 || p->magic[1] != ESPNOW_MAGIC_1) return;
|
||||
int next = (g_en_head + 1) % ESPNOW_Q_SIZE;
|
||||
if (next == g_en_tail) return;
|
||||
memcpy(&g_enq[g_en_head], p, sizeof(EspNowPacket));
|
||||
g_en_head = next;
|
||||
}
|
||||
#define UWB_RX_BUF_SIZE 64
|
||||
#define UWB_RX_Q_SIZE 8
|
||||
|
||||
/* ── ESP-NOW TX: broadcast own range to other anchor + tag ──────── */
|
||||
static uint8_t g_rx_q[UWB_RX_Q_SIZE][UWB_RX_BUF_SIZE];
|
||||
static uint16_t g_rx_q_len[UWB_RX_Q_SIZE];
|
||||
static volatile int g_rx_head = 0, g_rx_tail = 0;
|
||||
|
||||
static uint8_t broadcast_mac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static uint8_t g_seq = 0;
|
||||
/* Last seen sequence per tag to detect duplicates */
|
||||
static uint8_t g_last_seq[256] = {}; /* indexed by tag_id */
|
||||
static bool g_seq_seen[256] = {};
|
||||
|
||||
/* Track the other anchor's range (received via ESP-NOW) */
|
||||
static int32_t g_other_range_mm = -1;
|
||||
static float g_other_rssi = -100.0f;
|
||||
static uint32_t g_other_last_ms = 0;
|
||||
|
||||
static void espnow_send_range(int32_t range_mm, float rssi, uint16_t tag_addr) {
|
||||
EspNowPacket pkt = {};
|
||||
pkt.magic[0] = ESPNOW_MAGIC_0;
|
||||
pkt.magic[1] = ESPNOW_MAGIC_1;
|
||||
pkt.tag_id = (uint8_t)(tag_addr & 0xFF);
|
||||
pkt.msg_type = MSG_RANGE;
|
||||
pkt.anchor_id = ANCHOR_ID;
|
||||
pkt.range_mm = range_mm;
|
||||
pkt.rssi_dbm = rssi;
|
||||
pkt.timestamp_ms = millis();
|
||||
pkt.battery_pct = 0xFF;
|
||||
pkt.flags = 0x00;
|
||||
pkt.seq_num = g_seq++;
|
||||
esp_now_send(broadcast_mac, (uint8_t *)&pkt, sizeof(pkt));
|
||||
}
|
||||
/*
|
||||
* DW1000 RX callback for data frames.
|
||||
* Called from ISR context — keep it fast, just copy to ring buffer.
|
||||
*
|
||||
* Note: The DW1000Ranging library handles TWR frames internally.
|
||||
* We register a separate handler for non-ranging frames via DW1000 low-level API.
|
||||
* However, since DW1000Ranging manages the receiver state, we intercept
|
||||
* data frames by polling in loop() between ranging cycles.
|
||||
*/
|
||||
|
||||
/* ── AT command handler ─────────────────────────────────────────── */
|
||||
|
||||
@ -146,58 +157,107 @@ static void serial_poll(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── ESP-NOW processing ─────────────────────────────────────────── */
|
||||
/* ── Process received UWB data frames ────────────────────────────── */
|
||||
|
||||
static void espnow_process(void) {
|
||||
while (g_en_tail != g_en_head) {
|
||||
const EspNowPacket &pkt = (const EspNowPacket &)g_enq[g_en_tail];
|
||||
g_en_tail = (g_en_tail + 1) % ESPNOW_Q_SIZE;
|
||||
static void process_uwb_data(const uint8_t *data, uint16_t len) {
|
||||
if (len < sizeof(UwbFrameHeader)) return;
|
||||
|
||||
if (pkt.msg_type == MSG_ESTOP) {
|
||||
if (pkt.flags & 0x01)
|
||||
Serial.printf("+ESTOP:%d\r\n", pkt.tag_id);
|
||||
else
|
||||
Serial.printf("+ESTOP_CLEAR:%d\r\n", pkt.tag_id);
|
||||
continue;
|
||||
const UwbFrameHeader *hdr = (const UwbFrameHeader *)data;
|
||||
if (hdr->magic[0] != UWB_MAGIC_0 || hdr->magic[1] != UWB_MAGIC_1) return;
|
||||
|
||||
uint8_t tag = hdr->tag_id;
|
||||
uint8_t mtype = hdr->msg_type;
|
||||
uint8_t seq = hdr->seq;
|
||||
const uint8_t *payload = data + sizeof(UwbFrameHeader);
|
||||
uint16_t plen = len - sizeof(UwbFrameHeader);
|
||||
|
||||
/* Dedup: skip if we've seen this exact seq from this tag */
|
||||
if (g_seq_seen[tag] && g_last_seq[tag] == seq) return;
|
||||
g_last_seq[tag] = seq;
|
||||
g_seq_seen[tag] = true;
|
||||
|
||||
switch (mtype) {
|
||||
case UWB_MSG_GPS: {
|
||||
if (plen < sizeof(GpsInput)) return;
|
||||
const GpsInput *gps = (const GpsInput *)payload;
|
||||
Serial.printf("+GPS:%d,%ld,%ld,%d,%u,%u,%u,%u,%lu\r\n",
|
||||
tag,
|
||||
(long)gps->lat_e7, (long)gps->lon_e7,
|
||||
gps->alt_dm, gps->speed_cmps, gps->heading_cd,
|
||||
gps->accuracy_dm, gps->fix_type,
|
||||
(unsigned long)gps->timestamp_ms);
|
||||
break;
|
||||
}
|
||||
case UWB_MSG_PIMU: {
|
||||
if (plen < sizeof(PhoneImuInput)) return;
|
||||
const PhoneImuInput *pi = (const PhoneImuInput *)payload;
|
||||
Serial.printf("+PIMU:%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%lu\r\n",
|
||||
tag,
|
||||
pi->ax_mg, pi->ay_mg, pi->az_mg,
|
||||
pi->gx_cdps, pi->gy_cdps, pi->gz_cdps,
|
||||
pi->mx_ut, pi->my_ut, pi->mz_ut,
|
||||
(unsigned long)pi->timestamp_ms);
|
||||
break;
|
||||
}
|
||||
case UWB_MSG_TIMU: {
|
||||
if (plen < sizeof(TagImuPayload)) return;
|
||||
const TagImuPayload *ti = (const TagImuPayload *)payload;
|
||||
Serial.printf("+TIMU:%d,%d,%d,%d,%d,%d,%d,%u,%u\r\n",
|
||||
tag,
|
||||
ti->ax_mg, ti->ay_mg, ti->az_mg,
|
||||
ti->gx_dps10, ti->gy_dps10, ti->gz_dps10,
|
||||
ti->accel_mag_10, ti->flags);
|
||||
break;
|
||||
}
|
||||
case UWB_MSG_ESTOP: {
|
||||
if (plen < sizeof(TagImuPayload)) {
|
||||
/* Minimal e-stop with just flags byte */
|
||||
uint8_t flags = (plen >= 1) ? payload[plen - 1] : 0x01;
|
||||
Serial.printf("+ESTOP:%d,%u\r\n", tag, flags);
|
||||
} else {
|
||||
const TagImuPayload *ep = (const TagImuPayload *)payload;
|
||||
Serial.printf("+ESTOP:%d,%u\r\n", tag, ep->flags);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UWB_MSG_HB: {
|
||||
/* Heartbeat — just note tag is alive */
|
||||
Serial.printf("+HB:%d\r\n", tag);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Serial.printf("+UWB_UNK:%d,0x%02X,%d\r\n", tag, mtype, plen);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Try to read a data frame from DW1000.
|
||||
* Called between ranging cycles when the DW1000 might have buffered
|
||||
* a non-ranging frame. We briefly switch to receive mode, check for
|
||||
* data, then let ranging resume.
|
||||
*
|
||||
* This is a cooperative approach: the DW1000Ranging library uses
|
||||
* the transceiver for TWR, and we steal idle slots to listen for
|
||||
* data frames from the tag.
|
||||
*/
|
||||
static uint8_t g_uwb_rx_buf[UWB_RX_BUF_SIZE];
|
||||
|
||||
static void uwb_check_data_rx(void) {
|
||||
/* Check if DW1000 received a frame that DW1000Ranging didn't consume */
|
||||
if (!DW1000.isReceiveDone()) return;
|
||||
|
||||
uint16_t len = DW1000.getDataLength();
|
||||
if (len < sizeof(UwbFrameHeader) || len > UWB_RX_BUF_SIZE) {
|
||||
/* Not our frame or too large — let ranging handle it */
|
||||
return;
|
||||
}
|
||||
|
||||
/* If this is a range from the OTHER anchor, track it and print as +RANGE */
|
||||
if (pkt.msg_type == MSG_RANGE && pkt.anchor_id != ANCHOR_ID
|
||||
&& pkt.anchor_id < 2) {
|
||||
g_other_range_mm = pkt.range_mm;
|
||||
g_other_rssi = pkt.rssi_dbm;
|
||||
g_other_last_ms = millis();
|
||||
/* Print as +RANGE so Orin sees both anchors from either serial port */
|
||||
Serial.printf("+RANGE:%d,%02X,%ld,%.1f\r\n",
|
||||
pkt.anchor_id, pkt.tag_id,
|
||||
(long)pkt.range_mm, pkt.rssi_dbm);
|
||||
continue;
|
||||
}
|
||||
DW1000.getData(g_uwb_rx_buf, len);
|
||||
|
||||
/* Forward IMU and FALL packets to Jetson serial */
|
||||
if (pkt.msg_type == MSG_IMU || pkt.msg_type == MSG_FALL) {
|
||||
const uint8_t *raw = (const uint8_t *)&pkt;
|
||||
Serial.printf("+%s:%d,",
|
||||
pkt.msg_type == MSG_FALL ? "FALL" : "IMU",
|
||||
pkt.tag_id);
|
||||
/* Forward as hex for easy parsing: ax,ay,az,gx,gy,gz,mag */
|
||||
int16_t ax = (int16_t)(raw[4] | (raw[5] << 8));
|
||||
int16_t ay = (int16_t)(raw[6] | (raw[7] << 8));
|
||||
int16_t az = (int16_t)(raw[8] | (raw[9] << 8));
|
||||
int16_t gx = (int16_t)(raw[10] | (raw[11] << 8));
|
||||
int16_t gy = (int16_t)(raw[12] | (raw[13] << 8));
|
||||
int16_t gz = (int16_t)(raw[14] | (raw[15] << 8));
|
||||
uint8_t mag = raw[16];
|
||||
Serial.printf("%.3f,%.3f,%.3f,%.1f,%.1f,%.1f,%.1f\r\n",
|
||||
ax/1000.0f, ay/1000.0f, az/1000.0f,
|
||||
gx/10.0f, gy/10.0f, gz/10.0f,
|
||||
mag/10.0f);
|
||||
continue;
|
||||
}
|
||||
|
||||
Serial.printf("+ESPNOW:%d,%02X,%ld,%.1f,%d,%02X,%d\r\n",
|
||||
pkt.tag_id, pkt.msg_type, (long)pkt.range_mm,
|
||||
pkt.rssi_dbm, pkt.battery_pct, pkt.flags, pkt.seq_num);
|
||||
/* Quick magic check — only process v2 data frames */
|
||||
if (g_uwb_rx_buf[0] == UWB_MAGIC_0 && g_uwb_rx_buf[1] == UWB_MAGIC_1) {
|
||||
process_uwb_data(g_uwb_rx_buf, len);
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,9 +274,6 @@ static void newRange(void) {
|
||||
|
||||
Serial.printf("+RANGE:%d,%04X,%ld,%.1f\r\n",
|
||||
ANCHOR_ID, tag_addr, (long)range_mm, rxPow);
|
||||
|
||||
/* Broadcast own range via ESP-NOW → other anchor + tag receive it */
|
||||
espnow_send_range(range_mm, rxPow, tag_addr);
|
||||
}
|
||||
|
||||
static void newBlink(DW1000Device *device) {
|
||||
@ -232,26 +289,10 @@ static void inactiveDevice(DW1000Device *device) {
|
||||
void setup(void) {
|
||||
Serial.begin(SERIAL_BAUD);
|
||||
delay(300);
|
||||
Serial.printf("\r\n[uwb_anchor] id=%d addr=%s starting\r\n", ANCHOR_ID, ANCHOR_ADDR);
|
||||
Serial.printf("\r\n[uwb_anchor] id=%d addr=%s v2 (UWB data frames)\r\n",
|
||||
ANCHOR_ID, ANCHOR_ADDR);
|
||||
|
||||
/* ESP-NOW receiver */
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.disconnect();
|
||||
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
|
||||
if (esp_now_init() == ESP_OK) {
|
||||
esp_now_register_recv_cb(espnow_rx_cb);
|
||||
/* Add broadcast peer for TX */
|
||||
esp_now_peer_info_t peer = {};
|
||||
memcpy(peer.peer_addr, broadcast_mac, 6);
|
||||
peer.channel = 0;
|
||||
peer.encrypt = false;
|
||||
esp_now_add_peer(&peer);
|
||||
Serial.println("[uwb_anchor] ESP-NOW rx+tx ok");
|
||||
} else {
|
||||
Serial.println("[uwb_anchor] WARN: ESP-NOW failed");
|
||||
}
|
||||
|
||||
/* DW1000 */
|
||||
/* DW1000 ranging */
|
||||
SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI);
|
||||
DW1000Ranging.initCommunication(PIN_RST, PIN_CS, PIN_IRQ);
|
||||
|
||||
@ -262,26 +303,28 @@ void setup(void) {
|
||||
DW1000Ranging.startAsAnchor(ANCHOR_ADDR, DW1000.MODE_LONGDATA_RANGE_LOWPOWER, false);
|
||||
|
||||
Serial.printf("[uwb_anchor] DW1000 ready id=%d MODE_LONGDATA_RANGE_LOWPOWER\r\n", ANCHOR_ID);
|
||||
Serial.println("[uwb_anchor] Listening for tags...");
|
||||
Serial.println("[uwb_anchor] Listening for tags + UWB data frames...");
|
||||
}
|
||||
|
||||
/* ── Loop ───────────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Time-division multiplexing: 2 anchors share the same channel.
|
||||
* Each anchor only responds during its time slot to avoid RF collisions.
|
||||
* Slot duration = 50ms → each anchor gets 10 Hz effective rate.
|
||||
* Anchor 0 responds during even slots, anchor 1 during odd slots.
|
||||
* TDM: 2 anchors share the same channel.
|
||||
* Each anchor only responds during its time slot.
|
||||
* Slot = 50ms → each anchor gets 10 Hz effective rate.
|
||||
* Between slots, check for UWB data frames.
|
||||
*/
|
||||
#define SLOT_MS 50
|
||||
#define NUM_ANCHORS_TOTAL 2
|
||||
|
||||
void loop(void) {
|
||||
serial_poll();
|
||||
espnow_process();
|
||||
|
||||
uint32_t slot = (millis() / SLOT_MS) % NUM_ANCHORS_TOTAL;
|
||||
if (slot == ANCHOR_ID) {
|
||||
DW1000Ranging.loop();
|
||||
}
|
||||
|
||||
/* Check for UWB data frames from tag between ranging slots */
|
||||
uwb_check_data_rx();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user