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