use std::net::Ipv4Addr;
use std::time::Duration;
use tosca::device::{DeviceEnvironment, DeviceKind};
use tosca::hazards::{Hazard, Hazards};
use tosca::parameters::{ParameterKind, Parameters, ParametersData};
use tosca::response::ResponseKind;
use tosca::route::{LightOffRoute, LightOnRoute, RestKind, Route};
use tosca_os::devices::light::Light;
use tosca_os::extract::Path;
use tosca_os::responses::error::ErrorResponse;
use tosca_os::responses::ok::{OkResponse, mandatory_ok_stateless};
use tosca_os::responses::serial::{SerialResponse, serial_stateless};
use tosca_os::server::Server;
use tosca_os::service::ServiceConfig;
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::device::Device;
use crate::request::Request;
const PORT_ONE: u16 = 3000;
const PORT_TWO: u16 = 4000;
const FIRST_DEVICE_ROUTE: &str = "/with-toggle";
const SECOND_DEVICE_ROUTE: &str = "/without-toggle";
pub(crate) const DOMAIN: &str = "tosca";
async fn turn_light_on() -> Result<OkResponse, ErrorResponse> {
println!("Light on");
Ok(OkResponse::ok())
}
async fn turn_light_off() -> Result<OkResponse, ErrorResponse> {
println!("Light off");
Ok(OkResponse::ok())
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Brightness {
pub(crate) brightness: u64,
}
async fn toggle(Path(brightness): Path<u64>) -> Result<SerialResponse<Brightness>, ErrorResponse> {
println!("Brightness: {brightness}");
Ok(SerialResponse::new(Brightness { brightness }))
}
async fn light(
port: u16,
id: &str,
with_toggle: bool,
close_rx: tokio::sync::oneshot::Receiver<()>,
) {
let light_on_route = LightOnRoute::put("On")
.description("Turn light on.")
.with_hazard(Hazard::ElectricEnergyConsumption);
let light_off_route = LightOffRoute::put("Off")
.description("Turn light off.")
.with_hazard(Hazard::LogEnergyConsumption);
let light = Light::new()
.turn_light_on(light_on_route, mandatory_ok_stateless(turn_light_on))
.turn_light_off(light_off_route, mandatory_ok_stateless(turn_light_off));
let light = if with_toggle {
let toggle_route = Route::get("Toggle", "/toggle")
.description("Toggle a light.")
.with_hazards(
Hazards::new()
.insert(Hazard::FireHazard)
.insert(Hazard::ElectricEnergyConsumption),
)
.with_parameters(Parameters::new().rangeu64("brightness", (0, 20, 1)));
light
.main_route(FIRST_DEVICE_ROUTE)
.route(serial_stateless(toggle_route, toggle))
.unwrap()
} else {
light.main_route(SECOND_DEVICE_ROUTE)
};
info!(
"Inside the light device {} `toggle` route and port {port}",
if with_toggle { "with" } else { "without" }
);
Server::new(light.build())
.address(Ipv4Addr::UNSPECIFIED)
.port(port)
.well_known_service(id)
.discovery_service(ServiceConfig::mdns_sd(id).hostname("tosca").domain(DOMAIN))
.with_graceful_shutdown(async move {
_ = close_rx.await;
})
.run()
.await
.expect("Error in running a device server.");
}
pub(crate) async fn light_with_toggle(close_rx: tokio::sync::oneshot::Receiver<()>) {
light(PORT_ONE, "light-with-toggle", true, close_rx).await;
}
pub(crate) async fn light_without_toggle(close_rx: tokio::sync::oneshot::Receiver<()>) {
light(PORT_TWO, "light-without-toggle", false, close_rx).await;
}
fn build_route(device: &Device, route: &str) -> String {
format!(
"{}{}{}",
device.network_info().last_reachable_address,
device.description().main_route,
route
)
}
fn check_request(
device: &Device,
route: &str,
description: &str,
kind: RestKind,
hazards: Hazards,
parameters_data: ParametersData,
response_kind: ResponseKind,
) {
let request_sender = device.request(route);
assert_eq!(
request_sender,
Some(&Request {
kind,
hazards,
route: build_route(device, route),
description: Some(description.to_string()),
parameters_data,
response_kind,
device_environment: DeviceEnvironment::Os,
})
);
}
pub(crate) fn compare_device_data(device: &Device) {
assert!(device.network_info().port == PORT_ONE || device.network_info().port == PORT_TWO);
let scheme = device.network_info().properties.get("scheme");
assert!(scheme.is_some_and(|scheme| scheme == "http"));
let path = device.network_info().properties.get("path");
assert!(
path.is_some_and(|path| path == "/.well-known/light-with-toggle"
|| path == "/.well-known/light-without-toggle")
);
assert!(
device.description().main_route == FIRST_DEVICE_ROUTE
|| device.description().main_route == SECOND_DEVICE_ROUTE
);
assert_eq!(device.description().kind, DeviceKind::Light);
assert_eq!(device.description().environment, DeviceEnvironment::Os);
assert!(
device.description().main_route == FIRST_DEVICE_ROUTE && device.requests_count() == 3
|| device.description().main_route == SECOND_DEVICE_ROUTE
&& device.requests_count() == 2
);
if device.description().main_route == FIRST_DEVICE_ROUTE {
let parameters_data = ParametersData::new().insert(
"brightness".into(),
ParameterKind::RangeU64 {
min: 0,
max: 20,
step: 1,
default: 0,
},
);
check_request(
device,
"/toggle",
"Toggle a light.",
RestKind::Get,
Hazards::new()
.insert(Hazard::FireHazard)
.insert(Hazard::ElectricEnergyConsumption),
parameters_data,
ResponseKind::Serial,
);
}
check_request(
device,
"/on",
"Turn light on.",
RestKind::Put,
Hazards::init(Hazard::ElectricEnergyConsumption),
ParametersData::new(),
ResponseKind::Ok,
);
check_request(
device,
"/off",
"Turn light off.",
RestKind::Put,
Hazards::init(Hazard::LogEnergyConsumption),
ParametersData::new(),
ResponseKind::Ok,
);
}
pub(crate) async fn check_function_with_device<F>(function: F)
where
F: AsyncFnOnce(),
{
let _ = tracing_subscriber::fmt().try_init();
let (close_tx, close_rx) = tokio::sync::oneshot::channel();
let device_handle = tokio::spawn(async { light_with_toggle(close_rx).await });
tokio::time::sleep(Duration::from_millis(100)).await;
function().await;
_ = close_tx.send(());
_ = device_handle.await;
}
pub(crate) async fn check_function_with_two_devices<F>(function: F)
where
F: AsyncFnOnce(),
{
let _ = tracing_subscriber::fmt().try_init();
let (device1_tx, device1_rx) = tokio::sync::oneshot::channel();
let (device2_tx, device2_rx) = tokio::sync::oneshot::channel();
let device1_handle = tokio::spawn(async { light_without_toggle(device1_rx).await });
tokio::time::sleep(Duration::from_millis(100)).await;
let device2_handle = tokio::spawn(async { light_with_toggle(device2_rx).await });
tokio::time::sleep(Duration::from_millis(100)).await;
function().await;
_ = device1_tx.send(());
_ = device1_handle.await;
_ = device2_tx.send(());
_ = device2_handle.await;
}