Skip to content

Commit 627c8b9

Browse files
committed
feat(hardware): add Rust RTL8720F radar simulator
1 parent 09ca660 commit 627c8b9

6 files changed

Lines changed: 1022 additions & 11 deletions

File tree

docs/adr/ADR-263-rtl8720f-2-4ghz-fmcw-radar-platform.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@ This is not a drop-in replacement for ESP32 CSI:
3131
function signatures, callback ABI, binary layouts, toolchain version, licensing terms, or public
3232
RTL8720F board package.
3333

34-
Realtek's public Ameba RTOS repository is a plausible base and its public release history includes
35-
CSI and EDCCA APIs, but the reviewed public material does not establish that the RTL8720F radar
36-
API or firmware blobs are publicly available. Therefore an honest integration must be split at a
37-
vendor boundary.
34+
Realtek's public Ameba RTOS repository is the base. Release v1.2.1 includes the CSI API and fixes a
35+
CSI application-buffer semaphore issue, but does not expose the radar application surface. Open
36+
upstream PR #1336 (2026-07-18 snapshot) adds RTL8720F project artifacts, `AT+RAD`, `AT+RADDBG`, and
37+
the public configuration call `wifi_radar_config(struct rtw_radar_action_parm *)`. Its public
38+
parameter struct confirms mode, channel, 70/40/20 MHz bandwidth selector, trigger period, and
39+
enable/config actions. Report reception still crosses non-public/placeholder HAL symbols such as
40+
`wifi_hal_radar_recv_data(frame_num, frame_type, data)`, so the report layout and buffer lifetime
41+
remain vendor-gated. Therefore the integration stays split at that boundary.
3842

3943
## Decision
4044

@@ -69,7 +73,7 @@ claim millimetre-wave provenance.
6973

7074
### P0 — Vendor enablement
7175

72-
Obtain a redistributable RTL8720F SDK package, radar API headers/libraries, a supported evaluation
76+
Obtain the PR #1336-or-newer RTL8720F SDK package, radar API headers/libraries, a supported evaluation
7377
board, flashing/debug instructions, report definitions, and written redistribution terms.
7478

7579
**Gate:** compile and run Realtek's unmodified radar example and capture CFR plus near/far
@@ -78,7 +82,10 @@ Range-FFT output. Until this passes, device firmware is `VENDOR_BLOCKED`, not im
7882
### P1 — Host-first contract
7983

8084
Implement ADR-264 types, parsers, fixtures, fuzz tests, and replay support without linking vendor
81-
code. Generate deterministic synthetic fixtures whose provenance is explicitly synthetic.
85+
code. Use the Rust `Rtl8720fSimulator` as the only pre-hardware live source. It emits deterministic
86+
CFR, near/far Range-FFT, interference, and capabilities frames through the same ADR-264 encoder and
87+
parser used by hardware. Every simulated frame sets `RadarFlags::SYNTHETIC`; simulation results are
88+
never reported as device measurements.
8289

