From a5e73fd9814cf8577cae6ed883679d4156852cd0 Mon Sep 17 00:00:00 2001 From: Anatolii Kurotych Date: Wed, 20 Nov 2024 13:19:43 +0200 Subject: [PATCH] [Mobile Config] Add deployment_info field to GatewayInfo (#897) --- Cargo.lock | 4 +- mobile_config/src/gateway_info.rs | 190 ++++++++++++++++++++++--- mobile_config/tests/gateway_service.rs | 53 ++++++- 3 files changed, 220 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbcebc3ee..f487f8099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1615,7 +1615,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "beacon" version = "0.1.0" -source = "git+https://github.com/helium/proto?branch=master#ad4db785573778069b559f916b9329ab40854700" +source = "git+https://github.com/helium/proto?branch=master#99908bcedc340d1569f2001565b6f0fee4444fd8" dependencies = [ "base64 0.21.7", "byteorder", @@ -3821,7 +3821,7 @@ dependencies = [ [[package]] name = "helium-proto" version = "0.1.0" -source = "git+https://github.com/helium/proto?branch=master#ad4db785573778069b559f916b9329ab40854700" +source = "git+https://github.com/helium/proto?branch=master#99908bcedc340d1569f2001565b6f0fee4444fd8" dependencies = [ "bytes", "prost", diff --git a/mobile_config/src/gateway_info.rs b/mobile_config/src/gateway_info.rs index 30a49b9e4..1647df856 100644 --- a/mobile_config/src/gateway_info.rs +++ b/mobile_config/src/gateway_info.rs @@ -2,15 +2,99 @@ use chrono::{DateTime, TimeZone, Utc}; use futures::stream::BoxStream; use helium_crypto::PublicKeyBinary; use helium_proto::services::mobile_config::{ - DeviceType as DeviceTypeProto, GatewayInfo as GatewayInfoProto, - GatewayMetadata as GatewayMetadataProto, + gateway_metadata::DeploymentInfo as DeploymentInfoProto, + CbrsDeploymentInfo as CbrsDeploymentInfoProto, + CbrsRadioDeploymentInfo as CbrsRadioDeploymentInfoProto, DeviceType as DeviceTypeProto, + GatewayInfo as GatewayInfoProto, GatewayMetadata as GatewayMetadataProto, + WifiDeploymentInfo as WifiDeploymentInfoProto, }; +use serde::Deserialize; pub type GatewayInfoStream = BoxStream<'static, GatewayInfo>; +#[derive(Clone, Debug, Deserialize)] +pub struct WifiDeploymentInfo { + /// Antenna ID + pub antenna: u32, + /// The height of the hotspot above ground level in whole meters + pub elevation: u32, + pub azimuth: u32, + #[serde(rename = "mechanicalDownTilt")] + pub mechanical_down_tilt: u32, + #[serde(rename = "electricalDownTilt")] + pub electrical_down_tilt: u32, +} +impl From for WifiDeploymentInfo { + fn from(v: WifiDeploymentInfoProto) -> Self { + Self { + antenna: v.antenna, + elevation: v.elevation, + azimuth: v.azimuth, + mechanical_down_tilt: v.mechanical_down_tilt, + electrical_down_tilt: v.electrical_down_tilt, + } + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct CbrsDeploymentInfo { + pub cbrs_radios_deployment_info: Vec, +} + +impl From for CbrsDeploymentInfo { + fn from(v: CbrsDeploymentInfoProto) -> Self { + Self { + cbrs_radios_deployment_info: v + .cbrs_radios_deployment_info + .into_iter() + .map(|v| v.into()) + .collect(), + } + } +} + +impl From for CbrsRadioDeploymentInfo { + fn from(v: CbrsRadioDeploymentInfoProto) -> Self { + Self { + radio_id: v.radio_id, + elevation: v.elevation, + } + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct CbrsRadioDeploymentInfo { + /// CBSD_ID + pub radio_id: String, + /// The asserted elevation of the gateway above ground level in whole meters + pub elevation: u32, +} + +#[derive(Clone, Debug, Deserialize)] +pub enum DeploymentInfo { + #[serde(rename = "wifiInfoV0")] + WifiDeploymentInfo(WifiDeploymentInfo), + #[serde(rename = "cbrsInfoV0")] + CbrsDeploymentInfo(CbrsDeploymentInfo), +} + +impl From for DeploymentInfo { + fn from(v: DeploymentInfoProto) -> Self { + match v { + DeploymentInfoProto::WifiDeploymentInfo(v) => { + DeploymentInfo::WifiDeploymentInfo(v.into()) + } + DeploymentInfoProto::CbrsDeploymentInfo(v) => { + DeploymentInfo::CbrsDeploymentInfo(v.into()) + } + } + } +} + #[derive(Clone, Debug)] pub struct GatewayMetadata { pub location: u64, + pub deployment_info: Option, } #[derive(Clone, Debug)] @@ -42,40 +126,92 @@ impl TryFrom for GatewayInfo { type Error = GatewayInfoProtoParseError; fn try_from(info: GatewayInfoProto) -> Result { - let metadata = if let Some(ref metadata) = info.metadata { + let device_type_ = info.device_type().into(); + + let GatewayInfoProto { + address, + metadata, + device_type: _, + created_at, + refreshed_at, + } = info; + + let metadata = if let Some(metadata) = metadata { Some( - u64::from_str_radix(&metadata.location, 16) - .map(|location| GatewayMetadata { location })?, + u64::from_str_radix(&metadata.location, 16).map(|location| GatewayMetadata { + location, + deployment_info: metadata.deployment_info.map(|v| v.into()), + })?, ) } else { None }; - let device_type = info.device_type().into(); let created_at = Utc - .timestamp_opt(info.created_at as i64, 0) + .timestamp_opt(created_at as i64, 0) .single() - .ok_or(GatewayInfoProtoParseError::InvalidCreatedAt( - info.created_at, - ))?; + .ok_or(GatewayInfoProtoParseError::InvalidCreatedAt(created_at))?; - let refreshed_at = Utc - .timestamp_opt(info.refreshed_at as i64, 0) - .single() - .ok_or(GatewayInfoProtoParseError::InvalidRefreshedAt( - info.refreshed_at, - ))?; + let refreshed_at = Utc.timestamp_opt(refreshed_at as i64, 0).single().ok_or( + GatewayInfoProtoParseError::InvalidRefreshedAt(info.refreshed_at), + )?; Ok(Self { - address: info.address.into(), + address: address.into(), metadata, - device_type, + device_type: device_type_, created_at, refreshed_at, }) } } +impl From for WifiDeploymentInfoProto { + fn from(v: WifiDeploymentInfo) -> Self { + Self { + antenna: v.antenna, + elevation: v.elevation, + azimuth: v.azimuth, + mechanical_down_tilt: v.mechanical_down_tilt, + electrical_down_tilt: v.electrical_down_tilt, + } + } +} + +impl From for CbrsRadioDeploymentInfoProto { + fn from(v: CbrsRadioDeploymentInfo) -> Self { + Self { + radio_id: v.radio_id, + elevation: v.elevation, + } + } +} + +impl From for CbrsDeploymentInfoProto { + fn from(v: CbrsDeploymentInfo) -> Self { + Self { + cbrs_radios_deployment_info: v + .cbrs_radios_deployment_info + .into_iter() + .map(|v| v.into()) + .collect(), + } + } +} + +impl From for DeploymentInfoProto { + fn from(v: DeploymentInfo) -> Self { + match v { + DeploymentInfo::WifiDeploymentInfo(v) => { + DeploymentInfoProto::WifiDeploymentInfo(v.into()) + } + DeploymentInfo::CbrsDeploymentInfo(v) => { + DeploymentInfoProto::CbrsDeploymentInfo(v.into()) + } + } + } +} + impl TryFrom for GatewayInfoProto { type Error = hextree::Error; @@ -83,6 +219,7 @@ impl TryFrom for GatewayInfoProto { let metadata = if let Some(metadata) = info.metadata { Some(GatewayMetadataProto { location: hextree::Cell::from_raw(metadata.location)?.to_string(), + deployment_info: metadata.deployment_info.map(|v| v.into()), }) } else { None @@ -146,7 +283,7 @@ impl std::str::FromStr for DeviceType { } pub(crate) mod db { - use super::{DeviceType, GatewayInfo, GatewayMetadata}; + use super::{DeploymentInfo, DeviceType, GatewayInfo, GatewayMetadata}; use chrono::{DateTime, Utc}; use futures::stream::{Stream, StreamExt}; use helium_crypto::PublicKeyBinary; @@ -155,7 +292,7 @@ pub(crate) mod db { const GET_METADATA_SQL: &str = r#" select kta.entity_key, infos.location::bigint, infos.device_type, - infos.refreshed_at, infos.created_at + infos.refreshed_at, infos.created_at, infos.deployment_info from mobile_hotspot_infos infos join key_to_assets kta on infos.asset = kta.asset "#; @@ -230,11 +367,24 @@ pub(crate) mod db { impl sqlx::FromRow<'_, sqlx::postgres::PgRow> for GatewayInfo { fn from_row(row: &sqlx::postgres::PgRow) -> sqlx::Result { + let deployment_info = + match row.try_get::>, &str>("deployment_info") { + Ok(di) => di.map(|v| v.0), + // We shouldn't fail if an error occurs in this case. + // This is because the data in this column could be inconsistent, + // and we don't want to break backward compatibility. + Err(_e) => None, + }; + + // If location field is None, GatewayMetadata also is None, even if deployment_info is present. + // Because "location" is mandatory field let metadata = row .get::, &str>("location") .map(|loc| GatewayMetadata { location: loc as u64, + deployment_info, }); + let device_type = DeviceType::from_str( row.get::, &str>("device_type") .to_string() diff --git a/mobile_config/tests/gateway_service.rs b/mobile_config/tests/gateway_service.rs index 0ffaa2cc5..8237ba56c 100644 --- a/mobile_config/tests/gateway_service.rs +++ b/mobile_config/tests/gateway_service.rs @@ -3,7 +3,8 @@ use futures::stream::StreamExt; use helium_crypto::{KeyTag, Keypair, PublicKey, PublicKeyBinary, Sign}; use helium_proto::services::mobile_config::{ - self as proto, DeviceType, GatewayClient, GatewayInfoStreamReqV1, GatewayInfoStreamResV1, + self as proto, gateway_metadata::DeploymentInfo, DeviceType, GatewayClient, + GatewayInfoStreamReqV1, GatewayInfoStreamResV1, }; use mobile_config::{ gateway_service::GatewayService, @@ -126,6 +127,7 @@ async fn gateway_stream_info_refreshed_at(pool: PgPool) { asset1_pubkey.clone().into(), now, Some(now), + None, ) .await; add_db_record( @@ -136,6 +138,7 @@ async fn gateway_stream_info_refreshed_at(pool: PgPool) { asset2_pubkey.clone().into(), now_plus_10, Some(now_plus_10), + None, ) .await; @@ -199,6 +202,7 @@ async fn gateway_stream_info_refreshed_at_is_null(pool: PgPool) { asset1_pubkey.clone().into(), now, None, + None, ) .await; @@ -238,6 +242,7 @@ async fn gateway_stream_info_data_types(pool: PgPool) { asset1_pubkey.clone().into(), now, Some(now), + Some(r#"{"wifiInfoV0": {"antenna": 18, "azimuth": 160, "elevation": 5, "electricalDownTilt": 1, "mechanicalDownTilt": 2}}"#) ) .await; add_db_record( @@ -248,6 +253,8 @@ async fn gateway_stream_info_data_types(pool: PgPool) { asset2_pubkey.clone().into(), now, Some(now), + // Should be returned None in deployment info + Some(r#"{"wifiInfoV0Invalid": {"antenna": 18}}"#), ) .await; add_db_record( @@ -258,6 +265,7 @@ async fn gateway_stream_info_data_types(pool: PgPool) { asset3_pubkey.clone().into(), now, Some(now), + None, ) .await; @@ -312,9 +320,31 @@ async fn gateway_stream_info_data_types(pool: PgPool) { .collect::>() .await; let gateways = resp.first().unwrap().gateways.clone(); + + // Check deployment info assert_eq!(gateways.len(), 3); + for gw in gateways { + if let Some(metadata) = &gw.metadata { + if DeviceType::try_from(gw.device_type).unwrap() != DeviceType::WifiIndoor { + assert!(metadata.deployment_info.is_none()); + } else { + let deployment_info = metadata.deployment_info.as_ref().unwrap(); + match deployment_info { + DeploymentInfo::WifiDeploymentInfo(v) => { + assert_eq!(v.antenna, 18); + assert_eq!(v.azimuth, 160); + assert_eq!(v.elevation, 5); + assert_eq!(v.electrical_down_tilt, 1); + assert_eq!(v.mechanical_down_tilt, 2); + } + DeploymentInfo::CbrsDeploymentInfo(_) => panic!(), + }; + } + } + } } +#[allow(clippy::too_many_arguments)] async fn add_db_record( pool: &PgPool, asset: &str, @@ -323,8 +353,18 @@ async fn add_db_record( key: PublicKeyBinary, created_at: DateTime, refreshed_at: Option>, + deployment_info: Option<&str>, ) { - add_mobile_hotspot_infos(pool, asset, location, device_type, created_at, refreshed_at).await; + add_mobile_hotspot_infos( + pool, + asset, + location, + device_type, + created_at, + refreshed_at, + deployment_info, + ) + .await; add_asset_key(pool, asset, key).await; } @@ -335,13 +375,14 @@ async fn add_mobile_hotspot_infos( device_type: &str, created_at: DateTime, refreshed_at: Option>, + deployment_info: Option<&str>, ) { sqlx::query( r#" INSERT INTO -"mobile_hotspot_infos" ("asset", "location", "device_type", "created_at", "refreshed_at") +"mobile_hotspot_infos" ("asset", "location", "device_type", "created_at", "refreshed_at", "deployment_info") VALUES -($1, $2, $3::jsonb, $4, $5); +($1, $2, $3::jsonb, $4, $5, $6::jsonb); "#, ) .bind(asset) @@ -349,6 +390,7 @@ async fn add_mobile_hotspot_infos( .bind(device_type) .bind(created_at) .bind(refreshed_at) + .bind(deployment_info) .execute(pool) .await .unwrap(); @@ -378,7 +420,8 @@ async fn create_db_tables(pool: &PgPool) { location numeric NULL, device_type jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT NOW(), - refreshed_at timestamptz + refreshed_at timestamptz, + deployment_info jsonb );"#, ) .execute(pool)