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(|_| 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(|| CString::new(crate::SDK_VERSION).unwrap())
507 .as_ptr()
508}
509
510#[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#[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#[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(runtime);
602 create_agent_from_config(config)
603}
604
605fn 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#[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#[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#[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#[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 let _ = handle.runtime.block_on(handle.agent.shutdown());
739 }
741}
742
743#[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#[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#[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#[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 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#[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#[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#[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#[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
935macro_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#[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#[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#[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#[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 let handle = &mut *builder;
1309 let old_builder = std::mem::replace(
1310 &mut handle.builder,
1311 EventBuilder::new(&PhyTraceConfig::new("tmp")),
1312 );
1313
1314 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#[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 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#[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#[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#[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 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 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 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
1737unsafe 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#[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#[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#[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#[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#[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#[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 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#[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#[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#[cfg(test)]
2155mod tests {
2156 use super::*;
2157 use std::ffi::CString;
2158
2159 fn create_test_agent() -> *mut PhyTraceAgentHandle {
2164 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 #[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 #[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()); }
2222 }
2223
2224 #[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()); }
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 #[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 #[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()); }
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 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 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 #[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, f64::NAN, 90.0, f64::NAN,
2498 f64::NAN,
2499 f64::NAN,
2500 f64::NAN, ptr::null(),
2502 ptr::null(), i32::MIN, );
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, f64::NAN, 10.5,
2532 20.3,
2533 0.0,
2534 45.0, frame.as_ptr(),
2536 map.as_ptr(),
2537 2, );
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 #[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, 1.0,
2589 0.2,
2590 0.0, 0.0,
2592 0.0,
2593 15.0, f64::NAN,
2595 f64::NAN, ptr::null(), );
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, 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 #[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, 48.5, -2.1, 35.0, 0, 98.0, );
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, 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 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 #[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, 0.1, 30.0, -180.0, 180.0, 1.0, 0.5, 45.0, 10.0, );
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 #[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, 0.5, -0.3, 1.2, 25.0, -10.0, 45.0, 28.5, );
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 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 #[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, 0, PHYTRACE_ESTOP_TYPE_SOFTWARE,
2961 1.0, 2.5, 3.0, );
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, 1, PHYTRACE_ESTOP_TYPE_HARDWARE,
2991 0.0, 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 #[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, 1, PHYTRACE_PATH_STATE_EXECUTING,
3043 25.0, 10.0, 50.0,
3046 30.0, 90.0, );
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, f64::NAN, -1, 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 #[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(), ptr::null(), ptr::null(), );
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 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(), 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 #[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, 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 #[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 #[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 #[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 #[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 #[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()); }
3561 }
3562
3563 #[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 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 #[test]
3617 fn test_full_multi_domain_event() {
3618 let agent = create_test_agent();
3619 let builder = create_test_builder(agent);
3620 unsafe {
3621 phytrace_builder_set_event_type(builder, PHYTRACE_EVENT_TYPE_TELEMETRY_PERIODIC);
3623
3624 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 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 phytrace_builder_set_power(builder, 72.0, 48.0, -1.5, 30.0, 0, 95.0);
3658
3659 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 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 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 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 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 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 let json = build_and_get_json(builder);
3743 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3744
3745 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 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 #[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}