8390
**Gate:** malformed inputs never panic; encode/decode round trips; unknown versions and report
8491
types fail closed.
@@ -156,6 +163,9 @@ alone. If it does not, ship it only as an independent presence/range sensor.
156163
- Realtek Semiconductor, `RTL8720F-2.4G-Radar-Advantages_EN.pptx`, slides 3 and 10–19,
157164
supplied 2026-07-18. This is product material, not measured RuView validation.
158165
- [Ameba-AIoT/ameba-rtos releases](https://github.com/Ameba-AIoT/ameba-rtos/releases), reviewed
159-
2026-07-18; public release notes mention CSI and EDCCA APIs but do not document the deck's radar ABI.
166+
2026-07-18; v1.2.1 is the current QC release and includes a CSI buffer-semaphore fix.
167+
- [Ameba-AIoT/ameba-rtos PR #1336](https://github.com/Ameba-AIoT/ameba-rtos/pull/1336), reviewed
168+
2026-07-18; exposes RTL8720F build assets, `wifi_radar_config`, and radar AT commands while report
169+
internals remain in binary/private layers.
160170
- ADR-063 (mmWave sensor fusion), ADR-095/097 (source normalization), and ADR-260/262 (RuField
161171
multimodal event model and live bridge).

docs/adr/ADR-264-rtl8720f-radar-wire-protocol.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ sent by `memcpy`; firmware serializes each field explicitly.
4242
| 36 | 2 | bandwidth_mhz | 20, 40, or 70 |
4343
| 38 | 2 | flags | calibration/interference/saturation/time-sync flags |
4444
| 40 | 2 | element_count | complex samples or range bins |
45-
| 42 | 1 | element_format | 1 complex-i16, 2 complex-f32, 3 power-u16, 4 power-f32 |
45+
| 42 | 1 | element_format | 0 bytes/TLV, 1 complex-i16, 2 complex-f32, 3 power-u16, 4 power-f32 |
4646
| 43 | 1 | antenna_count | expected to be 1 for the deck's 1T1R configuration |
4747
| 44 | 4 | scale | quantized-to-physical multiplier; `1.0` for float payloads |
4848
| 48 | 4 | bin_spacing | Hz for CFR, metres for Range-FFT |
@@ -123,6 +123,14 @@ No vendor-provided presence probability bypasses RuView privacy, provenance, or
123123
host golden decoder.
124124
6. Revise this proposed ADR with measured element counts, rates, and API names before acceptance.
125125

126+
Host-side steps 1–3 are implemented in `wifi-densepose-hardware::rtl8720f`: typed report and
127+
element enums, semantic type/format validation, bounded length arithmetic, CRC verification,
128+
finite-float checks, encode/decode round trips, corruption/truncation tests, and deterministic
129+
arbitrary-input panic checks. Cross-language vectors remain blocked on the vendor SDK callback ABI.
130+
Bit 15 of `flags` is reserved by RuView as `SYNTHETIC`; the Rust simulator always sets it and real
131+
firmware must never set it. The simulator is deterministic by seed and exercises the production
132+
encoder/parser rather than a parallel mock representation.
133+
126134
## Acceptance criteria
127135

128136
- Rust encode/decode round-trip for every report type.

v2/crates/wifi-densepose-hardware/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@ hardware sources. All parsing operates on byte buffers with no C FFI or hardware
1313
compile time, making the crate fully portable and deterministic -- the same bytes in always produce
1414
the same parsed output.
1515

16+
## RTL8720F radar simulator (ADR-263/264)
17+
18+
Until Realtek hardware and the radar report SDK arrive, the Rust-only simulator exercises the same
19+
versioned CFR/Range-FFT wire codec used by the future device adapter. Every frame is marked
20+
`SYNTHETIC`.
21+
22+
```powershell
23+
cargo run -p wifi-densepose-hardware --bin rtl8720f-sim -- `
24+
--frames 100 --seed 0x8720f123456789ab `
25+
--output rtl8720f-synthetic.rtr
26+
```
27+
28+
Add `--udp 127.0.0.1:5005 --realtime` to stream one ADR-264 frame per UDP datagram. Replay files
29+
contain a little-endian `u32` frame length followed by the encoded frame.
30+
1631
## Features
1732

1833
- **ESP32 binary parser** -- Parses ADR-018 binary CSI frames streamed over UDP from ESP32 and
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//! Rust-only RTL8720F radar simulator for pre-hardware integration.
2+
3+
use std::{
4+
fs::File,
5+
io::{self, Write},
6+
net::{SocketAddr, UdpSocket},
7+
path::PathBuf,
8+
thread,
9+
time::Duration,
10+
};
11+
12+
use clap::Parser;
13+
use wifi_densepose_hardware::rtl8720f::{
14+
simulator::{Rtl8720fSimulator, SimulatorConfig},
15+
RadarFrame, ReportType,
16+
};
17+
18+
#[derive(Debug, Parser)]
19+
#[command(
20+
name = "rtl8720f-sim",
21+
about = "Emit synthetic ADR-264 RTL8720F radar frames"
22+
)]
23+
struct Args {
24+
#[arg(long, default_value_t = 100)]
25+
frames: u32,
26+
#[arg(long, default_value = "0x8720f123456789ab", value_parser = parse_u64)]
27+
seed: u64,
28+
#[arg(long, default_value_t = 40)]
29+
bandwidth: u16,
30+
#[arg(long, default_value_t = 15)]
31+
interval_ms: u64,
32+
/// UDP destination; each frame is one datagram.
33+
#[arg(long)]
34+
udp: Option<SocketAddr>,
35+
/// Replay file; LE u32 length followed by ADR-264 bytes.
36+
#[arg(long)]
37+
output: Option<PathBuf>,
38+
#[arg(long)]
39+
realtime: bool,
40+
}
41+
42+
fn parse_u64(value: &str) -> Result<u64, String> {
43+
if let Some(hex) = value
44+
.strip_prefix("0x")
45+
.or_else(|| value.strip_prefix("0X"))
46+
{
47+
u64::from_str_radix(hex, 16).map_err(|error| error.to_string())
48+
} else {
49+
value.parse::<u64>().map_err(|error| error.to_string())
50+
}
51+
}
52+
53+
fn emit(
54+
frame: RadarFrame,
55+
socket: Option<&UdpSocket>,
56+
destination: Option<SocketAddr>,
57+
output: &mut Option<File>,
58+
) -> Result<usize, Box<dyn std::error::Error>> {
59+
let wire = frame.to_bytes()?;
60+
if let (Some(socket), Some(destination)) = (socket, destination) {
61+
let sent = socket.send_to(&wire, destination)?;
62+
if sent != wire.len() {
63+
return Err(io::Error::new(io::ErrorKind::WriteZero, "partial UDP datagram").into());
64+
}
65+
}
66+
if let Some(file) = output {
67+
file.write_all(&(wire.len() as u32).to_le_bytes())?;
68+
file.write_all(&wire)?;
69+
}
70+
Ok(wire.len())
71+
}
72+
73+
fn main() -> Result<(), Box<dyn std::error::Error>> {
74+
let args = Args::parse();
75+
if args.udp.is_none() && args.output.is_none() {
76+
return Err("select at least one sink with --udp or --output".into());
77+
}
78+
let config = SimulatorConfig {
79+
seed: args.seed,
80+
bandwidth_mhz: args.bandwidth,
81+
frame_period_us: args.interval_ms * 1_000,
82+
..SimulatorConfig::default()
83+
};
84+
let mut simulator = Rtl8720fSimulator::new(config)?;
85+
let socket = args.udp.map(|_| UdpSocket::bind("0.0.0.0:0")).transpose()?;
86+
let mut output = args.output.as_ref().map(File::create).transpose()?;
87+
let mut bytes_emitted = emit(
88+
simulator.capabilities_frame(),
89+
socket.as_ref(),
90+
args.udp,
91+
&mut output,
92+
)?;
93+
94+
for index in 0..args.frames {
95+
let report_type = match index % 16 {
96+
15 => ReportType::Interference,
97+
value if value % 4 == 1 => ReportType::RangeNear,
98+
value if value % 4 == 3 => ReportType::RangeFar,
99+
_ => ReportType::Cfr,
100+
};
101+
bytes_emitted += emit(
102+
simulator.next_frame(report_type),
103+
socket.as_ref(),
104+
args.udp,
105+
&mut output,
106+
)?;
107+
if args.realtime {
108+
thread::sleep(Duration::from_millis(args.interval_ms));
109+
}
110+
}
111+
eprintln!(
112+
"emitted {} synthetic RTL8720F frames ({} bytes, seed={:#x})",
113+
args.frames + 1,
114+
bytes_emitted,
115+
args.seed
116+
);
117+
Ok(())
118+
}

