blob: 18bc6f7506fdd4b7bbd65ffa6ba549755790c279 [file] [log] [blame]
// 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<f32> {
(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);
}
}
}