1#![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
46pub const PHYTRACE_OK: i32 = 0;
52pub const PHYTRACE_ERR_NULL_PTR: i32 = -1;
54pub const PHYTRACE_ERR_INVALID_UTF8: i32 = -2;
56pub const PHYTRACE_ERR_CONFIG: i32 = -3;
58pub const PHYTRACE_ERR_AGENT: i32 = -4;
60pub const PHYTRACE_ERR_BUILDER: i32 = -5;
62pub const PHYTRACE_ERR_TRANSPORT: i32 = -6;
64pub const PHYTRACE_ERR_RUNTIME: i32 = -7;
66pub const PHYTRACE_ERR_SERIALIZATION: i32 = -8;
68pub const PHYTRACE_ERR_INVALID_ENUM: i32 = -9;
70
71pub const PHYTRACE_EVENT_TYPE_TELEMETRY_PERIODIC: i32 = 0;
77pub const PHYTRACE_EVENT_TYPE_TELEMETRY_ON_CHANGE: i32 = 1;
79pub const PHYTRACE_EVENT_TYPE_TELEMETRY_SNAPSHOT: i32 = 2;
81pub const PHYTRACE_EVENT_TYPE_STATE_TRANSITION: i32 = 3;
83pub const PHYTRACE_EVENT_TYPE_MODE_CHANGE: i32 = 4;
85pub const PHYTRACE_EVENT_TYPE_TASK_STARTED: i32 = 5;
87pub const PHYTRACE_EVENT_TYPE_TASK_COMPLETED: i32 = 6;
89pub const PHYTRACE_EVENT_TYPE_TASK_FAILED: i32 = 7;
91pub const PHYTRACE_EVENT_TYPE_TASK_CANCELLED: i32 = 8;
93pub const PHYTRACE_EVENT_TYPE_GOAL_REACHED: i32 = 9;
95pub const PHYTRACE_EVENT_TYPE_PATH_BLOCKED: i32 = 10;
97pub const PHYTRACE_EVENT_TYPE_REROUTING: i32 = 11;
99pub const PHYTRACE_EVENT_TYPE_SAFETY_VIOLATION: i32 = 12;
101pub const PHYTRACE_EVENT_TYPE_EMERGENCY_STOP: i32 = 13;
103pub const PHYTRACE_EVENT_TYPE_SYSTEM_STARTUP: i32 = 14;
105pub const PHYTRACE_EVENT_TYPE_SYSTEM_SHUTDOWN: i32 = 15;
107pub const PHYTRACE_EVENT_TYPE_ERROR: i32 = 16;
109pub const PHYTRACE_EVENT_TYPE_CUSTOM: i32 = 17;
111
112pub const PHYTRACE_SOURCE_TYPE_AMR: i32 = 0;
118pub const PHYTRACE_SOURCE_TYPE_AGV: i32 = 1;
120pub const PHYTRACE_SOURCE_TYPE_AUTONOMOUS_FORKLIFT: i32 = 2;
122pub const PHYTRACE_SOURCE_TYPE_DELIVERY_ROBOT: i32 = 3;
124pub const PHYTRACE_SOURCE_TYPE_CLEANING_ROBOT: i32 = 4;
126pub const PHYTRACE_SOURCE_TYPE_INSPECTION_ROBOT: i32 = 5;
128pub const PHYTRACE_SOURCE_TYPE_SECURITY_ROBOT: i32 = 6;
130pub const PHYTRACE_SOURCE_TYPE_INDUSTRIAL_ARM: i32 = 7;
132pub const PHYTRACE_SOURCE_TYPE_COBOT: i32 = 8;
134pub const PHYTRACE_SOURCE_TYPE_MOBILE_MANIPULATOR: i32 = 9;
136pub const PHYTRACE_SOURCE_TYPE_AUTONOMOUS_VEHICLE: i32 = 10;
138pub const PHYTRACE_SOURCE_TYPE_ELECTRIC_VEHICLE: i32 = 11;
140pub const PHYTRACE_SOURCE_TYPE_DRONE: i32 = 12;
142pub const PHYTRACE_SOURCE_TYPE_HUMANOID: i32 = 13;
144pub const PHYTRACE_SOURCE_TYPE_QUADRUPED: i32 = 14;
146pub const PHYTRACE_SOURCE_TYPE_HUMAN: i32 = 15;
148pub const PHYTRACE_SOURCE_TYPE_CUSTOM: i32 = 16;
150pub const PHYTRACE_SOURCE_TYPE_SIMULATION: i32 = 17;
152
153pub const PHYTRACE_SAFETY_STATE_NORMAL: i32 = 0;
159pub const PHYTRACE_SAFETY_STATE_WARNING: i32 = 1;
161pub const PHYTRACE_SAFETY_STATE_PROTECTIVE_STOP: i32 = 2;
163pub const PHYTRACE_SAFETY_STATE_EMERGENCY_STOP: i32 = 3;
165pub const PHYTRACE_SAFETY_STATE_SAFETY_INTERLOCK: i32 = 4;
167pub const PHYTRACE_SAFETY_STATE_REDUCED_SPEED: i32 = 5;
169
170pub const PHYTRACE_OPERATIONAL_MODE_AUTONOMOUS: i32 = 0;
176pub const PHYTRACE_OPERATIONAL_MODE_MANUAL: i32 = 1;
178pub const PHYTRACE_OPERATIONAL_MODE_SEMI_AUTONOMOUS: i32 = 2;
180pub const PHYTRACE_OPERATIONAL_MODE_TELEOPERATED: i32 = 3;
182pub const PHYTRACE_OPERATIONAL_MODE_LEARNING: i32 = 4;
184pub const PHYTRACE_OPERATIONAL_MODE_MAINTENANCE: i32 = 5;
186pub const PHYTRACE_OPERATIONAL_MODE_EMERGENCY: i32 = 6;
188pub const PHYTRACE_OPERATIONAL_MODE_IDLE: i32 = 7;
190
191pub const PHYTRACE_OPERATIONAL_STATE_OFF: i32 = 0;
197pub const PHYTRACE_OPERATIONAL_STATE_BOOTING: i32 = 1;
199pub const PHYTRACE_OPERATIONAL_STATE_READY: i32 = 2;
201pub const PHYTRACE_OPERATIONAL_STATE_EXECUTING_TASK: i32 = 3;
203pub const PHYTRACE_OPERATIONAL_STATE_NAVIGATING: i32 = 4;
205pub const PHYTRACE_OPERATIONAL_STATE_MANIPULATING: i32 = 5;
207pub const PHYTRACE_OPERATIONAL_STATE_CHARGING: i32 = 6;
209pub const PHYTRACE_OPERATIONAL_STATE_PAUSED: i32 = 7;
211pub const PHYTRACE_OPERATIONAL_STATE_ERROR: i32 = 8;
213pub const PHYTRACE_OPERATIONAL_STATE_EMERGENCY_STOPPED: i32 = 9;
215pub const PHYTRACE_OPERATIONAL_STATE_SHUTTING_DOWN: i32 = 10;
217pub const PHYTRACE_OPERATIONAL_STATE_RECOVERING: i32 = 11;
219
220pub const PHYTRACE_LOCALIZATION_QUALITY_EXCELLENT: i32 = 0;
226pub const PHYTRACE_LOCALIZATION_QUALITY_GOOD: i32 = 1;
228pub const PHYTRACE_LOCALIZATION_QUALITY_FAIR: i32 = 2;
230pub const PHYTRACE_LOCALIZATION_QUALITY_POOR: i32 = 3;
232pub const PHYTRACE_LOCALIZATION_QUALITY_LOST: i32 = 4;
234
235pub const PHYTRACE_PATH_STATE_NONE: i32 = 0;
241pub const PHYTRACE_PATH_STATE_PLANNING: i32 = 1;
243pub const PHYTRACE_PATH_STATE_VALID: i32 = 2;
245pub const PHYTRACE_PATH_STATE_EXECUTING: i32 = 3;
247pub const PHYTRACE_PATH_STATE_BLOCKED: i32 = 4;
249pub const PHYTRACE_PATH_STATE_COMPLETED: i32 = 5;
251pub const PHYTRACE_PATH_STATE_FAILED: i32 = 6;
253
254pub const PHYTRACE_ESTOP_TYPE_SOFTWARE: i32 = 0;
260pub const PHYTRACE_ESTOP_TYPE_HARDWARE: i32 = 1;
262pub const PHYTRACE_ESTOP_TYPE_REMOTE: i32 = 2;
264pub const PHYTRACE_ESTOP_TYPE_AUTOMATIC: i32 = 3;
266
267fn 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
399thread_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
421pub struct PhyTraceAgentHandle {
427 agent: PhyTraceAgent,
428 runtime: tokio::runtime::Runtime,
429}
430
431pub struct PhyTraceBuilderHandle {
433 builder: EventBuilder,
434}
435
436pub struct PhyTraceEventHandle {
438 event: UdmEvent,
439}
440
441unsafe fn cstr_to_str<'a>(ptr: *const c_char) -> Result<&'a str, i32> {
446 if ptr.is_null() {
447 return Err(PHYTRACE_ERR_NULL_PTR);
448 }
449 CStr::from_ptr(ptr)
450 .to_str()
451 .map_err(|_e| PHYTRACE_ERR_INVALID_UTF8)
452}
453
454unsafe 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#[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#[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#[no_mangle]
502pub extern "C" fn phytrace_sdk_version() -> *const c_char {
503 static VERSION_CSTR: std::sync::OnceLock<CString> = std::sync::OnceLock::new();
505 VERSION_CSTR
506 .get_or_init(|| {
507 CString::new(crate::SDK_VERSION).expect("SDK_VERSION contains no interior NUL bytes")
508 })
509 .as_ptr()
510}
511
512#[no_mangle]
516pub extern "C" fn phytrace_udm_version() -> *const c_char {
517 static UDM_VERSION_CSTR: std::sync::OnceLock<CString> = std::sync::OnceLock::new();
518 UDM_VERSION_CSTR
519 .get_or_init(|| {
520 CString::new(crate::UDM_VERSION).expect("UDM_VERSION contains no interior NUL bytes")
521 })
522 .as_ptr()
523}
524
525#[no_mangle]
536pub unsafe extern "C" fn phytrace_agent_create_from_yaml(
537 yaml_str: *const c_char,
538) -> *mut PhyTraceAgentHandle {
539 clear_last_error();
540
541 let yaml = match cstr_to_str(yaml_str) {
542 Ok(s) => s,
543 Err(code) => {
544 set_last_error(if code == PHYTRACE_ERR_NULL_PTR {
545 "yaml_str is null"
546 } else {
547 "yaml_str contains invalid UTF-8"
548 });
549 return ptr::null_mut();
550 }
551 };
552
553 let config = match PhyTraceConfig::from_yaml(yaml) {
554 Ok(c) => c,
555 Err(e) => {
556 set_last_error(format!("Config parse error: {e}"));
557 return ptr::null_mut();
558 }
559 };
560
561 create_agent_from_config(config)
562}
563
564#[no_mangle]
571pub unsafe extern "C" fn phytrace_agent_create_from_file(
572 path: *const c_char,
573) -> *mut PhyTraceAgentHandle {
574 clear_last_error();
575
576 let path_str = match cstr_to_str(path) {
577 Ok(s) => s,
578 Err(code) => {
579 set_last_error(if code == PHYTRACE_ERR_NULL_PTR {
580 "path is null"
581 } else {
582 "path contains invalid UTF-8"
583 });
584 return ptr::null_mut();
585 }
586 };
587
588 let runtime = match tokio::runtime::Runtime::new() {
589 Ok(rt) => rt,
590 Err(e) => {
591 set_last_error(format!("Failed to create tokio runtime: {e}"));
592 return ptr::null_mut();
593 }
594 };
595
596 let config = match PhyTraceConfig::from_file(path_str) {
597 Ok(c) => c,
598 Err(e) => {
599 set_last_error(format!("Config file error: {e}"));
600 return ptr::null_mut();
601 }
602 };
603
604 drop(runtime);
606 create_agent_from_config(config)
607}
608
609fn create_agent_from_config(config: PhyTraceConfig) -> *mut PhyTraceAgentHandle {
611 let runtime = match tokio::runtime::Runtime::new() {
612 Ok(rt) => rt,
613 Err(e) => {
614 set_last_error(format!("Failed to create tokio runtime: {e}"));
615 return ptr::null_mut();
616 }
617 };
618
619 let agent = match runtime.block_on(PhyTraceAgent::from_config(config)) {
620 Ok(a) => a,
621 Err(e) => {
622 set_last_error(format!("Agent creation error: {e}"));
623 return ptr::null_mut();
624 }
625 };
626
627 Box::into_raw(Box::new(PhyTraceAgentHandle { agent, runtime }))
628}
629
630#[no_mangle]
641pub unsafe extern "C" fn phytrace_agent_create_mock(
642 source_id: *const c_char,
643 source_type: i32,
644) -> *mut PhyTraceAgentHandle {
645 clear_last_error();
646
647 let sid = match cstr_to_str(source_id) {
648 Ok(s) => s,
649 Err(code) => {
650 set_last_error(if code == PHYTRACE_ERR_NULL_PTR {
651 "source_id is null"
652 } else {
653 "source_id contains invalid UTF-8"
654 });
655 return ptr::null_mut();
656 }
657 };
658
659 let st = match source_type_from_i32(source_type) {
660 Some(st) => st,
661 None => {
662 set_last_error(format!("Invalid source_type: {source_type}"));
663 return ptr::null_mut();
664 }
665 };
666
667 let config = PhyTraceConfig::new(sid).with_source_type(st);
668 let transport = Box::new(MockTransport::new());
669
670 let runtime = match tokio::runtime::Runtime::new() {
671 Ok(rt) => rt,
672 Err(e) => {
673 set_last_error(format!("Failed to create tokio runtime: {e}"));
674 return ptr::null_mut();
675 }
676 };
677
678 let agent = match runtime.block_on(PhyTraceAgent::with_transport(config, transport)) {
679 Ok(a) => a,
680 Err(e) => {
681 set_last_error(format!("Agent creation error: {e}"));
682 return ptr::null_mut();
683 }
684 };
685
686 Box::into_raw(Box::new(PhyTraceAgentHandle { agent, runtime }))
687}
688
689#[no_mangle]
693pub unsafe extern "C" fn phytrace_agent_start(agent: *mut PhyTraceAgentHandle) -> i32 {
694 clear_last_error();
695
696 if agent.is_null() {
697 set_last_error("agent handle is null");
698 return PHYTRACE_ERR_NULL_PTR;
699 }
700
701 let handle = &*agent;
702 match handle.runtime.block_on(handle.agent.start()) {
703 Ok(()) => PHYTRACE_OK,
704 Err(e) => {
705 set_last_error(format!("Agent start error: {e}"));
706 PHYTRACE_ERR_AGENT
707 }
708 }
709}
710
711#[no_mangle]
715pub unsafe extern "C" fn phytrace_agent_stop(agent: *mut PhyTraceAgentHandle) -> i32 {
716 clear_last_error();
717
718 if agent.is_null() {
719 set_last_error("agent handle is null");
720 return PHYTRACE_ERR_NULL_PTR;
721 }
722
723 let handle = &*agent;
724 match handle.runtime.block_on(handle.agent.shutdown()) {
725 Ok(()) => PHYTRACE_OK,
726 Err(e) => {
727 set_last_error(format!("Agent stop error: {e}"));
728 PHYTRACE_ERR_AGENT
729 }
730 }
731}
732
733#[no_mangle]
738pub unsafe extern "C" fn phytrace_agent_destroy(agent: *mut PhyTraceAgentHandle) {
739 if !agent.is_null() {
740 let handle = Box::from_raw(agent);
741 drop(handle.runtime.block_on(handle.agent.shutdown()));
743 }
745}
746
747#[no_mangle]
751pub unsafe extern "C" fn phytrace_agent_is_running(agent: *const PhyTraceAgentHandle) -> i32 {
752 if agent.is_null() {
753 set_last_error("agent handle is null");
754 return PHYTRACE_ERR_NULL_PTR;
755 }
756
757 if (*agent).agent.is_running() {
758 1
759 } else {
760 0
761 }
762}
763
764#[no_mangle]
768pub unsafe extern "C" fn phytrace_agent_flush(agent: *mut PhyTraceAgentHandle) -> i32 {
769 clear_last_error();
770
771 if agent.is_null() {
772 set_last_error("agent handle is null");
773 return PHYTRACE_ERR_NULL_PTR;
774 }
775
776 let handle = &*agent;
777 match handle.runtime.block_on(handle.agent.flush()) {
778 Ok(()) => PHYTRACE_OK,
779 Err(e) => {
780 set_last_error(format!("Flush error: {e}"));
781 PHYTRACE_ERR_TRANSPORT
782 }
783 }
784}
785
786#[no_mangle]
796pub unsafe extern "C" fn phytrace_builder_new(
797 agent: *const PhyTraceAgentHandle,
798) -> *mut PhyTraceBuilderHandle {
799 clear_last_error();
800
801 if agent.is_null() {
802 set_last_error("agent handle is null");
803 return ptr::null_mut();
804 }
805
806 let config = (*agent).agent.config();
807 let builder = EventBuilder::new(config);
808 Box::into_raw(Box::new(PhyTraceBuilderHandle { builder }))
809}
810
811#[no_mangle]
816pub unsafe extern "C" fn phytrace_builder_set_event_type(
817 builder: *mut PhyTraceBuilderHandle,
818 event_type: i32,
819) -> i32 {
820 clear_last_error();
821
822 if builder.is_null() {
823 set_last_error("builder handle is null");
824 return PHYTRACE_ERR_NULL_PTR;
825 }
826
827 let et = match event_type_from_i32(event_type) {
828 Some(et) => et,
829 None => {
830 set_last_error(format!("Invalid event_type: {event_type}"));
831 return PHYTRACE_ERR_INVALID_ENUM;
832 }
833 };
834
835 let handle = &mut *builder;
836 let old_builder = std::mem::replace(
838 &mut handle.builder,
839 EventBuilder::new(&PhyTraceConfig::new("tmp")),
840 );
841 handle.builder = old_builder.event_type(et);
842 PHYTRACE_OK
843}
844
845#[no_mangle]
850pub unsafe extern "C" fn phytrace_builder_set_source_type(
851 builder: *mut PhyTraceBuilderHandle,
852 source_type: i32,
853) -> i32 {
854 clear_last_error();
855
856 if builder.is_null() {
857 set_last_error("builder handle is null");
858 return PHYTRACE_ERR_NULL_PTR;
859 }
860
861 let st = match source_type_from_i32(source_type) {
862 Some(st) => st,
863 None => {
864 set_last_error(format!("Invalid source_type: {source_type}"));
865 return PHYTRACE_ERR_INVALID_ENUM;
866 }
867 };
868
869 let handle = &mut *builder;
870 let old_builder = std::mem::replace(
871 &mut handle.builder,
872 EventBuilder::new(&PhyTraceConfig::new("tmp")),
873 );
874 handle.builder = old_builder.source_type(st);
875 PHYTRACE_OK
876}
877
878#[no_mangle]
886pub unsafe extern "C" fn phytrace_builder_build(
887 builder: *mut PhyTraceBuilderHandle,
888) -> *mut PhyTraceEventHandle {
889 clear_last_error();
890
891 if builder.is_null() {
892 set_last_error("builder handle is null");
893 return ptr::null_mut();
894 }
895
896 let handle = Box::from_raw(builder);
897 match handle.builder.build() {
898 Ok(event) => Box::into_raw(Box::new(PhyTraceEventHandle { event })),
899 Err(e) => {
900 set_last_error(format!("Build error: {e}"));
901 ptr::null_mut()
902 }
903 }
904}
905
906#[no_mangle]
914pub unsafe extern "C" fn phytrace_builder_build_unchecked(
915 builder: *mut PhyTraceBuilderHandle,
916) -> *mut PhyTraceEventHandle {
917 clear_last_error();
918
919 if builder.is_null() {
920 set_last_error("builder handle is null");
921 return ptr::null_mut();
922 }
923
924 let handle = Box::from_raw(builder);
925 let event = handle.builder.build_unchecked();
926 Box::into_raw(Box::new(PhyTraceEventHandle { event }))
927}
928
929#[no_mangle]
933pub unsafe extern "C" fn phytrace_builder_destroy(builder: *mut PhyTraceBuilderHandle) {
934 if !builder.is_null() {
935 drop(Box::from_raw(builder));
936 }
937}
938
939macro_rules! builder_mutate {
945 ($builder:expr, |$b:ident| $body:expr) => {{
946 let handle = &mut *$builder;
947 let $b = std::mem::replace(
948 &mut handle.builder,
949 EventBuilder::new(&PhyTraceConfig::new("tmp")),
950 );
951 handle.builder = $body;
952 }};
953}
954
955#[no_mangle]
977pub unsafe extern "C" fn phytrace_builder_set_location(
978 builder: *mut PhyTraceBuilderHandle,
979 latitude: f64,
980 longitude: f64,
981 altitude_m: f64,
982 heading_deg: f64,
983 x_m: f64,
984 y_m: f64,
985 z_m: f64,
986 yaw_deg: f64,
987 frame_id: *const c_char,
988 map_id: *const c_char,
989 floor: i32,
990) -> i32 {
991 clear_last_error();
992 if builder.is_null() {
993 set_last_error("builder handle is null");
994 return PHYTRACE_ERR_NULL_PTR;
995 }
996
997 let has_local = !x_m.is_nan() || !y_m.is_nan() || !z_m.is_nan() || !yaw_deg.is_nan();
998
999 let local = if has_local {
1000 Some(location::LocalCoordinates {
1001 x_m: if x_m.is_nan() { None } else { Some(x_m) },
1002 y_m: if y_m.is_nan() { None } else { Some(y_m) },
1003 z_m: if z_m.is_nan() { None } else { Some(z_m) },
1004 yaw_deg: if yaw_deg.is_nan() {
1005 None
1006 } else {
1007 Some(yaw_deg)
1008 },
1009 ..Default::default()
1010 })
1011 } else {
1012 None
1013 };
1014
1015 let loc = LocationDomain {
1016 latitude: if latitude.is_nan() {
1017 None
1018 } else {
1019 Some(latitude)
1020 },
1021 longitude: if longitude.is_nan() {
1022 None
1023 } else {
1024 Some(longitude)
1025 },
1026 altitude_m: if altitude_m.is_nan() {
1027 None
1028 } else {
1029 Some(altitude_m)
1030 },
1031 heading_deg: if heading_deg.is_nan() {
1032 None
1033 } else {
1034 Some(heading_deg)
1035 },
1036 local,
1037 frame_id: optional_cstr_to_string(frame_id),
1038 map_id: optional_cstr_to_string(map_id),
1039 floor: if floor == i32::MIN { None } else { Some(floor) },
1040 ..Default::default()
1041 };
1042
1043 builder_mutate!(builder, |b| b.location(loc));
1044 PHYTRACE_OK
1045}
1046
1047#[no_mangle]
1063pub unsafe extern "C" fn phytrace_builder_set_motion(
1064 builder: *mut PhyTraceBuilderHandle,
1065 speed_mps: f64,
1066 vx: f64,
1067 vy: f64,
1068 vz: f64,
1069 roll_dps: f64,
1070 pitch_dps: f64,
1071 yaw_dps: f64,
1072 cmd_linear_mps: f64,
1073 cmd_angular_dps: f64,
1074 frame_id: *const c_char,
1075) -> i32 {
1076 clear_last_error();
1077 if builder.is_null() {
1078 set_last_error("builder handle is null");
1079 return PHYTRACE_ERR_NULL_PTR;
1080 }
1081
1082 let has_linear = !vx.is_nan() || !vy.is_nan() || !vz.is_nan();
1083 let has_angular = !roll_dps.is_nan() || !pitch_dps.is_nan() || !yaw_dps.is_nan();
1084
1085 let mot = MotionDomain {
1086 speed_mps: if speed_mps.is_nan() {
1087 None
1088 } else {
1089 Some(speed_mps)
1090 },
1091 linear_velocity: if has_linear {
1092 Some(motion::LinearVelocity {
1093 x_mps: if vx.is_nan() { None } else { Some(vx) },
1094 y_mps: if vy.is_nan() { None } else { Some(vy) },
1095 z_mps: if vz.is_nan() { None } else { Some(vz) },
1096 })
1097 } else {
1098 None
1099 },
1100 angular_velocity: if has_angular {
1101 Some(motion::AngularVelocity {
1102 roll_dps: if roll_dps.is_nan() {
1103 None
1104 } else {
1105 Some(roll_dps)
1106 },
1107 pitch_dps: if pitch_dps.is_nan() {
1108 None
1109 } else {
1110 Some(pitch_dps)
1111 },
1112 yaw_dps: if yaw_dps.is_nan() {
1113 None
1114 } else {
1115 Some(yaw_dps)
1116 },
1117 })
1118 } else {
1119 None
1120 },
1121 commanded_linear_mps: if cmd_linear_mps.is_nan() {
1122 None
1123 } else {
1124 Some(cmd_linear_mps)
1125 },
1126 commanded_angular_dps: if cmd_angular_dps.is_nan() {
1127 None
1128 } else {
1129 Some(cmd_angular_dps)
1130 },
1131 frame_id: optional_cstr_to_string(frame_id),
1132 ..Default::default()
1133 };
1134
1135 builder_mutate!(builder, |b| b.motion(mot));
1136 PHYTRACE_OK
1137}
1138
1139#[no_mangle]
1156pub unsafe extern "C" fn phytrace_builder_set_power(
1157 builder: *mut PhyTraceBuilderHandle,
1158 soc_pct: f64,
1159 voltage_v: f64,
1160 current_a: f64,
1161 temperature_c: f64,
1162 is_charging: i32,
1163 health_pct: f64,
1164) -> i32 {
1165 clear_last_error();
1166 if builder.is_null() {
1167 set_last_error("builder handle is null");
1168 return PHYTRACE_ERR_NULL_PTR;
1169 }
1170
1171 let battery = power::Battery {
1172 state_of_charge_pct: if soc_pct.is_nan() {
1173 None
1174 } else {
1175 Some(soc_pct)
1176 },
1177 voltage_v: if voltage_v.is_nan() {
1178 None
1179 } else {
1180 Some(voltage_v)
1181 },
1182 current_a: if current_a.is_nan() {
1183 None
1184 } else {
1185 Some(current_a)
1186 },
1187 temperature_c: if temperature_c.is_nan() {
1188 None
1189 } else {
1190 Some(temperature_c)
1191 },
1192 state_of_health_pct: if health_pct.is_nan() {
1193 None
1194 } else {
1195 Some(health_pct)
1196 },
1197 ..Default::default()
1198 };
1199
1200 let charging = if is_charging >= 0 {
1201 Some(power::Charging {
1202 is_charging: Some(is_charging != 0),
1203 state: Some(if is_charging != 0 {
1204 enums::ChargingState::Charging
1205 } else {
1206 enums::ChargingState::NotCharging
1207 }),
1208 ..Default::default()
1209 })
1210 } else {
1211 None
1212 };
1213
1214 let pwr = PowerDomain {
1215 battery: Some(battery),
1216 charging,
1217 ..Default::default()
1218 };
1219
1220 builder_mutate!(builder, |b| b.power(pwr));
1221 PHYTRACE_OK
1222}
1223
1224#[no_mangle]
1242pub unsafe extern "C" fn phytrace_builder_set_perception_lidar(
1243 builder: *mut PhyTraceBuilderHandle,
1244 sensor_id: *const c_char,
1245 point_count: u32,
1246 min_range_m: f64,
1247 max_range_m: f64,
1248 min_angle_deg: f64,
1249 max_angle_deg: f64,
1250 angular_resolution_deg: f64,
1251 closest_range_m: f64,
1252 closest_angle_deg: f64,
1253 scan_frequency_hz: f64,
1254) -> i32 {
1255 clear_last_error();
1256 if builder.is_null() {
1257 set_last_error("builder handle is null");
1258 return PHYTRACE_ERR_NULL_PTR;
1259 }
1260
1261 let lidar = perception::LidarSensor {
1262 sensor_id: optional_cstr_to_string(sensor_id),
1263 point_count: if point_count == 0 {
1264 None
1265 } else {
1266 Some(point_count)
1267 },
1268 min_range_m: if min_range_m.is_nan() {
1269 None
1270 } else {
1271 Some(min_range_m)
1272 },
1273 max_range_m: if max_range_m.is_nan() {
1274 None
1275 } else {
1276 Some(max_range_m)
1277 },
1278 min_angle_deg: if min_angle_deg.is_nan() {
1279 None
1280 } else {
1281 Some(min_angle_deg)
1282 },
1283 max_angle_deg: if max_angle_deg.is_nan() {
1284 None
1285 } else {
1286 Some(max_angle_deg)
1287 },
1288 angular_resolution_deg: if angular_resolution_deg.is_nan() {
1289 None
1290 } else {
1291 Some(angular_resolution_deg)
1292 },
1293 closest_range_m: if closest_range_m.is_nan() {
1294 None
1295 } else {
1296 Some(closest_range_m)
1297 },
1298 closest_angle_deg: if closest_angle_deg.is_nan() {
1299 None
1300 } else {
1301 Some(closest_angle_deg)
1302 },
1303 scan_frequency_hz: if scan_frequency_hz.is_nan() {
1304 None
1305 } else {
1306 Some(scan_frequency_hz)
1307 },
1308 ..Default::default()
1309 };
1310
1311 let handle = &mut *builder;
1313 let old_builder = std::mem::replace(
1314 &mut handle.builder,
1315 EventBuilder::new(&PhyTraceConfig::new("tmp")),
1316 );
1317
1318 let existing_perception = old_builder.peek().perception.clone();
1320 let mut perc = existing_perception.unwrap_or_default();
1321 let lidars = perc.lidar.get_or_insert_with(Vec::new);
1322 lidars.push(lidar);
1323
1324 handle.builder = old_builder.perception(perc);
1325 PHYTRACE_OK
1326}
1327
1328#[no_mangle]
1342pub unsafe extern "C" fn phytrace_builder_set_perception_imu(
1343 builder: *mut PhyTraceBuilderHandle,
1344 accel_x_mps2: f64,
1345 accel_y_mps2: f64,
1346 accel_z_mps2: f64,
1347 gyro_x_dps: f64,
1348 gyro_y_dps: f64,
1349 gyro_z_dps: f64,
1350 mag_x_ut: f64,
1351 mag_y_ut: f64,
1352 mag_z_ut: f64,
1353 temperature_c: f64,
1354) -> i32 {
1355 clear_last_error();
1356 if builder.is_null() {
1357 set_last_error("builder handle is null");
1358 return PHYTRACE_ERR_NULL_PTR;
1359 }
1360
1361 let imu = perception::ImuSensor {
1362 accel_x_mps2: if accel_x_mps2.is_nan() {
1363 None
1364 } else {
1365 Some(accel_x_mps2)
1366 },
1367 accel_y_mps2: if accel_y_mps2.is_nan() {
1368 None
1369 } else {
1370 Some(accel_y_mps2)
1371 },
1372 accel_z_mps2: if accel_z_mps2.is_nan() {
1373 None
1374 } else {
1375 Some(accel_z_mps2)
1376 },
1377 gyro_x_dps: if gyro_x_dps.is_nan() {
1378 None
1379 } else {
1380 Some(gyro_x_dps)
1381 },
1382 gyro_y_dps: if gyro_y_dps.is_nan() {
1383 None
1384 } else {
1385 Some(gyro_y_dps)
1386 },
1387 gyro_z_dps: if gyro_z_dps.is_nan() {
1388 None
1389 } else {
1390 Some(gyro_z_dps)
1391 },
1392 mag_x_ut: if mag_x_ut.is_nan() {
1393 None
1394 } else {
1395 Some(mag_x_ut)
1396 },
1397 mag_y_ut: if mag_y_ut.is_nan() {
1398 None
1399 } else {
1400 Some(mag_y_ut)
1401 },
1402 mag_z_ut: if mag_z_ut.is_nan() {
1403 None
1404 } else {
1405 Some(mag_z_ut)
1406 },
1407 temperature_c: if temperature_c.is_nan() {
1408 None
1409 } else {
1410 Some(temperature_c)
1411 },
1412 ..Default::default()
1413 };
1414
1415 let handle = &mut *builder;
1417 let old_builder = std::mem::replace(
1418 &mut handle.builder,
1419 EventBuilder::new(&PhyTraceConfig::new("tmp")),
1420 );
1421 let existing_perception = old_builder.peek().perception.clone();
1422 let mut perc = existing_perception.unwrap_or_default();
1423 perc.imu = Some(imu);
1424
1425 handle.builder = old_builder.perception(perc);
1426 PHYTRACE_OK
1427}
1428
1429#[no_mangle]
1447pub unsafe extern "C" fn phytrace_builder_set_safety(
1448 builder: *mut PhyTraceBuilderHandle,
1449 safety_state: i32,
1450 is_safe: i32,
1451 estop_active: i32,
1452 estop_type: i32,
1453 speed_limit_mps: f64,
1454 closest_distance_m: f64,
1455 closest_human_m: f64,
1456) -> i32 {
1457 clear_last_error();
1458 if builder.is_null() {
1459 set_last_error("builder handle is null");
1460 return PHYTRACE_ERR_NULL_PTR;
1461 }
1462
1463 let ss = if safety_state >= 0 {
1464 match safety_state_from_i32(safety_state) {
1465 Some(s) => Some(s),
1466 None => {
1467 set_last_error(format!("Invalid safety_state: {safety_state}"));
1468 return PHYTRACE_ERR_INVALID_ENUM;
1469 }
1470 }
1471 } else {
1472 None
1473 };
1474
1475 let estop = if estop_active >= 0 {
1476 let et = if estop_type >= 0 {
1477 match estop_type_from_i32(estop_type) {
1478 Some(t) => Some(t),
1479 None => {
1480 set_last_error(format!("Invalid estop_type: {estop_type}"));
1481 return PHYTRACE_ERR_INVALID_ENUM;
1482 }
1483 }
1484 } else {
1485 None
1486 };
1487
1488 Some(safety::EStopInfo {
1489 is_active: Some(estop_active != 0),
1490 e_stop_type: et,
1491 ..Default::default()
1492 })
1493 } else {
1494 None
1495 };
1496
1497 let proximity = if !closest_distance_m.is_nan() || !closest_human_m.is_nan() {
1498 Some(safety::ProximityInfo {
1499 closest_distance_m: if closest_distance_m.is_nan() {
1500 None
1501 } else {
1502 Some(closest_distance_m)
1503 },
1504 closest_human_m: if closest_human_m.is_nan() {
1505 None
1506 } else {
1507 Some(closest_human_m)
1508 },
1509 ..Default::default()
1510 })
1511 } else {
1512 None
1513 };
1514
1515 let saf = SafetyDomain {
1516 safety_state: ss,
1517 is_safe: if is_safe >= 0 {
1518 Some(is_safe != 0)
1519 } else {
1520 None
1521 },
1522 e_stop: estop,
1523 speed_limit_mps: if speed_limit_mps.is_nan() {
1524 None
1525 } else {
1526 Some(speed_limit_mps)
1527 },
1528 proximity,
1529 ..Default::default()
1530 };
1531
1532 builder_mutate!(builder, |b| b.safety(saf));
1533 PHYTRACE_OK
1534}
1535
1536#[no_mangle]
1554pub unsafe extern "C" fn phytrace_builder_set_navigation(
1555 builder: *mut PhyTraceBuilderHandle,
1556 loc_quality: i32,
1557 loc_confidence: f64,
1558 is_localized: i32,
1559 path_state: i32,
1560 path_length_m: f64,
1561 path_remaining_m: f64,
1562 goal_x: f64,
1563 goal_y: f64,
1564 goal_orientation_deg: f64,
1565) -> i32 {
1566 clear_last_error();
1567 if builder.is_null() {
1568 set_last_error("builder handle is null");
1569 return PHYTRACE_ERR_NULL_PTR;
1570 }
1571
1572 let localization = if loc_quality >= 0 || !loc_confidence.is_nan() || is_localized >= 0 {
1573 let quality = if loc_quality >= 0 {
1574 match localization_quality_from_i32(loc_quality) {
1575 Some(q) => Some(q),
1576 None => {
1577 set_last_error(format!("Invalid loc_quality: {loc_quality}"));
1578 return PHYTRACE_ERR_INVALID_ENUM;
1579 }
1580 }
1581 } else {
1582 None
1583 };
1584
1585 Some(navigation::Localization {
1586 quality,
1587 confidence: if loc_confidence.is_nan() {
1588 None
1589 } else {
1590 Some(loc_confidence)
1591 },
1592 is_localized: if is_localized >= 0 {
1593 Some(is_localized != 0)
1594 } else {
1595 None
1596 },
1597 ..Default::default()
1598 })
1599 } else {
1600 None
1601 };
1602
1603 let path = if path_state >= 0 || !path_length_m.is_nan() || !path_remaining_m.is_nan() {
1604 let ps = if path_state >= 0 {
1605 match path_state_from_i32(path_state) {
1606 Some(p) => Some(p),
1607 None => {
1608 set_last_error(format!("Invalid path_state: {path_state}"));
1609 return PHYTRACE_ERR_INVALID_ENUM;
1610 }
1611 }
1612 } else {
1613 None
1614 };
1615
1616 Some(navigation::PathInfo {
1617 state: ps,
1618 length_m: if path_length_m.is_nan() {
1619 None
1620 } else {
1621 Some(path_length_m)
1622 },
1623 remaining_m: if path_remaining_m.is_nan() {
1624 None
1625 } else {
1626 Some(path_remaining_m)
1627 },
1628 ..Default::default()
1629 })
1630 } else {
1631 None
1632 };
1633
1634 let goal = if !goal_x.is_nan() || !goal_y.is_nan() || !goal_orientation_deg.is_nan() {
1635 Some(navigation::NavigationGoal {
1636 position: if !goal_x.is_nan() || !goal_y.is_nan() {
1637 Some(Position2D {
1638 x_m: if goal_x.is_nan() { None } else { Some(goal_x) },
1639 y_m: if goal_y.is_nan() { None } else { Some(goal_y) },
1640 })
1641 } else {
1642 None
1643 },
1644 orientation_deg: if goal_orientation_deg.is_nan() {
1645 None
1646 } else {
1647 Some(goal_orientation_deg)
1648 },
1649 ..Default::default()
1650 })
1651 } else {
1652 None
1653 };
1654
1655 let nav = NavigationDomain {
1656 localization,
1657 path,
1658 goal,
1659 ..Default::default()
1660 };
1661
1662 builder_mutate!(builder, |b| b.navigation(nav));
1663 PHYTRACE_OK
1664}
1665
1666#[no_mangle]
1682pub unsafe extern "C" fn phytrace_builder_set_actuators_joints(
1683 builder: *mut PhyTraceBuilderHandle,
1684 names_json: *const c_char,
1685 positions_json: *const c_char,
1686 velocities_json: *const c_char,
1687 efforts_json: *const c_char,
1688) -> i32 {
1689 clear_last_error();
1690 if builder.is_null() {
1691 set_last_error("builder handle is null");
1692 return PHYTRACE_ERR_NULL_PTR;
1693 }
1694
1695 let names_str = match cstr_to_str(names_json) {
1697 Ok(s) => s,
1698 Err(code) => {
1699 set_last_error(if code == PHYTRACE_ERR_NULL_PTR {
1700 "names_json is null"
1701 } else {
1702 "names_json contains invalid UTF-8"
1703 });
1704 return code;
1705 }
1706 };
1707 let names: Vec<String> = match serde_json::from_str(names_str) {
1708 Ok(n) => n,
1709 Err(e) => {
1710 set_last_error(format!("Failed to parse names_json: {e}"));
1711 return PHYTRACE_ERR_SERIALIZATION;
1712 }
1713 };
1714
1715 let positions: Option<Vec<f64>> = parse_optional_json_array(positions_json);
1717 let velocities: Option<Vec<f64>> = parse_optional_json_array(velocities_json);
1718 let efforts: Option<Vec<f64>> = parse_optional_json_array(efforts_json);
1719
1720 let mut joints = Vec::with_capacity(names.len());
1722 for (i, name) in names.into_iter().enumerate() {
1723 joints.push(actuators::Joint {
1724 name: Some(name),
1725 position: positions.as_ref().and_then(|p| p.get(i).copied()),
1726 velocity: velocities.as_ref().and_then(|v| v.get(i).copied()),
1727 effort: efforts.as_ref().and_then(|e| e.get(i).copied()),
1728 ..Default::default()
1729 });
1730 }
1731
1732 let act = ActuatorsDomain {
1733 joints: Some(joints),
1734 ..Default::default()
1735 };
1736
1737 builder_mutate!(builder, |b| b.actuators(act));
1738 PHYTRACE_OK
1739}
1740
1741unsafe fn parse_optional_json_array<T: serde::de::DeserializeOwned>(
1743 ptr: *const c_char,
1744) -> Option<T> {
1745 if ptr.is_null() {
1746 return None;
1747 }
1748 let s = match CStr::from_ptr(ptr).to_str() {
1749 Ok(s) => s,
1750 Err(_) => return None,
1751 };
1752 serde_json::from_str(s).ok()
1753}
1754
1755#[no_mangle]
1772pub unsafe extern "C" fn phytrace_builder_set_operational(
1773 builder: *mut PhyTraceBuilderHandle,
1774 mode: i32,
1775 state: i32,
1776 task_id: *const c_char,
1777 task_type: *const c_char,
1778 uptime_sec: f64,
1779 mission_id: *const c_char,
1780) -> i32 {
1781 clear_last_error();
1782 if builder.is_null() {
1783 set_last_error("builder handle is null");
1784 return PHYTRACE_ERR_NULL_PTR;
1785 }
1786
1787 let op_mode = if mode >= 0 {
1788 match operational_mode_from_i32(mode) {
1789 Some(m) => Some(m),
1790 None => {
1791 set_last_error(format!("Invalid operational mode: {mode}"));
1792 return PHYTRACE_ERR_INVALID_ENUM;
1793 }
1794 }
1795 } else {
1796 None
1797 };
1798
1799 let op_state = if state >= 0 {
1800 match operational_state_from_i32(state) {
1801 Some(s) => Some(s),
1802 None => {
1803 set_last_error(format!("Invalid operational state: {state}"));
1804 return PHYTRACE_ERR_INVALID_ENUM;
1805 }
1806 }
1807 } else {
1808 None
1809 };
1810
1811 let task = {
1812 let tid = optional_cstr_to_string(task_id);
1813 let ttype = optional_cstr_to_string(task_type);
1814 if tid.is_some() || ttype.is_some() {
1815 Some(operational::Task {
1816 task_id: tid,
1817 task_type: ttype,
1818 ..Default::default()
1819 })
1820 } else {
1821 None
1822 }
1823 };
1824
1825 let ops = OperationalDomain {
1826 mode: op_mode,
1827 state: op_state,
1828 task,
1829 uptime_sec: if uptime_sec.is_nan() {
1830 None
1831 } else {
1832 Some(uptime_sec)
1833 },
1834 mission_id: optional_cstr_to_string(mission_id),
1835 ..Default::default()
1836 };
1837
1838 builder_mutate!(builder, |b| b.operational(ops));
1839 PHYTRACE_OK
1840}
1841
1842#[no_mangle]
1859pub unsafe extern "C" fn phytrace_builder_set_identity(
1860 builder: *mut PhyTraceBuilderHandle,
1861 source_id: *const c_char,
1862 platform: *const c_char,
1863 model: *const c_char,
1864 firmware_version: *const c_char,
1865 serial_number: *const c_char,
1866 fleet_id: *const c_char,
1867 site_id: *const c_char,
1868) -> i32 {
1869 clear_last_error();
1870 if builder.is_null() {
1871 set_last_error("builder handle is null");
1872 return PHYTRACE_ERR_NULL_PTR;
1873 }
1874
1875 let ident = IdentityDomain {
1876 source_id: optional_cstr_to_string(source_id),
1877 platform: optional_cstr_to_string(platform),
1878 model: optional_cstr_to_string(model),
1879 firmware_version: optional_cstr_to_string(firmware_version),
1880 serial_number: optional_cstr_to_string(serial_number),
1881 fleet_id: optional_cstr_to_string(fleet_id),
1882 site_id: optional_cstr_to_string(site_id),
1883 ..Default::default()
1884 };
1885
1886 builder_mutate!(builder, |b| b.identity(ident));
1887 PHYTRACE_OK
1888}
1889
1890#[no_mangle]
1905pub unsafe extern "C" fn phytrace_builder_set_communication(
1906 builder: *mut PhyTraceBuilderHandle,
1907 is_connected: i32,
1908 signal_strength_dbm: i32,
1909 latency_ms: f64,
1910 packet_loss_pct: f64,
1911) -> i32 {
1912 clear_last_error();
1913 if builder.is_null() {
1914 set_last_error("builder handle is null");
1915 return PHYTRACE_ERR_NULL_PTR;
1916 }
1917
1918 let network = communication::NetworkInfo {
1919 is_connected: if is_connected >= 0 {
1920 Some(is_connected != 0)
1921 } else {
1922 None
1923 },
1924 signal_strength_dbm: if signal_strength_dbm == i32::MIN {
1925 None
1926 } else {
1927 Some(signal_strength_dbm)
1928 },
1929 latency_ms: if latency_ms.is_nan() {
1930 None
1931 } else {
1932 Some(latency_ms)
1933 },
1934 packet_loss_pct: if packet_loss_pct.is_nan() {
1935 None
1936 } else {
1937 Some(packet_loss_pct)
1938 },
1939 ..Default::default()
1940 };
1941
1942 let comm = CommunicationDomain {
1943 network: Some(network),
1944 ..Default::default()
1945 };
1946
1947 builder_mutate!(builder, |b| b.communication(comm));
1948 PHYTRACE_OK
1949}
1950
1951#[no_mangle]
1966pub unsafe extern "C" fn phytrace_builder_set_context(
1967 builder: *mut PhyTraceBuilderHandle,
1968 timezone: *const c_char,
1969 facility_id: *const c_char,
1970 facility_name: *const c_char,
1971 human_count: i32,
1972 robot_count: i32,
1973) -> i32 {
1974 clear_last_error();
1975 if builder.is_null() {
1976 set_last_error("builder handle is null");
1977 return PHYTRACE_ERR_NULL_PTR;
1978 }
1979
1980 let time = {
1981 let tz = optional_cstr_to_string(timezone);
1982 if tz.is_some() {
1983 Some(context::TimeContext {
1984 timezone: tz,
1985 ..Default::default()
1986 })
1987 } else {
1988 None
1989 }
1990 };
1991
1992 let facility = {
1993 let fid = optional_cstr_to_string(facility_id);
1994 let fname = optional_cstr_to_string(facility_name);
1995 if fid.is_some() || fname.is_some() || human_count >= 0 || robot_count >= 0 {
1996 Some(context::FacilityContext {
1997 facility_id: fid,
1998 name: fname,
1999 human_count: if human_count >= 0 {
2000 Some(u32::try_from(human_count).expect("human_count is non-negative"))
2001 } else {
2002 None
2003 },
2004 robot_count: if robot_count >= 0 {
2005 Some(u32::try_from(robot_count).expect("robot_count is non-negative"))
2006 } else {
2007 None
2008 },
2009 ..Default::default()
2010 })
2011 } else {
2012 None
2013 }
2014 };
2015
2016 let ctx = ContextDomain {
2017 time,
2018 facility,
2019 ..Default::default()
2020 };
2021
2022 builder_mutate!(builder, |b| b.context(ctx));
2023 PHYTRACE_OK
2024}
2025
2026#[no_mangle]
2037pub unsafe extern "C" fn phytrace_builder_set_extensions_json(
2038 builder: *mut PhyTraceBuilderHandle,
2039 json_str: *const c_char,
2040) -> i32 {
2041 clear_last_error();
2042 if builder.is_null() {
2043 set_last_error("builder handle is null");
2044 return PHYTRACE_ERR_NULL_PTR;
2045 }
2046
2047 let s = match cstr_to_str(json_str) {
2048 Ok(s) => s,
2049 Err(code) => {
2050 set_last_error(if code == PHYTRACE_ERR_NULL_PTR {
2051 "json_str is null"
2052 } else {
2053 "json_str contains invalid UTF-8"
2054 });
2055 return code;
2056 }
2057 };
2058
2059 let value: serde_json::Value = match serde_json::from_str(s) {
2060 Ok(v) => v,
2061 Err(e) => {
2062 set_last_error(format!("Failed to parse extensions JSON: {e}"));
2063 return PHYTRACE_ERR_SERIALIZATION;
2064 }
2065 };
2066
2067 builder_mutate!(builder, |b| b.extensions(value));
2068 PHYTRACE_OK
2069}
2070
2071#[no_mangle]
2081pub unsafe extern "C" fn phytrace_agent_send(
2082 agent: *mut PhyTraceAgentHandle,
2083 event: *mut PhyTraceEventHandle,
2084) -> i32 {
2085 clear_last_error();
2086
2087 if agent.is_null() {
2088 set_last_error("agent handle is null");
2089 if !event.is_null() {
2091 drop(Box::from_raw(event));
2092 }
2093 return PHYTRACE_ERR_NULL_PTR;
2094 }
2095
2096 if event.is_null() {
2097 set_last_error("event handle is null");
2098 return PHYTRACE_ERR_NULL_PTR;
2099 }
2100
2101 let event_handle = Box::from_raw(event);
2102 let handle = &*agent;
2103
2104 match handle
2105 .runtime
2106 .block_on(handle.agent.send(event_handle.event))
2107 {
2108 Ok(()) => PHYTRACE_OK,
2109 Err(e) => {
2110 set_last_error(format!("Send error: {e}"));
2111 PHYTRACE_ERR_TRANSPORT
2112 }
2113 }
2114}
2115
2116#[no_mangle]
2121pub unsafe extern "C" fn phytrace_event_to_json(event: *const PhyTraceEventHandle) -> *mut c_char {
2122 clear_last_error();
2123
2124 if event.is_null() {
2125 set_last_error("event handle is null");
2126 return ptr::null_mut();
2127 }
2128
2129 match (*event).event.to_json() {
2130 Ok(json) => match CString::new(json) {
2131 Ok(cstr) => cstr.into_raw(),
2132 Err(e) => {
2133 set_last_error(format!("JSON contains null byte: {e}"));
2134 ptr::null_mut()
2135 }
2136 },
2137 Err(e) => {
2138 set_last_error(format!("Serialization error: {e}"));
2139 ptr::null_mut()
2140 }
2141 }
2142}
2143
2144#[no_mangle]
2148pub unsafe extern "C" fn phytrace_event_destroy(event: *mut PhyTraceEventHandle) {
2149 if !event.is_null() {
2150 drop(Box::from_raw(event));
2151 }
2152}
2153
2154#[cfg(test)]
2159#[allow(
2160 clippy::undocumented_unsafe_blocks,
2161 clippy::multiple_unsafe_ops_per_block,
2162 clippy::unwrap_used,
2163 reason = "FFI tests exercise unsafe C-ABI functions in tightly grouped sequences; \
2164 individual SAFETY comments and split blocks would obscure the test logic, \
2165 and unwrap is acceptable in tests for fail-fast behavior"
2166)]
2167mod tests {
2168 use super::*;
2169 use std::ffi::CString;
2170
2171 fn create_test_agent() -> *mut PhyTraceAgentHandle {
2176 std::env::set_var("PHYWARE_DEV_MODE", "1");
2178 let source_id = CString::new("test-robot-001").unwrap();
2179 unsafe { phytrace_agent_create_mock(source_id.as_ptr(), PHYTRACE_SOURCE_TYPE_AMR) }
2180 }
2181
2182 fn create_test_builder(agent: *const PhyTraceAgentHandle) -> *mut PhyTraceBuilderHandle {
2183 unsafe { phytrace_builder_new(agent) }
2184 }
2185
2186 #[test]
2191 fn test_sdk_version() {
2192 let version = phytrace_sdk_version();
2193 assert!(!version.is_null());
2194 let s = unsafe { CStr::from_ptr(version) }.to_str().unwrap();
2195 assert_eq!(s, env!("CARGO_PKG_VERSION"));
2196 }
2197
2198 #[test]
2199 fn test_udm_version() {
2200 let version = phytrace_udm_version();
2201 assert!(!version.is_null());
2202 let s = unsafe { CStr::from_ptr(version) }.to_str().unwrap();
2203 assert_eq!(s, "0.0.3");
2204 }
2205
2206 #[test]
2211 fn test_last_error_initially_null() {
2212 clear_last_error();
2213 let err = phytrace_last_error();
2214 assert!(err.is_null());
2215 }
2216
2217 #[test]
2218 fn test_last_error_after_null_ptr() {
2219 unsafe {
2220 let result = phytrace_agent_start(ptr::null_mut());
2221 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2222 let err = phytrace_last_error();
2223 assert!(!err.is_null());
2224 let msg = CStr::from_ptr(err).to_str().unwrap();
2225 assert!(msg.contains("null"));
2226 }
2227 }
2228
2229 #[test]
2230 fn test_string_free_null_is_noop() {
2231 unsafe {
2232 phytrace_string_free(ptr::null_mut()); }
2234 }
2235
2236 #[test]
2241 fn test_agent_create_mock() {
2242 let agent = create_test_agent();
2243 assert!(!agent.is_null());
2244 unsafe { phytrace_agent_destroy(agent) };
2245 }
2246
2247 #[test]
2248 fn test_agent_create_mock_null_source_id() {
2249 unsafe {
2250 let agent = phytrace_agent_create_mock(ptr::null(), PHYTRACE_SOURCE_TYPE_AMR);
2251 assert!(agent.is_null());
2252 let err = phytrace_last_error();
2253 assert!(!err.is_null());
2254 }
2255 }
2256
2257 #[test]
2258 fn test_agent_create_mock_invalid_source_type() {
2259 let sid = CString::new("test").unwrap();
2260 unsafe {
2261 let agent = phytrace_agent_create_mock(sid.as_ptr(), 9999);
2262 assert!(agent.is_null());
2263 let err = phytrace_last_error();
2264 assert!(!err.is_null());
2265 let msg = CStr::from_ptr(err).to_str().unwrap();
2266 assert!(msg.contains("Invalid source_type"));
2267 }
2268 }
2269
2270 #[test]
2271 fn test_agent_start_stop() {
2272 let agent = create_test_agent();
2273 assert!(!agent.is_null());
2274 unsafe {
2275 let result = phytrace_agent_start(agent);
2276 assert_eq!(result, PHYTRACE_OK);
2277
2278 assert_eq!(phytrace_agent_is_running(agent), 1);
2279
2280 let result = phytrace_agent_stop(agent);
2281 assert_eq!(result, PHYTRACE_OK);
2282
2283 phytrace_agent_destroy(agent);
2284 }
2285 }
2286
2287 #[test]
2288 fn test_agent_is_running_null() {
2289 unsafe {
2290 let result = phytrace_agent_is_running(ptr::null());
2291 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2292 }
2293 }
2294
2295 #[test]
2296 fn test_agent_destroy_null_is_noop() {
2297 unsafe {
2298 phytrace_agent_destroy(ptr::null_mut()); }
2300 }
2301
2302 #[test]
2303 fn test_agent_flush_null() {
2304 unsafe {
2305 let result = phytrace_agent_flush(ptr::null_mut());
2306 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2307 }
2308 }
2309
2310 #[test]
2311 fn test_agent_flush() {
2312 let agent = create_test_agent();
2313 unsafe {
2314 phytrace_agent_start(agent);
2315 let result = phytrace_agent_flush(agent);
2316 assert_eq!(result, PHYTRACE_OK);
2317 phytrace_agent_destroy(agent);
2318 }
2319 }
2320
2321 #[test]
2326 fn test_agent_create_from_yaml_null() {
2327 unsafe {
2328 let agent = phytrace_agent_create_from_yaml(ptr::null());
2329 assert!(agent.is_null());
2330 }
2331 }
2332
2333 #[test]
2334 fn test_agent_create_from_yaml_invalid() {
2335 let yaml = CString::new("not: {valid: yaml: :::").unwrap();
2336 unsafe {
2337 let agent = phytrace_agent_create_from_yaml(yaml.as_ptr());
2338 assert!(agent.is_null());
2339 let err = phytrace_last_error();
2340 assert!(!err.is_null());
2341 }
2342 }
2343
2344 #[test]
2345 fn test_agent_create_from_file_null() {
2346 unsafe {
2347 let agent = phytrace_agent_create_from_file(ptr::null());
2348 assert!(agent.is_null());
2349 }
2350 }
2351
2352 #[test]
2353 fn test_agent_create_from_file_nonexistent() {
2354 let path = CString::new("/nonexistent/path/config.yaml").unwrap();
2355 unsafe {
2356 let agent = phytrace_agent_create_from_file(path.as_ptr());
2357 assert!(agent.is_null());
2358 let err = phytrace_last_error();
2359 assert!(!err.is_null());
2360 }
2361 }
2362
2363 #[test]
2368 fn test_builder_new_and_destroy() {
2369 let agent = create_test_agent();
2370 let builder = create_test_builder(agent);
2371 assert!(!builder.is_null());
2372 unsafe {
2373 phytrace_builder_destroy(builder);
2374 phytrace_agent_destroy(agent);
2375 }
2376 }
2377
2378 #[test]
2379 fn test_builder_new_null_agent() {
2380 unsafe {
2381 let builder = phytrace_builder_new(ptr::null());
2382 assert!(builder.is_null());
2383 }
2384 }
2385
2386 #[test]
2387 fn test_builder_destroy_null_is_noop() {
2388 unsafe {
2389 phytrace_builder_destroy(ptr::null_mut()); }
2391 }
2392
2393 #[test]
2394 fn test_builder_set_event_type() {
2395 let agent = create_test_agent();
2396 let builder = create_test_builder(agent);
2397 unsafe {
2398 let result =
2399 phytrace_builder_set_event_type(builder, PHYTRACE_EVENT_TYPE_TELEMETRY_PERIODIC);
2400 assert_eq!(result, PHYTRACE_OK);
2401 phytrace_builder_destroy(builder);
2402 phytrace_agent_destroy(agent);
2403 }
2404 }
2405
2406 #[test]
2407 fn test_builder_set_event_type_invalid() {
2408 let agent = create_test_agent();
2409 let builder = create_test_builder(agent);
2410 unsafe {
2411 let result = phytrace_builder_set_event_type(builder, 999);
2412 assert_eq!(result, PHYTRACE_ERR_INVALID_ENUM);
2413 phytrace_builder_destroy(builder);
2414 phytrace_agent_destroy(agent);
2415 }
2416 }
2417
2418 #[test]
2419 fn test_builder_set_event_type_null_builder() {
2420 unsafe {
2421 let result = phytrace_builder_set_event_type(
2422 ptr::null_mut(),
2423 PHYTRACE_EVENT_TYPE_TELEMETRY_PERIODIC,
2424 );
2425 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2426 }
2427 }
2428
2429 #[test]
2430 fn test_builder_set_source_type() {
2431 let agent = create_test_agent();
2432 let builder = create_test_builder(agent);
2433 unsafe {
2434 let result = phytrace_builder_set_source_type(builder, PHYTRACE_SOURCE_TYPE_DRONE);
2435 assert_eq!(result, PHYTRACE_OK);
2436 phytrace_builder_destroy(builder);
2437 phytrace_agent_destroy(agent);
2438 }
2439 }
2440
2441 #[test]
2442 fn test_builder_set_source_type_invalid() {
2443 let agent = create_test_agent();
2444 let builder = create_test_builder(agent);
2445 unsafe {
2446 let result = phytrace_builder_set_source_type(builder, -99);
2447 assert_eq!(result, PHYTRACE_ERR_INVALID_ENUM);
2448 phytrace_builder_destroy(builder);
2449 phytrace_agent_destroy(agent);
2450 }
2451 }
2452
2453 unsafe fn build_and_get_json(builder: *mut PhyTraceBuilderHandle) -> String {
2459 let event = phytrace_builder_build(builder);
2460 assert!(
2461 !event.is_null(),
2462 "Builder build failed. Error: {:?}",
2463 CStr::from_ptr(phytrace_last_error()).to_str().ok()
2464 );
2465 let json_ptr = phytrace_event_to_json(event);
2466 assert!(!json_ptr.is_null());
2467 let json = CStr::from_ptr(json_ptr).to_str().unwrap().to_owned();
2468 phytrace_string_free(json_ptr);
2469 phytrace_event_destroy(event);
2470 json
2471 }
2472
2473 #[test]
2474 fn test_build_unchecked_empty_builder() {
2475 let agent = create_test_agent();
2476 let builder = create_test_builder(agent);
2477 unsafe {
2478 let event = phytrace_builder_build_unchecked(builder);
2480 assert!(!event.is_null());
2481 phytrace_event_destroy(event);
2482 phytrace_agent_destroy(agent);
2483 }
2484 }
2485
2486 #[test]
2487 fn test_build_unchecked_null_builder() {
2488 unsafe {
2489 let event = phytrace_builder_build_unchecked(ptr::null_mut());
2490 assert!(event.is_null());
2491 }
2492 }
2493
2494 #[test]
2499 fn test_location_gps() {
2500 let agent = create_test_agent();
2501 let builder = create_test_builder(agent);
2502 unsafe {
2503 let result = phytrace_builder_set_location(
2504 builder,
2505 41.8781,
2506 -87.6298, f64::NAN, 90.0, f64::NAN,
2510 f64::NAN,
2511 f64::NAN,
2512 f64::NAN, ptr::null(),
2514 ptr::null(), i32::MIN, );
2517 assert_eq!(result, PHYTRACE_OK);
2518
2519 let json = build_and_get_json(builder);
2520 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2521 assert_eq!(v["location"]["latitude"], 41.8781);
2522 assert_eq!(v["location"]["longitude"], -87.6298);
2523 assert_eq!(v["location"]["heading_deg"], 90.0);
2524 assert!(v["location"]["altitude_m"].is_null());
2525
2526 phytrace_agent_destroy(agent);
2527 }
2528 }
2529
2530 #[test]
2531 fn test_location_local_coords() {
2532 let agent = create_test_agent();
2533 let builder = create_test_builder(agent);
2534 unsafe {
2535 let frame = CString::new("odom").unwrap();
2536 let map = CString::new("warehouse-1").unwrap();
2537 let result = phytrace_builder_set_location(
2538 builder,
2539 f64::NAN,
2540 f64::NAN,
2541 f64::NAN, f64::NAN, 10.5,
2544 20.3,
2545 0.0,
2546 45.0, frame.as_ptr(),
2548 map.as_ptr(),
2549 2, );
2551 assert_eq!(result, PHYTRACE_OK);
2552
2553 let json = build_and_get_json(builder);
2554 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2555 assert_eq!(v["location"]["local"]["x_m"], 10.5);
2556 assert_eq!(v["location"]["local"]["y_m"], 20.3);
2557 assert_eq!(v["location"]["local"]["z_m"], 0.0);
2558 assert_eq!(v["location"]["local"]["yaw_deg"], 45.0);
2559 assert_eq!(v["location"]["frame_id"], "odom");
2560 assert_eq!(v["location"]["map_id"], "warehouse-1");
2561 assert_eq!(v["location"]["floor"], 2);
2562
2563 phytrace_agent_destroy(agent);
2564 }
2565 }
2566
2567 #[test]
2568 fn test_location_null_builder() {
2569 unsafe {
2570 let result = phytrace_builder_set_location(
2571 ptr::null_mut(),
2572 0.0,
2573 0.0,
2574 0.0,
2575 0.0,
2576 0.0,
2577 0.0,
2578 0.0,
2579 0.0,
2580 ptr::null(),
2581 ptr::null(),
2582 0,
2583 );
2584 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2585 }
2586 }
2587
2588 #[test]
2593 fn test_motion_velocity() {
2594 let agent = create_test_agent();
2595 let builder = create_test_builder(agent);
2596 unsafe {
2597 let result = phytrace_builder_set_motion(
2598 builder,
2599 1.5, 1.0,
2601 0.2,
2602 0.0, 0.0,
2604 0.0,
2605 15.0, f64::NAN,
2607 f64::NAN, ptr::null(), );
2610 assert_eq!(result, PHYTRACE_OK);
2611
2612 let json = build_and_get_json(builder);
2613 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2614 assert_eq!(v["motion"]["speed_mps"], 1.5);
2615 assert_eq!(v["motion"]["linear_velocity"]["x_mps"], 1.0);
2616 assert_eq!(v["motion"]["linear_velocity"]["y_mps"], 0.2);
2617 assert_eq!(v["motion"]["angular_velocity"]["yaw_dps"], 15.0);
2618
2619 phytrace_agent_destroy(agent);
2620 }
2621 }
2622
2623 #[test]
2624 fn test_motion_commanded() {
2625 let agent = create_test_agent();
2626 let builder = create_test_builder(agent);
2627 unsafe {
2628 let result = phytrace_builder_set_motion(
2629 builder,
2630 f64::NAN,
2631 f64::NAN,
2632 f64::NAN,
2633 f64::NAN,
2634 f64::NAN,
2635 f64::NAN,
2636 f64::NAN,
2637 0.5,
2638 10.0, ptr::null(),
2640 );
2641 assert_eq!(result, PHYTRACE_OK);
2642
2643 let json = build_and_get_json(builder);
2644 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2645 assert_eq!(v["motion"]["commanded_linear_mps"], 0.5);
2646 assert_eq!(v["motion"]["commanded_angular_dps"], 10.0);
2647 assert!(v["motion"]["linear_velocity"].is_null());
2648
2649 phytrace_agent_destroy(agent);
2650 }
2651 }
2652
2653 #[test]
2654 fn test_motion_null_builder() {
2655 unsafe {
2656 let result = phytrace_builder_set_motion(
2657 ptr::null_mut(),
2658 0.0,
2659 0.0,
2660 0.0,
2661 0.0,
2662 0.0,
2663 0.0,
2664 0.0,
2665 0.0,
2666 0.0,
2667 ptr::null(),
2668 );
2669 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2670 }
2671 }
2672
2673 #[test]
2678 fn test_power_battery() {
2679 let agent = create_test_agent();
2680 let builder = create_test_builder(agent);
2681 unsafe {
2682 let result = phytrace_builder_set_power(
2683 builder, 85.0, 48.5, -2.1, 35.0, 0, 98.0, );
2690 assert_eq!(result, PHYTRACE_OK);
2691
2692 let json = build_and_get_json(builder);
2693 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2694 assert_eq!(v["power"]["battery"]["state_of_charge_pct"], 85.0);
2695 assert_eq!(v["power"]["battery"]["voltage_v"], 48.5);
2696 assert_eq!(v["power"]["battery"]["current_a"], -2.1);
2697 assert_eq!(v["power"]["battery"]["temperature_c"], 35.0);
2698 assert_eq!(v["power"]["battery"]["state_of_health_pct"], 98.0);
2699 assert_eq!(v["power"]["charging"]["is_charging"], false);
2700
2701 phytrace_agent_destroy(agent);
2702 }
2703 }
2704
2705 #[test]
2706 fn test_power_charging_not_set() {
2707 let agent = create_test_agent();
2708 let builder = create_test_builder(agent);
2709 unsafe {
2710 let result = phytrace_builder_set_power(
2711 builder,
2712 50.0,
2713 f64::NAN,
2714 f64::NAN,
2715 f64::NAN,
2716 -1, f64::NAN,
2718 );
2719 assert_eq!(result, PHYTRACE_OK);
2720
2721 let json = build_and_get_json(builder);
2722 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2723 assert_eq!(v["power"]["battery"]["state_of_charge_pct"], 50.0);
2724 assert!(v["power"]["charging"].is_null());
2725
2726 phytrace_agent_destroy(agent);
2727 }
2728 }
2729
2730 #[test]
2731 fn test_power_boundary_values() {
2732 let agent = create_test_agent();
2733 let builder = create_test_builder(agent);
2734 unsafe {
2735 let result = phytrace_builder_set_power(
2737 builder,
2738 0.0,
2739 f64::NAN,
2740 f64::NAN,
2741 f64::NAN,
2742 -1,
2743 f64::NAN,
2744 );
2745 assert_eq!(result, PHYTRACE_OK);
2746
2747 let json = build_and_get_json(builder);
2748 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2749 assert_eq!(v["power"]["battery"]["state_of_charge_pct"], 0.0);
2750
2751 phytrace_agent_destroy(agent);
2752 }
2753 }
2754
2755 #[test]
2756 fn test_power_null_builder() {
2757 unsafe {
2758 let result = phytrace_builder_set_power(ptr::null_mut(), 0.0, 0.0, 0.0, 0.0, 0, 0.0);
2759 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2760 }
2761 }
2762
2763 #[test]
2768 fn test_perception_lidar() {
2769 let agent = create_test_agent();
2770 let builder = create_test_builder(agent);
2771 unsafe {
2772 let sid = CString::new("lidar_front").unwrap();
2773 let result = phytrace_builder_set_perception_lidar(
2774 builder,
2775 sid.as_ptr(),
2776 360, 0.1, 30.0, -180.0, 180.0, 1.0, 0.5, 45.0, 10.0, );
2786 assert_eq!(result, PHYTRACE_OK);
2787
2788 let json = build_and_get_json(builder);
2789 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2790 assert_eq!(v["perception"]["lidar"][0]["sensor_id"], "lidar_front");
2791 assert_eq!(v["perception"]["lidar"][0]["point_count"], 360);
2792 assert_eq!(v["perception"]["lidar"][0]["min_range_m"], 0.1);
2793 assert_eq!(v["perception"]["lidar"][0]["max_range_m"], 30.0);
2794 assert_eq!(v["perception"]["lidar"][0]["closest_range_m"], 0.5);
2795
2796 phytrace_agent_destroy(agent);
2797 }
2798 }
2799
2800 #[test]
2801 fn test_perception_lidar_multiple() {
2802 let agent = create_test_agent();
2803 let builder = create_test_builder(agent);
2804 unsafe {
2805 let sid1 = CString::new("lidar_front").unwrap();
2806 let sid2 = CString::new("lidar_rear").unwrap();
2807
2808 phytrace_builder_set_perception_lidar(
2809 builder,
2810 sid1.as_ptr(),
2811 360,
2812 0.1,
2813 30.0,
2814 -180.0,
2815 180.0,
2816 1.0,
2817 f64::NAN,
2818 f64::NAN,
2819 f64::NAN,
2820 );
2821 phytrace_builder_set_perception_lidar(
2822 builder,
2823 sid2.as_ptr(),
2824 180,
2825 0.2,
2826 15.0,
2827 -90.0,
2828 90.0,
2829 1.0,
2830 f64::NAN,
2831 f64::NAN,
2832 f64::NAN,
2833 );
2834
2835 let json = build_and_get_json(builder);
2836 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2837 assert_eq!(v["perception"]["lidar"].as_array().unwrap().len(), 2);
2838 assert_eq!(v["perception"]["lidar"][0]["sensor_id"], "lidar_front");
2839 assert_eq!(v["perception"]["lidar"][1]["sensor_id"], "lidar_rear");
2840
2841 phytrace_agent_destroy(agent);
2842 }
2843 }
2844
2845 #[test]
2846 fn test_perception_lidar_null_builder() {
2847 unsafe {
2848 let result = phytrace_builder_set_perception_lidar(
2849 ptr::null_mut(),
2850 ptr::null(),
2851 0,
2852 0.0,
2853 0.0,
2854 0.0,
2855 0.0,
2856 0.0,
2857 0.0,
2858 0.0,
2859 0.0,
2860 );
2861 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2862 }
2863 }
2864
2865 #[test]
2870 fn test_perception_imu() {
2871 let agent = create_test_agent();
2872 let builder = create_test_builder(agent);
2873 unsafe {
2874 let result = phytrace_builder_set_perception_imu(
2875 builder, 0.1, -0.2, 9.81, 0.5, -0.3, 1.2, 25.0, -10.0, 45.0, 28.5, );
2880 assert_eq!(result, PHYTRACE_OK);
2881
2882 let json = build_and_get_json(builder);
2883 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2884 assert_eq!(v["perception"]["imu"]["accel_z_mps2"], 9.81);
2885 assert_eq!(v["perception"]["imu"]["gyro_z_dps"], 1.2);
2886 assert_eq!(v["perception"]["imu"]["mag_x_ut"], 25.0);
2887 assert_eq!(v["perception"]["imu"]["temperature_c"], 28.5);
2888
2889 phytrace_agent_destroy(agent);
2890 }
2891 }
2892
2893 #[test]
2894 fn test_perception_lidar_then_imu_preserves_both() {
2895 let agent = create_test_agent();
2896 let builder = create_test_builder(agent);
2897 unsafe {
2898 let sid = CString::new("lidar0").unwrap();
2899 phytrace_builder_set_perception_lidar(
2900 builder,
2901 sid.as_ptr(),
2902 100,
2903 0.1,
2904 10.0,
2905 -90.0,
2906 90.0,
2907 1.0,
2908 f64::NAN,
2909 f64::NAN,
2910 f64::NAN,
2911 );
2912 phytrace_builder_set_perception_imu(
2913 builder,
2914 0.0,
2915 0.0,
2916 9.81,
2917 f64::NAN,
2918 f64::NAN,
2919 f64::NAN,
2920 f64::NAN,
2921 f64::NAN,
2922 f64::NAN,
2923 f64::NAN,
2924 );
2925
2926 let json = build_and_get_json(builder);
2927 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2928 assert!(!v["perception"]["lidar"].is_null());
2930 assert!(!v["perception"]["imu"].is_null());
2931 assert_eq!(v["perception"]["lidar"][0]["sensor_id"], "lidar0");
2932 assert_eq!(v["perception"]["imu"]["accel_z_mps2"], 9.81);
2933
2934 phytrace_agent_destroy(agent);
2935 }
2936 }
2937
2938 #[test]
2939 fn test_perception_imu_null_builder() {
2940 unsafe {
2941 let result = phytrace_builder_set_perception_imu(
2942 ptr::null_mut(),
2943 0.0,
2944 0.0,
2945 0.0,
2946 0.0,
2947 0.0,
2948 0.0,
2949 0.0,
2950 0.0,
2951 0.0,
2952 0.0,
2953 );
2954 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
2955 }
2956 }
2957
2958 #[test]
2963 fn test_safety() {
2964 let agent = create_test_agent();
2965 let builder = create_test_builder(agent);
2966 unsafe {
2967 let result = phytrace_builder_set_safety(
2968 builder,
2969 PHYTRACE_SAFETY_STATE_WARNING,
2970 1, 0, PHYTRACE_ESTOP_TYPE_SOFTWARE,
2973 1.0, 2.5, 3.0, );
2977 assert_eq!(result, PHYTRACE_OK);
2978
2979 let json = build_and_get_json(builder);
2980 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
2981 assert_eq!(v["safety"]["safety_state"], "warning");
2982 assert_eq!(v["safety"]["is_safe"], true);
2983 assert_eq!(v["safety"]["e_stop"]["is_active"], false);
2984 assert_eq!(v["safety"]["speed_limit_mps"], 1.0);
2985 assert_eq!(v["safety"]["proximity"]["closest_distance_m"], 2.5);
2986 assert_eq!(v["safety"]["proximity"]["closest_human_m"], 3.0);
2987
2988 phytrace_agent_destroy(agent);
2989 }
2990 }
2991
2992 #[test]
2993 fn test_safety_estop_active() {
2994 let agent = create_test_agent();
2995 let builder = create_test_builder(agent);
2996 unsafe {
2997 let result = phytrace_builder_set_safety(
2998 builder,
2999 PHYTRACE_SAFETY_STATE_EMERGENCY_STOP,
3000 0, 1, PHYTRACE_ESTOP_TYPE_HARDWARE,
3003 0.0, f64::NAN,
3005 f64::NAN,
3006 );
3007 assert_eq!(result, PHYTRACE_OK);
3008
3009 let json = build_and_get_json(builder);
3010 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3011 assert_eq!(v["safety"]["safety_state"], "emergency_stop");
3012 assert_eq!(v["safety"]["e_stop"]["is_active"], true);
3013 assert_eq!(v["safety"]["e_stop"]["e_stop_type"], "hardware");
3014
3015 phytrace_agent_destroy(agent);
3016 }
3017 }
3018
3019 #[test]
3020 fn test_safety_invalid_enum() {
3021 let agent = create_test_agent();
3022 let builder = create_test_builder(agent);
3023 unsafe {
3024 let result =
3025 phytrace_builder_set_safety(builder, 99, -1, -1, -1, f64::NAN, f64::NAN, f64::NAN);
3026 assert_eq!(result, PHYTRACE_ERR_INVALID_ENUM);
3027 phytrace_builder_destroy(builder);
3028 phytrace_agent_destroy(agent);
3029 }
3030 }
3031
3032 #[test]
3033 fn test_safety_null_builder() {
3034 unsafe {
3035 let result = phytrace_builder_set_safety(ptr::null_mut(), 0, 0, 0, 0, 0.0, 0.0, 0.0);
3036 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3037 }
3038 }
3039
3040 #[test]
3045 fn test_navigation() {
3046 let agent = create_test_agent();
3047 let builder = create_test_builder(agent);
3048 unsafe {
3049 let result = phytrace_builder_set_navigation(
3050 builder,
3051 PHYTRACE_LOCALIZATION_QUALITY_GOOD,
3052 0.95, 1, PHYTRACE_PATH_STATE_EXECUTING,
3055 25.0, 10.0, 50.0,
3058 30.0, 90.0, );
3061 assert_eq!(result, PHYTRACE_OK);
3062
3063 let json = build_and_get_json(builder);
3064 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3065 assert_eq!(v["navigation"]["localization"]["quality"], "good");
3066 assert_eq!(v["navigation"]["localization"]["confidence"], 0.95);
3067 assert_eq!(v["navigation"]["localization"]["is_localized"], true);
3068 assert_eq!(v["navigation"]["path"]["state"], "executing");
3069 assert_eq!(v["navigation"]["path"]["length_m"], 25.0);
3070 assert_eq!(v["navigation"]["goal"]["position"]["x_m"], 50.0);
3071 assert_eq!(v["navigation"]["goal"]["orientation_deg"], 90.0);
3072
3073 phytrace_agent_destroy(agent);
3074 }
3075 }
3076
3077 #[test]
3078 fn test_navigation_minimal() {
3079 let agent = create_test_agent();
3080 let builder = create_test_builder(agent);
3081 unsafe {
3082 let result = phytrace_builder_set_navigation(
3083 builder,
3084 -1, f64::NAN, -1, PHYTRACE_PATH_STATE_VALID,
3088 15.0,
3089 f64::NAN,
3090 f64::NAN,
3091 f64::NAN,
3092 f64::NAN,
3093 );
3094 assert_eq!(result, PHYTRACE_OK);
3095
3096 let json = build_and_get_json(builder);
3097 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3098 assert!(v["navigation"]["localization"].is_null());
3099 assert_eq!(v["navigation"]["path"]["state"], "valid");
3100
3101 phytrace_agent_destroy(agent);
3102 }
3103 }
3104
3105 #[test]
3106 fn test_navigation_invalid_enum() {
3107 let agent = create_test_agent();
3108 let builder = create_test_builder(agent);
3109 unsafe {
3110 let result = phytrace_builder_set_navigation(
3111 builder,
3112 99,
3113 f64::NAN,
3114 -1,
3115 -1,
3116 f64::NAN,
3117 f64::NAN,
3118 f64::NAN,
3119 f64::NAN,
3120 f64::NAN,
3121 );
3122 assert_eq!(result, PHYTRACE_ERR_INVALID_ENUM);
3123 phytrace_builder_destroy(builder);
3124 phytrace_agent_destroy(agent);
3125 }
3126 }
3127
3128 #[test]
3129 fn test_navigation_null_builder() {
3130 unsafe {
3131 let result = phytrace_builder_set_navigation(
3132 ptr::null_mut(),
3133 0,
3134 0.0,
3135 0,
3136 0,
3137 0.0,
3138 0.0,
3139 0.0,
3140 0.0,
3141 0.0,
3142 );
3143 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3144 }
3145 }
3146
3147 #[test]
3152 fn test_actuators_joints() {
3153 let agent = create_test_agent();
3154 let builder = create_test_builder(agent);
3155 unsafe {
3156 let names = CString::new(r#"["wheel_fl","wheel_fr","wheel_rl","wheel_rr"]"#).unwrap();
3157 let positions = CString::new("[0.0, 1.57, 3.14, 4.71]").unwrap();
3158 let velocities = CString::new("[10.0, 10.5, 9.8, 10.2]").unwrap();
3159 let efforts = CString::new("[5.0, 5.1, 4.9, 5.0]").unwrap();
3160
3161 let result = phytrace_builder_set_actuators_joints(
3162 builder,
3163 names.as_ptr(),
3164 positions.as_ptr(),
3165 velocities.as_ptr(),
3166 efforts.as_ptr(),
3167 );
3168 assert_eq!(result, PHYTRACE_OK);
3169
3170 let json = build_and_get_json(builder);
3171 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3172 let joints = v["actuators"]["joints"].as_array().unwrap();
3173 assert_eq!(joints.len(), 4);
3174 assert_eq!(joints[0]["name"], "wheel_fl");
3175 assert_eq!(joints[0]["position"], 0.0);
3176 assert_eq!(joints[1]["velocity"], 10.5);
3177 assert_eq!(joints[3]["effort"], 5.0);
3178
3179 phytrace_agent_destroy(agent);
3180 }
3181 }
3182
3183 #[test]
3184 fn test_actuators_joints_names_only() {
3185 let agent = create_test_agent();
3186 let builder = create_test_builder(agent);
3187 unsafe {
3188 let names = CString::new(r#"["j1","j2"]"#).unwrap();
3189
3190 let result = phytrace_builder_set_actuators_joints(
3191 builder,
3192 names.as_ptr(),
3193 ptr::null(), ptr::null(), ptr::null(), );
3197 assert_eq!(result, PHYTRACE_OK);
3198
3199 let json = build_and_get_json(builder);
3200 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3201 let joints = v["actuators"]["joints"].as_array().unwrap();
3202 assert_eq!(joints.len(), 2);
3203 assert_eq!(joints[0]["name"], "j1");
3204 assert!(joints[0]["position"].is_null());
3206
3207 phytrace_agent_destroy(agent);
3208 }
3209 }
3210
3211 #[test]
3212 fn test_actuators_joints_null_names() {
3213 let agent = create_test_agent();
3214 let builder = create_test_builder(agent);
3215 unsafe {
3216 let result = phytrace_builder_set_actuators_joints(
3217 builder,
3218 ptr::null(), ptr::null(),
3220 ptr::null(),
3221 ptr::null(),
3222 );
3223 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3224 phytrace_builder_destroy(builder);
3225 phytrace_agent_destroy(agent);
3226 }
3227 }
3228
3229 #[test]
3230 fn test_actuators_joints_invalid_json() {
3231 let agent = create_test_agent();
3232 let builder = create_test_builder(agent);
3233 unsafe {
3234 let bad_names = CString::new("not valid json").unwrap();
3235 let result = phytrace_builder_set_actuators_joints(
3236 builder,
3237 bad_names.as_ptr(),
3238 ptr::null(),
3239 ptr::null(),
3240 ptr::null(),
3241 );
3242 assert_eq!(result, PHYTRACE_ERR_SERIALIZATION);
3243 phytrace_builder_destroy(builder);
3244 phytrace_agent_destroy(agent);
3245 }
3246 }
3247
3248 #[test]
3249 fn test_actuators_joints_null_builder() {
3250 unsafe {
3251 let names = CString::new(r#"["j1"]"#).unwrap();
3252 let result = phytrace_builder_set_actuators_joints(
3253 ptr::null_mut(),
3254 names.as_ptr(),
3255 ptr::null(),
3256 ptr::null(),
3257 ptr::null(),
3258 );
3259 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3260 }
3261 }
3262
3263 #[test]
3268 fn test_operational() {
3269 let agent = create_test_agent();
3270 let builder = create_test_builder(agent);
3271 unsafe {
3272 let tid = CString::new("task-123").unwrap();
3273 let ttype = CString::new("delivery").unwrap();
3274 let mid = CString::new("mission-456").unwrap();
3275
3276 let result = phytrace_builder_set_operational(
3277 builder,
3278 PHYTRACE_OPERATIONAL_MODE_AUTONOMOUS,
3279 PHYTRACE_OPERATIONAL_STATE_NAVIGATING,
3280 tid.as_ptr(),
3281 ttype.as_ptr(),
3282 3600.0, mid.as_ptr(),
3284 );
3285 assert_eq!(result, PHYTRACE_OK);
3286
3287 let json = build_and_get_json(builder);
3288 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3289 assert_eq!(v["operational"]["mode"], "autonomous");
3290 assert_eq!(v["operational"]["state"], "navigating");
3291 assert_eq!(v["operational"]["task"]["task_id"], "task-123");
3292 assert_eq!(v["operational"]["task"]["task_type"], "delivery");
3293 assert_eq!(v["operational"]["uptime_sec"], 3600.0);
3294 assert_eq!(v["operational"]["mission_id"], "mission-456");
3295
3296 phytrace_agent_destroy(agent);
3297 }
3298 }
3299
3300 #[test]
3301 fn test_operational_invalid_enum() {
3302 let agent = create_test_agent();
3303 let builder = create_test_builder(agent);
3304 unsafe {
3305 let result = phytrace_builder_set_operational(
3306 builder,
3307 99,
3308 -1,
3309 ptr::null(),
3310 ptr::null(),
3311 f64::NAN,
3312 ptr::null(),
3313 );
3314 assert_eq!(result, PHYTRACE_ERR_INVALID_ENUM);
3315 phytrace_builder_destroy(builder);
3316 phytrace_agent_destroy(agent);
3317 }
3318 }
3319
3320 #[test]
3321 fn test_operational_null_builder() {
3322 unsafe {
3323 let result = phytrace_builder_set_operational(
3324 ptr::null_mut(),
3325 0,
3326 0,
3327 ptr::null(),
3328 ptr::null(),
3329 0.0,
3330 ptr::null(),
3331 );
3332 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3333 }
3334 }
3335
3336 #[test]
3341 fn test_identity() {
3342 let agent = create_test_agent();
3343 let builder = create_test_builder(agent);
3344 unsafe {
3345 let sid = CString::new("robot-42").unwrap();
3346 let platform = CString::new("TurtleBot4").unwrap();
3347 let model = CString::new("TB4-Standard").unwrap();
3348 let firmware = CString::new("1.2.3").unwrap();
3349 let serial = CString::new("SN-00042").unwrap();
3350 let fleet = CString::new("fleet-alpha").unwrap();
3351 let site = CString::new("warehouse-nyc").unwrap();
3352
3353 let result = phytrace_builder_set_identity(
3354 builder,
3355 sid.as_ptr(),
3356 platform.as_ptr(),
3357 model.as_ptr(),
3358 firmware.as_ptr(),
3359 serial.as_ptr(),
3360 fleet.as_ptr(),
3361 site.as_ptr(),
3362 );
3363 assert_eq!(result, PHYTRACE_OK);
3364
3365 let json = build_and_get_json(builder);
3366 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3367 assert_eq!(v["identity"]["source_id"], "robot-42");
3368 assert_eq!(v["identity"]["platform"], "TurtleBot4");
3369 assert_eq!(v["identity"]["model"], "TB4-Standard");
3370 assert_eq!(v["identity"]["firmware_version"], "1.2.3");
3371 assert_eq!(v["identity"]["serial_number"], "SN-00042");
3372 assert_eq!(v["identity"]["fleet_id"], "fleet-alpha");
3373 assert_eq!(v["identity"]["site_id"], "warehouse-nyc");
3374
3375 phytrace_agent_destroy(agent);
3376 }
3377 }
3378
3379 #[test]
3380 fn test_identity_null_builder() {
3381 unsafe {
3382 let result = phytrace_builder_set_identity(
3383 ptr::null_mut(),
3384 ptr::null(),
3385 ptr::null(),
3386 ptr::null(),
3387 ptr::null(),
3388 ptr::null(),
3389 ptr::null(),
3390 ptr::null(),
3391 );
3392 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3393 }
3394 }
3395
3396 #[test]
3401 fn test_communication() {
3402 let agent = create_test_agent();
3403 let builder = create_test_builder(agent);
3404 unsafe {
3405 let result = phytrace_builder_set_communication(builder, 1, -65, 12.5, 0.1);
3406 assert_eq!(result, PHYTRACE_OK);
3407
3408 let json = build_and_get_json(builder);
3409 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3410 assert_eq!(v["communication"]["network"]["is_connected"], true);
3411 assert_eq!(v["communication"]["network"]["signal_strength_dbm"], -65);
3412 assert_eq!(v["communication"]["network"]["latency_ms"], 12.5);
3413 assert_eq!(v["communication"]["network"]["packet_loss_pct"], 0.1);
3414
3415 phytrace_agent_destroy(agent);
3416 }
3417 }
3418
3419 #[test]
3420 fn test_communication_null_builder() {
3421 unsafe {
3422 let result = phytrace_builder_set_communication(ptr::null_mut(), 0, 0, 0.0, 0.0);
3423 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3424 }
3425 }
3426
3427 #[test]
3432 fn test_context() {
3433 let agent = create_test_agent();
3434 let builder = create_test_builder(agent);
3435 unsafe {
3436 let tz = CString::new("America/New_York").unwrap();
3437 let fid = CString::new("site-001").unwrap();
3438 let fname = CString::new("NYC Warehouse").unwrap();
3439
3440 let result = phytrace_builder_set_context(
3441 builder,
3442 tz.as_ptr(),
3443 fid.as_ptr(),
3444 fname.as_ptr(),
3445 5,
3446 3,
3447 );
3448 assert_eq!(result, PHYTRACE_OK);
3449
3450 let json = build_and_get_json(builder);
3451 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3452 assert_eq!(v["context"]["time"]["timezone"], "America/New_York");
3453 assert_eq!(v["context"]["facility"]["facility_id"], "site-001");
3454 assert_eq!(v["context"]["facility"]["name"], "NYC Warehouse");
3455 assert_eq!(v["context"]["facility"]["human_count"], 5);
3456 assert_eq!(v["context"]["facility"]["robot_count"], 3);
3457
3458 phytrace_agent_destroy(agent);
3459 }
3460 }
3461
3462 #[test]
3463 fn test_context_null_builder() {
3464 unsafe {
3465 let result = phytrace_builder_set_context(
3466 ptr::null_mut(),
3467 ptr::null(),
3468 ptr::null(),
3469 ptr::null(),
3470 0,
3471 0,
3472 );
3473 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3474 }
3475 }
3476
3477 #[test]
3482 fn test_extensions_json() {
3483 let agent = create_test_agent();
3484 let builder = create_test_builder(agent);
3485 unsafe {
3486 let json_str =
3487 CString::new(r#"{"raw_msg":{"topic":"/custom","data":[1,2,3]}}"#).unwrap();
3488 let result = phytrace_builder_set_extensions_json(builder, json_str.as_ptr());
3489 assert_eq!(result, PHYTRACE_OK);
3490
3491 let json = build_and_get_json(builder);
3492 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3493 assert_eq!(v["extensions"]["raw_msg"]["topic"], "/custom");
3494 assert_eq!(v["extensions"]["raw_msg"]["data"][0], 1);
3495
3496 phytrace_agent_destroy(agent);
3497 }
3498 }
3499
3500 #[test]
3501 fn test_extensions_json_invalid() {
3502 let agent = create_test_agent();
3503 let builder = create_test_builder(agent);
3504 unsafe {
3505 let bad = CString::new("not json").unwrap();
3506 let result = phytrace_builder_set_extensions_json(builder, bad.as_ptr());
3507 assert_eq!(result, PHYTRACE_ERR_SERIALIZATION);
3508 phytrace_builder_destroy(builder);
3509 phytrace_agent_destroy(agent);
3510 }
3511 }
3512
3513 #[test]
3514 fn test_extensions_json_null() {
3515 let agent = create_test_agent();
3516 let builder = create_test_builder(agent);
3517 unsafe {
3518 let result = phytrace_builder_set_extensions_json(builder, ptr::null());
3519 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3520 phytrace_builder_destroy(builder);
3521 phytrace_agent_destroy(agent);
3522 }
3523 }
3524
3525 #[test]
3526 fn test_extensions_json_null_builder() {
3527 unsafe {
3528 let s = CString::new("{}").unwrap();
3529 let result = phytrace_builder_set_extensions_json(ptr::null_mut(), s.as_ptr());
3530 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3531 }
3532 }
3533
3534 #[test]
3539 fn test_event_to_json() {
3540 let agent = create_test_agent();
3541 let builder = create_test_builder(agent);
3542 unsafe {
3543 phytrace_builder_set_power(builder, 75.0, f64::NAN, f64::NAN, f64::NAN, -1, f64::NAN);
3544 let event = phytrace_builder_build(builder);
3545 assert!(!event.is_null());
3546
3547 let json_ptr = phytrace_event_to_json(event);
3548 assert!(!json_ptr.is_null());
3549
3550 let json = CStr::from_ptr(json_ptr).to_str().unwrap();
3551 assert!(json.contains("state_of_charge_pct"));
3552 assert!(json.contains("75"));
3553
3554 phytrace_string_free(json_ptr);
3555 phytrace_event_destroy(event);
3556 phytrace_agent_destroy(agent);
3557 }
3558 }
3559
3560 #[test]
3561 fn test_event_to_json_null() {
3562 unsafe {
3563 let json = phytrace_event_to_json(ptr::null());
3564 assert!(json.is_null());
3565 }
3566 }
3567
3568 #[test]
3569 fn test_event_destroy_null_is_noop() {
3570 unsafe {
3571 phytrace_event_destroy(ptr::null_mut()); }
3573 }
3574
3575 #[test]
3580 fn test_send_event() {
3581 let agent = create_test_agent();
3582 unsafe {
3583 phytrace_agent_start(agent);
3584
3585 let builder = create_test_builder(agent);
3586 phytrace_builder_set_power(builder, 80.0, f64::NAN, f64::NAN, f64::NAN, -1, f64::NAN);
3587 let event = phytrace_builder_build(builder);
3588 assert!(!event.is_null());
3589
3590 let result = phytrace_agent_send(agent, event);
3591 assert_eq!(result, PHYTRACE_OK);
3592
3593 phytrace_agent_destroy(agent);
3594 }
3595 }
3596
3597 #[test]
3598 fn test_send_null_event() {
3599 let agent = create_test_agent();
3600 unsafe {
3601 let result = phytrace_agent_send(agent, ptr::null_mut());
3602 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3603 phytrace_agent_destroy(agent);
3604 }
3605 }
3606
3607 #[test]
3608 fn test_send_null_agent() {
3609 let agent = create_test_agent();
3610 let builder = create_test_builder(agent);
3611 unsafe {
3612 phytrace_builder_set_power(builder, 50.0, f64::NAN, f64::NAN, f64::NAN, -1, f64::NAN);
3613 let event = phytrace_builder_build(builder);
3614 assert!(!event.is_null());
3615
3616 let result = phytrace_agent_send(ptr::null_mut(), event);
3618 assert_eq!(result, PHYTRACE_ERR_NULL_PTR);
3619
3620 phytrace_agent_destroy(agent);
3621 }
3622 }
3623
3624 #[test]
3629 fn test_full_multi_domain_event() {
3630 let agent = create_test_agent();
3631 let builder = create_test_builder(agent);
3632 unsafe {
3633 phytrace_builder_set_event_type(builder, PHYTRACE_EVENT_TYPE_TELEMETRY_PERIODIC);
3635
3636 let frame = CString::new("odom").unwrap();
3638 phytrace_builder_set_location(
3639 builder,
3640 f64::NAN,
3641 f64::NAN,
3642 f64::NAN,
3643 45.0,
3644 10.0,
3645 20.0,
3646 0.0,
3647 45.0,
3648 frame.as_ptr(),
3649 ptr::null(),
3650 i32::MIN,
3651 );
3652
3653 phytrace_builder_set_motion(
3655 builder,
3656 1.2,
3657 1.0,
3658 0.1,
3659 0.0,
3660 0.0,
3661 0.0,
3662 5.0,
3663 f64::NAN,
3664 f64::NAN,
3665 ptr::null(),
3666 );
3667
3668 phytrace_builder_set_power(builder, 72.0, 48.0, -1.5, 30.0, 0, 95.0);
3670
3671 let lid = CString::new("rplidar").unwrap();
3673 phytrace_builder_set_perception_lidar(
3674 builder,
3675 lid.as_ptr(),
3676 720,
3677 0.15,
3678 12.0,
3679 -180.0,
3680 180.0,
3681 0.5,
3682 0.8,
3683 -30.0,
3684 10.0,
3685 );
3686
3687 phytrace_builder_set_perception_imu(
3689 builder,
3690 0.01,
3691 -0.02,
3692 9.81,
3693 0.5,
3694 -0.1,
3695 0.2,
3696 f64::NAN,
3697 f64::NAN,
3698 f64::NAN,
3699 32.0,
3700 );
3701
3702 phytrace_builder_set_safety(
3704 builder,
3705 PHYTRACE_SAFETY_STATE_NORMAL,
3706 1,
3707 0,
3708 -1,
3709 f64::NAN,
3710 3.5,
3711 f64::NAN,
3712 );
3713
3714 phytrace_builder_set_navigation(
3716 builder,
3717 PHYTRACE_LOCALIZATION_QUALITY_EXCELLENT,
3718 0.99,
3719 1,
3720 PHYTRACE_PATH_STATE_EXECUTING,
3721 50.0,
3722 35.0,
3723 100.0,
3724 50.0,
3725 0.0,
3726 );
3727
3728 let names = CString::new(r#"["wheel_l","wheel_r"]"#).unwrap();
3730 let pos = CString::new("[0.0, 0.0]").unwrap();
3731 let vel = CString::new("[15.0, 14.8]").unwrap();
3732 phytrace_builder_set_actuators_joints(
3733 builder,
3734 names.as_ptr(),
3735 pos.as_ptr(),
3736 vel.as_ptr(),
3737 ptr::null(),
3738 );
3739
3740 let tid = CString::new("delivery-789").unwrap();
3742 let ttype = CString::new("delivery").unwrap();
3743 phytrace_builder_set_operational(
3744 builder,
3745 PHYTRACE_OPERATIONAL_MODE_AUTONOMOUS,
3746 PHYTRACE_OPERATIONAL_STATE_NAVIGATING,
3747 tid.as_ptr(),
3748 ttype.as_ptr(),
3749 7200.0,
3750 ptr::null(),
3751 );
3752
3753 let json = build_and_get_json(builder);
3755 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3756
3757 assert_eq!(v["event_type"], "telemetry_periodic");
3759 assert!(!v["location"].is_null());
3760 assert!(!v["motion"].is_null());
3761 assert!(!v["power"].is_null());
3762 assert!(!v["perception"].is_null());
3763 assert!(!v["safety"].is_null());
3764 assert!(!v["navigation"].is_null());
3765 assert!(!v["actuators"].is_null());
3766 assert!(!v["operational"].is_null());
3767
3768 assert_eq!(v["location"]["local"]["x_m"], 10.0);
3770 assert_eq!(v["motion"]["speed_mps"], 1.2);
3771 assert_eq!(v["power"]["battery"]["state_of_charge_pct"], 72.0);
3772 assert_eq!(v["perception"]["lidar"][0]["point_count"], 720);
3773 assert_eq!(v["perception"]["imu"]["accel_z_mps2"], 9.81);
3774 assert_eq!(v["safety"]["safety_state"], "normal");
3775 assert_eq!(v["navigation"]["localization"]["quality"], "excellent");
3776 assert_eq!(v["actuators"]["joints"].as_array().unwrap().len(), 2);
3777 assert_eq!(v["operational"]["state"], "navigating");
3778
3779 phytrace_agent_destroy(agent);
3780 }
3781 }
3782
3783 #[test]
3788 fn test_all_event_types() {
3789 for i in 0..=17 {
3790 assert!(
3791 event_type_from_i32(i).is_some(),
3792 "EventType {i} should be valid"
3793 );
3794 }
3795 assert!(event_type_from_i32(18).is_none());
3796 assert!(event_type_from_i32(-1).is_none());
3797 }
3798
3799 #[test]
3800 fn test_all_source_types() {
3801 for i in 0..=17 {
3802 assert!(
3803 source_type_from_i32(i).is_some(),
3804 "SourceType {i} should be valid"
3805 );
3806 }
3807 assert!(source_type_from_i32(18).is_none());
3808 assert!(source_type_from_i32(-1).is_none());
3809 }
3810
3811 #[test]
3812 fn test_all_safety_states() {
3813 for i in 0..=5 {
3814 assert!(
3815 safety_state_from_i32(i).is_some(),
3816 "SafetyState {i} should be valid"
3817 );
3818 }
3819 assert!(safety_state_from_i32(6).is_none());
3820 }
3821
3822 #[test]
3823 fn test_all_operational_modes() {
3824 for i in 0..=7 {
3825 assert!(
3826 operational_mode_from_i32(i).is_some(),
3827 "OpMode {i} should be valid"
3828 );
3829 }
3830 assert!(operational_mode_from_i32(8).is_none());
3831 }
3832
3833 #[test]
3834 fn test_all_operational_states() {
3835 for i in 0..=11 {
3836 assert!(
3837 operational_state_from_i32(i).is_some(),
3838 "OpState {i} should be valid"
3839 );
3840 }
3841 assert!(operational_state_from_i32(12).is_none());
3842 }
3843
3844 #[test]
3845 fn test_all_localization_qualities() {
3846 for i in 0..=4 {
3847 assert!(
3848 localization_quality_from_i32(i).is_some(),
3849 "LocQuality {i} should be valid"
3850 );
3851 }
3852 assert!(localization_quality_from_i32(5).is_none());
3853 }
3854
3855 #[test]
3856 fn test_all_path_states() {
3857 for i in 0..=6 {
3858 assert!(
3859 path_state_from_i32(i).is_some(),
3860 "PathState {i} should be valid"
3861 );
3862 }
3863 assert!(path_state_from_i32(7).is_none());
3864 }
3865
3866 #[test]
3867 fn test_all_estop_types() {
3868 for i in 0..=3 {
3869 assert!(
3870 estop_type_from_i32(i).is_some(),
3871 "EStopType {i} should be valid"
3872 );
3873 }
3874 assert!(estop_type_from_i32(4).is_none());
3875 }
3876}