Skip to main content

phytrace_sdk/
ffi.rs

1//! C FFI bindings for the PhyTrace SDK.
2//!
3//! This module exposes the PhyTrace SDK's core functionality through a C-compatible
4//! ABI, enabling integration with C++ ROS2 nodes and other non-Rust consumers.
5//!
6//! # Architecture
7//!
8//! The FFI layer manages an internal `tokio::Runtime` per agent handle so that
9//! C callers never deal with async. All functions are synchronous from the caller's
10//! perspective.
11//!
12//! # Memory Model
13//!
14//! - Opaque handles (`*mut PhyTraceAgent`, `*mut PhyTraceBuilder`, `*mut PhyTraceEvent`)
15//!   are heap-allocated and must be freed by the corresponding `_destroy` function.
16//! - Strings returned by the library (e.g., `phytrace_last_error`, `phytrace_event_to_json`)
17//!   must be freed with `phytrace_string_free`.
18//! - Null pointers are handled gracefully — functions return `PHYTRACE_ERR_NULL_PTR`.
19//!
20//! # Domains Covered
21//!
22//! This FFI layer covers the domains required for ROS2 topic integration:
23//! - Identity, Location, Motion, Power, Perception (Lidar + IMU), Safety,
24//!   Navigation, Actuators (Joints), Operational, Communication, Context
25//!
26//! # Domains NOT Yet Covered (tracked in issue)
27//!
28//! AI, Audio, Compliance, Compute, Coordination, EnvironmentInteraction,
29//! HRI, Maintenance, Manipulation, Payload, Simulation, Thermal
30
31#![allow(clippy::missing_safety_doc)]
32
33use std::cell::RefCell;
34use std::ffi::{CStr, CString};
35use std::os::raw::c_char;
36use std::ptr;
37
38use crate::core::builder::EventBuilder;
39use crate::core::config::PhyTraceConfig;
40use crate::models::domains::*;
41use crate::models::enums;
42use crate::models::event::UdmEvent;
43use crate::transport::mock::MockTransport;
44use crate::PhyTraceAgent;
45
46// =============================================================================
47// Error Codes
48// =============================================================================
49
50/// Operation succeeded.
51pub const PHYTRACE_OK: i32 = 0;
52/// A null pointer was passed where a valid pointer was required.
53pub const PHYTRACE_ERR_NULL_PTR: i32 = -1;
54/// A string argument contained invalid UTF-8.
55pub const PHYTRACE_ERR_INVALID_UTF8: i32 = -2;
56/// Configuration error (invalid YAML, missing fields, validation failure).
57pub const PHYTRACE_ERR_CONFIG: i32 = -3;
58/// Agent lifecycle error (start, stop, flush).
59pub const PHYTRACE_ERR_AGENT: i32 = -4;
60/// Event builder error (build validation failed).
61pub const PHYTRACE_ERR_BUILDER: i32 = -5;
62/// Transport error (send failed).
63pub const PHYTRACE_ERR_TRANSPORT: i32 = -6;
64/// Tokio runtime creation failed.
65pub const PHYTRACE_ERR_RUNTIME: i32 = -7;
66/// Serialization error (JSON encode/decode).
67pub const PHYTRACE_ERR_SERIALIZATION: i32 = -8;
68/// Invalid enum value passed.
69pub const PHYTRACE_ERR_INVALID_ENUM: i32 = -9;
70
71// =============================================================================
72// Enum Integer Constants — EventType
73// =============================================================================
74
75/// `EventType::TelemetryPeriodic`
76pub const PHYTRACE_EVENT_TYPE_TELEMETRY_PERIODIC: i32 = 0;
77/// `EventType::TelemetryOnChange`
78pub const PHYTRACE_EVENT_TYPE_TELEMETRY_ON_CHANGE: i32 = 1;
79/// `EventType::TelemetrySnapshot`
80pub const PHYTRACE_EVENT_TYPE_TELEMETRY_SNAPSHOT: i32 = 2;
81/// `EventType::StateTransition`
82pub const PHYTRACE_EVENT_TYPE_STATE_TRANSITION: i32 = 3;
83/// `EventType::ModeChange`
84pub const PHYTRACE_EVENT_TYPE_MODE_CHANGE: i32 = 4;
85/// `EventType::TaskStarted`
86pub const PHYTRACE_EVENT_TYPE_TASK_STARTED: i32 = 5;
87/// `EventType::TaskCompleted`
88pub const PHYTRACE_EVENT_TYPE_TASK_COMPLETED: i32 = 6;
89/// `EventType::TaskFailed`
90pub const PHYTRACE_EVENT_TYPE_TASK_FAILED: i32 = 7;
91/// `EventType::TaskCancelled`
92pub const PHYTRACE_EVENT_TYPE_TASK_CANCELLED: i32 = 8;
93/// `EventType::GoalReached`
94pub const PHYTRACE_EVENT_TYPE_GOAL_REACHED: i32 = 9;
95/// `EventType::PathBlocked`
96pub const PHYTRACE_EVENT_TYPE_PATH_BLOCKED: i32 = 10;
97/// `EventType::Rerouting`
98pub const PHYTRACE_EVENT_TYPE_REROUTING: i32 = 11;
99/// `EventType::SafetyViolation`
100pub const PHYTRACE_EVENT_TYPE_SAFETY_VIOLATION: i32 = 12;
101/// `EventType::EmergencyStop`
102pub const PHYTRACE_EVENT_TYPE_EMERGENCY_STOP: i32 = 13;
103/// `EventType::SystemStartup`
104pub const PHYTRACE_EVENT_TYPE_SYSTEM_STARTUP: i32 = 14;
105/// `EventType::SystemShutdown`
106pub const PHYTRACE_EVENT_TYPE_SYSTEM_SHUTDOWN: i32 = 15;
107/// `EventType::Error`
108pub const PHYTRACE_EVENT_TYPE_ERROR: i32 = 16;
109/// `EventType::Custom`
110pub const PHYTRACE_EVENT_TYPE_CUSTOM: i32 = 17;
111
112// =============================================================================
113// Enum Integer Constants — SourceType
114// =============================================================================
115
116/// `SourceType::Amr`
117pub const PHYTRACE_SOURCE_TYPE_AMR: i32 = 0;
118/// `SourceType::Agv`
119pub const PHYTRACE_SOURCE_TYPE_AGV: i32 = 1;
120/// `SourceType::AutonomousForklift`
121pub const PHYTRACE_SOURCE_TYPE_AUTONOMOUS_FORKLIFT: i32 = 2;
122/// `SourceType::DeliveryRobot`
123pub const PHYTRACE_SOURCE_TYPE_DELIVERY_ROBOT: i32 = 3;
124/// `SourceType::CleaningRobot`
125pub const PHYTRACE_SOURCE_TYPE_CLEANING_ROBOT: i32 = 4;
126/// `SourceType::InspectionRobot`
127pub const PHYTRACE_SOURCE_TYPE_INSPECTION_ROBOT: i32 = 5;
128/// `SourceType::SecurityRobot`
129pub const PHYTRACE_SOURCE_TYPE_SECURITY_ROBOT: i32 = 6;
130/// `SourceType::IndustrialArm`
131pub const PHYTRACE_SOURCE_TYPE_INDUSTRIAL_ARM: i32 = 7;
132/// `SourceType::Cobot`
133pub const PHYTRACE_SOURCE_TYPE_COBOT: i32 = 8;
134/// `SourceType::MobileManipulator`
135pub const PHYTRACE_SOURCE_TYPE_MOBILE_MANIPULATOR: i32 = 9;
136/// `SourceType::AutonomousVehicle`
137pub const PHYTRACE_SOURCE_TYPE_AUTONOMOUS_VEHICLE: i32 = 10;
138/// `SourceType::ElectricVehicle`
139pub const PHYTRACE_SOURCE_TYPE_ELECTRIC_VEHICLE: i32 = 11;
140/// `SourceType::Drone`
141pub const PHYTRACE_SOURCE_TYPE_DRONE: i32 = 12;
142/// `SourceType::Humanoid`
143pub const PHYTRACE_SOURCE_TYPE_HUMANOID: i32 = 13;
144/// `SourceType::Quadruped`
145pub const PHYTRACE_SOURCE_TYPE_QUADRUPED: i32 = 14;
146/// `SourceType::Human`
147pub const PHYTRACE_SOURCE_TYPE_HUMAN: i32 = 15;
148/// `SourceType::Custom`
149pub const PHYTRACE_SOURCE_TYPE_CUSTOM: i32 = 16;
150/// `SourceType::Simulation`
151pub const PHYTRACE_SOURCE_TYPE_SIMULATION: i32 = 17;
152
153// =============================================================================
154// Enum Integer Constants — SafetyState
155// =============================================================================
156
157/// `SafetyState::Normal`
158pub const PHYTRACE_SAFETY_STATE_NORMAL: i32 = 0;
159/// `SafetyState::Warning`
160pub const PHYTRACE_SAFETY_STATE_WARNING: i32 = 1;
161/// `SafetyState::ProtectiveStop`
162pub const PHYTRACE_SAFETY_STATE_PROTECTIVE_STOP: i32 = 2;
163/// `SafetyState::EmergencyStop`
164pub const PHYTRACE_SAFETY_STATE_EMERGENCY_STOP: i32 = 3;
165/// `SafetyState::SafetyInterlock`
166pub const PHYTRACE_SAFETY_STATE_SAFETY_INTERLOCK: i32 = 4;
167/// `SafetyState::ReducedSpeed`
168pub const PHYTRACE_SAFETY_STATE_REDUCED_SPEED: i32 = 5;
169
170// =============================================================================
171// Enum Integer Constants — OperationalMode
172// =============================================================================
173
174/// `OperationalMode::Autonomous`
175pub const PHYTRACE_OPERATIONAL_MODE_AUTONOMOUS: i32 = 0;
176/// `OperationalMode::Manual`
177pub const PHYTRACE_OPERATIONAL_MODE_MANUAL: i32 = 1;
178/// `OperationalMode::SemiAutonomous`
179pub const PHYTRACE_OPERATIONAL_MODE_SEMI_AUTONOMOUS: i32 = 2;
180/// `OperationalMode::Teleoperated`
181pub const PHYTRACE_OPERATIONAL_MODE_TELEOPERATED: i32 = 3;
182/// `OperationalMode::Learning`
183pub const PHYTRACE_OPERATIONAL_MODE_LEARNING: i32 = 4;
184/// `OperationalMode::Maintenance`
185pub const PHYTRACE_OPERATIONAL_MODE_MAINTENANCE: i32 = 5;
186/// `OperationalMode::Emergency`
187pub const PHYTRACE_OPERATIONAL_MODE_EMERGENCY: i32 = 6;
188/// `OperationalMode::Idle`
189pub const PHYTRACE_OPERATIONAL_MODE_IDLE: i32 = 7;
190
191// =============================================================================
192// Enum Integer Constants — OperationalState
193// =============================================================================
194
195/// `OperationalState::Off`
196pub const PHYTRACE_OPERATIONAL_STATE_OFF: i32 = 0;
197/// `OperationalState::Booting`
198pub const PHYTRACE_OPERATIONAL_STATE_BOOTING: i32 = 1;
199/// `OperationalState::Ready`
200pub const PHYTRACE_OPERATIONAL_STATE_READY: i32 = 2;
201/// `OperationalState::ExecutingTask`
202pub const PHYTRACE_OPERATIONAL_STATE_EXECUTING_TASK: i32 = 3;
203/// `OperationalState::Navigating`
204pub const PHYTRACE_OPERATIONAL_STATE_NAVIGATING: i32 = 4;
205/// `OperationalState::Manipulating`
206pub const PHYTRACE_OPERATIONAL_STATE_MANIPULATING: i32 = 5;
207/// `OperationalState::Charging`
208pub const PHYTRACE_OPERATIONAL_STATE_CHARGING: i32 = 6;
209/// `OperationalState::Paused`
210pub const PHYTRACE_OPERATIONAL_STATE_PAUSED: i32 = 7;
211/// `OperationalState::Error`
212pub const PHYTRACE_OPERATIONAL_STATE_ERROR: i32 = 8;
213/// `OperationalState::EmergencyStopped`
214pub const PHYTRACE_OPERATIONAL_STATE_EMERGENCY_STOPPED: i32 = 9;
215/// `OperationalState::ShuttingDown`
216pub const PHYTRACE_OPERATIONAL_STATE_SHUTTING_DOWN: i32 = 10;
217/// `OperationalState::Recovering`
218pub const PHYTRACE_OPERATIONAL_STATE_RECOVERING: i32 = 11;
219
220// =============================================================================
221// Enum Integer Constants — LocalizationQuality
222// =============================================================================
223
224/// `LocalizationQuality::Excellent`
225pub const PHYTRACE_LOCALIZATION_QUALITY_EXCELLENT: i32 = 0;
226/// `LocalizationQuality::Good`
227pub const PHYTRACE_LOCALIZATION_QUALITY_GOOD: i32 = 1;
228/// `LocalizationQuality::Fair`
229pub const PHYTRACE_LOCALIZATION_QUALITY_FAIR: i32 = 2;
230/// `LocalizationQuality::Poor`
231pub const PHYTRACE_LOCALIZATION_QUALITY_POOR: i32 = 3;
232/// `LocalizationQuality::Lost`
233pub const PHYTRACE_LOCALIZATION_QUALITY_LOST: i32 = 4;
234
235// =============================================================================
236// Enum Integer Constants — PathState
237// =============================================================================
238
239/// `PathState::None`
240pub const PHYTRACE_PATH_STATE_NONE: i32 = 0;
241/// `PathState::Planning`
242pub const PHYTRACE_PATH_STATE_PLANNING: i32 = 1;
243/// `PathState::Valid`
244pub const PHYTRACE_PATH_STATE_VALID: i32 = 2;
245/// `PathState::Executing`
246pub const PHYTRACE_PATH_STATE_EXECUTING: i32 = 3;
247/// `PathState::Blocked`
248pub const PHYTRACE_PATH_STATE_BLOCKED: i32 = 4;
249/// `PathState::Completed`
250pub const PHYTRACE_PATH_STATE_COMPLETED: i32 = 5;
251/// `PathState::Failed`
252pub const PHYTRACE_PATH_STATE_FAILED: i32 = 6;
253
254// =============================================================================
255// Enum Integer Constants — EStopType
256// =============================================================================
257
258/// `EStopType::Software`
259pub const PHYTRACE_ESTOP_TYPE_SOFTWARE: i32 = 0;
260/// `EStopType::Hardware`
261pub const PHYTRACE_ESTOP_TYPE_HARDWARE: i32 = 1;
262/// `EStopType::Remote`
263pub const PHYTRACE_ESTOP_TYPE_REMOTE: i32 = 2;
264/// `EStopType::Automatic`
265pub const PHYTRACE_ESTOP_TYPE_AUTOMATIC: i32 = 3;
266
267// =============================================================================
268// Enum Conversion Helpers
269// =============================================================================
270
271fn event_type_from_i32(val: i32) -> Option<enums::EventType> {
272    match val {
273        PHYTRACE_EVENT_TYPE_TELEMETRY_PERIODIC => Some(enums::EventType::TelemetryPeriodic),
274        PHYTRACE_EVENT_TYPE_TELEMETRY_ON_CHANGE => Some(enums::EventType::TelemetryOnChange),
275        PHYTRACE_EVENT_TYPE_TELEMETRY_SNAPSHOT => Some(enums::EventType::TelemetrySnapshot),
276        PHYTRACE_EVENT_TYPE_STATE_TRANSITION => Some(enums::EventType::StateTransition),
277        PHYTRACE_EVENT_TYPE_MODE_CHANGE => Some(enums::EventType::ModeChange),
278        PHYTRACE_EVENT_TYPE_TASK_STARTED => Some(enums::EventType::TaskStarted),
279        PHYTRACE_EVENT_TYPE_TASK_COMPLETED => Some(enums::EventType::TaskCompleted),
280        PHYTRACE_EVENT_TYPE_TASK_FAILED => Some(enums::EventType::TaskFailed),
281        PHYTRACE_EVENT_TYPE_TASK_CANCELLED => Some(enums::EventType::TaskCancelled),
282        PHYTRACE_EVENT_TYPE_GOAL_REACHED => Some(enums::EventType::GoalReached),
283        PHYTRACE_EVENT_TYPE_PATH_BLOCKED => Some(enums::EventType::PathBlocked),
284        PHYTRACE_EVENT_TYPE_REROUTING => Some(enums::EventType::Rerouting),
285        PHYTRACE_EVENT_TYPE_SAFETY_VIOLATION => Some(enums::EventType::SafetyViolation),
286        PHYTRACE_EVENT_TYPE_EMERGENCY_STOP => Some(enums::EventType::EmergencyStop),
287        PHYTRACE_EVENT_TYPE_SYSTEM_STARTUP => Some(enums::EventType::SystemStartup),
288        PHYTRACE_EVENT_TYPE_SYSTEM_SHUTDOWN => Some(enums::EventType::SystemShutdown),
289        PHYTRACE_EVENT_TYPE_ERROR => Some(enums::EventType::Error),
290        PHYTRACE_EVENT_TYPE_CUSTOM => Some(enums::EventType::Custom),
291        _ => None,
292    }
293}
294
295fn source_type_from_i32(val: i32) -> Option<enums::SourceType> {
296    match val {
297        PHYTRACE_SOURCE_TYPE_AMR => Some(enums::SourceType::Amr),
298        PHYTRACE_SOURCE_TYPE_AGV => Some(enums::SourceType::Agv),
299        PHYTRACE_SOURCE_TYPE_AUTONOMOUS_FORKLIFT => Some(enums::SourceType::AutonomousForklift),
300        PHYTRACE_SOURCE_TYPE_DELIVERY_ROBOT => Some(enums::SourceType::DeliveryRobot),
301        PHYTRACE_SOURCE_TYPE_CLEANING_ROBOT => Some(enums::SourceType::CleaningRobot),
302        PHYTRACE_SOURCE_TYPE_INSPECTION_ROBOT => Some(enums::SourceType::InspectionRobot),
303        PHYTRACE_SOURCE_TYPE_SECURITY_ROBOT => Some(enums::SourceType::SecurityRobot),
304        PHYTRACE_SOURCE_TYPE_INDUSTRIAL_ARM => Some(enums::SourceType::IndustrialArm),
305        PHYTRACE_SOURCE_TYPE_COBOT => Some(enums::SourceType::Cobot),
306        PHYTRACE_SOURCE_TYPE_MOBILE_MANIPULATOR => Some(enums::SourceType::MobileManipulator),
307        PHYTRACE_SOURCE_TYPE_AUTONOMOUS_VEHICLE => Some(enums::SourceType::AutonomousVehicle),
308        PHYTRACE_SOURCE_TYPE_ELECTRIC_VEHICLE => Some(enums::SourceType::ElectricVehicle),
309        PHYTRACE_SOURCE_TYPE_DRONE => Some(enums::SourceType::Drone),
310        PHYTRACE_SOURCE_TYPE_HUMANOID => Some(enums::SourceType::Humanoid),
311        PHYTRACE_SOURCE_TYPE_QUADRUPED => Some(enums::SourceType::Quadruped),
312        PHYTRACE_SOURCE_TYPE_HUMAN => Some(enums::SourceType::Human),
313        PHYTRACE_SOURCE_TYPE_CUSTOM => Some(enums::SourceType::Custom),
314        PHYTRACE_SOURCE_TYPE_SIMULATION => Some(enums::SourceType::Simulation),
315        _ => None,
316    }
317}
318
319fn safety_state_from_i32(val: i32) -> Option<enums::SafetyState> {
320    match val {
321        PHYTRACE_SAFETY_STATE_NORMAL => Some(enums::SafetyState::Normal),
322        PHYTRACE_SAFETY_STATE_WARNING => Some(enums::SafetyState::Warning),
323        PHYTRACE_SAFETY_STATE_PROTECTIVE_STOP => Some(enums::SafetyState::ProtectiveStop),
324        PHYTRACE_SAFETY_STATE_EMERGENCY_STOP => Some(enums::SafetyState::EmergencyStop),
325        PHYTRACE_SAFETY_STATE_SAFETY_INTERLOCK => Some(enums::SafetyState::SafetyInterlock),
326        PHYTRACE_SAFETY_STATE_REDUCED_SPEED => Some(enums::SafetyState::ReducedSpeed),
327        _ => None,
328    }
329}
330
331fn operational_mode_from_i32(val: i32) -> Option<enums::OperationalMode> {
332    match val {
333        PHYTRACE_OPERATIONAL_MODE_AUTONOMOUS => Some(enums::OperationalMode::Autonomous),
334        PHYTRACE_OPERATIONAL_MODE_MANUAL => Some(enums::OperationalMode::Manual),
335        PHYTRACE_OPERATIONAL_MODE_SEMI_AUTONOMOUS => Some(enums::OperationalMode::SemiAutonomous),
336        PHYTRACE_OPERATIONAL_MODE_TELEOPERATED => Some(enums::OperationalMode::Teleoperated),
337        PHYTRACE_OPERATIONAL_MODE_LEARNING => Some(enums::OperationalMode::Learning),
338        PHYTRACE_OPERATIONAL_MODE_MAINTENANCE => Some(enums::OperationalMode::Maintenance),
339        PHYTRACE_OPERATIONAL_MODE_EMERGENCY => Some(enums::OperationalMode::Emergency),
340        PHYTRACE_OPERATIONAL_MODE_IDLE => Some(enums::OperationalMode::Idle),
341        _ => None,
342    }
343}
344
345fn operational_state_from_i32(val: i32) -> Option<enums::OperationalState> {
346    match val {
347        PHYTRACE_OPERATIONAL_STATE_OFF => Some(enums::OperationalState::Off),
348        PHYTRACE_OPERATIONAL_STATE_BOOTING => Some(enums::OperationalState::Booting),
349        PHYTRACE_OPERATIONAL_STATE_READY => Some(enums::OperationalState::Ready),
350        PHYTRACE_OPERATIONAL_STATE_EXECUTING_TASK => Some(enums::OperationalState::ExecutingTask),
351        PHYTRACE_OPERATIONAL_STATE_NAVIGATING => Some(enums::OperationalState::Navigating),
352        PHYTRACE_OPERATIONAL_STATE_MANIPULATING => Some(enums::OperationalState::Manipulating),
353        PHYTRACE_OPERATIONAL_STATE_CHARGING => Some(enums::OperationalState::Charging),
354        PHYTRACE_OPERATIONAL_STATE_PAUSED => Some(enums::OperationalState::Paused),
355        PHYTRACE_OPERATIONAL_STATE_ERROR => Some(enums::OperationalState::Error),
356        PHYTRACE_OPERATIONAL_STATE_EMERGENCY_STOPPED => {
357            Some(enums::OperationalState::EmergencyStopped)
358        }
359        PHYTRACE_OPERATIONAL_STATE_SHUTTING_DOWN => Some(enums::OperationalState::ShuttingDown),
360        PHYTRACE_OPERATIONAL_STATE_RECOVERING => Some(enums::OperationalState::Recovering),
361        _ => None,
362    }
363}
364
365fn localization_quality_from_i32(val: i32) -> Option<enums::LocalizationQuality> {
366    match val {
367        PHYTRACE_LOCALIZATION_QUALITY_EXCELLENT => Some(enums::LocalizationQuality::Excellent),
368        PHYTRACE_LOCALIZATION_QUALITY_GOOD => Some(enums::LocalizationQuality::Good),
369        PHYTRACE_LOCALIZATION_QUALITY_FAIR => Some(enums::LocalizationQuality::Fair),
370        PHYTRACE_LOCALIZATION_QUALITY_POOR => Some(enums::LocalizationQuality::Poor),
371        PHYTRACE_LOCALIZATION_QUALITY_LOST => Some(enums::LocalizationQuality::Lost),
372        _ => None,
373    }
374}
375
376fn path_state_from_i32(val: i32) -> Option<enums::PathState> {
377    match val {
378        PHYTRACE_PATH_STATE_NONE => Some(enums::PathState::None),
379        PHYTRACE_PATH_STATE_PLANNING => Some(enums::PathState::Planning),
380        PHYTRACE_PATH_STATE_VALID => Some(enums::PathState::Valid),
381        PHYTRACE_PATH_STATE_EXECUTING => Some(enums::PathState::Executing),
382        PHYTRACE_PATH_STATE_BLOCKED => Some(enums::PathState::Blocked),
383        PHYTRACE_PATH_STATE_COMPLETED => Some(enums::PathState::Completed),
384        PHYTRACE_PATH_STATE_FAILED => Some(enums::PathState::Failed),
385        _ => None,
386    }
387}
388
389fn estop_type_from_i32(val: i32) -> Option<enums::EStopType> {
390    match val {
391        PHYTRACE_ESTOP_TYPE_SOFTWARE => Some(enums::EStopType::Software),
392        PHYTRACE_ESTOP_TYPE_HARDWARE => Some(enums::EStopType::Hardware),
393        PHYTRACE_ESTOP_TYPE_REMOTE => Some(enums::EStopType::Remote),
394        PHYTRACE_ESTOP_TYPE_AUTOMATIC => Some(enums::EStopType::Automatic),
395        _ => None,
396    }
397}
398
399// =============================================================================
400// Thread-local Error Storage
401// =============================================================================
402
403thread_local! {
404    static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
405}
406
407fn set_last_error(msg: impl Into<String>) {
408    let msg = msg.into();
409    tracing::error!(ffi_error = %msg);
410    LAST_ERROR.with(|e| {
411        *e.borrow_mut() = CString::new(msg).ok();
412    });
413}
414
415fn clear_last_error() {
416    LAST_ERROR.with(|e| {
417        *e.borrow_mut() = None;
418    });
419}
420
421// =============================================================================
422// Opaque Handle Types
423// =============================================================================
424
425/// Opaque handle to a PhyTrace agent with an embedded tokio runtime.
426pub struct PhyTraceAgentHandle {
427    agent: PhyTraceAgent,
428    runtime: tokio::runtime::Runtime,
429}
430
431/// Opaque handle to an event builder.
432pub struct PhyTraceBuilderHandle {
433    builder: EventBuilder,
434}
435
436/// Opaque handle to a built UDM event.
437pub struct PhyTraceEventHandle {
438    event: UdmEvent,
439}
440
441// =============================================================================
442// Helper: convert C string to &str
443// =============================================================================
444
445unsafe fn cstr_to_str<'a>(ptr: *const c_char) -> Result<&'a str, i32> {
446    if ptr.is_null() {
447        return Err(PHYTRACE_ERR_NULL_PTR);
448    }
449    CStr::from_ptr(ptr)
450        .to_str()
451        .map_err(|_e| PHYTRACE_ERR_INVALID_UTF8)
452}
453
454/// Helper: convert optional C string to Option<String>
455unsafe fn optional_cstr_to_string(ptr: *const c_char) -> Option<String> {
456    if ptr.is_null() {
457        None
458    } else {
459        CStr::from_ptr(ptr).to_str().ok().map(|s| s.to_owned())
460    }
461}
462
463// =============================================================================
464// Error Retrieval
465// =============================================================================
466
467/// Retrieve the last error message from the FFI layer.
468///
469/// Returns a pointer to a null-terminated UTF-8 string describing the last error.
470/// Returns null if no error has occurred. The returned string is valid until the
471/// next FFI call on the same thread. Caller must NOT free this pointer.
472#[no_mangle]
473pub extern "C" fn phytrace_last_error() -> *const c_char {
474    LAST_ERROR.with(|e| {
475        let borrow = e.borrow();
476        match borrow.as_ref() {
477            Some(cstr) => cstr.as_ptr(),
478            None => ptr::null(),
479        }
480    })
481}
482
483/// Free a string allocated by the PhyTrace FFI layer.
484///
485/// Must be called on strings returned by `phytrace_event_to_json`.
486/// Passing null is a no-op.
487#[no_mangle]
488pub unsafe extern "C" fn phytrace_string_free(s: *mut c_char) {
489    if !s.is_null() {
490        drop(CString::from_raw(s));
491    }
492}
493
494// =============================================================================
495// Version
496// =============================================================================
497
498/// Get the PhyTrace SDK version string.
499///
500/// Returns a pointer to a static null-terminated string. Caller must NOT free this.
501#[no_mangle]
502pub extern "C" fn phytrace_sdk_version() -> *const c_char {
503    // SDK_VERSION is a &'static str from env!, always valid
504    static VERSION_CSTR: std::sync::OnceLock<CString> = std::sync::OnceLock::new();
505    VERSION_CSTR
506        .get_or_init(|| {
507            CString::new(crate::SDK_VERSION).expect("SDK_VERSION contains no interior NUL bytes")
508        })
509        .as_ptr()
510}
511
512/// Get the UDM version string supported by this SDK.
513///
514/// Returns a pointer to a static null-terminated string. Caller must NOT free this.
515#[no_mangle]
516pub extern "C" fn phytrace_udm_version() -> *const c_char {
517    static UDM_VERSION_CSTR: std::sync::OnceLock<CString> = std::sync::OnceLock::new();
518    UDM_VERSION_CSTR
519        .get_or_init(|| {
520            CString::new(crate::UDM_VERSION).expect("UDM_VERSION contains no interior NUL bytes")
521        })
522        .as_ptr()
523}
524
525// =============================================================================
526// Config & Agent Lifecycle
527// =============================================================================
528
529/// Create a PhyTrace agent from a YAML configuration string.
530///
531/// Returns a heap-allocated agent handle on success, or null on failure.
532/// On failure, call `phytrace_last_error()` for details.
533///
534/// The returned handle must be freed with `phytrace_agent_destroy()`.
535#[no_mangle]
536pub unsafe extern "C" fn phytrace_agent_create_from_yaml(
537    yaml_str: *const c_char,
538) -> *mut PhyTraceAgentHandle {
539    clear_last_error();
540
541    let yaml = match cstr_to_str(yaml_str) {
542        Ok(s) => s,
543        Err(code) => {
544            set_last_error(if code == PHYTRACE_ERR_NULL_PTR {
545                "yaml_str is null"
546            } else {
547                "yaml_str contains invalid UTF-8"
548            });
549            return ptr::null_mut();
550        }
551    };
552
553    let config = match PhyTraceConfig::from_yaml(yaml) {
554        Ok(c) => c,
555        Err(e) => {
556            set_last_error(format!("Config parse error: {e}"));
557            return ptr::null_mut();
558        }
559    };
560
561    create_agent_from_config(config)
562}
563
564/// Create a PhyTrace agent from a YAML configuration file path.
565///
566/// Returns a heap-allocated agent handle on success, or null on failure.
567/// On failure, call `phytrace_last_error()` for details.
568///
569/// The returned handle must be freed with `phytrace_agent_destroy()`.
570#[no_mangle]
571pub unsafe extern "C" fn phytrace_agent_create_from_file(
572    path: *const c_char,
573) -> *mut PhyTraceAgentHandle {
574    clear_last_error();
575
576    let path_str = match cstr_to_str(path) {
577        Ok(s) => s,
578        Err(code) => {
579            set_last_error(if code == PHYTRACE_ERR_NULL_PTR {
580                "path is null"
581            } else {
582                "path contains invalid UTF-8"
583            });
584            return ptr::null_mut();
585        }
586    };
587
588    let runtime = match tokio::runtime::Runtime::new() {
589        Ok(rt) => rt,
590        Err(e) => {
591            set_last_error(format!("Failed to create tokio runtime: {e}"));
592            return ptr::null_mut();
593        }
594    };
595
596    let config = match PhyTraceConfig::from_file(path_str) {
597        Ok(c) => c,
598        Err(e) => {
599            set_last_error(format!("Config file error: {e}"));
600            return ptr::null_mut();
601        }
602    };
603
604    // Drop the temporary runtime — create_agent_from_config makes its own
605    drop(runtime);
606    create_agent_from_config(config)
607}
608
609/// Internal helper to create an agent handle from a validated config.
610fn create_agent_from_config(config: PhyTraceConfig) -> *mut PhyTraceAgentHandle {
611    let runtime = match tokio::runtime::Runtime::new() {
612        Ok(rt) => rt,
613        Err(e) => {
614            set_last_error(format!("Failed to create tokio runtime: {e}"));
615            return ptr::null_mut();
616        }
617    };
618
619    let agent = match runtime.block_on(PhyTraceAgent::from_config(config)) {
620        Ok(a) => a,
621        Err(e) => {
622            set_last_error(format!("Agent creation error: {e}"));
623            return ptr::null_mut();
624        }
625    };
626
627    Box::into_raw(Box::new(PhyTraceAgentHandle { agent, runtime }))
628}
629
630/// Create a PhyTrace agent with a mock transport for testing.
631///
632/// This is a convenience function that creates an agent configured with
633/// a `MockTransport` — useful for unit tests and integration tests where
634/// no real PhyCloud endpoint is needed.
635///
636/// `source_id` — Null-terminated source identifier string.
637/// `source_type` — Integer source type constant (e.g., `PHYTRACE_SOURCE_TYPE_AMR`).
638///
639/// Returns a heap-allocated agent handle on success, or null on failure.
640#[no_mangle]
641pub unsafe extern "C" fn phytrace_agent_create_mock(
642    source_id: *const c_char,
643    source_type: i32,
644) -> *mut PhyTraceAgentHandle {
645    clear_last_error();
646
647    let sid = match cstr_to_str(source_id) {
648        Ok(s) => s,
649        Err(code) => {
650            set_last_error(if code == PHYTRACE_ERR_NULL_PTR {
651                "source_id is null"
652            } else {
653                "source_id contains invalid UTF-8"
654            });
655            return ptr::null_mut();
656        }
657    };
658
659    let st = match source_type_from_i32(source_type) {
660        Some(st) => st,
661        None => {
662            set_last_error(format!("Invalid source_type: {source_type}"));
663            return ptr::null_mut();
664        }
665    };
666
667    let config = PhyTraceConfig::new(sid).with_source_type(st);
668    let transport = Box::new(MockTransport::new());
669
670    let runtime = match tokio::runtime::Runtime::new() {
671        Ok(rt) => rt,
672        Err(e) => {
673            set_last_error(format!("Failed to create tokio runtime: {e}"));
674            return ptr::null_mut();
675        }
676    };
677
678    let agent = match runtime.block_on(PhyTraceAgent::with_transport(config, transport)) {
679        Ok(a) => a,
680        Err(e) => {
681            set_last_error(format!("Agent creation error: {e}"));
682            return ptr::null_mut();
683        }
684    };
685
686    Box::into_raw(Box::new(PhyTraceAgentHandle { agent, runtime }))
687}
688
689/// Start the agent (connects transport, begins background workers).
690///
691/// Returns `PHYTRACE_OK` on success, negative error code on failure.
692#[no_mangle]
693pub unsafe extern "C" fn phytrace_agent_start(agent: *mut PhyTraceAgentHandle) -> i32 {
694    clear_last_error();
695
696    if agent.is_null() {
697        set_last_error("agent handle is null");
698        return PHYTRACE_ERR_NULL_PTR;
699    }
700
701    let handle = &*agent;
702    match handle.runtime.block_on(handle.agent.start()) {
703        Ok(()) => PHYTRACE_OK,
704        Err(e) => {
705            set_last_error(format!("Agent start error: {e}"));
706            PHYTRACE_ERR_AGENT
707        }
708    }
709}
710
711/// Stop the agent gracefully (flushes buffer, disconnects transport).
712///
713/// Returns `PHYTRACE_OK` on success, negative error code on failure.
714#[no_mangle]
715pub unsafe extern "C" fn phytrace_agent_stop(agent: *mut PhyTraceAgentHandle) -> i32 {
716    clear_last_error();
717
718    if agent.is_null() {
719        set_last_error("agent handle is null");
720        return PHYTRACE_ERR_NULL_PTR;
721    }
722
723    let handle = &*agent;
724    match handle.runtime.block_on(handle.agent.shutdown()) {
725        Ok(()) => PHYTRACE_OK,
726        Err(e) => {
727            set_last_error(format!("Agent stop error: {e}"));
728            PHYTRACE_ERR_AGENT
729        }
730    }
731}
732
733/// Destroy and free an agent handle.
734///
735/// Calls stop if the agent is still running, then frees all memory.
736/// Passing null is a no-op.
737#[no_mangle]
738pub unsafe extern "C" fn phytrace_agent_destroy(agent: *mut PhyTraceAgentHandle) {
739    if !agent.is_null() {
740        let handle = Box::from_raw(agent);
741        // Attempt graceful shutdown — ignore errors during destroy
742        drop(handle.runtime.block_on(handle.agent.shutdown()));
743        // handle is dropped here, freeing agent + runtime
744    }
745}
746
747/// Check if the agent is currently running.
748///
749/// Returns 1 if running, 0 if not, `PHYTRACE_ERR_NULL_PTR` if agent is null.
750#[no_mangle]
751pub unsafe extern "C" fn phytrace_agent_is_running(agent: *const PhyTraceAgentHandle) -> i32 {
752    if agent.is_null() {
753        set_last_error("agent handle is null");
754        return PHYTRACE_ERR_NULL_PTR;
755    }
756
757    if (*agent).agent.is_running() {
758        1
759    } else {
760        0
761    }
762}
763
764/// Flush any buffered events to the transport.
765///
766/// Returns `PHYTRACE_OK` on success, negative error code on failure.
767#[no_mangle]
768pub unsafe extern "C" fn phytrace_agent_flush(agent: *mut PhyTraceAgentHandle) -> i32 {
769    clear_last_error();
770
771    if agent.is_null() {
772        set_last_error("agent handle is null");
773        return PHYTRACE_ERR_NULL_PTR;
774    }
775
776    let handle = &*agent;
777    match handle.runtime.block_on(handle.agent.flush()) {
778        Ok(()) => PHYTRACE_OK,
779        Err(e) => {
780            set_last_error(format!("Flush error: {e}"));
781            PHYTRACE_ERR_TRANSPORT
782        }
783    }
784}
785
786// =============================================================================
787// Event Builder
788// =============================================================================
789
790/// Create a new event builder from an agent's configuration.
791///
792/// Returns a heap-allocated builder handle, or null on failure.
793/// The builder must be freed with `phytrace_builder_destroy()` or consumed
794/// by `phytrace_builder_build()`.
795#[no_mangle]
796pub unsafe extern "C" fn phytrace_builder_new(
797    agent: *const PhyTraceAgentHandle,
798) -> *mut PhyTraceBuilderHandle {
799    clear_last_error();
800
801    if agent.is_null() {
802        set_last_error("agent handle is null");
803        return ptr::null_mut();
804    }
805
806    let config = (*agent).agent.config();
807    let builder = EventBuilder::new(config);
808    Box::into_raw(Box::new(PhyTraceBuilderHandle { builder }))
809}
810
811/// Set the event type on the builder.
812///
813/// `event_type` is one of the `PHYTRACE_EVENT_TYPE_*` constants.
814/// Returns `PHYTRACE_OK` on success, negative error code on failure.
815#[no_mangle]
816pub unsafe extern "C" fn phytrace_builder_set_event_type(
817    builder: *mut PhyTraceBuilderHandle,
818    event_type: i32,
819) -> i32 {
820    clear_last_error();
821
822    if builder.is_null() {
823        set_last_error("builder handle is null");
824        return PHYTRACE_ERR_NULL_PTR;
825    }
826
827    let et = match event_type_from_i32(event_type) {
828        Some(et) => et,
829        None => {
830            set_last_error(format!("Invalid event_type: {event_type}"));
831            return PHYTRACE_ERR_INVALID_ENUM;
832        }
833    };
834
835    let handle = &mut *builder;
836    // EventBuilder uses a consuming pattern — we need to swap
837    let old_builder = std::mem::replace(
838        &mut handle.builder,
839        EventBuilder::new(&PhyTraceConfig::new("tmp")),
840    );
841    handle.builder = old_builder.event_type(et);
842    PHYTRACE_OK
843}
844
845/// Set the source type on the builder.
846///
847/// `source_type` is one of the `PHYTRACE_SOURCE_TYPE_*` constants.
848/// Returns `PHYTRACE_OK` on success, negative error code on failure.
849#[no_mangle]
850pub unsafe extern "C" fn phytrace_builder_set_source_type(
851    builder: *mut PhyTraceBuilderHandle,
852    source_type: i32,
853) -> i32 {
854    clear_last_error();
855
856    if builder.is_null() {
857        set_last_error("builder handle is null");
858        return PHYTRACE_ERR_NULL_PTR;
859    }
860
861    let st = match source_type_from_i32(source_type) {
862        Some(st) => st,
863        None => {
864            set_last_error(format!("Invalid source_type: {source_type}"));
865            return PHYTRACE_ERR_INVALID_ENUM;
866        }
867    };
868
869    let handle = &mut *builder;
870    let old_builder = std::mem::replace(
871        &mut handle.builder,
872        EventBuilder::new(&PhyTraceConfig::new("tmp")),
873    );
874    handle.builder = old_builder.source_type(st);
875    PHYTRACE_OK
876}
877
878/// Build the event, consuming the builder.
879///
880/// Returns a heap-allocated event handle on success, or null on failure
881/// (e.g., validation error). On failure, the builder is still consumed/freed.
882///
883/// The returned event must be freed with `phytrace_event_destroy()` or
884/// consumed by `phytrace_agent_send()`.
885#[no_mangle]
886pub unsafe extern "C" fn phytrace_builder_build(
887    builder: *mut PhyTraceBuilderHandle,
888) -> *mut PhyTraceEventHandle {
889    clear_last_error();
890
891    if builder.is_null() {
892        set_last_error("builder handle is null");
893        return ptr::null_mut();
894    }
895
896    let handle = Box::from_raw(builder);
897    match handle.builder.build() {
898        Ok(event) => Box::into_raw(Box::new(PhyTraceEventHandle { event })),
899        Err(e) => {
900            set_last_error(format!("Build error: {e}"));
901            ptr::null_mut()
902        }
903    }
904}
905
906/// Build the event without validation, consuming the builder.
907///
908/// This always succeeds and returns a handle. Useful for testing or when
909/// the caller knows the event is well-formed.
910///
911/// The returned event must be freed with `phytrace_event_destroy()` or
912/// consumed by `phytrace_agent_send()`.
913#[no_mangle]
914pub unsafe extern "C" fn phytrace_builder_build_unchecked(
915    builder: *mut PhyTraceBuilderHandle,
916) -> *mut PhyTraceEventHandle {
917    clear_last_error();
918
919    if builder.is_null() {
920        set_last_error("builder handle is null");
921        return ptr::null_mut();
922    }
923
924    let handle = Box::from_raw(builder);
925    let event = handle.builder.build_unchecked();
926    Box::into_raw(Box::new(PhyTraceEventHandle { event }))
927}
928
929/// Destroy a builder without building an event.
930///
931/// Passing null is a no-op.
932#[no_mangle]
933pub unsafe extern "C" fn phytrace_builder_destroy(builder: *mut PhyTraceBuilderHandle) {
934    if !builder.is_null() {
935        drop(Box::from_raw(builder));
936    }
937}
938
939// =============================================================================
940// Builder Helper — macro for consuming builder pattern
941// =============================================================================
942
943/// Internal macro that handles the mem::replace dance for consuming builder methods.
944macro_rules! builder_mutate {
945    ($builder:expr, |$b:ident| $body:expr) => {{
946        let handle = &mut *$builder;
947        let $b = std::mem::replace(
948            &mut handle.builder,
949            EventBuilder::new(&PhyTraceConfig::new("tmp")),
950        );
951        handle.builder = $body;
952    }};
953}
954
955// =============================================================================
956// Domain Setters — Location
957// =============================================================================
958
959/// Set the location domain on the builder.
960///
961/// All floating-point parameters use `f64::NAN` to indicate "not set".
962/// String parameters use null to indicate "not set".
963///
964/// # Parameters
965/// - `latitude` — GPS latitude in decimal degrees (-90 to 90), or NAN
966/// - `longitude` — GPS longitude in decimal degrees (-180 to 180), or NAN
967/// - `altitude_m` — Altitude in meters above sea level, or NAN
968/// - `heading_deg` — Heading in degrees (0-360), or NAN
969/// - `x_m` — Local X coordinate in meters, or NAN
970/// - `y_m` — Local Y coordinate in meters, or NAN
971/// - `z_m` — Local Z coordinate in meters, or NAN
972/// - `yaw_deg` — Local yaw angle in degrees, or NAN
973/// - `frame_id` — Reference frame ID (e.g., "map", "odom"), or null
974/// - `map_id` — Map identifier, or null
975/// - `floor` — Floor number; `i32::MIN` means "not set"
976#[no_mangle]
977pub unsafe extern "C" fn phytrace_builder_set_location(
978    builder: *mut PhyTraceBuilderHandle,
979    latitude: f64,
980    longitude: f64,
981    altitude_m: f64,
982    heading_deg: f64,
983    x_m: f64,
984    y_m: f64,
985    z_m: f64,
986    yaw_deg: f64,
987    frame_id: *const c_char,
988    map_id: *const c_char,
989    floor: i32,
990) -> i32 {
991    clear_last_error();
992    if builder.is_null() {
993        set_last_error("builder handle is null");
994        return PHYTRACE_ERR_NULL_PTR;
995    }
996
997    let has_local = !x_m.is_nan() || !y_m.is_nan() || !z_m.is_nan() || !yaw_deg.is_nan();
998
999    let local = if has_local {
1000        Some(location::LocalCoordinates {
1001            x_m: if x_m.is_nan() { None } else { Some(x_m) },
1002            y_m: if y_m.is_nan() { None } else { Some(y_m) },
1003            z_m: if z_m.is_nan() { None } else { Some(z_m) },
1004            yaw_deg: if yaw_deg.is_nan() {
1005                None
1006            } else {
1007                Some(yaw_deg)
1008            },
1009            ..Default::default()
1010        })
1011    } else {
1012        None
1013    };
1014
1015    let loc = LocationDomain {
1016        latitude: if latitude.is_nan() {
1017            None
1018        } else {
1019            Some(latitude)
1020        },
1021        longitude: if longitude.is_nan() {
1022            None
1023        } else {
1024            Some(longitude)
1025        },
1026        altitude_m: if altitude_m.is_nan() {
1027            None
1028        } else {
1029            Some(altitude_m)
1030        },
1031        heading_deg: if heading_deg.is_nan() {
1032            None
1033        } else {
1034            Some(heading_deg)
1035        },
1036        local,
1037        frame_id: optional_cstr_to_string(frame_id),
1038        map_id: optional_cstr_to_string(map_id),
1039        floor: if floor == i32::MIN { None } else { Some(floor) },
1040        ..Default::default()
1041    };
1042
1043    builder_mutate!(builder, |b| b.location(loc));
1044    PHYTRACE_OK
1045}
1046
1047// =============================================================================
1048// Domain Setters — Motion
1049// =============================================================================
1050
1051/// Set the motion domain on the builder.
1052///
1053/// All floating-point parameters use `f64::NAN` to indicate "not set".
1054///
1055/// # Parameters
1056/// - `speed_mps` — Speed in meters/second, or NAN
1057/// - `vx`, `vy`, `vz` — Linear velocity components in m/s, or NAN
1058/// - `roll_dps`, `pitch_dps`, `yaw_dps` — Angular velocity in deg/s, or NAN
1059/// - `cmd_linear_mps` — Commanded linear velocity m/s, or NAN
1060/// - `cmd_angular_dps` — Commanded angular velocity deg/s, or NAN
1061/// - `frame_id` — Reference frame, or null
1062#[no_mangle]
1063pub unsafe extern "C" fn phytrace_builder_set_motion(
1064    builder: *mut PhyTraceBuilderHandle,
1065    speed_mps: f64,
1066    vx: f64,
1067    vy: f64,
1068    vz: f64,
1069    roll_dps: f64,
1070    pitch_dps: f64,
1071    yaw_dps: f64,
1072    cmd_linear_mps: f64,
1073    cmd_angular_dps: f64,
1074    frame_id: *const c_char,
1075) -> i32 {
1076    clear_last_error();
1077    if builder.is_null() {
1078        set_last_error("builder handle is null");
1079        return PHYTRACE_ERR_NULL_PTR;
1080    }
1081
1082    let has_linear = !vx.is_nan() || !vy.is_nan() || !vz.is_nan();
1083    let has_angular = !roll_dps.is_nan() || !pitch_dps.is_nan() || !yaw_dps.is_nan();
1084
1085    let mot = MotionDomain {
1086        speed_mps: if speed_mps.is_nan() {
1087            None
1088        } else {
1089            Some(speed_mps)
1090        },
1091        linear_velocity: if has_linear {
1092            Some(motion::LinearVelocity {
1093                x_mps: if vx.is_nan() { None } else { Some(vx) },
1094                y_mps: if vy.is_nan() { None } else { Some(vy) },
1095                z_mps: if vz.is_nan() { None } else { Some(vz) },
1096            })
1097        } else {
1098            None
1099        },
1100        angular_velocity: if has_angular {
1101            Some(motion::AngularVelocity {
1102                roll_dps: if roll_dps.is_nan() {
1103                    None
1104                } else {
1105                    Some(roll_dps)
1106                },
1107                pitch_dps: if pitch_dps.is_nan() {
1108                    None
1109                } else {
1110                    Some(pitch_dps)
1111                },
1112                yaw_dps: if yaw_dps.is_nan() {
1113                    None
1114                } else {
1115                    Some(yaw_dps)
1116                },
1117            })
1118        } else {
1119            None
1120        },
1121        commanded_linear_mps: if cmd_linear_mps.is_nan() {
1122            None
1123        } else {
1124            Some(cmd_linear_mps)
1125        },
1126        commanded_angular_dps: if cmd_angular_dps.is_nan() {
1127            None
1128        } else {
1129            Some(cmd_angular_dps)
1130        },
1131        frame_id: optional_cstr_to_string(frame_id),
1132        ..Default::default()
1133    };
1134
1135    builder_mutate!(builder, |b| b.motion(mot));
1136    PHYTRACE_OK
1137}
1138
1139// =============================================================================
1140// Domain Setters — Power
1141// =============================================================================
1142
1143/// Set the power domain on the builder.
1144///
1145/// Floating-point parameters use `f64::NAN` for "not set".
1146/// `is_charging`: 1 = true, 0 = false, -1 = not set.
1147///
1148/// # Parameters
1149/// - `soc_pct` — State of charge percentage (0-100), or NAN
1150/// - `voltage_v` — Battery voltage, or NAN
1151/// - `current_a` — Battery current in amps, or NAN
1152/// - `temperature_c` — Battery temperature in Celsius, or NAN
1153/// - `is_charging` — 1/0/-1
1154/// - `health_pct` — State of health percentage (0-100), or NAN
1155#[no_mangle]
1156pub unsafe extern "C" fn phytrace_builder_set_power(
1157    builder: *mut PhyTraceBuilderHandle,
1158    soc_pct: f64,
1159    voltage_v: f64,
1160    current_a: f64,
1161    temperature_c: f64,
1162    is_charging: i32,
1163    health_pct: f64,
1164) -> i32 {
1165    clear_last_error();
1166    if builder.is_null() {
1167        set_last_error("builder handle is null");
1168        return PHYTRACE_ERR_NULL_PTR;
1169    }
1170
1171    let battery = power::Battery {
1172        state_of_charge_pct: if soc_pct.is_nan() {
1173            None
1174        } else {
1175            Some(soc_pct)
1176        },
1177        voltage_v: if voltage_v.is_nan() {
1178            None
1179        } else {
1180            Some(voltage_v)
1181        },
1182        current_a: if current_a.is_nan() {
1183            None
1184        } else {
1185            Some(current_a)
1186        },
1187        temperature_c: if temperature_c.is_nan() {
1188            None
1189        } else {
1190            Some(temperature_c)
1191        },
1192        state_of_health_pct: if health_pct.is_nan() {
1193            None
1194        } else {
1195            Some(health_pct)
1196        },
1197        ..Default::default()
1198    };
1199
1200    let charging = if is_charging >= 0 {
1201        Some(power::Charging {
1202            is_charging: Some(is_charging != 0),
1203            state: Some(if is_charging != 0 {
1204                enums::ChargingState::Charging
1205            } else {
1206                enums::ChargingState::NotCharging
1207            }),
1208            ..Default::default()
1209        })
1210    } else {
1211        None
1212    };
1213
1214    let pwr = PowerDomain {
1215        battery: Some(battery),
1216        charging,
1217        ..Default::default()
1218    };
1219
1220    builder_mutate!(builder, |b| b.power(pwr));
1221    PHYTRACE_OK
1222}
1223
1224// =============================================================================
1225// Domain Setters — Perception: Lidar
1226// =============================================================================
1227
1228/// Set a lidar sensor in the perception domain.
1229///
1230/// String parameters use null for "not set". Float parameters use NAN for "not set".
1231///
1232/// # Parameters
1233/// - `sensor_id` — Sensor identifier string, or null
1234/// - `point_count` — Number of points in scan; 0 means not set
1235/// - `min_range_m`, `max_range_m` — Sensor range limits, or NAN
1236/// - `min_angle_deg`, `max_angle_deg` — Scan angle limits, or NAN
1237/// - `angular_resolution_deg` — Angular resolution, or NAN
1238/// - `closest_range_m` — Closest detected range, or NAN
1239/// - `closest_angle_deg` — Angle of closest detection, or NAN
1240/// - `scan_frequency_hz` — Scan frequency, or NAN
1241#[no_mangle]
1242pub unsafe extern "C" fn phytrace_builder_set_perception_lidar(
1243    builder: *mut PhyTraceBuilderHandle,
1244    sensor_id: *const c_char,
1245    point_count: u32,
1246    min_range_m: f64,
1247    max_range_m: f64,
1248    min_angle_deg: f64,
1249    max_angle_deg: f64,
1250    angular_resolution_deg: f64,
1251    closest_range_m: f64,
1252    closest_angle_deg: f64,
1253    scan_frequency_hz: f64,
1254) -> i32 {
1255    clear_last_error();
1256    if builder.is_null() {
1257        set_last_error("builder handle is null");
1258        return PHYTRACE_ERR_NULL_PTR;
1259    }
1260
1261    let lidar = perception::LidarSensor {
1262        sensor_id: optional_cstr_to_string(sensor_id),
1263        point_count: if point_count == 0 {
1264            None
1265        } else {
1266            Some(point_count)
1267        },
1268        min_range_m: if min_range_m.is_nan() {
1269            None
1270        } else {
1271            Some(min_range_m)
1272        },
1273        max_range_m: if max_range_m.is_nan() {
1274            None
1275        } else {
1276            Some(max_range_m)
1277        },
1278        min_angle_deg: if min_angle_deg.is_nan() {
1279            None
1280        } else {
1281            Some(min_angle_deg)
1282        },
1283        max_angle_deg: if max_angle_deg.is_nan() {
1284            None
1285        } else {
1286            Some(max_angle_deg)
1287        },
1288        angular_resolution_deg: if angular_resolution_deg.is_nan() {
1289            None
1290        } else {
1291            Some(angular_resolution_deg)
1292        },
1293        closest_range_m: if closest_range_m.is_nan() {
1294            None
1295        } else {
1296            Some(closest_range_m)
1297        },
1298        closest_angle_deg: if closest_angle_deg.is_nan() {
1299            None
1300        } else {
1301            Some(closest_angle_deg)
1302        },
1303        scan_frequency_hz: if scan_frequency_hz.is_nan() {
1304            None
1305        } else {
1306            Some(scan_frequency_hz)
1307        },
1308        ..Default::default()
1309    };
1310
1311    // Get or create the perception domain, append to lidar vec
1312    let handle = &mut *builder;
1313    let old_builder = std::mem::replace(
1314        &mut handle.builder,
1315        EventBuilder::new(&PhyTraceConfig::new("tmp")),
1316    );
1317
1318    // Peek at existing perception to preserve any IMU data already set
1319    let existing_perception = old_builder.peek().perception.clone();
1320    let mut perc = existing_perception.unwrap_or_default();
1321    let lidars = perc.lidar.get_or_insert_with(Vec::new);
1322    lidars.push(lidar);
1323
1324    handle.builder = old_builder.perception(perc);
1325    PHYTRACE_OK
1326}
1327
1328// =============================================================================
1329// Domain Setters — Perception: IMU
1330// =============================================================================
1331
1332/// Set the IMU sensor in the perception domain.
1333///
1334/// Float parameters use NAN for "not set".
1335///
1336/// # Parameters
1337/// - `accel_x_mps2`, `accel_y_mps2`, `accel_z_mps2` — Linear acceleration (m/s²)
1338/// - `gyro_x_dps`, `gyro_y_dps`, `gyro_z_dps` — Angular velocity (deg/s)
1339/// - `mag_x_ut`, `mag_y_ut`, `mag_z_ut` — Magnetometer readings (microtesla)
1340/// - `temperature_c` — Sensor temperature, or NAN
1341#[no_mangle]
1342pub unsafe extern "C" fn phytrace_builder_set_perception_imu(
1343    builder: *mut PhyTraceBuilderHandle,
1344    accel_x_mps2: f64,
1345    accel_y_mps2: f64,
1346    accel_z_mps2: f64,
1347    gyro_x_dps: f64,
1348    gyro_y_dps: f64,
1349    gyro_z_dps: f64,
1350    mag_x_ut: f64,
1351    mag_y_ut: f64,
1352    mag_z_ut: f64,
1353    temperature_c: f64,
1354) -> i32 {
1355    clear_last_error();
1356    if builder.is_null() {
1357        set_last_error("builder handle is null");
1358        return PHYTRACE_ERR_NULL_PTR;
1359    }
1360
1361    let imu = perception::ImuSensor {
1362        accel_x_mps2: if accel_x_mps2.is_nan() {
1363            None
1364        } else {
1365            Some(accel_x_mps2)
1366        },
1367        accel_y_mps2: if accel_y_mps2.is_nan() {
1368            None
1369        } else {
1370            Some(accel_y_mps2)
1371        },
1372        accel_z_mps2: if accel_z_mps2.is_nan() {
1373            None
1374        } else {
1375            Some(accel_z_mps2)
1376        },
1377        gyro_x_dps: if gyro_x_dps.is_nan() {
1378            None
1379        } else {
1380            Some(gyro_x_dps)
1381        },
1382        gyro_y_dps: if gyro_y_dps.is_nan() {
1383            None
1384        } else {
1385            Some(gyro_y_dps)
1386        },
1387        gyro_z_dps: if gyro_z_dps.is_nan() {
1388            None
1389        } else {
1390            Some(gyro_z_dps)
1391        },
1392        mag_x_ut: if mag_x_ut.is_nan() {
1393            None
1394        } else {
1395            Some(mag_x_ut)
1396        },
1397        mag_y_ut: if mag_y_ut.is_nan() {
1398            None
1399        } else {
1400            Some(mag_y_ut)
1401        },
1402        mag_z_ut: if mag_z_ut.is_nan() {
1403            None
1404        } else {
1405            Some(mag_z_ut)
1406        },
1407        temperature_c: if temperature_c.is_nan() {
1408            None
1409        } else {
1410            Some(temperature_c)
1411        },
1412        ..Default::default()
1413    };
1414
1415    // Preserve existing perception data (lidar, etc.)
1416    let handle = &mut *builder;
1417    let old_builder = std::mem::replace(
1418        &mut handle.builder,
1419        EventBuilder::new(&PhyTraceConfig::new("tmp")),
1420    );
1421    let existing_perception = old_builder.peek().perception.clone();
1422    let mut perc = existing_perception.unwrap_or_default();
1423    perc.imu = Some(imu);
1424
1425    handle.builder = old_builder.perception(perc);
1426    PHYTRACE_OK
1427}
1428
1429// =============================================================================
1430// Domain Setters — Safety
1431// =============================================================================
1432
1433/// Set the safety domain on the builder.
1434///
1435/// Enum parameters use -1 for "not set". Boolean parameters use -1 for "not set",
1436/// 0 for false, 1 for true. Float parameters use NAN for "not set".
1437///
1438/// # Parameters
1439/// - `safety_state` — `PHYTRACE_SAFETY_STATE_*` constant, or -1
1440/// - `is_safe` — 1/0/-1
1441/// - `estop_active` — 1/0/-1
1442/// - `estop_type` — `PHYTRACE_ESTOP_TYPE_*` constant, or -1
1443/// - `speed_limit_mps` — Speed limit in m/s, or NAN
1444/// - `closest_distance_m` — Closest obstacle distance, or NAN
1445/// - `closest_human_m` — Closest human distance, or NAN
1446#[no_mangle]
1447pub unsafe extern "C" fn phytrace_builder_set_safety(
1448    builder: *mut PhyTraceBuilderHandle,
1449    safety_state: i32,
1450    is_safe: i32,
1451    estop_active: i32,
1452    estop_type: i32,
1453    speed_limit_mps: f64,
1454    closest_distance_m: f64,
1455    closest_human_m: f64,
1456) -> i32 {
1457    clear_last_error();
1458    if builder.is_null() {
1459        set_last_error("builder handle is null");
1460        return PHYTRACE_ERR_NULL_PTR;
1461    }
1462
1463    let ss = if safety_state >= 0 {
1464        match safety_state_from_i32(safety_state) {
1465            Some(s) => Some(s),
1466            None => {
1467                set_last_error(format!("Invalid safety_state: {safety_state}"));
1468                return PHYTRACE_ERR_INVALID_ENUM;
1469            }
1470        }
1471    } else {
1472        None
1473    };
1474
1475    let estop = if estop_active >= 0 {
1476        let et = if estop_type >= 0 {
1477            match estop_type_from_i32(estop_type) {
1478                Some(t) => Some(t),
1479                None => {
1480                    set_last_error(format!("Invalid estop_type: {estop_type}"));
1481                    return PHYTRACE_ERR_INVALID_ENUM;
1482                }
1483            }
1484        } else {
1485            None
1486        };
1487
1488        Some(safety::EStopInfo {
1489            is_active: Some(estop_active != 0),
1490            e_stop_type: et,
1491            ..Default::default()
1492        })
1493    } else {
1494        None
1495    };
1496
1497    let proximity = if !closest_distance_m.is_nan() || !closest_human_m.is_nan() {
1498        Some(safety::ProximityInfo {
1499            closest_distance_m: if closest_distance_m.is_nan() {
1500                None
1501            } else {
1502                Some(closest_distance_m)
1503            },
1504            closest_human_m: if closest_human_m.is_nan() {
1505                None
1506            } else {
1507                Some(closest_human_m)
1508            },
1509            ..Default::default()
1510        })
1511    } else {
1512        None
1513    };
1514
1515    let saf = SafetyDomain {
1516        safety_state: ss,
1517        is_safe: if is_safe >= 0 {
1518            Some(is_safe != 0)
1519        } else {
1520            None
1521        },
1522        e_stop: estop,
1523        speed_limit_mps: if speed_limit_mps.is_nan() {
1524            None
1525        } else {
1526            Some(speed_limit_mps)
1527        },
1528        proximity,
1529        ..Default::default()
1530    };
1531
1532    builder_mutate!(builder, |b| b.safety(saf));
1533    PHYTRACE_OK
1534}
1535
1536// =============================================================================
1537// Domain Setters — Navigation
1538// =============================================================================
1539
1540/// Set the navigation domain on the builder.
1541///
1542/// Enum parameters use -1 for "not set". Float/bool follow the NAN/-1 conventions.
1543///
1544/// # Parameters
1545/// - `loc_quality` — `PHYTRACE_LOCALIZATION_QUALITY_*` constant, or -1
1546/// - `loc_confidence` — Localization confidence (0.0-1.0), or NAN
1547/// - `is_localized` — 1/0/-1
1548/// - `path_state` — `PHYTRACE_PATH_STATE_*` constant, or -1
1549/// - `path_length_m` — Total path length in meters, or NAN
1550/// - `path_remaining_m` — Remaining path length, or NAN
1551/// - `goal_x`, `goal_y` — Goal position in meters, or NAN
1552/// - `goal_orientation_deg` — Goal orientation, or NAN
1553#[no_mangle]
1554pub unsafe extern "C" fn phytrace_builder_set_navigation(
1555    builder: *mut PhyTraceBuilderHandle,
1556    loc_quality: i32,
1557    loc_confidence: f64,
1558    is_localized: i32,
1559    path_state: i32,
1560    path_length_m: f64,
1561    path_remaining_m: f64,
1562    goal_x: f64,
1563    goal_y: f64,
1564    goal_orientation_deg: f64,
1565) -> i32 {
1566    clear_last_error();
1567    if builder.is_null() {
1568        set_last_error("builder handle is null");
1569        return PHYTRACE_ERR_NULL_PTR;
1570    }
1571
1572    let localization = if loc_quality >= 0 || !loc_confidence.is_nan() || is_localized >= 0 {
1573        let quality = if loc_quality >= 0 {
1574            match localization_quality_from_i32(loc_quality) {
1575                Some(q) => Some(q),
1576                None => {
1577                    set_last_error(format!("Invalid loc_quality: {loc_quality}"));
1578                    return PHYTRACE_ERR_INVALID_ENUM;
1579                }
1580            }
1581        } else {
1582            None
1583        };
1584
1585        Some(navigation::Localization {
1586            quality,
1587            confidence: if loc_confidence.is_nan() {
1588                None
1589            } else {
1590                Some(loc_confidence)
1591            },
1592            is_localized: if is_localized >= 0 {
1593                Some(is_localized != 0)
1594            } else {
1595                None
1596            },
1597            ..Default::default()
1598        })
1599    } else {
1600        None
1601    };
1602
1603    let path = if path_state >= 0 || !path_length_m.is_nan() || !path_remaining_m.is_nan() {
1604        let ps = if path_state >= 0 {
1605            match path_state_from_i32(path_state) {
1606                Some(p) => Some(p),
1607                None => {
1608                    set_last_error(format!("Invalid path_state: {path_state}"));
1609                    return PHYTRACE_ERR_INVALID_ENUM;
1610                }
1611            }
1612        } else {
1613            None
1614        };
1615
1616        Some(navigation::PathInfo {
1617            state: ps,
1618            length_m: if path_length_m.is_nan() {
1619                None
1620            } else {
1621                Some(path_length_m)
1622            },
1623            remaining_m: if path_remaining_m.is_nan() {
1624                None
1625            } else {
1626                Some(path_remaining_m)
1627            },
1628            ..Default::default()
1629        })
1630    } else {
1631        None
1632    };
1633
1634    let goal = if !goal_x.is_nan() || !goal_y.is_nan() || !goal_orientation_deg.is_nan() {
1635        Some(navigation::NavigationGoal {
1636            position: if !goal_x.is_nan() || !goal_y.is_nan() {
1637                Some(Position2D {
1638                    x_m: if goal_x.is_nan() { None } else { Some(goal_x) },
1639                    y_m: if goal_y.is_nan() { None } else { Some(goal_y) },
1640                })
1641            } else {
1642                None
1643            },
1644            orientation_deg: if goal_orientation_deg.is_nan() {
1645                None
1646            } else {
1647                Some(goal_orientation_deg)
1648            },
1649            ..Default::default()
1650        })
1651    } else {
1652        None
1653    };
1654
1655    let nav = NavigationDomain {
1656        localization,
1657        path,
1658        goal,
1659        ..Default::default()
1660    };
1661
1662    builder_mutate!(builder, |b| b.navigation(nav));
1663    PHYTRACE_OK
1664}
1665
1666// =============================================================================
1667// Domain Setters — Actuators (Joints)
1668// =============================================================================
1669
1670/// Set the actuators domain with joint state data.
1671///
1672/// Joint arrays are passed as JSON-encoded arrays (e.g., `["joint1","joint2"]`
1673/// for names, `[1.0, 2.0]` for values). This simplifies the C interface for
1674/// variable-length arrays.
1675///
1676/// # Parameters
1677/// - `names_json` — JSON array of joint name strings, e.g. `["joint1","joint2"]`
1678/// - `positions_json` — JSON array of position values (radians), or null
1679/// - `velocities_json` — JSON array of velocity values, or null
1680/// - `efforts_json` — JSON array of effort/torque values, or null
1681#[no_mangle]
1682pub unsafe extern "C" fn phytrace_builder_set_actuators_joints(
1683    builder: *mut PhyTraceBuilderHandle,
1684    names_json: *const c_char,
1685    positions_json: *const c_char,
1686    velocities_json: *const c_char,
1687    efforts_json: *const c_char,
1688) -> i32 {
1689    clear_last_error();
1690    if builder.is_null() {
1691        set_last_error("builder handle is null");
1692        return PHYTRACE_ERR_NULL_PTR;
1693    }
1694
1695    // Parse names (required)
1696    let names_str = match cstr_to_str(names_json) {
1697        Ok(s) => s,
1698        Err(code) => {
1699            set_last_error(if code == PHYTRACE_ERR_NULL_PTR {
1700                "names_json is null"
1701            } else {
1702                "names_json contains invalid UTF-8"
1703            });
1704            return code;
1705        }
1706    };
1707    let names: Vec<String> = match serde_json::from_str(names_str) {
1708        Ok(n) => n,
1709        Err(e) => {
1710            set_last_error(format!("Failed to parse names_json: {e}"));
1711            return PHYTRACE_ERR_SERIALIZATION;
1712        }
1713    };
1714
1715    // Parse optional float arrays
1716    let positions: Option<Vec<f64>> = parse_optional_json_array(positions_json);
1717    let velocities: Option<Vec<f64>> = parse_optional_json_array(velocities_json);
1718    let efforts: Option<Vec<f64>> = parse_optional_json_array(efforts_json);
1719
1720    // Build joint structs
1721    let mut joints = Vec::with_capacity(names.len());
1722    for (i, name) in names.into_iter().enumerate() {
1723        joints.push(actuators::Joint {
1724            name: Some(name),
1725            position: positions.as_ref().and_then(|p| p.get(i).copied()),
1726            velocity: velocities.as_ref().and_then(|v| v.get(i).copied()),
1727            effort: efforts.as_ref().and_then(|e| e.get(i).copied()),
1728            ..Default::default()
1729        });
1730    }
1731
1732    let act = ActuatorsDomain {
1733        joints: Some(joints),
1734        ..Default::default()
1735    };
1736
1737    builder_mutate!(builder, |b| b.actuators(act));
1738    PHYTRACE_OK
1739}
1740
1741/// Helper to parse an optional JSON array from a C string.
1742unsafe fn parse_optional_json_array<T: serde::de::DeserializeOwned>(
1743    ptr: *const c_char,
1744) -> Option<T> {
1745    if ptr.is_null() {
1746        return None;
1747    }
1748    let s = match CStr::from_ptr(ptr).to_str() {
1749        Ok(s) => s,
1750        Err(_) => return None,
1751    };
1752    serde_json::from_str(s).ok()
1753}
1754
1755// =============================================================================
1756// Domain Setters — Operational
1757// =============================================================================
1758
1759/// Set the operational domain on the builder.
1760///
1761/// Enum parameters use -1 for "not set". String parameters use null.
1762/// Float parameters use NAN.
1763///
1764/// # Parameters
1765/// - `mode` — `PHYTRACE_OPERATIONAL_MODE_*` constant, or -1
1766/// - `state` — `PHYTRACE_OPERATIONAL_STATE_*` constant, or -1
1767/// - `task_id` — Current task ID string, or null
1768/// - `task_type` — Current task type string, or null
1769/// - `uptime_sec` — System uptime in seconds, or NAN
1770/// - `mission_id` — Mission ID string, or null
1771#[no_mangle]
1772pub unsafe extern "C" fn phytrace_builder_set_operational(
1773    builder: *mut PhyTraceBuilderHandle,
1774    mode: i32,
1775    state: i32,
1776    task_id: *const c_char,
1777    task_type: *const c_char,
1778    uptime_sec: f64,
1779    mission_id: *const c_char,
1780) -> i32 {
1781    clear_last_error();
1782    if builder.is_null() {
1783        set_last_error("builder handle is null");
1784        return PHYTRACE_ERR_NULL_PTR;
1785    }
1786
1787    let op_mode = if mode >= 0 {
1788        match operational_mode_from_i32(mode) {
1789            Some(m) => Some(m),
1790            None => {
1791                set_last_error(format!("Invalid operational mode: {mode}"));
1792                return PHYTRACE_ERR_INVALID_ENUM;
1793            }
1794        }
1795    } else {
1796        None
1797    };
1798
1799    let op_state = if state >= 0 {
1800        match operational_state_from_i32(state) {
1801            Some(s) => Some(s),
1802            None => {
1803                set_last_error(format!("Invalid operational state: {state}"));
1804                return PHYTRACE_ERR_INVALID_ENUM;
1805            }
1806        }
1807    } else {
1808        None
1809    };
1810
1811    let task = {
1812        let tid = optional_cstr_to_string(task_id);
1813        let ttype = optional_cstr_to_string(task_type);
1814        if tid.is_some() || ttype.is_some() {
1815            Some(operational::Task {
1816                task_id: tid,
1817                task_type: ttype,
1818                ..Default::default()
1819            })
1820        } else {
1821            None
1822        }
1823    };
1824
1825    let ops = OperationalDomain {
1826        mode: op_mode,
1827        state: op_state,
1828        task,
1829        uptime_sec: if uptime_sec.is_nan() {
1830            None
1831        } else {
1832            Some(uptime_sec)
1833        },
1834        mission_id: optional_cstr_to_string(mission_id),
1835        ..Default::default()
1836    };
1837
1838    builder_mutate!(builder, |b| b.operational(ops));
1839    PHYTRACE_OK
1840}
1841
1842// =============================================================================
1843// Domain Setters — Identity
1844// =============================================================================
1845
1846/// Set the identity domain on the builder.
1847///
1848/// String parameters use null for "not set".
1849///
1850/// # Parameters
1851/// - `source_id` — Robot/system identifier, or null (uses config default)
1852/// - `platform` — Platform name, or null
1853/// - `model` — Model name, or null
1854/// - `firmware_version` — Firmware version string, or null
1855/// - `serial_number` — Serial number, or null
1856/// - `fleet_id` — Fleet identifier, or null
1857/// - `site_id` — Site identifier, or null
1858#[no_mangle]
1859pub unsafe extern "C" fn phytrace_builder_set_identity(
1860    builder: *mut PhyTraceBuilderHandle,
1861    source_id: *const c_char,
1862    platform: *const c_char,
1863    model: *const c_char,
1864    firmware_version: *const c_char,
1865    serial_number: *const c_char,
1866    fleet_id: *const c_char,
1867    site_id: *const c_char,
1868) -> i32 {
1869    clear_last_error();
1870    if builder.is_null() {
1871        set_last_error("builder handle is null");
1872        return PHYTRACE_ERR_NULL_PTR;
1873    }
1874
1875    let ident = IdentityDomain {
1876        source_id: optional_cstr_to_string(source_id),
1877        platform: optional_cstr_to_string(platform),
1878        model: optional_cstr_to_string(model),
1879        firmware_version: optional_cstr_to_string(firmware_version),
1880        serial_number: optional_cstr_to_string(serial_number),
1881        fleet_id: optional_cstr_to_string(fleet_id),
1882        site_id: optional_cstr_to_string(site_id),
1883        ..Default::default()
1884    };
1885
1886    builder_mutate!(builder, |b| b.identity(ident));
1887    PHYTRACE_OK
1888}
1889
1890// =============================================================================
1891// Domain Setters — Communication
1892// =============================================================================
1893
1894/// Set the communication domain on the builder.
1895///
1896/// This is a simplified interface for the most common communication fields.
1897/// String parameters use null for "not set". Integer parameters use -1 for "not set".
1898///
1899/// # Parameters
1900/// - `is_connected` — Network connected: 1/0/-1
1901/// - `signal_strength_dbm` — WiFi/cellular signal strength, or `i32::MIN`
1902/// - `latency_ms` — Network latency, or NAN
1903/// - `packet_loss_pct` — Packet loss percentage, or NAN
1904#[no_mangle]
1905pub unsafe extern "C" fn phytrace_builder_set_communication(
1906    builder: *mut PhyTraceBuilderHandle,
1907    is_connected: i32,
1908    signal_strength_dbm: i32,
1909    latency_ms: f64,
1910    packet_loss_pct: f64,
1911) -> i32 {
1912    clear_last_error();
1913    if builder.is_null() {
1914        set_last_error("builder handle is null");
1915        return PHYTRACE_ERR_NULL_PTR;
1916    }
1917
1918    let network = communication::NetworkInfo {
1919        is_connected: if is_connected >= 0 {
1920            Some(is_connected != 0)
1921        } else {
1922            None
1923        },
1924        signal_strength_dbm: if signal_strength_dbm == i32::MIN {
1925            None
1926        } else {
1927            Some(signal_strength_dbm)
1928        },
1929        latency_ms: if latency_ms.is_nan() {
1930            None
1931        } else {
1932            Some(latency_ms)
1933        },
1934        packet_loss_pct: if packet_loss_pct.is_nan() {
1935            None
1936        } else {
1937            Some(packet_loss_pct)
1938        },
1939        ..Default::default()
1940    };
1941
1942    let comm = CommunicationDomain {
1943        network: Some(network),
1944        ..Default::default()
1945    };
1946
1947    builder_mutate!(builder, |b| b.communication(comm));
1948    PHYTRACE_OK
1949}
1950
1951// =============================================================================
1952// Domain Setters — Context
1953// =============================================================================
1954
1955/// Set the context domain on the builder.
1956///
1957/// String parameters use null for "not set". Integer parameters use -1 for "not set".
1958///
1959/// # Parameters
1960/// - `timezone` — Timezone string (e.g., "America/New_York"), or null
1961/// - `facility_id` — Facility identifier, or null
1962/// - `facility_name` — Facility name, or null
1963/// - `human_count` — Number of humans in area; -1 means not set
1964/// - `robot_count` — Number of robots in area; -1 means not set
1965#[no_mangle]
1966pub unsafe extern "C" fn phytrace_builder_set_context(
1967    builder: *mut PhyTraceBuilderHandle,
1968    timezone: *const c_char,
1969    facility_id: *const c_char,
1970    facility_name: *const c_char,
1971    human_count: i32,
1972    robot_count: i32,
1973) -> i32 {
1974    clear_last_error();
1975    if builder.is_null() {
1976        set_last_error("builder handle is null");
1977        return PHYTRACE_ERR_NULL_PTR;
1978    }
1979
1980    let time = {
1981        let tz = optional_cstr_to_string(timezone);
1982        if tz.is_some() {
1983            Some(context::TimeContext {
1984                timezone: tz,
1985                ..Default::default()
1986            })
1987        } else {
1988            None
1989        }
1990    };
1991
1992    let facility = {
1993        let fid = optional_cstr_to_string(facility_id);
1994        let fname = optional_cstr_to_string(facility_name);
1995        if fid.is_some() || fname.is_some() || human_count >= 0 || robot_count >= 0 {
1996            Some(context::FacilityContext {
1997                facility_id: fid,
1998                name: fname,
1999                human_count: if human_count >= 0 {
2000                    Some(u32::try_from(human_count).expect("human_count is non-negative"))
2001                } else {
2002                    None
2003                },
2004                robot_count: if robot_count >= 0 {
2005                    Some(u32::try_from(robot_count).expect("robot_count is non-negative"))
2006                } else {
2007                    None
2008                },
2009                ..Default::default()
2010            })
2011        } else {
2012            None
2013        }
2014    };
2015
2016    let ctx = ContextDomain {
2017        time,
2018        facility,
2019        ..Default::default()
2020    };
2021
2022    builder_mutate!(builder, |b| b.context(ctx));
2023    PHYTRACE_OK
2024}
2025
2026// =============================================================================
2027// Domain Setters — Extensions (raw JSON)
2028// =============================================================================
2029
2030/// Set arbitrary extensions on the builder as a JSON string.
2031///
2032/// This is used by the ROS2 generic converter to stash raw message payloads.
2033///
2034/// # Parameters
2035/// - `json_str` — JSON object string, e.g. `{"raw_msg": {...}}`
2036#[no_mangle]
2037pub unsafe extern "C" fn phytrace_builder_set_extensions_json(
2038    builder: *mut PhyTraceBuilderHandle,
2039    json_str: *const c_char,
2040) -> i32 {
2041    clear_last_error();
2042    if builder.is_null() {
2043        set_last_error("builder handle is null");
2044        return PHYTRACE_ERR_NULL_PTR;
2045    }
2046
2047    let s = match cstr_to_str(json_str) {
2048        Ok(s) => s,
2049        Err(code) => {
2050            set_last_error(if code == PHYTRACE_ERR_NULL_PTR {
2051                "json_str is null"
2052            } else {
2053                "json_str contains invalid UTF-8"
2054            });
2055            return code;
2056        }
2057    };
2058
2059    let value: serde_json::Value = match serde_json::from_str(s) {
2060        Ok(v) => v,
2061        Err(e) => {
2062            set_last_error(format!("Failed to parse extensions JSON: {e}"));
2063            return PHYTRACE_ERR_SERIALIZATION;
2064        }
2065    };
2066
2067    builder_mutate!(builder, |b| b.extensions(value));
2068    PHYTRACE_OK
2069}
2070
2071// =============================================================================
2072// Event Operations
2073// =============================================================================
2074
2075/// Send a built event through the agent's transport.
2076///
2077/// This consumes the event handle (frees it). On failure, the event is still freed.
2078///
2079/// Returns `PHYTRACE_OK` on success, negative error code on failure.
2080#[no_mangle]
2081pub unsafe extern "C" fn phytrace_agent_send(
2082    agent: *mut PhyTraceAgentHandle,
2083    event: *mut PhyTraceEventHandle,
2084) -> i32 {
2085    clear_last_error();
2086
2087    if agent.is_null() {
2088        set_last_error("agent handle is null");
2089        // Still free the event if non-null
2090        if !event.is_null() {
2091            drop(Box::from_raw(event));
2092        }
2093        return PHYTRACE_ERR_NULL_PTR;
2094    }
2095
2096    if event.is_null() {
2097        set_last_error("event handle is null");
2098        return PHYTRACE_ERR_NULL_PTR;
2099    }
2100
2101    let event_handle = Box::from_raw(event);
2102    let handle = &*agent;
2103
2104    match handle
2105        .runtime
2106        .block_on(handle.agent.send(event_handle.event))
2107    {
2108        Ok(()) => PHYTRACE_OK,
2109        Err(e) => {
2110            set_last_error(format!("Send error: {e}"));
2111            PHYTRACE_ERR_TRANSPORT
2112        }
2113    }
2114}
2115
2116/// Serialize a built event to JSON.
2117///
2118/// Returns a heap-allocated null-terminated JSON string, or null on failure.
2119/// The returned string must be freed with `phytrace_string_free()`.
2120#[no_mangle]
2121pub unsafe extern "C" fn phytrace_event_to_json(event: *const PhyTraceEventHandle) -> *mut c_char {
2122    clear_last_error();
2123
2124    if event.is_null() {
2125        set_last_error("event handle is null");
2126        return ptr::null_mut();
2127    }
2128
2129    match (*event).event.to_json() {
2130        Ok(json) => match CString::new(json) {
2131            Ok(cstr) => cstr.into_raw(),
2132            Err(e) => {
2133                set_last_error(format!("JSON contains null byte: {e}"));
2134                ptr::null_mut()
2135            }
2136        },
2137        Err(e) => {
2138            set_last_error(format!("Serialization error: {e}"));
2139            ptr::null_mut()
2140        }
2141    }
2142}
2143
2144/// Destroy a built event without sending it.
2145///
2146/// Passing null is a no-op.
2147#[no_mangle]
2148pub unsafe extern "C" fn phytrace_event_destroy(event: *mut PhyTraceEventHandle) {
2149    if !event.is_null() {
2150        drop(Box::from_raw(event));
2151    }
2152}
2153
2154// =============================================================================
2155// Unit Tests
2156// =============================================================================
2157
2158#[cfg(test)]
2159#[allow(
2160    clippy::undocumented_unsafe_blocks,
2161    clippy::multiple_unsafe_ops_per_block,
2162    clippy::unwrap_used,
2163    reason = "FFI tests exercise unsafe C-ABI functions in tightly grouped sequences; \
2164              individual SAFETY comments and split blocks would obscure the test logic, \
2165              and unwrap is acceptable in tests for fail-fast behavior"
2166)]
2167mod tests {
2168    use super::*;
2169    use std::ffi::CString;
2170
2171    // =========================================================================
2172    // Helper to create a mock agent for tests
2173    // =========================================================================
2174
2175    fn create_test_agent() -> *mut PhyTraceAgentHandle {
2176        // Enable dev mode to bypass license validation in tests
2177        std::env::set_var("PHYWARE_DEV_MODE", "1");
2178        let source_id = CString::new("test-robot-001").unwrap();
2179        unsafe { phytrace_agent_create_mock(source_id.as_ptr(), PHYTRACE_SOURCE_TYPE_AMR) }
2180    }
2181
2182    fn create_test_builder(agent: *const PhyTraceAgentHandle) -> *mut PhyTraceBuilderHandle {
2183        unsafe { phytrace_builder_new(agent) }
2184    }
2185
2186    // =========================================================================
2187    // Version
2188    // =========================================================================
2189
2190    #[test]
2191    fn test_sdk_version() {
2192        let version = phytrace_sdk_version();
2193        assert!(!version.is_null());
2194        let s = unsafe { CStr::from_ptr(version) }.to_str().unwrap();
2195        assert_eq!(s, env!("CARGO_PKG_VERSION"));
2196    }
2197
2198    #[test]
2199    fn test_udm_version() {
2200        let version = phytrace_udm_version();
2201        assert!(!version.is_null());
2202        let s = unsafe { CStr::from_ptr(version) }.to_str().unwrap();
2203        assert_eq!(s, "0.0.3");
2204    }
2205
2206    // =========================================================================
2207    // Error handling
2208    // =========================================================================
2209
2210    #[test]
2211    fn test_last_error_initially_null() {
2212        clear_last_error();
2213        let err = phytrace_last_error();
2214        assert!(err.is_null());
2215    }
2216
2217    #[test]
2218    fn test_last_error_after_null_ptr() {
2219        unsafe {
2220            let result = phytrace_agent_start(ptr::null_mut());
2221            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2222            let err = phytrace_last_error();
2223            assert!(!err.is_null());
2224            let msg = CStr::from_ptr(err).to_str().unwrap();
2225            assert!(msg.contains("null"));
2226        }
2227    }
2228
2229    #[test]
2230    fn test_string_free_null_is_noop() {
2231        unsafe {
2232            phytrace_string_free(ptr::null_mut()); // Should not crash
2233        }
2234    }
2235
2236    // =========================================================================
2237    // Agent lifecycle
2238    // =========================================================================
2239
2240    #[test]
2241    fn test_agent_create_mock() {
2242        let agent = create_test_agent();
2243        assert!(!agent.is_null());
2244        unsafe { phytrace_agent_destroy(agent) };
2245    }
2246
2247    #[test]
2248    fn test_agent_create_mock_null_source_id() {
2249        unsafe {
2250            let agent = phytrace_agent_create_mock(ptr::null(), PHYTRACE_SOURCE_TYPE_AMR);
2251            assert!(agent.is_null());
2252            let err = phytrace_last_error();
2253            assert!(!err.is_null());
2254        }
2255    }
2256
2257    #[test]
2258    fn test_agent_create_mock_invalid_source_type() {
2259        let sid = CString::new("test").unwrap();
2260        unsafe {
2261            let agent = phytrace_agent_create_mock(sid.as_ptr(), 9999);
2262            assert!(agent.is_null());
2263            let err = phytrace_last_error();
2264            assert!(!err.is_null());
2265            let msg = CStr::from_ptr(err).to_str().unwrap();
2266            assert!(msg.contains("Invalid source_type"));
2267        }
2268    }
2269
2270    #[test]
2271    fn test_agent_start_stop() {
2272        let agent = create_test_agent();
2273        assert!(!agent.is_null());
2274        unsafe {
2275            let result = phytrace_agent_start(agent);
2276            assert_eq!(result, PHYTRACE_OK);
2277
2278            assert_eq!(phytrace_agent_is_running(agent), 1);
2279
2280            let result = phytrace_agent_stop(agent);
2281            assert_eq!(result, PHYTRACE_OK);
2282
2283            phytrace_agent_destroy(agent);
2284        }
2285    }
2286
2287    #[test]
2288    fn test_agent_is_running_null() {
2289        unsafe {
2290            let result = phytrace_agent_is_running(ptr::null());
2291            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2292        }
2293    }
2294
2295    #[test]
2296    fn test_agent_destroy_null_is_noop() {
2297        unsafe {
2298            phytrace_agent_destroy(ptr::null_mut()); // Should not crash
2299        }
2300    }
2301
2302    #[test]
2303    fn test_agent_flush_null() {
2304        unsafe {
2305            let result = phytrace_agent_flush(ptr::null_mut());
2306            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2307        }
2308    }
2309
2310    #[test]
2311    fn test_agent_flush() {
2312        let agent = create_test_agent();
2313        unsafe {
2314            phytrace_agent_start(agent);
2315            let result = phytrace_agent_flush(agent);
2316            assert_eq!(result, PHYTRACE_OK);
2317            phytrace_agent_destroy(agent);
2318        }
2319    }
2320
2321    // =========================================================================
2322    // Config errors
2323    // =========================================================================
2324
2325    #[test]
2326    fn test_agent_create_from_yaml_null() {
2327        unsafe {
2328            let agent = phytrace_agent_create_from_yaml(ptr::null());
2329            assert!(agent.is_null());
2330        }
2331    }
2332
2333    #[test]
2334    fn test_agent_create_from_yaml_invalid() {
2335        let yaml = CString::new("not: {valid: yaml: :::").unwrap();
2336        unsafe {
2337            let agent = phytrace_agent_create_from_yaml(yaml.as_ptr());
2338            assert!(agent.is_null());
2339            let err = phytrace_last_error();
2340            assert!(!err.is_null());
2341        }
2342    }
2343
2344    #[test]
2345    fn test_agent_create_from_file_null() {
2346        unsafe {
2347            let agent = phytrace_agent_create_from_file(ptr::null());
2348            assert!(agent.is_null());
2349        }
2350    }
2351
2352    #[test]
2353    fn test_agent_create_from_file_nonexistent() {
2354        let path = CString::new("/nonexistent/path/config.yaml").unwrap();
2355        unsafe {
2356            let agent = phytrace_agent_create_from_file(path.as_ptr());
2357            assert!(agent.is_null());
2358            let err = phytrace_last_error();
2359            assert!(!err.is_null());
2360        }
2361    }
2362
2363    // =========================================================================
2364    // Builder lifecycle
2365    // =========================================================================
2366
2367    #[test]
2368    fn test_builder_new_and_destroy() {
2369        let agent = create_test_agent();
2370        let builder = create_test_builder(agent);
2371        assert!(!builder.is_null());
2372        unsafe {
2373            phytrace_builder_destroy(builder);
2374            phytrace_agent_destroy(agent);
2375        }
2376    }
2377
2378    #[test]
2379    fn test_builder_new_null_agent() {
2380        unsafe {
2381            let builder = phytrace_builder_new(ptr::null());
2382            assert!(builder.is_null());
2383        }
2384    }
2385
2386    #[test]
2387    fn test_builder_destroy_null_is_noop() {
2388        unsafe {
2389            phytrace_builder_destroy(ptr::null_mut()); // Should not crash
2390        }
2391    }
2392
2393    #[test]
2394    fn test_builder_set_event_type() {
2395        let agent = create_test_agent();
2396        let builder = create_test_builder(agent);
2397        unsafe {
2398            let result =
2399                phytrace_builder_set_event_type(builder, PHYTRACE_EVENT_TYPE_TELEMETRY_PERIODIC);
2400            assert_eq!(result, PHYTRACE_OK);
2401            phytrace_builder_destroy(builder);
2402            phytrace_agent_destroy(agent);
2403        }
2404    }
2405
2406    #[test]
2407    fn test_builder_set_event_type_invalid() {
2408        let agent = create_test_agent();
2409        let builder = create_test_builder(agent);
2410        unsafe {
2411            let result = phytrace_builder_set_event_type(builder, 999);
2412            assert_eq!(result, PHYTRACE_ERR_INVALID_ENUM);
2413            phytrace_builder_destroy(builder);
2414            phytrace_agent_destroy(agent);
2415        }
2416    }
2417
2418    #[test]
2419    fn test_builder_set_event_type_null_builder() {
2420        unsafe {
2421            let result = phytrace_builder_set_event_type(
2422                ptr::null_mut(),
2423                PHYTRACE_EVENT_TYPE_TELEMETRY_PERIODIC,
2424            );
2425            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2426        }
2427    }
2428
2429    #[test]
2430    fn test_builder_set_source_type() {
2431        let agent = create_test_agent();
2432        let builder = create_test_builder(agent);
2433        unsafe {
2434            let result = phytrace_builder_set_source_type(builder, PHYTRACE_SOURCE_TYPE_DRONE);
2435            assert_eq!(result, PHYTRACE_OK);
2436            phytrace_builder_destroy(builder);
2437            phytrace_agent_destroy(agent);
2438        }
2439    }
2440
2441    #[test]
2442    fn test_builder_set_source_type_invalid() {
2443        let agent = create_test_agent();
2444        let builder = create_test_builder(agent);
2445        unsafe {
2446            let result = phytrace_builder_set_source_type(builder, -99);
2447            assert_eq!(result, PHYTRACE_ERR_INVALID_ENUM);
2448            phytrace_builder_destroy(builder);
2449            phytrace_agent_destroy(agent);
2450        }
2451    }
2452
2453    // =========================================================================
2454    // Build and verify JSON output
2455    // =========================================================================
2456
2457    /// Build an event and return its JSON representation.
2458    unsafe fn build_and_get_json(builder: *mut PhyTraceBuilderHandle) -> String {
2459        let event = phytrace_builder_build(builder);
2460        assert!(
2461            !event.is_null(),
2462            "Builder build failed. Error: {:?}",
2463            CStr::from_ptr(phytrace_last_error()).to_str().ok()
2464        );
2465        let json_ptr = phytrace_event_to_json(event);
2466        assert!(!json_ptr.is_null());
2467        let json = CStr::from_ptr(json_ptr).to_str().unwrap().to_owned();
2468        phytrace_string_free(json_ptr);
2469        phytrace_event_destroy(event);
2470        json
2471    }
2472
2473    #[test]
2474    fn test_build_unchecked_empty_builder() {
2475        let agent = create_test_agent();
2476        let builder = create_test_builder(agent);
2477        unsafe {
2478            // build_unchecked should always succeed
2479            let event = phytrace_builder_build_unchecked(builder);
2480            assert!(!event.is_null());
2481            phytrace_event_destroy(event);
2482            phytrace_agent_destroy(agent);
2483        }
2484    }
2485
2486    #[test]
2487    fn test_build_unchecked_null_builder() {
2488        unsafe {
2489            let event = phytrace_builder_build_unchecked(ptr::null_mut());
2490            assert!(event.is_null());
2491        }
2492    }
2493
2494    // =========================================================================
2495    // Location domain
2496    // =========================================================================
2497
2498    #[test]
2499    fn test_location_gps() {
2500        let agent = create_test_agent();
2501        let builder = create_test_builder(agent);
2502        unsafe {
2503            let result = phytrace_builder_set_location(
2504                builder,
2505                41.8781,
2506                -87.6298, // lat, lon
2507                f64::NAN, // altitude
2508                90.0,     // heading
2509                f64::NAN,
2510                f64::NAN,
2511                f64::NAN,
2512                f64::NAN, // local coords
2513                ptr::null(),
2514                ptr::null(), // frame_id, map_id
2515                i32::MIN,    // floor
2516            );
2517            assert_eq!(result, PHYTRACE_OK);
2518
2519            let json = build_and_get_json(builder);
2520            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2521            assert_eq!(v["location"]["latitude"], 41.8781);
2522            assert_eq!(v["location"]["longitude"], -87.6298);
2523            assert_eq!(v["location"]["heading_deg"], 90.0);
2524            assert!(v["location"]["altitude_m"].is_null());
2525
2526            phytrace_agent_destroy(agent);
2527        }
2528    }
2529
2530    #[test]
2531    fn test_location_local_coords() {
2532        let agent = create_test_agent();
2533        let builder = create_test_builder(agent);
2534        unsafe {
2535            let frame = CString::new("odom").unwrap();
2536            let map = CString::new("warehouse-1").unwrap();
2537            let result = phytrace_builder_set_location(
2538                builder,
2539                f64::NAN,
2540                f64::NAN,
2541                f64::NAN, // no GPS
2542                f64::NAN, // no heading
2543                10.5,
2544                20.3,
2545                0.0,
2546                45.0, // local x, y, z, yaw
2547                frame.as_ptr(),
2548                map.as_ptr(),
2549                2, // floor
2550            );
2551            assert_eq!(result, PHYTRACE_OK);
2552
2553            let json = build_and_get_json(builder);
2554            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2555            assert_eq!(v["location"]["local"]["x_m"], 10.5);
2556            assert_eq!(v["location"]["local"]["y_m"], 20.3);
2557            assert_eq!(v["location"]["local"]["z_m"], 0.0);
2558            assert_eq!(v["location"]["local"]["yaw_deg"], 45.0);
2559            assert_eq!(v["location"]["frame_id"], "odom");
2560            assert_eq!(v["location"]["map_id"], "warehouse-1");
2561            assert_eq!(v["location"]["floor"], 2);
2562
2563            phytrace_agent_destroy(agent);
2564        }
2565    }
2566
2567    #[test]
2568    fn test_location_null_builder() {
2569        unsafe {
2570            let result = phytrace_builder_set_location(
2571                ptr::null_mut(),
2572                0.0,
2573                0.0,
2574                0.0,
2575                0.0,
2576                0.0,
2577                0.0,
2578                0.0,
2579                0.0,
2580                ptr::null(),
2581                ptr::null(),
2582                0,
2583            );
2584            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2585        }
2586    }
2587
2588    // =========================================================================
2589    // Motion domain
2590    // =========================================================================
2591
2592    #[test]
2593    fn test_motion_velocity() {
2594        let agent = create_test_agent();
2595        let builder = create_test_builder(agent);
2596        unsafe {
2597            let result = phytrace_builder_set_motion(
2598                builder,
2599                1.5, // speed
2600                1.0,
2601                0.2,
2602                0.0, // linear velocity
2603                0.0,
2604                0.0,
2605                15.0, // angular velocity (dps)
2606                f64::NAN,
2607                f64::NAN,    // no commanded velocity
2608                ptr::null(), // no frame_id
2609            );
2610            assert_eq!(result, PHYTRACE_OK);
2611
2612            let json = build_and_get_json(builder);
2613            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2614            assert_eq!(v["motion"]["speed_mps"], 1.5);
2615            assert_eq!(v["motion"]["linear_velocity"]["x_mps"], 1.0);
2616            assert_eq!(v["motion"]["linear_velocity"]["y_mps"], 0.2);
2617            assert_eq!(v["motion"]["angular_velocity"]["yaw_dps"], 15.0);
2618
2619            phytrace_agent_destroy(agent);
2620        }
2621    }
2622
2623    #[test]
2624    fn test_motion_commanded() {
2625        let agent = create_test_agent();
2626        let builder = create_test_builder(agent);
2627        unsafe {
2628            let result = phytrace_builder_set_motion(
2629                builder,
2630                f64::NAN,
2631                f64::NAN,
2632                f64::NAN,
2633                f64::NAN,
2634                f64::NAN,
2635                f64::NAN,
2636                f64::NAN,
2637                0.5,
2638                10.0, // commanded linear, angular
2639                ptr::null(),
2640            );
2641            assert_eq!(result, PHYTRACE_OK);
2642
2643            let json = build_and_get_json(builder);
2644            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2645            assert_eq!(v["motion"]["commanded_linear_mps"], 0.5);
2646            assert_eq!(v["motion"]["commanded_angular_dps"], 10.0);
2647            assert!(v["motion"]["linear_velocity"].is_null());
2648
2649            phytrace_agent_destroy(agent);
2650        }
2651    }
2652
2653    #[test]
2654    fn test_motion_null_builder() {
2655        unsafe {
2656            let result = phytrace_builder_set_motion(
2657                ptr::null_mut(),
2658                0.0,
2659                0.0,
2660                0.0,
2661                0.0,
2662                0.0,
2663                0.0,
2664                0.0,
2665                0.0,
2666                0.0,
2667                ptr::null(),
2668            );
2669            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2670        }
2671    }
2672
2673    // =========================================================================
2674    // Power domain
2675    // =========================================================================
2676
2677    #[test]
2678    fn test_power_battery() {
2679        let agent = create_test_agent();
2680        let builder = create_test_builder(agent);
2681        unsafe {
2682            let result = phytrace_builder_set_power(
2683                builder, 85.0, // soc
2684                48.5, // voltage
2685                -2.1, // current (discharging)
2686                35.0, // temperature
2687                0,    // not charging
2688                98.0, // health
2689            );
2690            assert_eq!(result, PHYTRACE_OK);
2691
2692            let json = build_and_get_json(builder);
2693            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2694            assert_eq!(v["power"]["battery"]["state_of_charge_pct"], 85.0);
2695            assert_eq!(v["power"]["battery"]["voltage_v"], 48.5);
2696            assert_eq!(v["power"]["battery"]["current_a"], -2.1);
2697            assert_eq!(v["power"]["battery"]["temperature_c"], 35.0);
2698            assert_eq!(v["power"]["battery"]["state_of_health_pct"], 98.0);
2699            assert_eq!(v["power"]["charging"]["is_charging"], false);
2700
2701            phytrace_agent_destroy(agent);
2702        }
2703    }
2704
2705    #[test]
2706    fn test_power_charging_not_set() {
2707        let agent = create_test_agent();
2708        let builder = create_test_builder(agent);
2709        unsafe {
2710            let result = phytrace_builder_set_power(
2711                builder,
2712                50.0,
2713                f64::NAN,
2714                f64::NAN,
2715                f64::NAN,
2716                -1, // charging not set
2717                f64::NAN,
2718            );
2719            assert_eq!(result, PHYTRACE_OK);
2720
2721            let json = build_and_get_json(builder);
2722            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2723            assert_eq!(v["power"]["battery"]["state_of_charge_pct"], 50.0);
2724            assert!(v["power"]["charging"].is_null());
2725
2726            phytrace_agent_destroy(agent);
2727        }
2728    }
2729
2730    #[test]
2731    fn test_power_boundary_values() {
2732        let agent = create_test_agent();
2733        let builder = create_test_builder(agent);
2734        unsafe {
2735            // 0% SOC
2736            let result = phytrace_builder_set_power(
2737                builder,
2738                0.0,
2739                f64::NAN,
2740                f64::NAN,
2741                f64::NAN,
2742                -1,
2743                f64::NAN,
2744            );
2745            assert_eq!(result, PHYTRACE_OK);
2746
2747            let json = build_and_get_json(builder);
2748            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2749            assert_eq!(v["power"]["battery"]["state_of_charge_pct"], 0.0);
2750
2751            phytrace_agent_destroy(agent);
2752        }
2753    }
2754
2755    #[test]
2756    fn test_power_null_builder() {
2757        unsafe {
2758            let result = phytrace_builder_set_power(ptr::null_mut(), 0.0, 0.0, 0.0, 0.0, 0, 0.0);
2759            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2760        }
2761    }
2762
2763    // =========================================================================
2764    // Perception — Lidar
2765    // =========================================================================
2766
2767    #[test]
2768    fn test_perception_lidar() {
2769        let agent = create_test_agent();
2770        let builder = create_test_builder(agent);
2771        unsafe {
2772            let sid = CString::new("lidar_front").unwrap();
2773            let result = phytrace_builder_set_perception_lidar(
2774                builder,
2775                sid.as_ptr(),
2776                360,    // point_count
2777                0.1,    // min_range
2778                30.0,   // max_range
2779                -180.0, // min_angle
2780                180.0,  // max_angle
2781                1.0,    // angular_resolution
2782                0.5,    // closest_range
2783                45.0,   // closest_angle
2784                10.0,   // scan_frequency
2785            );
2786            assert_eq!(result, PHYTRACE_OK);
2787
2788            let json = build_and_get_json(builder);
2789            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2790            assert_eq!(v["perception"]["lidar"][0]["sensor_id"], "lidar_front");
2791            assert_eq!(v["perception"]["lidar"][0]["point_count"], 360);
2792            assert_eq!(v["perception"]["lidar"][0]["min_range_m"], 0.1);
2793            assert_eq!(v["perception"]["lidar"][0]["max_range_m"], 30.0);
2794            assert_eq!(v["perception"]["lidar"][0]["closest_range_m"], 0.5);
2795
2796            phytrace_agent_destroy(agent);
2797        }
2798    }
2799
2800    #[test]
2801    fn test_perception_lidar_multiple() {
2802        let agent = create_test_agent();
2803        let builder = create_test_builder(agent);
2804        unsafe {
2805            let sid1 = CString::new("lidar_front").unwrap();
2806            let sid2 = CString::new("lidar_rear").unwrap();
2807
2808            phytrace_builder_set_perception_lidar(
2809                builder,
2810                sid1.as_ptr(),
2811                360,
2812                0.1,
2813                30.0,
2814                -180.0,
2815                180.0,
2816                1.0,
2817                f64::NAN,
2818                f64::NAN,
2819                f64::NAN,
2820            );
2821            phytrace_builder_set_perception_lidar(
2822                builder,
2823                sid2.as_ptr(),
2824                180,
2825                0.2,
2826                15.0,
2827                -90.0,
2828                90.0,
2829                1.0,
2830                f64::NAN,
2831                f64::NAN,
2832                f64::NAN,
2833            );
2834
2835            let json = build_and_get_json(builder);
2836            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2837            assert_eq!(v["perception"]["lidar"].as_array().unwrap().len(), 2);
2838            assert_eq!(v["perception"]["lidar"][0]["sensor_id"], "lidar_front");
2839            assert_eq!(v["perception"]["lidar"][1]["sensor_id"], "lidar_rear");
2840
2841            phytrace_agent_destroy(agent);
2842        }
2843    }
2844
2845    #[test]
2846    fn test_perception_lidar_null_builder() {
2847        unsafe {
2848            let result = phytrace_builder_set_perception_lidar(
2849                ptr::null_mut(),
2850                ptr::null(),
2851                0,
2852                0.0,
2853                0.0,
2854                0.0,
2855                0.0,
2856                0.0,
2857                0.0,
2858                0.0,
2859                0.0,
2860            );
2861            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2862        }
2863    }
2864
2865    // =========================================================================
2866    // Perception — IMU
2867    // =========================================================================
2868
2869    #[test]
2870    fn test_perception_imu() {
2871        let agent = create_test_agent();
2872        let builder = create_test_builder(agent);
2873        unsafe {
2874            let result = phytrace_builder_set_perception_imu(
2875                builder, 0.1, -0.2, 9.81, // accel
2876                0.5, -0.3, 1.2, // gyro
2877                25.0, -10.0, 45.0, // mag
2878                28.5, // temperature
2879            );
2880            assert_eq!(result, PHYTRACE_OK);
2881
2882            let json = build_and_get_json(builder);
2883            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2884            assert_eq!(v["perception"]["imu"]["accel_z_mps2"], 9.81);
2885            assert_eq!(v["perception"]["imu"]["gyro_z_dps"], 1.2);
2886            assert_eq!(v["perception"]["imu"]["mag_x_ut"], 25.0);
2887            assert_eq!(v["perception"]["imu"]["temperature_c"], 28.5);
2888
2889            phytrace_agent_destroy(agent);
2890        }
2891    }
2892
2893    #[test]
2894    fn test_perception_lidar_then_imu_preserves_both() {
2895        let agent = create_test_agent();
2896        let builder = create_test_builder(agent);
2897        unsafe {
2898            let sid = CString::new("lidar0").unwrap();
2899            phytrace_builder_set_perception_lidar(
2900                builder,
2901                sid.as_ptr(),
2902                100,
2903                0.1,
2904                10.0,
2905                -90.0,
2906                90.0,
2907                1.0,
2908                f64::NAN,
2909                f64::NAN,
2910                f64::NAN,
2911            );
2912            phytrace_builder_set_perception_imu(
2913                builder,
2914                0.0,
2915                0.0,
2916                9.81,
2917                f64::NAN,
2918                f64::NAN,
2919                f64::NAN,
2920                f64::NAN,
2921                f64::NAN,
2922                f64::NAN,
2923                f64::NAN,
2924            );
2925
2926            let json = build_and_get_json(builder);
2927            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2928            // Both lidar and IMU should be present
2929            assert!(!v["perception"]["lidar"].is_null());
2930            assert!(!v["perception"]["imu"].is_null());
2931            assert_eq!(v["perception"]["lidar"][0]["sensor_id"], "lidar0");
2932            assert_eq!(v["perception"]["imu"]["accel_z_mps2"], 9.81);
2933
2934            phytrace_agent_destroy(agent);
2935        }
2936    }
2937
2938    #[test]
2939    fn test_perception_imu_null_builder() {
2940        unsafe {
2941            let result = phytrace_builder_set_perception_imu(
2942                ptr::null_mut(),
2943                0.0,
2944                0.0,
2945                0.0,
2946                0.0,
2947                0.0,
2948                0.0,
2949                0.0,
2950                0.0,
2951                0.0,
2952                0.0,
2953            );
2954            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2955        }
2956    }
2957
2958    // =========================================================================
2959    // Safety domain
2960    // =========================================================================
2961
2962    #[test]
2963    fn test_safety() {
2964        let agent = create_test_agent();
2965        let builder = create_test_builder(agent);
2966        unsafe {
2967            let result = phytrace_builder_set_safety(
2968                builder,
2969                PHYTRACE_SAFETY_STATE_WARNING,
2970                1, // is_safe = true
2971                0, // estop not active
2972                PHYTRACE_ESTOP_TYPE_SOFTWARE,
2973                1.0, // speed limit
2974                2.5, // closest distance
2975                3.0, // closest human
2976            );
2977            assert_eq!(result, PHYTRACE_OK);
2978
2979            let json = build_and_get_json(builder);
2980            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2981            assert_eq!(v["safety"]["safety_state"], "warning");
2982            assert_eq!(v["safety"]["is_safe"], true);
2983            assert_eq!(v["safety"]["e_stop"]["is_active"], false);
2984            assert_eq!(v["safety"]["speed_limit_mps"], 1.0);
2985            assert_eq!(v["safety"]["proximity"]["closest_distance_m"], 2.5);
2986            assert_eq!(v["safety"]["proximity"]["closest_human_m"], 3.0);
2987
2988            phytrace_agent_destroy(agent);
2989        }
2990    }
2991
2992    #[test]
2993    fn test_safety_estop_active() {
2994        let agent = create_test_agent();
2995        let builder = create_test_builder(agent);
2996        unsafe {
2997            let result = phytrace_builder_set_safety(
2998                builder,
2999                PHYTRACE_SAFETY_STATE_EMERGENCY_STOP,
3000                0, // not safe
3001                1, // estop active
3002                PHYTRACE_ESTOP_TYPE_HARDWARE,
3003                0.0, // speed limit 0
3004                f64::NAN,
3005                f64::NAN,
3006            );
3007            assert_eq!(result, PHYTRACE_OK);
3008
3009            let json = build_and_get_json(builder);
3010            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3011            assert_eq!(v["safety"]["safety_state"], "emergency_stop");
3012            assert_eq!(v["safety"]["e_stop"]["is_active"], true);
3013            assert_eq!(v["safety"]["e_stop"]["e_stop_type"], "hardware");
3014
3015            phytrace_agent_destroy(agent);
3016        }
3017    }
3018
3019    #[test]
3020    fn test_safety_invalid_enum() {
3021        let agent = create_test_agent();
3022        let builder = create_test_builder(agent);
3023        unsafe {
3024            let result =
3025                phytrace_builder_set_safety(builder, 99, -1, -1, -1, f64::NAN, f64::NAN, f64::NAN);
3026            assert_eq!(result, PHYTRACE_ERR_INVALID_ENUM);
3027            phytrace_builder_destroy(builder);
3028            phytrace_agent_destroy(agent);
3029        }
3030    }
3031
3032    #[test]
3033    fn test_safety_null_builder() {
3034        unsafe {
3035            let result = phytrace_builder_set_safety(ptr::null_mut(), 0, 0, 0, 0, 0.0, 0.0, 0.0);
3036            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3037        }
3038    }
3039
3040    // =========================================================================
3041    // Navigation domain
3042    // =========================================================================
3043
3044    #[test]
3045    fn test_navigation() {
3046        let agent = create_test_agent();
3047        let builder = create_test_builder(agent);
3048        unsafe {
3049            let result = phytrace_builder_set_navigation(
3050                builder,
3051                PHYTRACE_LOCALIZATION_QUALITY_GOOD,
3052                0.95, // confidence
3053                1,    // is_localized
3054                PHYTRACE_PATH_STATE_EXECUTING,
3055                25.0, // path length
3056                10.0, // remaining
3057                50.0,
3058                30.0, // goal x, y
3059                90.0, // goal orientation
3060            );
3061            assert_eq!(result, PHYTRACE_OK);
3062
3063            let json = build_and_get_json(builder);
3064            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3065            assert_eq!(v["navigation"]["localization"]["quality"], "good");
3066            assert_eq!(v["navigation"]["localization"]["confidence"], 0.95);
3067            assert_eq!(v["navigation"]["localization"]["is_localized"], true);
3068            assert_eq!(v["navigation"]["path"]["state"], "executing");
3069            assert_eq!(v["navigation"]["path"]["length_m"], 25.0);
3070            assert_eq!(v["navigation"]["goal"]["position"]["x_m"], 50.0);
3071            assert_eq!(v["navigation"]["goal"]["orientation_deg"], 90.0);
3072
3073            phytrace_agent_destroy(agent);
3074        }
3075    }
3076
3077    #[test]
3078    fn test_navigation_minimal() {
3079        let agent = create_test_agent();
3080        let builder = create_test_builder(agent);
3081        unsafe {
3082            let result = phytrace_builder_set_navigation(
3083                builder,
3084                -1,       // no quality
3085                f64::NAN, // no confidence
3086                -1,       // no localized flag
3087                PHYTRACE_PATH_STATE_VALID,
3088                15.0,
3089                f64::NAN,
3090                f64::NAN,
3091                f64::NAN,
3092                f64::NAN,
3093            );
3094            assert_eq!(result, PHYTRACE_OK);
3095
3096            let json = build_and_get_json(builder);
3097            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3098            assert!(v["navigation"]["localization"].is_null());
3099            assert_eq!(v["navigation"]["path"]["state"], "valid");
3100
3101            phytrace_agent_destroy(agent);
3102        }
3103    }
3104
3105    #[test]
3106    fn test_navigation_invalid_enum() {
3107        let agent = create_test_agent();
3108        let builder = create_test_builder(agent);
3109        unsafe {
3110            let result = phytrace_builder_set_navigation(
3111                builder,
3112                99,
3113                f64::NAN,
3114                -1,
3115                -1,
3116                f64::NAN,
3117                f64::NAN,
3118                f64::NAN,
3119                f64::NAN,
3120                f64::NAN,
3121            );
3122            assert_eq!(result, PHYTRACE_ERR_INVALID_ENUM);
3123            phytrace_builder_destroy(builder);
3124            phytrace_agent_destroy(agent);
3125        }
3126    }
3127
3128    #[test]
3129    fn test_navigation_null_builder() {
3130        unsafe {
3131            let result = phytrace_builder_set_navigation(
3132                ptr::null_mut(),
3133                0,
3134                0.0,
3135                0,
3136                0,
3137                0.0,
3138                0.0,
3139                0.0,
3140                0.0,
3141                0.0,
3142            );
3143            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3144        }
3145    }
3146
3147    // =========================================================================
3148    // Actuators domain (joints)
3149    // =========================================================================
3150
3151    #[test]
3152    fn test_actuators_joints() {
3153        let agent = create_test_agent();
3154        let builder = create_test_builder(agent);
3155        unsafe {
3156            let names = CString::new(r#"["wheel_fl","wheel_fr","wheel_rl","wheel_rr"]"#).unwrap();
3157            let positions = CString::new("[0.0, 1.57, 3.14, 4.71]").unwrap();
3158            let velocities = CString::new("[10.0, 10.5, 9.8, 10.2]").unwrap();
3159            let efforts = CString::new("[5.0, 5.1, 4.9, 5.0]").unwrap();
3160
3161            let result = phytrace_builder_set_actuators_joints(
3162                builder,
3163                names.as_ptr(),
3164                positions.as_ptr(),
3165                velocities.as_ptr(),
3166                efforts.as_ptr(),
3167            );
3168            assert_eq!(result, PHYTRACE_OK);
3169
3170            let json = build_and_get_json(builder);
3171            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3172            let joints = v["actuators"]["joints"].as_array().unwrap();
3173            assert_eq!(joints.len(), 4);
3174            assert_eq!(joints[0]["name"], "wheel_fl");
3175            assert_eq!(joints[0]["position"], 0.0);
3176            assert_eq!(joints[1]["velocity"], 10.5);
3177            assert_eq!(joints[3]["effort"], 5.0);
3178
3179            phytrace_agent_destroy(agent);
3180        }
3181    }
3182
3183    #[test]
3184    fn test_actuators_joints_names_only() {
3185        let agent = create_test_agent();
3186        let builder = create_test_builder(agent);
3187        unsafe {
3188            let names = CString::new(r#"["j1","j2"]"#).unwrap();
3189
3190            let result = phytrace_builder_set_actuators_joints(
3191                builder,
3192                names.as_ptr(),
3193                ptr::null(), // no positions
3194                ptr::null(), // no velocities
3195                ptr::null(), // no efforts
3196            );
3197            assert_eq!(result, PHYTRACE_OK);
3198
3199            let json = build_and_get_json(builder);
3200            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3201            let joints = v["actuators"]["joints"].as_array().unwrap();
3202            assert_eq!(joints.len(), 2);
3203            assert_eq!(joints[0]["name"], "j1");
3204            // Position should not be present
3205            assert!(joints[0]["position"].is_null());
3206
3207            phytrace_agent_destroy(agent);
3208        }
3209    }
3210
3211    #[test]
3212    fn test_actuators_joints_null_names() {
3213        let agent = create_test_agent();
3214        let builder = create_test_builder(agent);
3215        unsafe {
3216            let result = phytrace_builder_set_actuators_joints(
3217                builder,
3218                ptr::null(), // null names — error
3219                ptr::null(),
3220                ptr::null(),
3221                ptr::null(),
3222            );
3223            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3224            phytrace_builder_destroy(builder);
3225            phytrace_agent_destroy(agent);
3226        }
3227    }
3228
3229    #[test]
3230    fn test_actuators_joints_invalid_json() {
3231        let agent = create_test_agent();
3232        let builder = create_test_builder(agent);
3233        unsafe {
3234            let bad_names = CString::new("not valid json").unwrap();
3235            let result = phytrace_builder_set_actuators_joints(
3236                builder,
3237                bad_names.as_ptr(),
3238                ptr::null(),
3239                ptr::null(),
3240                ptr::null(),
3241            );
3242            assert_eq!(result, PHYTRACE_ERR_SERIALIZATION);
3243            phytrace_builder_destroy(builder);
3244            phytrace_agent_destroy(agent);
3245        }
3246    }
3247
3248    #[test]
3249    fn test_actuators_joints_null_builder() {
3250        unsafe {
3251            let names = CString::new(r#"["j1"]"#).unwrap();
3252            let result = phytrace_builder_set_actuators_joints(
3253                ptr::null_mut(),
3254                names.as_ptr(),
3255                ptr::null(),
3256                ptr::null(),
3257                ptr::null(),
3258            );
3259            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3260        }
3261    }
3262
3263    // =========================================================================
3264    // Operational domain
3265    // =========================================================================
3266
3267    #[test]
3268    fn test_operational() {
3269        let agent = create_test_agent();
3270        let builder = create_test_builder(agent);
3271        unsafe {
3272            let tid = CString::new("task-123").unwrap();
3273            let ttype = CString::new("delivery").unwrap();
3274            let mid = CString::new("mission-456").unwrap();
3275
3276            let result = phytrace_builder_set_operational(
3277                builder,
3278                PHYTRACE_OPERATIONAL_MODE_AUTONOMOUS,
3279                PHYTRACE_OPERATIONAL_STATE_NAVIGATING,
3280                tid.as_ptr(),
3281                ttype.as_ptr(),
3282                3600.0, // uptime
3283                mid.as_ptr(),
3284            );
3285            assert_eq!(result, PHYTRACE_OK);
3286
3287            let json = build_and_get_json(builder);
3288            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3289            assert_eq!(v["operational"]["mode"], "autonomous");
3290            assert_eq!(v["operational"]["state"], "navigating");
3291            assert_eq!(v["operational"]["task"]["task_id"], "task-123");
3292            assert_eq!(v["operational"]["task"]["task_type"], "delivery");
3293            assert_eq!(v["operational"]["uptime_sec"], 3600.0);
3294            assert_eq!(v["operational"]["mission_id"], "mission-456");
3295
3296            phytrace_agent_destroy(agent);
3297        }
3298    }
3299
3300    #[test]
3301    fn test_operational_invalid_enum() {
3302        let agent = create_test_agent();
3303        let builder = create_test_builder(agent);
3304        unsafe {
3305            let result = phytrace_builder_set_operational(
3306                builder,
3307                99,
3308                -1,
3309                ptr::null(),
3310                ptr::null(),
3311                f64::NAN,
3312                ptr::null(),
3313            );
3314            assert_eq!(result, PHYTRACE_ERR_INVALID_ENUM);
3315            phytrace_builder_destroy(builder);
3316            phytrace_agent_destroy(agent);
3317        }
3318    }
3319
3320    #[test]
3321    fn test_operational_null_builder() {
3322        unsafe {
3323            let result = phytrace_builder_set_operational(
3324                ptr::null_mut(),
3325                0,
3326                0,
3327                ptr::null(),
3328                ptr::null(),
3329                0.0,
3330                ptr::null(),
3331            );
3332            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3333        }
3334    }
3335
3336    // =========================================================================
3337    // Identity domain
3338    // =========================================================================
3339
3340    #[test]
3341    fn test_identity() {
3342        let agent = create_test_agent();
3343        let builder = create_test_builder(agent);
3344        unsafe {
3345            let sid = CString::new("robot-42").unwrap();
3346            let platform = CString::new("TurtleBot4").unwrap();
3347            let model = CString::new("TB4-Standard").unwrap();
3348            let firmware = CString::new("1.2.3").unwrap();
3349            let serial = CString::new("SN-00042").unwrap();
3350            let fleet = CString::new("fleet-alpha").unwrap();
3351            let site = CString::new("warehouse-nyc").unwrap();
3352
3353            let result = phytrace_builder_set_identity(
3354                builder,
3355                sid.as_ptr(),
3356                platform.as_ptr(),
3357                model.as_ptr(),
3358                firmware.as_ptr(),
3359                serial.as_ptr(),
3360                fleet.as_ptr(),
3361                site.as_ptr(),
3362            );
3363            assert_eq!(result, PHYTRACE_OK);
3364
3365            let json = build_and_get_json(builder);
3366            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3367            assert_eq!(v["identity"]["source_id"], "robot-42");
3368            assert_eq!(v["identity"]["platform"], "TurtleBot4");
3369            assert_eq!(v["identity"]["model"], "TB4-Standard");
3370            assert_eq!(v["identity"]["firmware_version"], "1.2.3");
3371            assert_eq!(v["identity"]["serial_number"], "SN-00042");
3372            assert_eq!(v["identity"]["fleet_id"], "fleet-alpha");
3373            assert_eq!(v["identity"]["site_id"], "warehouse-nyc");
3374
3375            phytrace_agent_destroy(agent);
3376        }
3377    }
3378
3379    #[test]
3380    fn test_identity_null_builder() {
3381        unsafe {
3382            let result = phytrace_builder_set_identity(
3383                ptr::null_mut(),
3384                ptr::null(),
3385                ptr::null(),
3386                ptr::null(),
3387                ptr::null(),
3388                ptr::null(),
3389                ptr::null(),
3390                ptr::null(),
3391            );
3392            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3393        }
3394    }
3395
3396    // =========================================================================
3397    // Communication domain
3398    // =========================================================================
3399
3400    #[test]
3401    fn test_communication() {
3402        let agent = create_test_agent();
3403        let builder = create_test_builder(agent);
3404        unsafe {
3405            let result = phytrace_builder_set_communication(builder, 1, -65, 12.5, 0.1);
3406            assert_eq!(result, PHYTRACE_OK);
3407
3408            let json = build_and_get_json(builder);
3409            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3410            assert_eq!(v["communication"]["network"]["is_connected"], true);
3411            assert_eq!(v["communication"]["network"]["signal_strength_dbm"], -65);
3412            assert_eq!(v["communication"]["network"]["latency_ms"], 12.5);
3413            assert_eq!(v["communication"]["network"]["packet_loss_pct"], 0.1);
3414
3415            phytrace_agent_destroy(agent);
3416        }
3417    }
3418
3419    #[test]
3420    fn test_communication_null_builder() {
3421        unsafe {
3422            let result = phytrace_builder_set_communication(ptr::null_mut(), 0, 0, 0.0, 0.0);
3423            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3424        }
3425    }
3426
3427    // =========================================================================
3428    // Context domain
3429    // =========================================================================
3430
3431    #[test]
3432    fn test_context() {
3433        let agent = create_test_agent();
3434        let builder = create_test_builder(agent);
3435        unsafe {
3436            let tz = CString::new("America/New_York").unwrap();
3437            let fid = CString::new("site-001").unwrap();
3438            let fname = CString::new("NYC Warehouse").unwrap();
3439
3440            let result = phytrace_builder_set_context(
3441                builder,
3442                tz.as_ptr(),
3443                fid.as_ptr(),
3444                fname.as_ptr(),
3445                5,
3446                3,
3447            );
3448            assert_eq!(result, PHYTRACE_OK);
3449
3450            let json = build_and_get_json(builder);
3451            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3452            assert_eq!(v["context"]["time"]["timezone"], "America/New_York");
3453            assert_eq!(v["context"]["facility"]["facility_id"], "site-001");
3454            assert_eq!(v["context"]["facility"]["name"], "NYC Warehouse");
3455            assert_eq!(v["context"]["facility"]["human_count"], 5);
3456            assert_eq!(v["context"]["facility"]["robot_count"], 3);
3457
3458            phytrace_agent_destroy(agent);
3459        }
3460    }
3461
3462    #[test]
3463    fn test_context_null_builder() {
3464        unsafe {
3465            let result = phytrace_builder_set_context(
3466                ptr::null_mut(),
3467                ptr::null(),
3468                ptr::null(),
3469                ptr::null(),
3470                0,
3471                0,
3472            );
3473            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3474        }
3475    }
3476
3477    // =========================================================================
3478    // Extensions (raw JSON)
3479    // =========================================================================
3480
3481    #[test]
3482    fn test_extensions_json() {
3483        let agent = create_test_agent();
3484        let builder = create_test_builder(agent);
3485        unsafe {
3486            let json_str =
3487                CString::new(r#"{"raw_msg":{"topic":"/custom","data":[1,2,3]}}"#).unwrap();
3488            let result = phytrace_builder_set_extensions_json(builder, json_str.as_ptr());
3489            assert_eq!(result, PHYTRACE_OK);
3490
3491            let json = build_and_get_json(builder);
3492            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3493            assert_eq!(v["extensions"]["raw_msg"]["topic"], "/custom");
3494            assert_eq!(v["extensions"]["raw_msg"]["data"][0], 1);
3495
3496            phytrace_agent_destroy(agent);
3497        }
3498    }
3499
3500    #[test]
3501    fn test_extensions_json_invalid() {
3502        let agent = create_test_agent();
3503        let builder = create_test_builder(agent);
3504        unsafe {
3505            let bad = CString::new("not json").unwrap();
3506            let result = phytrace_builder_set_extensions_json(builder, bad.as_ptr());
3507            assert_eq!(result, PHYTRACE_ERR_SERIALIZATION);
3508            phytrace_builder_destroy(builder);
3509            phytrace_agent_destroy(agent);
3510        }
3511    }
3512
3513    #[test]
3514    fn test_extensions_json_null() {
3515        let agent = create_test_agent();
3516        let builder = create_test_builder(agent);
3517        unsafe {
3518            let result = phytrace_builder_set_extensions_json(builder, ptr::null());
3519            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3520            phytrace_builder_destroy(builder);
3521            phytrace_agent_destroy(agent);
3522        }
3523    }
3524
3525    #[test]
3526    fn test_extensions_json_null_builder() {
3527        unsafe {
3528            let s = CString::new("{}").unwrap();
3529            let result = phytrace_builder_set_extensions_json(ptr::null_mut(), s.as_ptr());
3530            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3531        }
3532    }
3533
3534    // =========================================================================
3535    // Event operations
3536    // =========================================================================
3537
3538    #[test]
3539    fn test_event_to_json() {
3540        let agent = create_test_agent();
3541        let builder = create_test_builder(agent);
3542        unsafe {
3543            phytrace_builder_set_power(builder, 75.0, f64::NAN, f64::NAN, f64::NAN, -1, f64::NAN);
3544            let event = phytrace_builder_build(builder);
3545            assert!(!event.is_null());
3546
3547            let json_ptr = phytrace_event_to_json(event);
3548            assert!(!json_ptr.is_null());
3549
3550            let json = CStr::from_ptr(json_ptr).to_str().unwrap();
3551            assert!(json.contains("state_of_charge_pct"));
3552            assert!(json.contains("75"));
3553
3554            phytrace_string_free(json_ptr);
3555            phytrace_event_destroy(event);
3556            phytrace_agent_destroy(agent);
3557        }
3558    }
3559
3560    #[test]
3561    fn test_event_to_json_null() {
3562        unsafe {
3563            let json = phytrace_event_to_json(ptr::null());
3564            assert!(json.is_null());
3565        }
3566    }
3567
3568    #[test]
3569    fn test_event_destroy_null_is_noop() {
3570        unsafe {
3571            phytrace_event_destroy(ptr::null_mut()); // Should not crash
3572        }
3573    }
3574
3575    // =========================================================================
3576    // Send flow
3577    // =========================================================================
3578
3579    #[test]
3580    fn test_send_event() {
3581        let agent = create_test_agent();
3582        unsafe {
3583            phytrace_agent_start(agent);
3584
3585            let builder = create_test_builder(agent);
3586            phytrace_builder_set_power(builder, 80.0, f64::NAN, f64::NAN, f64::NAN, -1, f64::NAN);
3587            let event = phytrace_builder_build(builder);
3588            assert!(!event.is_null());
3589
3590            let result = phytrace_agent_send(agent, event);
3591            assert_eq!(result, PHYTRACE_OK);
3592
3593            phytrace_agent_destroy(agent);
3594        }
3595    }
3596
3597    #[test]
3598    fn test_send_null_event() {
3599        let agent = create_test_agent();
3600        unsafe {
3601            let result = phytrace_agent_send(agent, ptr::null_mut());
3602            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3603            phytrace_agent_destroy(agent);
3604        }
3605    }
3606
3607    #[test]
3608    fn test_send_null_agent() {
3609        let agent = create_test_agent();
3610        let builder = create_test_builder(agent);
3611        unsafe {
3612            phytrace_builder_set_power(builder, 50.0, f64::NAN, f64::NAN, f64::NAN, -1, f64::NAN);
3613            let event = phytrace_builder_build(builder);
3614            assert!(!event.is_null());
3615
3616            // Send with null agent — event should still be freed
3617            let result = phytrace_agent_send(ptr::null_mut(), event);
3618            assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3619
3620            phytrace_agent_destroy(agent);
3621        }
3622    }
3623
3624    // =========================================================================
3625    // Full multi-domain event
3626    // =========================================================================
3627
3628    #[test]
3629    fn test_full_multi_domain_event() {
3630        let agent = create_test_agent();
3631        let builder = create_test_builder(agent);
3632        unsafe {
3633            // Set event type
3634            phytrace_builder_set_event_type(builder, PHYTRACE_EVENT_TYPE_TELEMETRY_PERIODIC);
3635
3636            // Location (odom)
3637            let frame = CString::new("odom").unwrap();
3638            phytrace_builder_set_location(
3639                builder,
3640                f64::NAN,
3641                f64::NAN,
3642                f64::NAN,
3643                45.0,
3644                10.0,
3645                20.0,
3646                0.0,
3647                45.0,
3648                frame.as_ptr(),
3649                ptr::null(),
3650                i32::MIN,
3651            );
3652
3653            // Motion (odom twist)
3654            phytrace_builder_set_motion(
3655                builder,
3656                1.2,
3657                1.0,
3658                0.1,
3659                0.0,
3660                0.0,
3661                0.0,
3662                5.0,
3663                f64::NAN,
3664                f64::NAN,
3665                ptr::null(),
3666            );
3667
3668            // Power (battery_state)
3669            phytrace_builder_set_power(builder, 72.0, 48.0, -1.5, 30.0, 0, 95.0);
3670
3671            // Lidar (scan)
3672            let lid = CString::new("rplidar").unwrap();
3673            phytrace_builder_set_perception_lidar(
3674                builder,
3675                lid.as_ptr(),
3676                720,
3677                0.15,
3678                12.0,
3679                -180.0,
3680                180.0,
3681                0.5,
3682                0.8,
3683                -30.0,
3684                10.0,
3685            );
3686
3687            // IMU
3688            phytrace_builder_set_perception_imu(
3689                builder,
3690                0.01,
3691                -0.02,
3692                9.81,
3693                0.5,
3694                -0.1,
3695                0.2,
3696                f64::NAN,
3697                f64::NAN,
3698                f64::NAN,
3699                32.0,
3700            );
3701
3702            // Safety
3703            phytrace_builder_set_safety(
3704                builder,
3705                PHYTRACE_SAFETY_STATE_NORMAL,
3706                1,
3707                0,
3708                -1,
3709                f64::NAN,
3710                3.5,
3711                f64::NAN,
3712            );
3713
3714            // Navigation
3715            phytrace_builder_set_navigation(
3716                builder,
3717                PHYTRACE_LOCALIZATION_QUALITY_EXCELLENT,
3718                0.99,
3719                1,
3720                PHYTRACE_PATH_STATE_EXECUTING,
3721                50.0,
3722                35.0,
3723                100.0,
3724                50.0,
3725                0.0,
3726            );
3727
3728            // Joints
3729            let names = CString::new(r#"["wheel_l","wheel_r"]"#).unwrap();
3730            let pos = CString::new("[0.0, 0.0]").unwrap();
3731            let vel = CString::new("[15.0, 14.8]").unwrap();
3732            phytrace_builder_set_actuators_joints(
3733                builder,
3734                names.as_ptr(),
3735                pos.as_ptr(),
3736                vel.as_ptr(),
3737                ptr::null(),
3738            );
3739
3740            // Operational
3741            let tid = CString::new("delivery-789").unwrap();
3742            let ttype = CString::new("delivery").unwrap();
3743            phytrace_builder_set_operational(
3744                builder,
3745                PHYTRACE_OPERATIONAL_MODE_AUTONOMOUS,
3746                PHYTRACE_OPERATIONAL_STATE_NAVIGATING,
3747                tid.as_ptr(),
3748                ttype.as_ptr(),
3749                7200.0,
3750                ptr::null(),
3751            );
3752
3753            // Build and verify
3754            let json = build_and_get_json(builder);
3755            let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3756
3757            // Verify all domains present
3758            assert_eq!(v["event_type"], "telemetry_periodic");
3759            assert!(!v["location"].is_null());
3760            assert!(!v["motion"].is_null());
3761            assert!(!v["power"].is_null());
3762            assert!(!v["perception"].is_null());
3763            assert!(!v["safety"].is_null());
3764            assert!(!v["navigation"].is_null());
3765            assert!(!v["actuators"].is_null());
3766            assert!(!v["operational"].is_null());
3767
3768            // Spot-check key values
3769            assert_eq!(v["location"]["local"]["x_m"], 10.0);
3770            assert_eq!(v["motion"]["speed_mps"], 1.2);
3771            assert_eq!(v["power"]["battery"]["state_of_charge_pct"], 72.0);
3772            assert_eq!(v["perception"]["lidar"][0]["point_count"], 720);
3773            assert_eq!(v["perception"]["imu"]["accel_z_mps2"], 9.81);
3774            assert_eq!(v["safety"]["safety_state"], "normal");
3775            assert_eq!(v["navigation"]["localization"]["quality"], "excellent");
3776            assert_eq!(v["actuators"]["joints"].as_array().unwrap().len(), 2);
3777            assert_eq!(v["operational"]["state"], "navigating");
3778
3779            phytrace_agent_destroy(agent);
3780        }
3781    }
3782
3783    // =========================================================================
3784    // Enum conversion coverage
3785    // =========================================================================
3786
3787    #[test]
3788    fn test_all_event_types() {
3789        for i in 0..=17 {
3790            assert!(
3791                event_type_from_i32(i).is_some(),
3792                "EventType {i} should be valid"
3793            );
3794        }
3795        assert!(event_type_from_i32(18).is_none());
3796        assert!(event_type_from_i32(-1).is_none());
3797    }
3798
3799    #[test]
3800    fn test_all_source_types() {
3801        for i in 0..=17 {
3802            assert!(
3803                source_type_from_i32(i).is_some(),
3804                "SourceType {i} should be valid"
3805            );
3806        }
3807        assert!(source_type_from_i32(18).is_none());
3808        assert!(source_type_from_i32(-1).is_none());
3809    }
3810
3811    #[test]
3812    fn test_all_safety_states() {
3813        for i in 0..=5 {
3814            assert!(
3815                safety_state_from_i32(i).is_some(),
3816                "SafetyState {i} should be valid"
3817            );
3818        }
3819        assert!(safety_state_from_i32(6).is_none());
3820    }
3821
3822    #[test]
3823    fn test_all_operational_modes() {
3824        for i in 0..=7 {
3825            assert!(
3826                operational_mode_from_i32(i).is_some(),
3827                "OpMode {i} should be valid"
3828            );
3829        }
3830        assert!(operational_mode_from_i32(8).is_none());
3831    }
3832
3833    #[test]
3834    fn test_all_operational_states() {
3835        for i in 0..=11 {
3836            assert!(
3837                operational_state_from_i32(i).is_some(),
3838                "OpState {i} should be valid"
3839            );
3840        }
3841        assert!(operational_state_from_i32(12).is_none());
3842    }
3843
3844    #[test]
3845    fn test_all_localization_qualities() {
3846        for i in 0..=4 {
3847            assert!(
3848                localization_quality_from_i32(i).is_some(),
3849                "LocQuality {i} should be valid"
3850            );
3851        }
3852        assert!(localization_quality_from_i32(5).is_none());
3853    }
3854
3855    #[test]
3856    fn test_all_path_states() {
3857        for i in 0..=6 {
3858            assert!(
3859                path_state_from_i32(i).is_some(),
3860                "PathState {i} should be valid"
3861            );
3862        }
3863        assert!(path_state_from_i32(7).is_none());
3864    }
3865
3866    #[test]
3867    fn test_all_estop_types() {
3868        for i in 0..=3 {
3869            assert!(
3870                estop_type_from_i32(i).is_some(),
3871                "EStopType {i} should be valid"
3872            );
3873        }
3874        assert!(estop_type_from_i32(4).is_none());
3875    }
3876}