| // 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. |
| |
| // devices_handler.rs |
| // |
| // Provides the API for the frontend and backend to interact with devices. |
| // |
| // The Devices struct is a singleton for the devices collection. |
| // |
| // Additional functions are |
| // -- inactivity instant |
| // -- vending device identifiers |
| |
| use super::chip::ChipIdentifier; |
| use super::device::DeviceIdentifier; |
| use super::id_factory::IdFactory; |
| use crate::bluetooth as bluetooth_facade; |
| use crate::devices::device::AddChipResult; |
| use crate::devices::device::Device; |
| use crate::events; |
| use crate::events::Event; |
| use crate::ffi::ffi_response_writable::CxxServerResponseWriter; |
| use crate::ffi::CxxServerResponseWriterWrapper; |
| use crate::http_server::server_response::ResponseWritable; |
| use crate::wifi as wifi_facade; |
| use cxx::{CxxString, CxxVector}; |
| use http::Request; |
| use http::Version; |
| use lazy_static::lazy_static; |
| use log::{info, warn}; |
| use netsim_proto::common::ChipKind as ProtoChipKind; |
| use netsim_proto::configuration::Controller; |
| use netsim_proto::frontend::CreateDeviceRequest; |
| use netsim_proto::frontend::CreateDeviceResponse; |
| use netsim_proto::frontend::DeleteChipRequest; |
| use netsim_proto::frontend::ListDeviceResponse; |
| use netsim_proto::frontend::PatchDeviceRequest; |
| use netsim_proto::model::chip_create::Chip as ProtoBuiltin; |
| use netsim_proto::model::ChipCreate; |
| use netsim_proto::model::Position as ProtoPosition; |
| use netsim_proto::model::Scene as ProtoScene; |
| use protobuf::Message; |
| use protobuf::MessageField; |
| use protobuf_json_mapping::merge_from_str; |
| use protobuf_json_mapping::print_to_string; |
| use protobuf_json_mapping::print_to_string_with_options; |
| use protobuf_json_mapping::PrintOptions; |
| use std::collections::btree_map::Entry; |
| use std::collections::BTreeMap; |
| use std::pin::Pin; |
| use std::sync::mpsc::Receiver; |
| use std::sync::Arc; |
| use std::sync::RwLock; |
| use std::sync::RwLockWriteGuard; |
| use std::time::{Duration, Instant}; |
| |
| // The amount of seconds netsimd will wait until the first device has attached. |
| static IDLE_SECS_FOR_SHUTDOWN: u64 = 15; |
| |
| const INITIAL_DEVICE_ID: DeviceIdentifier = 1; |
| const JSON_PRINT_OPTION: PrintOptions = PrintOptions { |
| enum_values_int: false, |
| proto_field_name: false, |
| always_output_default_values: true, |
| _future_options: (), |
| }; |
| |
| lazy_static! { |
| static ref DEVICES: Arc<RwLock<Devices>> = Arc::new(RwLock::new(Devices::new())); |
| } |
| |
| fn get_devices() -> Arc<RwLock<Devices>> { |
| Arc::clone(&DEVICES) |
| } |
| |
| /// The Device resource is a singleton that manages all devices. |
| struct Devices { |
| // BTreeMap allows ListDevice to output devices in order of identifiers. |
| entries: BTreeMap<DeviceIdentifier, Device>, |
| id_factory: IdFactory<DeviceIdentifier>, |
| } |
| |
| impl Devices { |
| fn new() -> Self { |
| Devices { entries: BTreeMap::new(), id_factory: IdFactory::new(INITIAL_DEVICE_ID, 1) } |
| } |
| } |
| |
| #[allow(dead_code)] |
| fn notify_all() { |
| // TODO |
| } |
| |
| /// Returns a Result<AddChipResult, String> after adding chip to resource. |
| /// add_chip is called by the transport layer when a new chip is attached. |
| /// |
| /// The guid is a transport layer identifier for the device (host:port) |
| /// that is adding the chip. |
| /// |
| /// TODO: Replace the parameter of add_chip with a single protobuf |
| pub fn add_chip( |
| device_guid: &str, |
| device_name: &str, |
| chip_create_proto: &ChipCreate, |
| ) -> Result<AddChipResult, String> { |
| let chip_kind = chip_create_proto.kind.enum_value_or(ProtoChipKind::UNSPECIFIED); |
| let result = { |
| let devices_arc = get_devices(); |
| let mut devices = devices_arc.write().unwrap(); |
| let (device_id, _) = get_or_create_device( |
| &mut devices, |
| Some(device_guid), |
| Some(device_name), |
| chip_kind == ProtoChipKind::BLUETOOTH_BEACON, |
| ); |
| |
| let chip_name = (chip_create_proto.name != String::default()) |
| .then_some(chip_create_proto.name.as_str()); |
| // This is infrequent, so we can afford to do another lookup for the device. |
| devices |
| .entries |
| .get_mut(&device_id) |
| .ok_or(format!("Device not found for device_id: {device_id}"))? |
| .add_chip( |
| chip_kind, |
| &chip_create_proto.address, |
| chip_name, |
| &chip_create_proto.manufacturer, |
| &chip_create_proto.product_name, |
| ) |
| }; |
| |
| // Device resource is no longer locked |
| match result { |
| // id_tuple = (DeviceIdentifier, ChipIdentifier) |
| Ok((device_id, chip_id)) => { |
| let facade_id = match chip_kind { |
| ProtoChipKind::BLUETOOTH => bluetooth_facade::bluetooth_add( |
| device_id, |
| &chip_create_proto.address, |
| &chip_create_proto.bt_properties, |
| ), |
| ProtoChipKind::BLUETOOTH_BEACON => bluetooth_facade::ble_beacon_add( |
| device_id, |
| String::from(device_name), |
| chip_id, |
| chip_create_proto, |
| )?, |
| ProtoChipKind::WIFI => wifi_facade::wifi_add(device_id), |
| _ => return Err(format!("Unknown chip kind: {:?}", chip_kind)), |
| }; |
| // Add the facade_id into the resources |
| { |
| get_devices() |
| .write() |
| .unwrap() |
| .entries |
| .get_mut(&device_id) |
| .ok_or(format!("Device not found for device_id: {device_id}"))? |
| .chips |
| .get_mut(&chip_id) |
| .ok_or(format!("Chip not found for device_id: {device_id}, chip_id:{chip_id}"))? |
| .facade_id = Some(facade_id); |
| } |
| info!( |
| "Added Chip: device_name: {device_name}, chip_kind: {chip_kind:?}, device_id: {device_id}, chip_id: {chip_id}, facade_id: {facade_id}", |
| ); |
| // Update Capture resource |
| events::publish(Event::ChipAdded { |
| chip_id, |
| chip_kind, |
| facade_id, |
| device_name: device_name.to_string(), |
| builtin: chip_kind == ProtoChipKind::BLUETOOTH_BEACON, |
| }); |
| Ok(AddChipResult { device_id, chip_id, facade_id }) |
| } |
| Err(err) => { |
| warn!( |
| "Failed to add chip: device_name: {device_name}, chip_kind: {chip_kind:?}, error: {err}", |
| ); |
| Err(err) |
| } |
| } |
| } |
| |
| /// AddChipResult for C++ to handle |
| pub struct AddChipResultCxx { |
| device_id: u32, |
| chip_id: u32, |
| facade_id: u32, |
| is_error: bool, |
| } |
| |
| impl AddChipResultCxx { |
| pub fn get_device_id(&self) -> u32 { |
| self.device_id |
| } |
| |
| pub fn get_chip_id(&self) -> u32 { |
| self.chip_id |
| } |
| |
| pub fn get_facade_id(&self) -> u32 { |
| self.facade_id |
| } |
| |
| pub fn is_error(&self) -> bool { |
| self.is_error |
| } |
| } |
| |
| /// An AddChip function for Rust Device API. |
| /// The backend gRPC code will be invoking this method. |
| #[allow(clippy::too_many_arguments)] |
| pub fn add_chip_cxx( |
| device_guid: &str, |
| device_name: &str, |
| chip_kind: &CxxString, |
| chip_address: &str, |
| chip_name: &str, |
| chip_manufacturer: &str, |
| chip_product_name: &str, |
| bt_properties: &CxxVector<u8>, |
| ) -> Box<AddChipResultCxx> { |
| let chip_kind_proto = match chip_kind.to_string().as_str() { |
| "BLUETOOTH" => ProtoChipKind::BLUETOOTH, |
| "WIFI" => ProtoChipKind::WIFI, |
| "UWB" => ProtoChipKind::UWB, |
| _ => ProtoChipKind::UNSPECIFIED, |
| }; |
| let mut chip_create_proto = ChipCreate { |
| kind: chip_kind_proto.into(), |
| address: chip_address.to_string(), |
| name: chip_name.to_string(), |
| manufacturer: chip_manufacturer.to_string(), |
| product_name: chip_product_name.to_string(), |
| ..Default::default() |
| }; |
| if let Ok(bt_properties_proto) = Controller::parse_from_bytes(bt_properties.as_slice()) { |
| chip_create_proto.bt_properties = Some(bt_properties_proto).into(); |
| } |
| match add_chip(device_guid, device_name, &chip_create_proto) { |
| Ok(result) => Box::new(AddChipResultCxx { |
| device_id: result.device_id, |
| chip_id: result.chip_id, |
| facade_id: result.facade_id, |
| is_error: false, |
| }), |
| Err(_) => Box::new(AddChipResultCxx { |
| device_id: u32::MAX, |
| chip_id: u32::MAX, |
| facade_id: u32::MAX, |
| is_error: true, |
| }), |
| } |
| } |
| |
| /// Get or create a device. |
| /// Returns a (device_id, device_name) pair. |
| fn get_or_create_device( |
| devices: &mut Devices, |
| guid: Option<&str>, |
| name: Option<&str>, |
| builtin: bool, |
| ) -> (DeviceIdentifier, String) { |
| // Check if a device with the same guid already exists and if so, return it |
| if let Some(guid) = guid { |
| if let Some(existing_device) = devices.entries.values().find(|d| d.guid == *guid) { |
| if existing_device.builtin != builtin { |
| warn!("builtin mismatch for device {} during add_chip", existing_device.name); |
| } |
| return (existing_device.id, existing_device.name.clone()); |
| } |
| } |
| |
| // A new device needs to be created and inserted |
| let id = devices.id_factory.next_id(); |
| let default = format!("device-{}", id); |
| let name = name.unwrap_or(&default); |
| devices.entries.insert( |
| id, |
| Device::new(id, String::from(guid.unwrap_or(&default)), String::from(name), builtin), |
| ); |
| events::publish(Event::DeviceAdded { id, name: name.to_string(), builtin }); |
| |
| (id, String::from(name)) |
| } |
| |
| /// Remove a device from the simulation. |
| /// |
| /// Called when the last chip for the device is removed. |
| fn remove_device( |
| guard: &mut RwLockWriteGuard<Devices>, |
| id: DeviceIdentifier, |
| ) -> Result<(), String> { |
| let device = guard.entries.get(&id).ok_or(format!("Error fetching device with {id}"))?; |
| let name = device.name.clone(); |
| let builtin = device.builtin; |
| guard.entries.remove(&id).ok_or(format!("Error removing device with id {id}"))?; |
| // Publish DeviceRemoved Event |
| events::publish(Event::DeviceRemoved { id, name, builtin }); |
| Ok(()) |
| } |
| |
| /// Remove a chip from a device. |
| /// |
| /// Called when the packet transport for the chip shuts down. |
| pub fn remove_chip(device_id: DeviceIdentifier, chip_id: ChipIdentifier) -> Result<(), String> { |
| let result = { |
| let devices_arc = get_devices(); |
| let mut devices = devices_arc.write().unwrap(); |
| let (is_empty, (facade_id_option, _device_name, chip_kind, radio_stats)) = match devices |
| .entries |
| .entry(device_id) |
| { |
| Entry::Occupied(mut entry) => { |
| let device = entry.get_mut(); |
| let remove_result = device.remove_chip(chip_id)?; |
| (device.chips.is_empty(), remove_result) |
| } |
| Entry::Vacant(_) => return Err(format!("RemoveChip device id {device_id} not found")), |
| }; |
| if is_empty { |
| remove_device(&mut devices, device_id)?; |
| } |
| Ok(( |
| facade_id_option, |
| device_id, |
| chip_kind, |
| devices.entries.values().filter(|device| !device.builtin).count(), |
| radio_stats, |
| )) |
| }; |
| match result { |
| Ok((facade_id_option, device_id, chip_kind, remaining_nonbuiltin_devices, radio_stats)) => { |
| match facade_id_option { |
| Some(facade_id) => match chip_kind { |
| ProtoChipKind::BLUETOOTH => { |
| bluetooth_facade::bluetooth_remove(facade_id); |
| } |
| ProtoChipKind::WIFI => { |
| wifi_facade::wifi_remove(facade_id); |
| } |
| ProtoChipKind::BLUETOOTH_BEACON => { |
| bluetooth_facade::ble_beacon_remove(device_id, chip_id, facade_id)?; |
| } |
| _ => Err(format!("Unknown chip kind: {:?}", chip_kind))?, |
| }, |
| None => Err(format!( |
| "Facade Id hasn't been added yet to frontend resource for chip_id: {chip_id}" |
| ))?, |
| } |
| info!("Removed Chip: device_id: {device_id}, chip_id: {chip_id}"); |
| events::publish(Event::ChipRemoved { |
| chip_id, |
| device_id, |
| remaining_nonbuiltin_devices, |
| radio_stats, |
| }); |
| Ok(()) |
| } |
| Err(err) => { |
| warn!("Failed to remove chip: device_id: {device_id}, chip_id: {chip_id}"); |
| Err(err) |
| } |
| } |
| } |
| |
| pub fn delete_chip(delete_json: &str) -> Result<(), String> { |
| let mut request = DeleteChipRequest::new(); |
| if merge_from_str(&mut request, delete_json).is_err() { |
| return Err(format!( |
| "failed to delete chip: incorrectly formatted delete json: {}", |
| delete_json |
| )); |
| }; |
| |
| let device_id = { |
| let devices_arc = get_devices(); |
| let devices = devices_arc.read().unwrap(); |
| devices |
| .entries |
| .iter() |
| .find(|(_, device)| device.chips.contains_key(&request.id)) |
| .map(|(id, _)| *id) |
| .ok_or(format!("failed to delete chip: could not find chip with id {}", request.id))? |
| }; |
| |
| remove_chip(device_id, request.id) |
| } |
| |
| /// A RemoveChip function for Rust Device API. |
| /// The backend gRPC code will be invoking this method. |
| pub fn remove_chip_cxx(device_id: u32, chip_id: u32) { |
| let _ = remove_chip(device_id, chip_id); |
| } |
| |
| /// Create a device from a CreateDeviceRequest json. |
| /// Uses a default name if none is provided. |
| /// Returns an error if the device already exists. |
| pub fn create_device(create_json: &str) -> Result<DeviceIdentifier, String> { |
| let mut create_device_request = CreateDeviceRequest::new(); |
| if merge_from_str(&mut create_device_request, create_json).is_err() { |
| return Err(format!( |
| "failed to create device: incorrectly formatted create json: {}", |
| create_json |
| )); |
| } |
| |
| let new_device = create_device_request.device; |
| let devices_arc = get_devices(); |
| let mut devices = devices_arc.write().unwrap(); |
| // Check if specified device name is already mapped. |
| if new_device.name != String::default() |
| && devices.entries.values().any(|d| d.guid == new_device.name) |
| { |
| return Err(String::from("failed to create device: device already exists")); |
| } |
| |
| if new_device.chips.is_empty() { |
| return Err(String::from("failed to create device: device must contain at least 1 chip")); |
| } |
| new_device.chips.iter().try_for_each(|chip| match chip.chip { |
| Some(ProtoBuiltin::BleBeacon(_)) => Ok(()), |
| Some(_) => Err(format!("failed to create device: chip {} was not a built-in", chip.name)), |
| None => Err(format!("failed to create device: chip {} was missing a radio", chip.name)), |
| })?; |
| |
| let device_name = (new_device.name != String::default()).then_some(new_device.name.as_str()); |
| let (device_id, device_name) = |
| get_or_create_device(&mut devices, device_name, device_name, true); |
| |
| // Release devices lock so that add_chip can take it. |
| drop(devices); |
| new_device |
| .chips |
| .iter() |
| .try_for_each(|chip| add_chip(&device_name, &device_name, chip).map(|_| ()))?; |
| |
| Ok(device_id) |
| } |
| |
| // lock the devices, find the id and call the patch function |
| #[allow(dead_code)] |
| fn patch_device(id_option: Option<DeviceIdentifier>, patch_json: &str) -> Result<(), String> { |
| let mut patch_device_request = PatchDeviceRequest::new(); |
| if merge_from_str(&mut patch_device_request, patch_json).is_ok() { |
| let devices_arc = get_devices(); |
| let mut devices = devices_arc.write().unwrap(); |
| let proto_device = patch_device_request.device; |
| match id_option { |
| Some(id) => match devices.entries.get_mut(&id) { |
| Some(device) => { |
| let result = device.patch(&proto_device); |
| if result.is_ok() { |
| // Publish Device Patched event |
| events::publish(Event::DevicePatched { id, name: device.name.clone() }); |
| } |
| result |
| } |
| None => Err(format!("No such device with id {id}")), |
| }, |
| None => { |
| let mut multiple_matches = false; |
| let mut target: Option<&mut Device> = None; |
| for device in devices.entries.values_mut() { |
| if device.name.contains(&proto_device.name) { |
| if device.name == proto_device.name { |
| let result = device.patch(&proto_device); |
| if result.is_ok() { |
| // Publish Device Patched event |
| events::publish(Event::DevicePatched { |
| id: device.id, |
| name: device.name.clone(), |
| }); |
| } |
| return result; |
| } |
| multiple_matches = target.is_some(); |
| target = Some(device); |
| } |
| } |
| if multiple_matches { |
| return Err(format!( |
| "Multiple ambiguous matches were found with substring {}", |
| proto_device.name |
| )); |
| } |
| match target { |
| Some(device) => { |
| let result = device.patch(&proto_device); |
| if result.is_ok() { |
| // Publish Device Patched event |
| events::publish(Event::DevicePatched { |
| id: device.id, |
| name: device.name.clone(), |
| }); |
| } |
| result |
| } |
| None => Err(format!("No such device with name {}", proto_device.name)), |
| } |
| } |
| } |
| } else { |
| Err(format!("Incorrect format of patch json {}", patch_json)) |
| } |
| } |
| |
| fn distance(a: &ProtoPosition, b: &ProtoPosition) -> f32 { |
| ((b.x - a.x).powf(2.0) + (b.y - a.y).powf(2.0) + (b.z - a.z).powf(2.0)).sqrt() |
| } |
| |
| #[allow(dead_code)] |
| fn get_distance(id: DeviceIdentifier, other_id: DeviceIdentifier) -> Result<f32, String> { |
| let devices_arc = get_devices(); |
| let devices = devices_arc.read().unwrap(); |
| let a = devices |
| .entries |
| .get(&id) |
| .map(|device_ref| device_ref.position.clone()) |
| .ok_or(format!("No such device with id {id}"))?; |
| let b = devices |
| .entries |
| .get(&other_id) |
| .map(|device_ref| device_ref.position.clone()) |
| .ok_or(format!("No such device with id {other_id}"))?; |
| Ok(distance(&a, &b)) |
| } |
| |
| /// A GetDistance function for Rust Device API. |
| /// The backend gRPC code will be invoking this method. |
| pub fn get_distance_cxx(a: u32, b: u32) -> f32 { |
| match get_distance(a, b) { |
| Ok(distance) => distance, |
| Err(err) => { |
| warn!("get_distance Error: {err}"); |
| 0.0 |
| } |
| } |
| } |
| |
| #[allow(dead_code)] |
| pub fn get_devices_proto() -> Result<ProtoScene, String> { |
| let mut scene = ProtoScene::new(); |
| // iterate over the devices and add each to the scene |
| let devices_arc = get_devices(); |
| let devices = devices_arc.read().unwrap(); |
| for device in devices.entries.values() { |
| scene.devices.push(device.get()?); |
| } |
| Ok(scene) |
| } |
| |
| fn reset_all() -> Result<(), String> { |
| let devices_arc = get_devices(); |
| let mut devices = devices_arc.write().unwrap(); |
| for device in devices.entries.values_mut() { |
| device.reset()?; |
| // Publish Device Patched event |
| events::publish(Event::DevicePatched { id: device.id, name: device.name.clone() }); |
| } |
| Ok(()) |
| } |
| |
| fn handle_device_create(writer: ResponseWritable, create_json: &str) { |
| let mut response = CreateDeviceResponse::new(); |
| |
| let mut collate_results = || { |
| let id = create_device(create_json)?; |
| |
| let devices_arc = get_devices(); |
| let devices = devices_arc.read().unwrap(); |
| let device_proto = devices.entries.get(&id).ok_or("failed to create device")?.get()?; |
| response.device = MessageField::some(device_proto); |
| print_to_string(&response).map_err(|_| String::from("failed to convert device to json")) |
| }; |
| |
| match collate_results() { |
| Ok(response) => writer.put_ok("text/json", &response, vec![]), |
| Err(err) => writer.put_error(404, err.as_str()), |
| } |
| } |
| |
| /// Performs PatchDevice to patch a single device |
| fn handle_device_patch(writer: ResponseWritable, id: Option<DeviceIdentifier>, patch_json: &str) { |
| match patch_device(id, patch_json) { |
| Ok(()) => writer.put_ok("text/plain", "Device Patch Success", vec![]), |
| Err(err) => writer.put_error(404, err.as_str()), |
| } |
| } |
| |
| fn handle_chip_delete(writer: ResponseWritable, delete_json: &str) { |
| match delete_chip(delete_json) { |
| Ok(()) => writer.put_ok("text/plain", "Chip Delete Success", vec![]), |
| Err(err) => writer.put_error(404, err.as_str()), |
| } |
| } |
| |
| /// Performs ListDevices to get the list of Devices and write to writer. |
| fn handle_device_list(writer: ResponseWritable) { |
| let devices_arc = get_devices(); |
| let devices = devices_arc.read().unwrap(); |
| // Instantiate ListDeviceResponse and add Devices |
| let mut response = ListDeviceResponse::new(); |
| for device in devices.entries.values() { |
| response.devices.push(device.get().unwrap()); |
| } |
| |
| // Perform protobuf-json-mapping with the given protobuf |
| if let Ok(json_response) = print_to_string_with_options(&response, &JSON_PRINT_OPTION) { |
| writer.put_ok("text/json", &json_response, vec![]) |
| } else { |
| writer.put_error(404, "proto to JSON mapping failure") |
| } |
| } |
| |
| /// Performs ResetDevice for all devices |
| fn handle_device_reset(writer: ResponseWritable) { |
| match reset_all() { |
| Ok(()) => writer.put_ok("text/plain", "Device Reset Success", vec![]), |
| Err(err) => writer.put_error(404, err.as_str()), |
| } |
| } |
| |
| /// Performs SubscribeDevice |
| fn handle_device_subscribe(writer: ResponseWritable) { |
| let event_rx = events::subscribe(); |
| // Timeout after 15 seconds with no event received |
| match event_rx.recv_timeout(Duration::from_secs(15)) { |
| Ok(Event::ChipAdded { .. }) |
| | Ok(Event::ChipRemoved { .. }) |
| | Ok(Event::DevicePatched { .. }) => handle_device_list(writer), |
| Err(err) => writer.put_error(404, format!("{err:?}").as_str()), |
| _ => writer.put_error(404, "disconnecting due to unrelated event"), |
| } |
| } |
| |
| /// The Rust device handler used directly by Http frontend or handle_device_cxx for LIST, GET, and PATCH |
| pub fn handle_device(request: &Request<Vec<u8>>, param: &str, writer: ResponseWritable) { |
| // Route handling |
| if request.uri() == "/v1/devices" { |
| // Routes with ID not specified |
| match request.method().as_str() { |
| "GET" => { |
| handle_device_list(writer); |
| } |
| "PUT" => { |
| handle_device_reset(writer); |
| } |
| "SUBSCRIBE" => { |
| handle_device_subscribe(writer); |
| } |
| "PATCH" => { |
| let body = request.body(); |
| let patch_json = String::from_utf8(body.to_vec()).unwrap(); |
| handle_device_patch(writer, None, patch_json.as_str()); |
| } |
| "POST" => { |
| let body = &request.body(); |
| let create_json = String::from_utf8(body.to_vec()).unwrap(); |
| handle_device_create(writer, create_json.as_str()); |
| } |
| "DELETE" => { |
| let body = &request.body(); |
| let delete_json = String::from_utf8(body.to_vec()).unwrap(); |
| handle_chip_delete(writer, delete_json.as_str()); |
| } |
| _ => writer.put_error(404, "Not found."), |
| } |
| } else { |
| // Routes with ID specified |
| match request.method().as_str() { |
| "PATCH" => { |
| let id = match param.parse::<u32>() { |
| Ok(num) => num, |
| Err(_) => { |
| writer.put_error(404, "Incorrect Id type for devices, ID should be u32."); |
| return; |
| } |
| }; |
| let body = request.body(); |
| let patch_json = String::from_utf8(body.to_vec()).unwrap(); |
| handle_device_patch(writer, Some(id), patch_json.as_str()); |
| } |
| _ => writer.put_error(404, "Not found."), |
| } |
| } |
| } |
| |
| /// Device handler cxx for grpc server to call |
| pub fn handle_device_cxx( |
| responder: Pin<&mut CxxServerResponseWriter>, |
| method: String, |
| param: String, |
| body: String, |
| ) { |
| let mut builder = Request::builder().method(method.as_str()); |
| if param.is_empty() { |
| builder = builder.uri("/v1/devices"); |
| } else { |
| builder = builder.uri(format!("/v1/devices/{}", param)); |
| } |
| builder = builder.version(Version::HTTP_11); |
| let request = match builder.body(body.as_bytes().to_vec()) { |
| Ok(request) => request, |
| Err(err) => { |
| warn!("{err:?}"); |
| return; |
| } |
| }; |
| handle_device( |
| &request, |
| param.as_str(), |
| &mut CxxServerResponseWriterWrapper { writer: responder }, |
| ) |
| } |
| |
| /// Get Facade ID from given chip_id |
| #[allow(dead_code)] |
| pub fn get_facade_id(chip_id: u32) -> Result<u32, String> { |
| let devices_arc = get_devices(); |
| let devices = devices_arc.read().unwrap(); |
| for device in devices.entries.values() { |
| for (id, chip) in &device.chips { |
| if *id == chip_id { |
| return chip.facade_id.ok_or(format!( |
| "Facade Id hasn't been added yet to frontend resource for chip_id: {chip_id}" |
| )); |
| } |
| } |
| } |
| Err(format!("Cannot find facade_id for {chip_id}")) |
| } |
| |
| /// return enum type for wait_devices |
| #[derive(Debug, PartialEq)] |
| enum DeviceWaitStatus { |
| LastDeviceRemoved, |
| DeviceAdded, |
| Timeout, |
| IgnoreEvent, |
| } |
| |
| /// listening to events |
| fn check_device_event( |
| events_rx: &Receiver<Event>, |
| timeout_time: Option<Instant>, |
| ) -> DeviceWaitStatus { |
| let wait_time = timeout_time.map_or(Duration::from_secs(u64::MAX), |t| t - Instant::now()); |
| match events_rx.recv_timeout(wait_time) { |
| Ok(Event::ChipRemoved { remaining_nonbuiltin_devices: 0, .. }) => { |
| DeviceWaitStatus::LastDeviceRemoved |
| } |
| // DeviceAdded (event from CreateDevice) |
| // ChipAdded (event from add_chip or add_chip_cxx) |
| Ok(Event::DeviceAdded { builtin: false, .. }) |
| | Ok(Event::ChipAdded { builtin: false, .. }) => DeviceWaitStatus::DeviceAdded, |
| Err(_) => DeviceWaitStatus::Timeout, |
| _ => DeviceWaitStatus::IgnoreEvent, |
| } |
| } |
| |
| /// wait loop logic for devices |
| /// the function will publish a ShutDown event when |
| /// 1. Initial timeout before first device is added |
| /// 2. Last Chip Removed from netsimd |
| pub fn wait_devices(events_rx: Receiver<Event>) { |
| // TODO (b/303281633): Add unit tests for wait_devices |
| let _ = |
| std::thread::Builder::new().name("device_event_subscriber".to_string()).spawn(move || { |
| let mut timeout_time = |
| Some(Instant::now() + Duration::from_secs(IDLE_SECS_FOR_SHUTDOWN)); |
| loop { |
| match check_device_event(&events_rx, timeout_time) { |
| DeviceWaitStatus::LastDeviceRemoved => { |
| events::publish(Event::ShutDown { |
| reason: "last device disconnected".to_string(), |
| }); |
| return; |
| } |
| DeviceWaitStatus::DeviceAdded => { |
| timeout_time = None; |
| } |
| DeviceWaitStatus::Timeout => { |
| events::publish(Event::ShutDown { |
| reason: format!( |
| "no devices connected within {IDLE_SECS_FOR_SHUTDOWN}s" |
| ), |
| }); |
| return; |
| } |
| DeviceWaitStatus::IgnoreEvent => continue, |
| } |
| } |
| }); |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use crate::events; |
| use netsim_common::util::netsim_logger::init_for_test; |
| use netsim_proto::model::{ |
| Device as ProtoDevice, DeviceCreate as ProtoDeviceCreate, Orientation as ProtoOrientation, |
| State, |
| }; |
| use protobuf_json_mapping::print_to_string; |
| use std::{sync::Once, thread}; |
| |
| use super::*; |
| |
| // This allows Log init method to be invoked once when running all tests. |
| static INIT: Once = Once::new(); |
| |
| /// Logger setup function that is only run once, even if called multiple times. |
| fn logger_setup() { |
| INIT.call_once(|| { |
| init_for_test(); |
| }); |
| } |
| |
| /// TestChipParameters struct to invoke add_chip |
| /// This struct contains parameters required to invoke add_chip. |
| /// This will eventually be invoked by the facades. |
| struct TestChipParameters { |
| device_guid: String, |
| device_name: String, |
| chip_kind: ProtoChipKind, |
| chip_name: String, |
| chip_manufacturer: String, |
| chip_product_name: String, |
| } |
| |
| impl TestChipParameters { |
| fn add_chip(&self) -> Result<AddChipResult, String> { |
| let chip_create_proto = ChipCreate { |
| kind: self.chip_kind.into(), |
| name: self.chip_name.to_string(), |
| manufacturer: self.chip_manufacturer.to_string(), |
| product_name: self.chip_product_name.to_string(), |
| ..Default::default() |
| }; |
| super::add_chip(&self.device_guid, &self.device_name, &chip_create_proto) |
| } |
| |
| fn get_or_create_device(&self) -> DeviceIdentifier { |
| let devices_arc = get_devices(); |
| let mut devices = devices_arc.write().unwrap(); |
| super::get_or_create_device( |
| &mut devices, |
| Some(&self.device_guid), |
| Some(&self.device_name), |
| false, |
| ) |
| .0 |
| } |
| } |
| |
| /// helper function for test cases to instantiate ProtoPosition |
| fn new_position(x: f32, y: f32, z: f32) -> ProtoPosition { |
| ProtoPosition { x, y, z, ..Default::default() } |
| } |
| |
| fn new_orientation(yaw: f32, pitch: f32, roll: f32) -> ProtoOrientation { |
| ProtoOrientation { yaw, pitch, roll, ..Default::default() } |
| } |
| |
| fn test_chip_1_bt() -> TestChipParameters { |
| TestChipParameters { |
| device_guid: format!("guid-fs-1-{:?}", thread::current().id()), |
| device_name: format!("test-device-name-1-{:?}", thread::current().id()), |
| chip_kind: ProtoChipKind::BLUETOOTH, |
| chip_name: "bt_chip_name".to_string(), |
| chip_manufacturer: "netsim".to_string(), |
| chip_product_name: "netsim_bt".to_string(), |
| } |
| } |
| |
| fn test_chip_1_wifi() -> TestChipParameters { |
| TestChipParameters { |
| device_guid: format!("guid-fs-1-{:?}", thread::current().id()), |
| device_name: format!("test-device-name-1-{:?}", thread::current().id()), |
| chip_kind: ProtoChipKind::WIFI, |
| chip_name: "wifi_chip_name".to_string(), |
| chip_manufacturer: "netsim".to_string(), |
| chip_product_name: "netsim_wifi".to_string(), |
| } |
| } |
| |
| fn test_chip_2_bt() -> TestChipParameters { |
| TestChipParameters { |
| device_guid: format!("guid-fs-2-{:?}", thread::current().id()), |
| device_name: format!("test-device-name-2-{:?}", thread::current().id()), |
| chip_kind: ProtoChipKind::BLUETOOTH, |
| chip_name: "bt_chip_name".to_string(), |
| chip_manufacturer: "netsim".to_string(), |
| chip_product_name: "netsim_bt".to_string(), |
| } |
| } |
| |
| fn reset(id: DeviceIdentifier) -> Result<(), String> { |
| let devices_arc = get_devices(); |
| let mut devices = devices_arc.write().unwrap(); |
| match devices.entries.get_mut(&id) { |
| Some(device) => device.reset(), |
| None => Err(format!("No such device with id {id}")), |
| } |
| } |
| |
| #[test] |
| fn test_distance() { |
| // Pythagorean quadruples |
| let a = new_position(0.0, 0.0, 0.0); |
| let mut b = new_position(1.0, 2.0, 2.0); |
| assert_eq!(distance(&a, &b), 3.0); |
| b = new_position(2.0, 3.0, 6.0); |
| assert_eq!(distance(&a, &b), 7.0); |
| } |
| |
| #[test] |
| fn test_add_chip() { |
| // Initializing Logger |
| logger_setup(); |
| |
| // Adding a chip |
| let chip_params = test_chip_1_bt(); |
| let chip_result = chip_params.add_chip().unwrap(); |
| match get_devices().read().unwrap().entries.get(&chip_result.device_id) { |
| Some(device) => { |
| let chip = device.chips.get(&chip_result.chip_id).unwrap(); |
| assert_eq!(chip_params.chip_kind, chip.kind); |
| assert_eq!(chip_params.chip_manufacturer, chip.manufacturer); |
| assert_eq!(chip_params.chip_name, chip.name); |
| assert_eq!(chip_params.chip_product_name, chip.product_name); |
| assert_eq!(chip_params.device_name, device.name); |
| } |
| None => unreachable!(), |
| } |
| let chip_id = chip_result.chip_id; |
| |
| // Adding duplicate chip |
| let chip_result = chip_params.add_chip(); |
| assert!(chip_result.is_err()); |
| assert_eq!( |
| chip_result.unwrap_err(), |
| format!("Device::AddChip - duplicate at id {chip_id}, skipping.") |
| ); |
| } |
| |
| #[test] |
| fn test_get_or_create_device() { |
| // Initializing Logger |
| logger_setup(); |
| |
| // Creating a device and getting device |
| let bt_chip_params = test_chip_1_bt(); |
| let device_id_1 = bt_chip_params.get_or_create_device(); |
| let wifi_chip_params = test_chip_1_wifi(); |
| let device_id_2 = wifi_chip_params.get_or_create_device(); |
| assert_eq!(device_id_1, device_id_2); |
| } |
| |
| #[test] |
| fn test_patch_device() { |
| // Initializing Logger |
| logger_setup(); |
| |
| // Patching device position and orientation by id |
| let chip_params = test_chip_1_bt(); |
| let chip_result = chip_params.add_chip().unwrap(); |
| let mut patch_device_request = PatchDeviceRequest::new(); |
| let mut proto_device = ProtoDevice::new(); |
| let request_position = new_position(1.1, 2.2, 3.3); |
| let request_orientation = new_orientation(4.4, 5.5, 6.6); |
| proto_device.name = chip_params.device_name; |
| proto_device.visible = State::OFF.into(); |
| proto_device.position = Some(request_position.clone()).into(); |
| proto_device.orientation = Some(request_orientation.clone()).into(); |
| patch_device_request.device = Some(proto_device.clone()).into(); |
| let patch_json = print_to_string(&patch_device_request).unwrap(); |
| patch_device(Some(chip_result.device_id), patch_json.as_str()).unwrap(); |
| match get_devices().read().unwrap().entries.get(&chip_result.device_id) { |
| Some(device) => { |
| assert_eq!(device.position.x, request_position.x); |
| assert_eq!(device.position.y, request_position.y); |
| assert_eq!(device.position.z, request_position.z); |
| assert_eq!(device.orientation.yaw, request_orientation.yaw); |
| assert_eq!(device.orientation.pitch, request_orientation.pitch); |
| assert_eq!(device.orientation.roll, request_orientation.roll); |
| assert_eq!(device.visible, State::OFF); |
| } |
| None => unreachable!(), |
| } |
| |
| // Patch device by name with substring match |
| proto_device.name = format!("test-device-name-1-{:?}", thread::current().id()); |
| patch_device_request.device = Some(proto_device).into(); |
| let patch_json = print_to_string(&patch_device_request).unwrap(); |
| assert!(patch_device(None, patch_json.as_str()).is_ok()); |
| } |
| |
| #[test] |
| fn test_patch_error() { |
| // Initializing Logger |
| logger_setup(); |
| |
| // Patch Error Testing |
| let bt_chip_params = test_chip_1_bt(); |
| let bt_chip2_params = test_chip_2_bt(); |
| let bt_chip_result = bt_chip_params.add_chip().unwrap(); |
| bt_chip2_params.add_chip().unwrap(); |
| |
| // Incorrect value type |
| let error_json = format!( |
| "{{\"device\": {{\"name\": \"test-device-name-1-{:?}\", \"position\": 1.1}}}}", |
| thread::current().id() |
| ); |
| let patch_result = patch_device(Some(bt_chip_result.device_id), error_json.as_str()); |
| assert!(patch_result.is_err()); |
| assert_eq!( |
| patch_result.unwrap_err(), |
| format!("Incorrect format of patch json {}", error_json) |
| ); |
| |
| // Incorrect key |
| let error_json = format!( |
| "{{\"device\": {{\"name\": \"test-device-name-1-{:?}\", \"hello\": \"world\"}}}}", |
| thread::current().id() |
| ); |
| let patch_result = patch_device(Some(bt_chip_result.device_id), error_json.as_str()); |
| assert!(patch_result.is_err()); |
| assert_eq!( |
| patch_result.unwrap_err(), |
| format!("Incorrect format of patch json {}", error_json) |
| ); |
| |
| // Incorrect Id |
| let error_json = r#"{"device": {"name": "test-device-name-1"}}"#; |
| let patch_result = patch_device(Some(INITIAL_DEVICE_ID - 1), error_json); |
| assert!(patch_result.is_err()); |
| assert_eq!( |
| patch_result.unwrap_err(), |
| format!("No such device with id {}", INITIAL_DEVICE_ID - 1) |
| ); |
| |
| // Incorrect name |
| let error_json = r#"{"device": {"name": "wrong-name"}}"#; |
| let patch_result = patch_device(None, error_json); |
| assert!(patch_result.is_err()); |
| assert_eq!(patch_result.unwrap_err(), "No such device with name wrong-name"); |
| |
| // Multiple ambiguous matching |
| let error_json = r#"{"device": {"name": "test-device"}}"#; |
| let patch_result = patch_device(None, error_json); |
| assert!(patch_result.is_err()); |
| assert_eq!( |
| patch_result.unwrap_err(), |
| "Multiple ambiguous matches were found with substring test-device" |
| ); |
| } |
| |
| #[test] |
| fn test_adding_two_chips() { |
| // Initializing Logger |
| logger_setup(); |
| |
| // Adding two chips of the same device |
| let bt_chip_params = test_chip_1_bt(); |
| let wifi_chip_params = test_chip_1_wifi(); |
| let bt_chip_result = bt_chip_params.add_chip().unwrap(); |
| let wifi_chip_result = wifi_chip_params.add_chip().unwrap(); |
| assert_eq!(bt_chip_result.device_id, wifi_chip_result.device_id); |
| let binding = get_devices(); |
| let binding = binding.read().unwrap(); |
| let device = binding.entries.get(&bt_chip_result.device_id).unwrap(); |
| assert_eq!(device.id, bt_chip_result.device_id); |
| assert_eq!(device.name, bt_chip_params.device_name); |
| assert_eq!(device.chips.len(), 2); |
| for chip in device.chips.values() { |
| assert!(chip.id == bt_chip_result.chip_id || chip.id == wifi_chip_result.chip_id); |
| if chip.id == bt_chip_result.chip_id { |
| assert_eq!(chip.kind, ProtoChipKind::BLUETOOTH); |
| } else if chip.id == wifi_chip_result.chip_id { |
| assert_eq!(chip.kind, ProtoChipKind::WIFI); |
| } else { |
| unreachable!(); |
| } |
| } |
| } |
| |
| #[test] |
| fn test_reset() { |
| // Initializing Logger |
| logger_setup(); |
| |
| // Patching Device and Resetting scene |
| let chip_params = test_chip_1_bt(); |
| let chip_result = chip_params.add_chip().unwrap(); |
| let mut patch_device_request = PatchDeviceRequest::new(); |
| let mut proto_device = ProtoDevice::new(); |
| let request_position = new_position(10.0, 20.0, 30.0); |
| let request_orientation = new_orientation(1.0, 2.0, 3.0); |
| proto_device.name = chip_params.device_name; |
| proto_device.visible = State::OFF.into(); |
| proto_device.position = Some(request_position).into(); |
| proto_device.orientation = Some(request_orientation).into(); |
| patch_device_request.device = Some(proto_device).into(); |
| patch_device( |
| Some(chip_result.device_id), |
| print_to_string(&patch_device_request).unwrap().as_str(), |
| ) |
| .unwrap(); |
| match get_devices().read().unwrap().entries.get(&chip_result.device_id) { |
| Some(device) => { |
| assert_eq!(device.position.x, 10.0); |
| assert_eq!(device.orientation.yaw, 1.0); |
| assert_eq!(device.visible, State::OFF); |
| } |
| None => unreachable!(), |
| } |
| reset(chip_result.device_id).unwrap(); |
| match get_devices().read().unwrap().entries.get(&chip_result.device_id) { |
| Some(device) => { |
| assert_eq!(device.position.x, 0.0); |
| assert_eq!(device.position.y, 0.0); |
| assert_eq!(device.position.z, 0.0); |
| assert_eq!(device.orientation.yaw, 0.0); |
| assert_eq!(device.orientation.pitch, 0.0); |
| assert_eq!(device.orientation.roll, 0.0); |
| assert_eq!(device.visible, State::ON); |
| } |
| None => unreachable!(), |
| } |
| } |
| |
| #[test] |
| fn test_remove_chip() { |
| // Initializing Logger |
| logger_setup(); |
| |
| // Add 2 chips of same device and 1 chip of different device |
| let bt_chip_params = test_chip_1_bt(); |
| let wifi_chip_params = test_chip_1_wifi(); |
| let bt_chip_2_params = test_chip_2_bt(); |
| let bt_chip_result = bt_chip_params.add_chip().unwrap(); |
| let wifi_chip_result = wifi_chip_params.add_chip().unwrap(); |
| let bt_chip_2_result = bt_chip_2_params.add_chip().unwrap(); |
| |
| // Remove a bt chip of first device |
| remove_chip(bt_chip_result.device_id, bt_chip_result.chip_id).unwrap(); |
| match get_devices().read().unwrap().entries.get(&bt_chip_result.device_id) { |
| Some(device) => { |
| assert_eq!(device.chips.len(), 1); |
| assert_eq!( |
| device.chips.get(&wifi_chip_result.chip_id).unwrap().kind, |
| ProtoChipKind::WIFI |
| ); |
| } |
| None => unreachable!(), |
| } |
| |
| // Remove a wifi chip of first device |
| remove_chip(wifi_chip_result.device_id, wifi_chip_result.chip_id).unwrap(); |
| assert!(get_devices().read().unwrap().entries.get(&wifi_chip_result.device_id).is_none()); |
| |
| // Remove a bt chip of second device |
| remove_chip(bt_chip_2_result.device_id, bt_chip_2_result.chip_id).unwrap(); |
| assert!(get_devices().read().unwrap().entries.get(&bt_chip_2_result.device_id).is_none()); |
| } |
| |
| #[test] |
| fn test_remove_chip_error() { |
| // Initializing Logger |
| logger_setup(); |
| |
| // Add 2 chips of same device and 1 chip of different device |
| let bt_chip_params = test_chip_1_bt(); |
| let bt_chip_result = bt_chip_params.add_chip().unwrap(); |
| |
| // Invoke remove_chip with incorrect chip_id. |
| match remove_chip(bt_chip_result.device_id, 9999) { |
| Ok(_) => unreachable!(), |
| Err(err) => assert_eq!(err, "RemoveChip chip id 9999 not found"), |
| } |
| |
| // Invoke remove_chip with incorrect device_id |
| match remove_chip(9999, bt_chip_result.chip_id) { |
| Ok(_) => unreachable!(), |
| Err(err) => assert_eq!(err, "RemoveChip device id 9999 not found"), |
| } |
| assert!(get_devices().read().unwrap().entries.get(&bt_chip_result.device_id).is_some()); |
| } |
| |
| #[test] |
| fn test_get_facade_id() { |
| // Initializing Logger |
| logger_setup(); |
| |
| // Add bt, wifi chips of the same device and bt chip of second device |
| let bt_chip_params = test_chip_1_bt(); |
| let bt_chip_result = bt_chip_params.add_chip().unwrap(); |
| let wifi_chip_params = test_chip_1_wifi(); |
| let wifi_chip_result = wifi_chip_params.add_chip().unwrap(); |
| let bt_chip_2_params = test_chip_2_bt(); |
| let bt_chip_2_result = bt_chip_2_params.add_chip().unwrap(); |
| |
| // Invoke get_facade_id from first bt chip |
| match get_facade_id(bt_chip_result.chip_id) { |
| Ok(facade_id) => assert_eq!(facade_id, bt_chip_result.facade_id), |
| Err(err) => { |
| unreachable!("{err}"); |
| } |
| } |
| |
| // Invoke get_facade_id from first wifi chip |
| match get_facade_id(wifi_chip_result.chip_id) { |
| Ok(facade_id) => assert_eq!(facade_id, wifi_chip_result.facade_id), |
| Err(err) => { |
| unreachable!("{err}"); |
| } |
| } |
| |
| // Invoke get_facade_id from second bt chip |
| match get_facade_id(bt_chip_2_result.chip_id) { |
| Ok(facade_id) => assert_eq!(facade_id, bt_chip_2_result.facade_id), |
| Err(err) => { |
| unreachable!("{err}"); |
| } |
| } |
| } |
| |
| #[allow(dead_code)] |
| fn list_request() -> Request<Vec<u8>> { |
| Request::builder() |
| .method("GET") |
| .uri("/v1/devices") |
| .version(Version::HTTP_11) |
| .body(Vec::<u8>::new()) |
| .unwrap() |
| } |
| |
| use netsim_proto::model::chip::{ |
| ble_beacon::AdvertiseData, ble_beacon::AdvertiseSettings, BleBeacon, Chip, |
| }; |
| use netsim_proto::model::chip_create::{BleBeaconCreate, Chip as BuiltChipProto}; |
| use netsim_proto::model::Chip as ChipProto; |
| use netsim_proto::model::ChipCreate; |
| use netsim_proto::model::Device as DeviceProto; |
| use protobuf::{EnumOrUnknown, MessageField}; |
| |
| fn get_test_create_device_request(device_name: Option<String>) -> CreateDeviceRequest { |
| let beacon_proto = BleBeaconCreate { |
| settings: MessageField::some(AdvertiseSettings { ..Default::default() }), |
| adv_data: MessageField::some(AdvertiseData { ..Default::default() }), |
| ..Default::default() |
| }; |
| |
| let chip_proto = ChipCreate { |
| name: String::from("test-beacon-chip"), |
| kind: ProtoChipKind::BLUETOOTH_BEACON.into(), |
| chip: Some(BuiltChipProto::BleBeacon(beacon_proto)), |
| ..Default::default() |
| }; |
| |
| let device_proto = ProtoDeviceCreate { |
| name: device_name.unwrap_or_default(), |
| chips: vec![chip_proto], |
| ..Default::default() |
| }; |
| |
| CreateDeviceRequest { device: MessageField::some(device_proto), ..Default::default() } |
| } |
| |
| fn get_device_proto(id: DeviceIdentifier) -> DeviceProto { |
| let devices = get_devices(); |
| let devices_guard = devices.read().unwrap(); |
| let device = |
| devices_guard.entries.get(&id).expect("could not find test bluetooth beacon device"); |
| |
| let device_proto = device.get(); |
| assert!(device_proto.is_ok(), "{}", device_proto.unwrap_err()); |
| |
| device_proto.unwrap() |
| } |
| |
| #[test] |
| fn test_create_device_succeeds() { |
| logger_setup(); |
| |
| let request = get_test_create_device_request(Some(format!( |
| "bob-the-beacon-{:?}", |
| thread::current().id() |
| ))); |
| |
| let id = create_device(&print_to_string(&request).unwrap()); |
| assert!(id.is_ok(), "{}", id.unwrap_err()); |
| let id = id.unwrap(); |
| |
| let device_proto = get_device_proto(id); |
| assert_eq!(request.device.name, device_proto.name); |
| assert_eq!(1, device_proto.chips.len()); |
| assert_eq!(request.device.chips[0].name, device_proto.chips[0].name); |
| } |
| |
| #[test] |
| fn test_create_chipless_device_fails() { |
| logger_setup(); |
| |
| let request = CreateDeviceRequest { |
| device: MessageField::some(ProtoDeviceCreate { ..Default::default() }), |
| ..Default::default() |
| }; |
| |
| let id = create_device(&print_to_string(&request).unwrap()); |
| assert!(id.is_err(), "{}", id.unwrap()); |
| } |
| |
| #[test] |
| fn test_create_radioless_device_fails() { |
| logger_setup(); |
| |
| let request = CreateDeviceRequest { |
| device: MessageField::some(ProtoDeviceCreate { |
| chips: vec![ChipCreate::default()], |
| ..Default::default() |
| }), |
| ..Default::default() |
| }; |
| |
| let id = create_device(&print_to_string(&request).unwrap()); |
| assert!(id.is_err(), "{}", id.unwrap()); |
| } |
| |
| #[test] |
| fn test_get_beacon_device() { |
| logger_setup(); |
| |
| let request = get_test_create_device_request(Some(format!( |
| "bob-the-beacon-{:?}", |
| thread::current().id() |
| ))); |
| |
| let id = create_device(&print_to_string(&request).unwrap()); |
| assert!(id.is_ok(), "{}", id.unwrap_err()); |
| let id = id.unwrap(); |
| |
| let device_proto = get_device_proto(id); |
| assert_eq!(1, device_proto.chips.len()); |
| assert!(device_proto.chips[0].chip.is_some()); |
| assert!(matches!(device_proto.chips[0].chip, Some(Chip::BleBeacon(_)))); |
| } |
| |
| #[test] |
| fn test_create_device_default_name() { |
| logger_setup(); |
| |
| let request = get_test_create_device_request(None); |
| |
| let id = create_device(&print_to_string(&request).unwrap()); |
| assert!(id.is_ok(), "{}", id.unwrap_err()); |
| let id = id.unwrap(); |
| |
| let device_proto = get_device_proto(id); |
| assert_eq!(format!("device-{id}"), device_proto.name); |
| } |
| |
| #[test] |
| fn test_create_existing_device_fails() { |
| logger_setup(); |
| |
| let request = get_test_create_device_request(Some(format!( |
| "existing-device-{:?}", |
| thread::current().id() |
| ))); |
| |
| let request_json = print_to_string(&request).unwrap(); |
| |
| let id = create_device(&request_json); |
| assert!(id.is_ok(), "{}", id.unwrap_err()); |
| |
| // Attempt to create the device again. This should fail because the devices have the same name. |
| let id = create_device(&request_json); |
| assert!(id.is_err()); |
| } |
| |
| #[test] |
| fn test_patch_beacon_device() { |
| logger_setup(); |
| |
| let request = get_test_create_device_request(Some(format!( |
| "bob-the-beacon-{:?}", |
| thread::current().id() |
| ))); |
| |
| let id = create_device(&print_to_string(&request).unwrap()); |
| assert!(id.is_ok(), "{}", id.unwrap_err()); |
| let id = id.unwrap(); |
| |
| let devices = get_devices(); |
| let mut devices_guard = devices.write().unwrap(); |
| let device = devices_guard |
| .entries |
| .get_mut(&id) |
| .expect("could not find test bluetooth beacon device"); |
| |
| let device_proto = device.get(); |
| assert!(device_proto.is_ok(), "{}", device_proto.unwrap_err()); |
| let device_proto = device_proto.unwrap(); |
| |
| let patch_result = device.patch(&DeviceProto { |
| name: device_proto.name.clone(), |
| id, |
| chips: vec![ChipProto { |
| name: request.device.chips[0].name.clone(), |
| kind: EnumOrUnknown::new(ProtoChipKind::BLUETOOTH_BEACON), |
| chip: Some(Chip::BleBeacon(BleBeacon { |
| bt: MessageField::some(Default::default()), |
| ..Default::default() |
| })), |
| ..Default::default() |
| }], |
| ..Default::default() |
| }); |
| assert!(patch_result.is_ok(), "{}", patch_result.unwrap_err()); |
| |
| let patched_device = device.get(); |
| assert!(patched_device.is_ok(), "{}", patched_device.unwrap_err()); |
| let patched_device = patched_device.unwrap(); |
| assert_eq!(1, patched_device.chips.len()); |
| assert!(matches!(patched_device.chips[0].chip, Some(Chip::BleBeacon(_)))); |
| } |
| |
| #[test] |
| fn test_remove_beacon_device_succeeds() { |
| logger_setup(); |
| |
| let create_request = get_test_create_device_request(None); |
| let device_id = create_device(&print_to_string(&create_request).unwrap()); |
| assert!(device_id.is_ok(), "{}", device_id.unwrap_err()); |
| |
| let device_id = device_id.unwrap(); |
| let chip_id = { |
| let devices = get_devices(); |
| let devices_guard = devices.read().unwrap(); |
| let device = devices_guard.entries.get(&device_id).unwrap(); |
| device.chips.first_key_value().map(|(id, _)| *id).unwrap() |
| }; |
| |
| let delete_request = DeleteChipRequest { id: chip_id, ..Default::default() }; |
| let delete_result = delete_chip(&print_to_string(&delete_request).unwrap()); |
| assert!(delete_result.is_ok(), "{}", delete_result.unwrap_err()); |
| |
| let devices = get_devices(); |
| let devices_guard = devices.read().unwrap(); |
| assert!(devices_guard.entries.get(&device_id).is_none()) |
| } |
| |
| #[test] |
| fn test_remove_beacon_device_fails() { |
| logger_setup(); |
| |
| let create_request = get_test_create_device_request(None); |
| let device_id = create_device(&print_to_string(&create_request).unwrap()); |
| assert!(device_id.is_ok(), "{}", device_id.unwrap_err()); |
| |
| let device_id = device_id.unwrap(); |
| let chip_id = { |
| let devices = get_devices(); |
| let devices_guard = devices.read().unwrap(); |
| let device = devices_guard.entries.get(&device_id).unwrap(); |
| device.chips.first_key_value().map(|(id, _)| *id).unwrap() |
| }; |
| |
| let delete_request = DeleteChipRequest { id: chip_id, ..Default::default() }; |
| let delete_result = delete_chip(&print_to_string(&delete_request).unwrap()); |
| assert!(delete_result.is_ok(), "{}", delete_result.unwrap_err()); |
| |
| let delete_result = delete_chip(&print_to_string(&delete_request).unwrap()); |
| assert!(delete_result.is_err()); |
| } |
| |
| #[test] |
| fn test_wait_devices_initial_timeout() { |
| logger_setup(); |
| |
| let mut events = events::test::new(); |
| let events_rx = events::test::subscribe(&mut events); |
| assert_eq!( |
| check_device_event(&events_rx, Some(std::time::Instant::now())), |
| DeviceWaitStatus::Timeout |
| ); |
| } |
| |
| #[test] |
| fn test_wait_devices_last_device_removed() { |
| logger_setup(); |
| |
| let mut events = events::test::new(); |
| let events_rx = events::test::subscribe(&mut events); |
| events::test::publish( |
| &mut events, |
| Event::ChipRemoved { |
| chip_id: 0, |
| device_id: 0, |
| remaining_nonbuiltin_devices: 0, |
| radio_stats: Vec::new(), |
| }, |
| ); |
| assert_eq!(check_device_event(&events_rx, None), DeviceWaitStatus::LastDeviceRemoved); |
| } |
| |
| #[test] |
| fn test_wait_devices_device_chip_added() { |
| logger_setup(); |
| |
| let mut events = events::test::new(); |
| let events_rx = events::test::subscribe(&mut events); |
| events::test::publish( |
| &mut events, |
| Event::DeviceAdded { id: 0, name: "".to_string(), builtin: false }, |
| ); |
| assert_eq!(check_device_event(&events_rx, None), DeviceWaitStatus::DeviceAdded); |
| events::test::publish( |
| &mut events, |
| Event::ChipAdded { |
| chip_id: 0, |
| chip_kind: ProtoChipKind::BLUETOOTH, |
| facade_id: 0, |
| device_name: "".to_string(), |
| builtin: false, |
| }, |
| ); |
| assert_eq!(check_device_event(&events_rx, None), DeviceWaitStatus::DeviceAdded); |
| } |
| |
| #[test] |
| fn test_wait_devices_ignore_event() { |
| logger_setup(); |
| |
| let mut events = events::test::new(); |
| let events_rx = events::test::subscribe(&mut events); |
| events::test::publish(&mut events, Event::DevicePatched { id: 0, name: "".to_string() }); |
| assert_eq!(check_device_event(&events_rx, None), DeviceWaitStatus::IgnoreEvent); |
| events::test::publish( |
| &mut events, |
| Event::ChipRemoved { |
| chip_id: 0, |
| device_id: 0, |
| remaining_nonbuiltin_devices: 1, |
| radio_stats: Vec::new(), |
| }, |
| ); |
| assert_eq!(check_device_event(&events_rx, None), DeviceWaitStatus::IgnoreEvent); |
| } |
| |
| #[test] |
| fn test_wait_devices_ignore_beacon() { |
| logger_setup(); |
| |
| let mut events = events::test::new(); |
| let events_rx = events::test::subscribe(&mut events); |
| events::test::publish( |
| &mut events, |
| Event::ChipAdded { |
| chip_id: 0, |
| chip_kind: ProtoChipKind::BLUETOOTH_BEACON, |
| facade_id: 0, |
| device_name: "".to_string(), |
| builtin: true, |
| }, |
| ); |
| assert_eq!(check_device_event(&events_rx, None), DeviceWaitStatus::IgnoreEvent); |
| } |
| } |