cam_sender_receiver/cam_sender_receiver.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2024 Fundació Privada Internet i Innovació Digital a Catalunya (i2CAT)
3
4//! CAM Sender and Receiver Example
5//!
6//! This example mirrors `FlexStack/examples/cam_sender_and_receiver.py` and
7//! shows how to use the `rustflexstack` library to build a minimal V2X node
8//! that:
9//!
10//! 1. Initialises a GeoNetworking router and a BTP router.
11//! 2. Connects them to a raw Ethernet link layer.
12//! 3. Uses `CooperativeAwarenessBasicService` to send UPER-encoded CAMs at
13//! up to 10 Hz and receive + decode incoming CAMs on BTP port 2001.
14//! 4. Uses `LocationService` to fan GPS fixes out to both the GN router
15//! (position vector updates) and the CA Basic Service (CAM generation).
16//!
17//! # Running
18//! ```text
19//! # The raw socket requires CAP_NET_RAW or root:
20//! sudo cargo run --example cam_sender_receiver -- <interface>
21//! # e.g.:
22//! sudo cargo run --example cam_sender_receiver -- eth0
23//! ```
24//!
25//! If no interface is given the example falls back to `lo` (loopback), which
26//! is sufficient to test send→receive on a single machine.
27
28use rustflexstack::btp::router::Router as BTPRouter;
29use rustflexstack::facilities::ca_basic_service::{CooperativeAwarenessBasicService, VehicleData};
30use rustflexstack::facilities::local_dynamic_map::{
31 ldm_constants::ITS_AID_CAM,
32 ldm_storage::ItsDataObject,
33 ldm_types::{RegisterDataConsumerReq, RegisterDataProviderReq, RequestDataObjectsReq},
34 LdmFacility,
35};
36use rustflexstack::facilities::location_service::{GpsFix, LocationService};
37use rustflexstack::geonet::gn_address::{GNAddress, M, MID, ST};
38use rustflexstack::geonet::mib::Mib;
39use rustflexstack::geonet::position_vector::LongPositionVector;
40use rustflexstack::geonet::router::Router as GNRouter;
41use rustflexstack::link_layer::raw_link_layer::RawLinkLayer;
42use std::env;
43use std::sync::mpsc;
44use std::thread;
45use std::time::Duration;
46
47fn main() {
48 // ── Parse optional interface argument ────────────────────────────────────
49 let iface = env::args().nth(1).unwrap_or_else(|| "lo".to_string());
50
51 println!("=== CAM Sender/Receiver Example (CA Basic Service) ===");
52 println!("Interface: {}", iface);
53
54 // ── Generate a random locally-administered MAC ────────────────────────
55 let mac = {
56 use std::time::{SystemTime, UNIX_EPOCH};
57 let seed = SystemTime::now()
58 .duration_since(UNIX_EPOCH)
59 .unwrap()
60 .subsec_nanos();
61 [
62 0x02u8,
63 (seed >> 24) as u8,
64 (seed >> 16) as u8,
65 (seed >> 8) as u8,
66 seed as u8,
67 0xAA,
68 ]
69 };
70 println!(
71 "MAC: {:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
72 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
73 );
74
75 // ── MIB ──────────────────────────────────────────────────────────────────
76 let mut mib = Mib::new();
77 mib.itsGnLocalGnAddr = GNAddress::new(M::GnMulticast, ST::PassengerCar, MID::new(mac));
78 mib.itsGnBeaconServiceRetransmitTimer = 0;
79
80 // ── Location Service ─────────────────────────────────────────────────────
81 //
82 // A single LocationService publishes GPS fixes to multiple subscribers.
83 // We subscribe twice: once for the GN router's position vector, and once
84 // for the CA Basic Service CAM generation.
85 let mut loc_svc = LocationService::new();
86 let gn_gps_rx = loc_svc.subscribe(); // → GN position vector updates
87 let ca_gps_rx = loc_svc.subscribe(); // → CAM transmission
88
89 // ── Spawn GeoNetworking router ────────────────────────────────────────────
90 let (gn_handle, gn_to_ll_rx, gn_to_btp_rx) = GNRouter::spawn(mib, None, None, None);
91
92 // ── Spawn BTP router ─────────────────────────────────────────────────────
93 let (btp_handle, btp_to_gn_rx) = BTPRouter::spawn(mib);
94
95 // ── Wire RawLinkLayer ────────────────────────────────────────────────────
96 let (ll_to_gn_tx, ll_to_gn_rx) = mpsc::channel::<Vec<u8>>();
97 let raw_ll = RawLinkLayer::new(ll_to_gn_tx, gn_to_ll_rx, &iface, mac);
98 raw_ll.start();
99
100 // ── Wire threads ─────────────────────────────────────────────────────────
101
102 // LL → GN
103 let gn_h1 = gn_handle.clone();
104 thread::spawn(move || {
105 while let Ok(pkt) = ll_to_gn_rx.recv() {
106 gn_h1.send_incoming_packet(pkt);
107 }
108 });
109
110 // GN → BTP
111 let btp_h1 = btp_handle.clone();
112 thread::spawn(move || {
113 while let Ok(ind) = gn_to_btp_rx.recv() {
114 btp_h1.send_gn_data_indication(ind);
115 }
116 });
117
118 // BTP → GN
119 let gn_h2 = gn_handle.clone();
120 thread::spawn(move || {
121 while let Ok(req) = btp_to_gn_rx.recv() {
122 gn_h2.send_gn_data_request(req);
123 }
124 });
125
126 // ── Bridge: LocationService → GN router position vector ──────────────────
127 //
128 // Converts each GpsFix into a LongPositionVector and pushes it into the
129 // GeoNetworking router so the EPV in every outgoing GN header is current.
130 let gn_h3 = gn_handle.clone();
131 thread::spawn(move || {
132 while let Ok(fix) = gn_gps_rx.recv() {
133 let mut epv = LongPositionVector::decode([0u8; 24]);
134 epv.update_from_gps(
135 fix.latitude,
136 fix.longitude,
137 fix.speed_mps,
138 fix.heading_deg,
139 fix.pai,
140 );
141 gn_h3.update_position_vector(epv);
142 }
143 });
144
145 // ── Local Dynamic Map ─────────────────────────────────────────────────────
146 //
147 // Create the LDM centred on the simulated GPS position with a 5 km radius.
148 // Parc Tecnològic del Vallès: 41.552°N 2.134°E → ETSI integers × 1e7.
149 let ldm = LdmFacility::new(415_520_000, 21_340_000, 5_000.0);
150
151 // Register this node as a CAM data provider and consumer.
152 ldm.if_ldm_3
153 .register_data_provider(RegisterDataProviderReq {
154 application_id: ITS_AID_CAM,
155 });
156 ldm.if_ldm_4
157 .register_data_consumer(RegisterDataConsumerReq {
158 application_id: ITS_AID_CAM,
159 });
160
161 // ── CA Basic Service ─────────────────────────────────────────────────────
162 //
163 // VehicleData carries static vehicle metadata that goes into every CAM.
164 // station_id should match the GN address mid bytes in a real deployment.
165 let station_id = u32::from_be_bytes([mac[2], mac[3], mac[4], mac[5]]);
166 let vehicle_data = VehicleData {
167 station_id,
168 ..VehicleData::default() // PassengerCar, unavailable lengths
169 };
170
171 // Pass Some(ldm) so every received CAM is stored in the LDM before being
172 // forwarded. The _cam_rx channel is still available for direct consumers
173 // but in this example we read via IF.LDM.4 instead.
174 let (ca_svc, _cam_rx) =
175 CooperativeAwarenessBasicService::new(btp_handle.clone(), vehicle_data, Some(ldm.clone()));
176
177 // start() consumes the service handle and spawns the TX + RX threads.
178 ca_svc.start(ca_gps_rx);
179
180 // ── LDM query printer ─────────────────────────────────────────────────────
181 //
182 // Every second, query the LDM for all CAM records and print them.
183 // This uses the ETSI IF.LDM.4 interface and confirms that received CAMs
184 // are correctly stored and retrievable.
185 let ldm_reader = ldm.clone();
186 thread::spawn(move || loop {
187 thread::sleep(Duration::from_secs(1));
188 let resp = ldm_reader
189 .if_ldm_4
190 .request_data_objects(RequestDataObjectsReq {
191 application_id: ITS_AID_CAM,
192 data_object_types: vec![ITS_AID_CAM],
193 filter: None,
194 order: None,
195 max_results: None,
196 });
197 if resp.data_objects.is_empty() {
198 println!("[LDM] No CAM records in store");
199 } else {
200 println!("[LDM] {} CAM record(s):", resp.data_objects.len());
201 for entry in &resp.data_objects {
202 if let ItsDataObject::Cam(cam) = &entry.data_object {
203 let lat = cam
204 .cam
205 .cam_parameters
206 .basic_container
207 .reference_position
208 .latitude
209 .0 as f64
210 / 1e7;
211 let lon = cam
212 .cam
213 .cam_parameters
214 .basic_container
215 .reference_position
216 .longitude
217 .0 as f64
218 / 1e7;
219 println!(
220 " [LDM CAM] record={:>5} station={:>10} lat={:.5} lon={:.5}",
221 entry.record_id, cam.header.station_id.0, lat, lon,
222 );
223 }
224 }
225 }
226 });
227
228 // ── GPS publisher (simulates a real GNSS sensor at 1 Hz) ─────────────────
229 //
230 // In a real application this thread would read from gpsd or a serial port.
231 // Wait briefly for the routers to finish initialising.
232 thread::sleep(Duration::from_millis(100));
233 println!("Publishing GPS fixes @ 10 Hz — Ctrl+C to stop\n");
234
235 loop {
236 thread::sleep(Duration::from_millis(100));
237 // 41.552°N 2.134°E — Parc Tecnològic del Vallès
238 loc_svc.publish(GpsFix {
239 latitude: 41.552,
240 longitude: 2.134,
241 altitude_m: 120.0,
242 speed_mps: 0.0,
243 heading_deg: 0.0,
244 pai: true,
245 });
246 }
247}