// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Ranging library use glam::{EulerRot, Quat, Vec3}; /// The Free Space Path Loss (FSPL) model is considered as the standard /// under the ideal scenario. /// (dBm) PATH_LOSS at 1m for isotropic antenna transmitting BLE. const PATH_LOSS_AT_1M: f32 = 40.20; /// Convert distance to RSSI using the free space path loss equation. /// See [Free-space_path_loss][1]. /// /// [1]: http://en.wikipedia.org/wiki/Free-space_path_loss /// /// # Parameters /// /// * `distance`: distance in meters (m). /// * `tx_power`: transmitted power (dBm) calibrated to 1 meter. /// /// # Returns /// /// The rssi that would be measured at that distance, in the /// range -120..20 dBm, pub fn distance_to_rssi(tx_power: i8, distance: f32) -> i8 { // TODO(b/285634913) // Rootcanal reporting tx_power of 0 or 1 during Nearby Share let new_tx_power = match tx_power { 0 | 1 => -49, _ => tx_power, }; match distance == 0.0 { true => (new_tx_power as f32 + PATH_LOSS_AT_1M).clamp(-120.0, 20.0) as i8, false => (new_tx_power as f32 - 20.0 * distance.log10()).clamp(-120.0, 20.0) as i8, } } // helper function for performing division with // zero division check #[allow(unused)] fn checked_div(num: f32, den: f32) -> Option { (den != 0.).then_some(num / den) } // helper function for calculating azimuth angle // from a given 3D delta vector. #[allow(unused)] fn azimuth(delta: Vec3) -> f32 { checked_div(delta.x, delta.z).map_or( match delta.x == 0. { true => 0., false => delta.x.signum() * std::f32::consts::FRAC_2_PI, }, f32::atan, ) + if delta.z >= 0. { 0. } else { delta.x.signum() * std::f32::consts::PI } } // helper function for calculating elevation angle // from a given 3D delta vector. #[allow(unused)] fn elevation(delta: Vec3) -> f32 { checked_div(delta.y, f32::sqrt(delta.x.powi(2) + delta.z.powi(2))) .map_or(delta.y.signum() * std::f32::consts::FRAC_PI_2, f32::atan) } /// Pose struct /// /// This struct allows for a mathematical representation of /// position and orientation values from the protobufs, which /// would enable to compute range, azimuth, and elevation. #[allow(unused)] pub struct Pose { position: Vec3, orientation: Quat, } impl Pose { #[allow(unused)] pub fn new(x: f32, y: f32, z: f32, yaw: f32, pitch: f32, roll: f32) -> Self { Pose { // Converts x, y, z from meters to centimeters position: Vec3::new(x * 100., y * 100., z * 100.), // Converts roll, pitch, yaw from degrees to radians orientation: Quat::from_euler( EulerRot::ZXY, roll.to_radians(), pitch.to_radians(), yaw.to_radians(), ), } } } /// UWB Ranging Model for computing range, azimuth, and elevation /// The raning model brought from https://github.com/google/pica #[allow(unused)] pub fn compute_range_azimuth_elevation(a: &Pose, b: &Pose) -> anyhow::Result<(f32, i16, i8)> { let delta = b.position - a.position; let distance = delta.length().clamp(0.0, u16::MAX as f32); let direction = a.orientation.mul_vec3(delta); let azimuth = azimuth(direction).to_degrees().round(); let elevation = elevation(direction).to_degrees().round(); if !(-180. ..=180.).contains(&azimuth) { return Err(anyhow::anyhow!("azimuth is not between -180 and 180. value: {azimuth}")); } if !(-90. ..=90.).contains(&elevation) { return Err(anyhow::anyhow!("elevation is not between -90 and 90. value: {elevation}")); } Ok((distance, azimuth as i16, elevation as i8)) } #[cfg(test)] mod tests { use super::*; #[test] fn rssi_at_0m() { let rssi_at_0m = distance_to_rssi(-120, 0.0); assert_eq!(rssi_at_0m, -79); } #[test] fn rssi_at_1m() { // With transmit power at 0 dBm verify a reasonable rssi at 1m let rssi_at_1m = distance_to_rssi(0, 1.0); assert!(rssi_at_1m < -35 && rssi_at_1m > -55); } #[test] fn rssi_saturate_inf() { // Verify that the rssi saturates at -120 for very large distances. let rssi_inf = distance_to_rssi(-120, 1000.0); assert_eq!(rssi_inf, -120); } #[test] fn rssi_saturate_sup() { // Verify that the rssi saturates at +20 for the largest tx power // and nearest distance. let rssi_sup = distance_to_rssi(20, 0.0); assert_eq!(rssi_sup, 20); } #[test] fn range() { let a_pose = Pose::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); { let b_pose = Pose::new(10.0, 0.0, 0.0, 0.0, 0.0, 0.0); let (range, _, _) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(range, 1000.); } { let b_pose = Pose::new(-10.0, 0.0, 0.0, 0.0, 0.0, 0.0); let (range, _, _) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(range, 1000.); } { let b_pose = Pose::new(10.0, 10.0, 0.0, 0.0, 0.0, 0.0); let (range, _, _) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(range, f32::sqrt(2000000.)); } { let b_pose = Pose::new(-10.0, -10.0, -10.0, 0.0, 0.0, 0.0); let (range, _, _) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(range, f32::sqrt(3000000.)); } } #[test] fn azimuth_without_rotation() { let a_pose = Pose::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); { let b_pose = Pose::new(10.0, 0.0, 10.0, 0.0, 0.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 45); assert_eq!(elevation, 0); } { let b_pose = Pose::new(-10.0, 0.0, 10.0, 0.0, 0.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, -45); assert_eq!(elevation, 0); } { let b_pose = Pose::new(10.0, 0.0, -10.0, 0.0, 0.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 135); assert_eq!(elevation, 0); } { let b_pose = Pose::new(-10.0, 0.0, -10.0, 0.0, 0.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, -135); assert_eq!(elevation, 0); } } #[test] fn elevation_without_rotation() { let a_pose = Pose::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); { let b_pose = Pose::new(0.0, 10.0, 10.0, 0.0, 0.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 0); assert_eq!(elevation, 45); } { let b_pose = Pose::new(0.0, -10.0, 10.0, 0.0, 0.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 0); assert_eq!(elevation, -45); } { let b_pose = Pose::new(0.0, 10.0, -10.0, 0.0, 0.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert!(azimuth == 180 || azimuth == -180); assert_eq!(elevation, 45); } { let b_pose = Pose::new(0.0, -10.0, -10.0, 0.0, 0.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert!(azimuth == 180 || azimuth == -180); assert_eq!(elevation, -45); } } #[test] fn rotation_only() { let b_pose = Pose::new(0.0, 0.0, 10.0, 0.0, 0.0, 0.0); { let a_pose = Pose::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 0); assert_eq!(elevation, 0); } { let a_pose = Pose::new(0.0, 0.0, 0.0, 45.0, 0.0, 0.0); // <=> azimuth = -45deg let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 45); assert_eq!(elevation, 0); } { let a_pose = Pose::new(0.0, 0.0, 0.0, 0.0, 45.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 0); assert_eq!(elevation, -45); } { let a_pose = Pose::new(0.0, 0.0, 0.0, 0.0, 0.0, 45.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 0); assert_eq!(elevation, 0); } } #[test] fn rotation_only_complex_position() { let b_pose = Pose::new(10.0, 10.0, 10.0, 0.0, 0.0, 0.0); { let a_pose = Pose::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 45); assert_eq!(elevation, 35); } { let a_pose = Pose::new(0.0, 0.0, 0.0, 90.0, 0.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 135); assert_eq!(elevation, 35); } { let a_pose = Pose::new(0.0, 0.0, 0.0, 0.0, 90.0, 0.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 45); assert_eq!(elevation, -35); } { let a_pose = Pose::new(0.0, 0.0, 0.0, 0.0, 0.0, 90.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, -45); assert_eq!(elevation, 35); } { let a_pose = Pose::new(0.0, 0.0, 0.0, -45.0, 35.0, 42.0); let (_, azimuth, elevation) = compute_range_azimuth_elevation(&a_pose, &b_pose).unwrap(); assert_eq!(azimuth, 0); assert_eq!(elevation, 0); } } }