Skip to main content

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}