Skip to main content

sesh_stdlib/
lib.rs

1// ============================================================================
2// SESH STANDARD LIBRARY (1.0.0) — REAL ROBOTICS COMPUTATIONAL ENGINE
3// ============================================================================
4// Implements actual mathematical algorithms for PID control, Kalman filtering,
5// 6-DOF / 3-DOF Inverse Kinematics, Differential Drive kinematics, LQR control,
6// and Geodetic GPS navigation.
7// ============================================================================
8
9/// Real PID Controller state and algorithm
10#[derive(Debug, Clone)]
11pub struct PidState {
12    pub kp: f64,
13    pub ki: f64,
14    pub kd: f64,
15    pub integral: f64,
16    pub prev_error: f64,
17    pub integral_max: f64,
18}
19
20impl PidState {
21    pub fn new(kp: f64, ki: f64, kd: f64) -> Self {
22        Self {
23            kp,
24            ki,
25            kd,
26            integral: 0.0,
27            prev_error: 0.0,
28            integral_max: 100.0,
29        }
30    }
31
32    /// Compute real PID output with integral windup clamping and derivative term
33    pub fn compute(&mut self, setpoint: f64, actual: f64, dt_s: f64) -> f64 {
34        let error = setpoint - actual;
35        let dt = if dt_s <= 0.0 { 0.005 } else { dt_s };
36
37        // Proportional term
38        let p_term = self.kp * error;
39
40        // Integral term with windup clamp
41        self.integral += error * dt;
42        self.integral = self.integral.clamp(-self.integral_max, self.integral_max);
43        let i_term = self.ki * self.integral;
44
45        // Derivative term
46        let d_error = (error - self.prev_error) / dt;
47        self.prev_error = error;
48        let d_term = self.kd * d_error;
49
50        p_term + i_term + d_term
51    }
52}
53
54use std::cell::RefCell;
55
56thread_local! {
57    static DEFAULT_PID: RefCell<PidState> = RefCell::new(PidState::new(1.0, 0.0, 0.0));
58}
59
60/// Stateful PID calculation function for Sesh scripts with genuine damping & integral clamping
61pub fn pid_compute(setpoint: f64, actual: f64, kp: f64, ki: f64, kd: f64) -> f64 {
62    DEFAULT_PID.with(|pid_cell| {
63        let mut pid = pid_cell.borrow_mut();
64        pid.kp = kp;
65        pid.ki = ki;
66        pid.kd = kd;
67        pid.compute(setpoint, actual, 0.005).clamp(-100.0, 100.0)
68    })
69}
70
71/// Pure Pursuit geometric path-tracking algorithm
72/// Returns (curvature_kappa, angular_velocity_w) given robot pose and target waypoint
73pub fn pure_pursuit(
74    rx: f64,
75    ry: f64,
76    rtheta: f64,
77    wx: f64,
78    wy: f64,
79    linear_v: f64,
80) -> (f64, f64) {
81    let dx = wx - rx;
82    let dy = wy - ry;
83    let local_x = dx * rtheta.cos() + dy * rtheta.sin();
84    let local_y = -dx * rtheta.sin() + dy * rtheta.cos();
85    let l2 = (local_x * local_x + local_y * local_y).max(0.01);
86    let curvature = 2.0 * local_y / l2;
87    let angular_w = linear_v * curvature;
88    (curvature, angular_w)
89}
90
91/// Feed-Forward + PID Controller for high-speed trajectory tracking
92pub fn feedforward_pid(
93    setpoint: f64,
94    actual: f64,
95    kp: f64,
96    ki: f64,
97    kd: f64,
98    kf: f64,
99    prev_setpoint: f64,
100    dt_s: f64,
101) -> f64 {
102    let dt = if dt_s <= 0.0 { 0.005 } else { dt_s };
103    let ff_term = kf * ((setpoint - prev_setpoint) / dt);
104    let pid_term = pid_compute(setpoint, actual, kp, ki, kd);
105    (ff_term + pid_term).clamp(-100.0, 100.0)
106}
107
108/// 1D Extended Kalman Filter (EKF) state estimation step
109pub fn ekf_update(
110    prior_x: f64,
111    meas_z: f64,
112    cov_p: f64,
113    meas_noise_r: f64,
114) -> (f64, f64) {
115    let r = if meas_noise_r <= 0.0 { 0.1 } else { meas_noise_r };
116    // Kalman gain K = P / (P + R)
117    let kalman_gain = cov_p / (cov_p + r);
118    // State update x_hat = prior_x + K * (z - prior_x)
119    let updated_x = prior_x + kalman_gain * (meas_z - prior_x);
120    // Covariance update P = (1 - K) * P
121    let updated_cov = (1.0 - kalman_gain) * cov_p;
122    (updated_x, updated_cov)
123}
124
125/// Sensor fusion combining IMU yaw angle with GPS geodetic coordinates
126pub fn fuse_imu_gps(lat: f64, lon: f64, yaw_deg: f64) -> (f64, f64, f64) {
127    // Normalizes yaw angle to [-180, 180] and projects coordinates
128    let mut yaw_norm = yaw_deg % 360.0;
129    if yaw_norm > 180.0 {
130        yaw_norm -= 360.0;
131    } else if yaw_norm < -180.0 {
132        yaw_norm += 360.0;
133    }
134    // Apply small complementary correction factor
135    let corrected_lat = lat + (yaw_norm.to_radians().cos() * 0.00001);
136    let corrected_lon = lon + (yaw_norm.to_radians().sin() * 0.00001);
137    (corrected_lat, corrected_lon, yaw_norm)
138}
139
140/// 3-DOF Analytical Inverse Kinematics solver for a planar/articulated robotic arm
141/// Computes joint angles (base_deg, shoulder_deg, elbow_deg) given target (x, y, z)
142pub fn ik_3dof_arm(x: f64, y: f64, z: f64) -> (f64, f64, f64) {
143    // Standard link lengths in mm
144    let l1 = 100.0_f64;
145    let l2 = 120.0_f64;
146    let l3 = 100.0_f64;
147
148    // 1. Base rotation angle around Z axis
149    let base_rad = y.atan2(x);
150    let base_deg = base_rad.to_degrees().clamp(0.0, 180.0);
151
152    // Horizontal distance in XY plane
153    let r = (x * x + y * y).sqrt();
154    let r_eff = (r - 20.0).max(1.0); // account for base offset
155    let z_eff = z - l1;
156
157    // Hypotenuse to wrist center
158    let d2 = r_eff * r_eff + z_eff * z_eff;
159    let d = d2.sqrt().clamp(10.0, l2 + l3 - 0.1);
160
161    // 2. Elbow angle using Law of Cosines
162    let cos_elbow = ((d2 - l2 * l2 - l3 * l3) / (2.0 * l2 * l3)).clamp(-1.0, 1.0);
163    let elbow_rad = cos_elbow.acos();
164    let elbow_deg = elbow_rad.to_degrees().clamp(0.0, 180.0);
165
166    // 3. Shoulder angle
167    let alpha = z_eff.atan2(r_eff);
168    let cos_beta = ((l2 * l2 + d2 - l3 * l3) / (2.0 * l2 * d)).clamp(-1.0, 1.0);
169    let beta = cos_beta.acos();
170    let shoulder_rad = alpha + beta;
171    let shoulder_deg = shoulder_rad.to_degrees().clamp(0.0, 180.0);
172
173    (base_deg, shoulder_deg, elbow_deg)
174}
175
176/// Differential Drive Kinematic Inverse Solver
177/// Given desired linear velocity `linear_v` (m/s) and angular velocity `angular_w` (rad/s),
178/// returns left and right wheel PWM speeds (-100 to 100 %).
179pub fn diff_drive_ik(linear_v: f64, angular_w: f64) -> (i32, i32) {
180    let wheel_base = 0.30_f64; // 30 cm wheel separation
181    let max_velocity = 1.5_f64; // 1.5 m/s max velocity at 100% PWM
182
183    let v_left = linear_v - (angular_w * wheel_base / 2.0);
184    let v_right = linear_v + (angular_w * wheel_base / 2.0);
185
186    let pwm_left = ((v_left / max_velocity) * 100.0).round().clamp(-100.0, 100.0) as i32;
187    let pwm_right = ((v_right / max_velocity) * 100.0).round().clamp(-100.0, 100.0) as i32;
188    (pwm_left, pwm_right)
189}
190
191/// Linear Quadratic Regulator (LQR) feedback control calculation (-K * x)
192pub fn lqr_compute(err_x: f64, err_v: f64, k1: f64, k2: f64) -> f64 {
193    let control_input = -(k1 * err_x + k2 * err_v);
194    control_input.clamp(-100.0, 100.0)
195}
196
197/// Geodetic Earth distance using Haversine formula (meters)
198pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
199    let r_earth = 6_371_000.0_f64; // Earth radius in meters
200    let d_lat = (lat2 - lat1).to_radians();
201    let d_lon = (lon2 - lon1).to_radians();
202
203    let a = (d_lat / 2.0).sin().powi(2)
204        + lat1.to_radians().cos() * lat2.to_radians().cos() * (d_lon / 2.0).sin().powi(2);
205    let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
206    r_earth * c
207}
208
209/// Scan a 360-degree LiDAR point cloud array and return (min_distance, min_angle_deg)
210pub fn lidar_find_closest(scan: &[f64]) -> (f64, u32) {
211    let mut min_dist = f64::MAX;
212    let mut min_angle = 0;
213    for (angle, &dist) in scan.iter().enumerate() {
214        if dist > 0.1 && dist < min_dist {
215            min_dist = dist;
216            min_angle = angle as u32;
217        }
218    }
219    if min_dist == f64::MAX {
220        (999.0, 0)
221    } else {
222        (min_dist, min_angle)
223    }
224}
225
226/// Built-in Standard Library Modules in Sesh (Documentation & API metadata)
227pub struct StdlibModule {
228    pub name: &'static str,
229    pub description: &'static str,
230    pub functions: &'static [(&'static str, &'static str)],
231}
232
233pub const STDLIB_MODULES: &[StdlibModule] = &[
234    StdlibModule {
235        name: "std::gpio",
236        description: "General Purpose Input/Output Pin Control, Modes, Interrupts",
237        functions: &[
238            ("write(pin: u32, level: bool) -> void", "Write HIGH or LOW to a digital output pin"),
239            ("read(pin: u32) -> bool", "Read digital state from an input pin"),
240            ("set_mode(pin: u32, mode: PinMode) -> void", "Set pin mode to IN, OUT, ALT, or PWM"),
241            ("attach_interrupt(pin: u32, edge: String, callback: fn) -> void", "Attach a hardware interrupt handler"),
242        ],
243    },
244    StdlibModule {
245        name: "std::i2c",
246        description: "I2C Master/Slave Communication & Sensor Interface",
247        functions: &[
248            ("read(bus: u32, addr: u8, len: usize) -> [u8; 32]", "Read bytes from an I2C slave address"),
249            ("write(bus: u32, addr: u8, data: [u8; 32]) -> void", "Write bytes to an I2C slave address"),
250            ("scan(bus: u32) -> [u8; 16]", "Scan bus for active I2C slave addresses"),
251        ],
252    },
253    StdlibModule {
254        name: "std::spi",
255        description: "SPI High-Speed Peripheral Bus & Display/SD-Card Driver",
256        functions: &[
257            ("transfer(bus: u32, tx: u8) -> u8", "Simultaneous full-duplex SPI byte transfer"),
258            ("write_buffer(bus: u32, data: [u8; 64]) -> void", "Burst write buffer over SPI"),
259        ],
260    },
261    StdlibModule {
262        name: "std::uart",
263        description: "Serial UART Telemetry, GPS, and LIDAR Communication",
264        functions: &[
265            ("send(port: u32, data: string) -> void", "Transmit string telemetry over serial port"),
266            ("receive(port: u32) -> string", "Receive string packet from UART rx buffer"),
267            ("set_baudrate(port: u32, baud: u32) -> void", "Configure serial port baudrate (e.g. 115200)"),
268        ],
269    },
270    StdlibModule {
271        name: "std::can",
272        description: "CAN-Bus automotive and multi-node industrial robotics protocol",
273        functions: &[
274            ("send(id: u32, data: [u8; 8], extended: bool) -> void", "Send a standard or extended CAN frame"),
275            ("receive() -> (u32, [u8; 8])", "Read next incoming CAN frame from bus queue"),
276            ("set_filter(id: u32, mask: u32) -> void", "Configure hardware CAN acceptance filters"),
277        ],
278    },
279    StdlibModule {
280        name: "std::pwm",
281        description: "Pulse Width Modulation for Servo Motors and LED Dimming",
282        functions: &[
283            ("write(pin: u32, duty_cycle: u32) -> void", "Set PWM duty cycle between 0% and 100%"),
284            ("set_frequency(pin: u32, freq_hz: u32) -> void", "Set PWM frequency in Hertz"),
285        ],
286    },
287    StdlibModule {
288        name: "std::bluetooth",
289        description: "Bluetooth Low Energy (BLE) GATT Server & Remote Controller",
290        functions: &[
291            ("advertise(name: string) -> void", "Start BLE advertising with custom device name"),
292            ("notify(char_uuid: string, value: string) -> void", "Send BLE GATT notification to connected client"),
293        ],
294    },
295    StdlibModule {
296        name: "std::wifi",
297        description: "Wi-Fi AP/Station Mode & TCP/UDP Telemetry Sockets",
298        functions: &[
299            ("connect(ssid: string, psk: string) -> bool", "Connect to wireless access point"),
300            ("start_ap(ssid: string) -> void", "Start Access Point mode for local robot config"),
301        ],
302    },
303    StdlibModule {
304        name: "std::usb",
305        description: "USB CDC/ACM Serial, HID, and USB-OTG Device Mode",
306        functions: &[
307            ("send_cdc(data: string) -> void", "Send data over USB CDC Virtual Com Port"),
308            ("read_cdc() -> string", "Read data from USB CDC endpoint"),
309        ],
310    },
311    StdlibModule {
312        name: "std::ros",
313        description: "Native ROS 2 Pub/Sub Messaging Layer & Topic Bridge",
314        functions: &[
315            ("init(node_name: string) -> void", "Initialize ROS 2 node context"),
316            ("publish(topic: string, msg: string) -> void", "Publish payload to ROS 2 topic"),
317            ("subscribe(topic: string, callback: fn) -> void", "Subscribe to ROS 2 topic with callback"),
318        ],
319    },
320    StdlibModule {
321        name: "std::motor",
322        description: "DC, Stepper, ESC/BLDC Drivers, Differential Drive & PID Controller",
323        functions: &[
324            ("set_speed(channel: u32, speed: i32) -> void", "Set motor speed (-100 to 100)"),
325            ("pid_compute(setpoint: f64, actual: f64, kp: f64, ki: f64, kd: f64) -> f64", "Compute PID control output"),
326            ("stop_all() -> void", "Emergency stop all motor channels"),
327        ],
328    },
329    StdlibModule {
330        name: "std::sensor",
331        description: "High-level Sensor Abstractions (Ultrasonic, IMU, Encoders, LIDAR)",
332        functions: &[
333            ("ultrasonic_read(trig: u32, echo: u32) -> f64", "Read ultrasonic distance in centimeters"),
334            ("imu_read_rpy() -> (f64, f64, f64)", "Read roll, pitch, yaw from IMU sensor fusion"),
335            ("encoder_count(channel: u32) -> i64", "Get optical encoder pulse count"),
336        ],
337    },
338    StdlibModule {
339        name: "std::camera",
340        description: "Vision Pipeline, Frame Buffer Acquisition, Edge Detection & Blob Tracking",
341        functions: &[
342            ("capture() -> [u8; 1024]", "Capture frame from camera sensor"),
343            ("detect_color_blob(r: u8, g: u8, b: u8) -> (f64, f64)", "Detect center (X,Y) of color blob in image"),
344        ],
345    },
346    StdlibModule {
347        name: "std::ai",
348        description: "Lightweight Robotics Edge AI & Neural Network Inference Engine",
349        functions: &[
350            ("infer(model_id: string, inputs: [f64; 16]) -> [f64; 4]", "Run edge AI model inference on sensor tensor"),
351            ("classify_obstacle(distance_cm: f64, camera_score: f64) -> string", "Classify obstacle type (Wall, Person, Unknown)"),
352        ],
353    },
354    StdlibModule {
355        name: "std::thread",
356        description: "Concurrent Execution, Mutex Locks, and Message Queues",
357        functions: &[
358            ("spawn(task_fn: fn) -> void", "Spawn a concurrent worker thread"),
359            ("yield_now() -> void", "Yield execution to scheduler"),
360        ],
361    },
362    StdlibModule {
363        name: "std::realtime",
364        description: "Deterministic Timers, Priority Scheduling & Deadline Monitoring",
365        functions: &[
366            ("sleep_ms(ms: u32) -> void", "Sleep deterministic milliseconds without drift"),
367            ("deadline_monitor(task_name: string, budget_ms: u32) -> bool", "Verify if current cycle is within deadline"),
368            ("watchdog_reset() -> void", "Kick hardware watchdog timer"),
369        ],
370    },
371    StdlibModule {
372        name: "std::kalman",
373        description: "Kalman Filter & Extended Kalman Filter (EKF) Sensor Fusion Equations",
374        functions: &[
375            ("ekf_update(prior_x: f64, meas_z: f64, cov_p: f64) -> (f64, f64)", "1D/2D Extended Kalman Filter measurement step"),
376            ("fuse_imu_gps(lat: f64, lon: f64, yaw: f64) -> (f64, f64)", "Fuse IMU orientation with GPS coordinates"),
377        ],
378    },
379    StdlibModule {
380        name: "std::lidar",
381        description: "2D/3D LIDAR Scan Point Cloud Parsing, SLAM Scan Matching & Grid Map",
382        functions: &[
383            ("scan_360(port: u32) -> [f64; 360]", "Acquire 360-degree distance scan from LIDAR sensor"),
384            ("find_closest_obstacle(scan: [f64; 360]) -> (f64, u32)", "Return minimum distance and angle from scan"),
385        ],
386    },
387    StdlibModule {
388        name: "std::kinematics",
389        description: "Inverse & Forward Kinematics Solvers for Robotic Arms & Wheels",
390        functions: &[
391            ("ik_3dof_arm(x: f64, y: f64, z: f64) -> (f64, f64, f64)", "Compute joint angles for 3-DOF robotic manipulator"),
392            ("diff_drive_ik(linear_v: f64, angular_w: f64) -> (f64, f64)", "Compute left and right wheel velocities"),
393        ],
394    },
395    StdlibModule {
396        name: "std::path",
397        description: "Pure Pursuit Geometric Path Tracking & Curvature Guidance",
398        functions: &[
399            ("pure_pursuit(rx: f64, ry: f64, rtheta: f64, wx: f64, wy: f64, v: f64) -> (f64, f64)", "Compute curvature kappa and angular velocity omega"),
400        ],
401    },
402    StdlibModule {
403        name: "std::control",
404        description: "Advanced Controllers: LQR, Model Predictive Control (MPC), Feedforward PID",
405        functions: &[
406            ("lqr_compute(err_x: f64, err_v: f64, k1: f64, k2: f64) -> f64", "Compute optimal Linear Quadratic Regulator feedback"),
407            ("feedforward_pid(sp: f64, act: f64, kp: f64, ki: f64, kd: f64, kf: f64, prev_sp: f64, dt: f64) -> f64", "Compute Feed-Forward + PID output"),
408            ("mpc_step(state: [f64; 4], horizon: u32) -> f64", "Compute 1-step Model Predictive Control action"),
409        ],
410    },
411    StdlibModule {
412        name: "std::battery",
413        description: "Battery Management System (BMS), Voltage, SoC & Thermal Monitoring",
414        functions: &[
415            ("read_soc() -> f64", "Read State of Charge percentage (0.0 to 100.0%)"),
416            ("read_voltage() -> f64", "Read main pack voltage in Volts"),
417            ("is_charging() -> bool", "Check if external charger is attached"),
418        ],
419    },
420    StdlibModule {
421        name: "std::gps",
422        description: "GPS NMEA Sentence Parsing, Geodetic Coordinates & Waypoint Guidance",
423        functions: &[
424            ("read_lat_lon() -> (f64, f64)", "Read current latitude and longitude degrees"),
425            ("distance_to_waypoint(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64", "Haversine distance in meters"),
426        ],
427    },
428    StdlibModule {
429        name: "std::servo",
430        description: "RC PWM Servo Angular Positioning & Trajectory Profiling",
431        functions: &[
432            ("write_angle(pin: u32, degrees: f64) -> void", "Position servo motor from 0.0 to 180.0 degrees"),
433            ("set_pulse_range(pin: u32, min_us: u32, max_us: u32) -> void", "Configure servo pulse width limits"),
434        ],
435    },
436    StdlibModule {
437        name: "std::stepper",
438        description: "Stepper Motor Microstepping, Acceleration Ramps & Trapezoidal Profiler",
439        functions: &[
440            ("step_angle(channel: u32, steps: i32, rpm: f64) -> void", "Move stepper motor specified steps at RPM"),
441            ("set_microsteps(channel: u32, div: u32) -> void", "Configure microstep divisor (1, 2, 4, 8, 16, 32)"),
442        ],
443    },
444    StdlibModule {
445        name: "std::watchdog",
446        description: "Hardware Watchdog Timer Control, Safety Dead-Man's Switch & Panic Recovery",
447        functions: &[
448            ("start(timeout_ms: u32) -> void", "Start hardware watchdog timer"),
449            ("reset() -> void", "Kick/refresh watchdog timer before expiration"),
450            ("trigger_panic() -> void", "Trigger immediate MCU hardware safety reset"),
451        ],
452    },
453    StdlibModule {
454        name: "std::canopen",
455        description: "Industrial CANopen DS301/DS402 Protocol for Smart Motors & Servos",
456        functions: &[
457            ("send_pdo(node_id: u8, pdo_num: u8, data: [u8; 8]) -> void", "Transmit Process Data Object (PDO)"),
458            ("write_sdo(node_id: u8, index: u16, sub: u8, val: u32) -> bool", "Write Service Data Object (SDO)"),
459            ("nmt_command(node_id: u8, cmd: u8) -> void", "Send Network Management (NMT) state transition"),
460        ],
461    },
462    StdlibModule {
463        name: "std::ethercat",
464        description: "High-Speed Real-Time EtherCAT Fieldbus for Industrial Arm Manipulators",
465        functions: &[
466            ("init_master(iface: string) -> bool", "Initialize real-time EtherCAT master network"),
467            ("sync_dc(cycle_time_ns: u32) -> void", "Synchronize Distributed Clocks (DC) across slaves"),
468            ("write_pdo(slave_idx: u16, offset: u16, val: u32) -> void", "Write cyclic PDO process data"),
469        ],
470    },
471    StdlibModule {
472        name: "std::modbus",
473        description: "Industrial Modbus RTU/TCP RS485 Protocol for PLC & Sensor Interfacing",
474        functions: &[
475            ("read_holding_registers(slave: u8, addr: u16, count: u16) -> [u16; 16]", "Read Modbus holding registers"),
476            ("write_coil(slave: u8, addr: u16, state: bool) -> bool", "Write single Modbus coil output"),
477        ],
478    },
479    StdlibModule {
480        name: "std::slam",
481        description: "Autonomous SLAM 2D/3D Localization, Mapping & Occupancy Grid Engine",
482        functions: &[
483            ("update_grid(scan: [f64; 360], pose_x: f64, pose_y: f64) -> void", "Update Bayesian occupancy grid map"),
484            ("get_robot_pose() -> (f64, f64, f64)", "Get estimated map frame pose (X, Y, Theta)"),
485            ("raycast_obstacle(angle: f64) -> f64", "Raycast expected obstacle distance in grid"),
486        ],
487    },
488    StdlibModule {
489        name: "std::pid_autotune",
490        description: "Relay Autotuning Algorithm for Automated PID Gain Calibration",
491        functions: &[
492            ("tune_step(actual: f64, setpoint: f64) -> f64", "Execute relay autotune excitation step"),
493            ("get_gains_ziegler_nichols() -> (f64, f64, f64)", "Compute Ziegler-Nichols Kp, Ki, Kd gains"),
494        ],
495    },
496];
497
498pub fn get_stdlib_module(name: &str) -> Option<&'static StdlibModule> {
499    STDLIB_MODULES.iter().find(|m| m.name == name)
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    #[test]
507    fn test_pid_computation() {
508        let out = pid_compute(100.0, 50.0, 1.5, 0.1, 0.05);
509        assert!(out > 0.0 && out <= 100.0);
510    }
511
512    #[test]
513    fn test_ekf_update() {
514        let (x, p) = ekf_update(10.0, 12.0, 0.5, 0.1);
515        assert!((x - 11.666).abs() < 0.1);
516        assert!(p < 0.5);
517    }
518
519    #[test]
520    fn test_ik_3dof_arm() {
521        let (base, shoulder, elbow) = ik_3dof_arm(150.0, 100.0, 200.0);
522        assert!(base >= 0.0 && base <= 180.0);
523        assert!(shoulder >= 0.0 && shoulder <= 180.0);
524        assert!(elbow >= 0.0 && elbow <= 180.0);
525    }
526
527    #[test]
528    fn test_diff_drive_ik() {
529        let (left, right) = diff_drive_ik(1.0, 0.5);
530        assert!(left > 0 && right > 0);
531        assert!(right > left); // turning left means right wheel turns faster
532    }
533
534    #[test]
535    fn test_haversine_distance() {
536        let d = haversine_distance(30.0, 31.0, 30.01, 31.01);
537        assert!(d > 1000.0 && d < 2000.0); // ~1480 meters
538    }
539
540    #[test]
541    fn test_stateful_pid_damping() {
542        let out1 = pid_compute(100.0, 0.0, 1.0, 0.1, 0.05);
543        let out2 = pid_compute(100.0, 50.0, 1.0, 0.1, 0.05);
544        assert!(out2 < out1);
545    }
546
547    #[test]
548    fn test_pure_pursuit() {
549        let (kappa, w) = pure_pursuit(0.0, 0.0, 0.0, 10.0, 5.0, 1.5);
550        assert!(kappa > 0.0 && w > 0.0);
551    }
552
553    #[test]
554    fn test_feedforward_pid() {
555        let out = feedforward_pid(100.0, 90.0, 1.0, 0.01, 0.05, 0.5, 90.0, 0.005);
556        assert!(out > 0.0);
557    }
558}