v2/crates/wifi-densepose-hardware/src/lib.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ pub mod sync_packet;
5353
// coordinator-node Rust code drive the controller stack without
5454
// touching any downstream signal/ruvector/train/mat crate.
5555
pub mod radio_ops;
56+
/// ADR-264 host-side framing for Realtek RTL8720F CFR and FMCW radar reports.
57+
/// This module has no dependency on the vendor SDK.
58+
pub mod rtl8720f;
5659

5760
pub use bridge::CsiData;
5861
pub use csi_frame::{
@@ -64,12 +67,18 @@ pub use esp32_parser::{
6467
RUVIEW_FEATURE_MAGIC, RUVIEW_FEATURE_STATE_MAGIC, RUVIEW_FUSED_VITALS_MAGIC,
6568
RUVIEW_TEMPORAL_MAGIC, RUVIEW_VITALS_MAGIC,
6669
};
67-
pub use sync_packet::{
68-
SyncPacket, SyncPacketFlags, SYNC_PACKET_MAGIC, SYNC_PACKET_SIZE, SYNC_PACKET_PROTO_VER,
69-
};
7070
pub use radio_ops::{
7171
crc32_ieee, decode_anomaly_alert, decode_mesh, decode_node_status, encode_health, AnomalyAlert,
7272
AuthClass, CaptureProfile, MeshError, MeshHeader, MeshMsgType, MeshRole, MockRadio, NodeStatus,
7373
RadioError, RadioHealth, RadioMode, RadioOps, MESH_HEADER_SIZE, MESH_MAGIC, MESH_MAX_PAYLOAD,
7474
MESH_VERSION,
7575
};
76+
pub use rtl8720f::{
77+
ElementFormat as Rtl8720fElementFormat, RadarFlags as Rtl8720fRadarFlags,
78+
RadarFrame as Rtl8720fRadarFrame, RadarParseError as Rtl8720fRadarParseError,
79+
RadarPayload as Rtl8720fRadarPayload, ReportType as Rtl8720fReportType,
80+
RTL8720F_RADAR_HEADER_LEN, RTL8720F_RADAR_MAGIC, RTL8720F_RADAR_VERSION,
81+
};
82+
pub use sync_packet::{
83+
SyncPacket, SyncPacketFlags, SYNC_PACKET_MAGIC, SYNC_PACKET_PROTO_VER, SYNC_PACKET_SIZE,
84+
};

0 commit comments

Comments
 (0)