diff --git a/examples/patterns.rs b/examples/patterns.rs new file mode 100644 index 0000000..aaa786d --- /dev/null +++ b/examples/patterns.rs @@ -0,0 +1,76 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use smart_home::{ + Device, DeviceInfo, HomeBuilder, Reporter, Room, Socket, Subscriber, Temperature, Thermometer, +}; + +#[derive(Default)] +struct MySubscriber { + added: Rc>>, +} + +impl MySubscriber { + fn with_log(added: Rc>>) -> Self { + Self { added } + } +} + +impl Subscriber for MySubscriber { + fn on_event(&mut self, device: &Device) { + self.added.borrow_mut().push(device.name().to_string()); + } +} + +fn main() { + let home = HomeBuilder::new() + .add_room("First room") + .add_device("Socket_1", Socket::default()) + .add_device("Socket_2", Socket::default()) + .add_device("Thermo_1", Thermometer::default()) + .add_room("Second room") + .add_device("Socket_3", Socket::default()) + .add_device( + "Thermo_2", + Thermometer::new("Thermo_2".to_string(), Temperature::celsius(24.0)), + ) + .build(); + + println!("=== HomeBuilder report ==="); + Reporter::new().add(&home).report(); + + let mut room = Room::default(); + let subscriber_log = Rc::new(RefCell::new(Vec::new())); + room.subscribe(MySubscriber::with_log(Rc::clone(&subscriber_log))); + + let closure_log = Rc::new(RefCell::new(Vec::new())); + let closure_log_handle = Rc::clone(&closure_log); + room.subscribe(move |device: &Device| { + closure_log_handle + .borrow_mut() + .push(format!("closure: {}", device.name())); + }); + + room.insert_device("Socket_4".to_string(), Socket::default().into()); + room.insert_device("Thermo_3".to_string(), Thermometer::default().into()); + + println!("\n=== Observer logs ==="); + println!("subscriber: {:?}", subscriber_log.borrow()); + println!("closure: {:?}", closure_log.borrow()); + + let device = Device::default(); + let socket1 = Socket::default(); + let socket2 = Socket::default(); + let thermo1 = Thermometer::default(); + let thermo2 = Thermometer::new("Thermo".to_string(), Temperature::celsius(21.5)); + + println!("\n=== Reporter composite ==="); + Reporter::new() + .add(&room) + .add(&device) + .add(&socket1) + .add(&socket2) + .add(&thermo1) + .add(&thermo2) + .report(); +} diff --git a/src/bin/socket_sim.rs b/src/bin/socket_sim.rs index 1b8c078..8729af7 100644 --- a/src/bin/socket_sim.rs +++ b/src/bin/socket_sim.rs @@ -1,4 +1,4 @@ -//! TCP smart-outlet simulator: non-blocking accept/read, shared state, many clients. +//! TCP smart-outlet simulator: non-blocking accept/read, length-prefixed frames, shared state, many clients. //! //! Usage: `socket_sim [--watts ] [--on|--off]` @@ -37,11 +37,11 @@ impl Client { }) } - fn push_response(&mut self, line: &str) { - self.out.extend_from_slice(line.as_bytes()); - if !line.ends_with('\n') { - self.out.push(b'\n'); - } + fn push_response(&mut self, payload: &str) { + let bytes = payload.as_bytes(); + let len = u32::try_from(bytes.len()).expect("socket_sim response frame is too large"); + self.out.extend_from_slice(&len.to_be_bytes()); + self.out.extend_from_slice(bytes); } fn flush_writes(&mut self) -> io::Result<()> { @@ -78,18 +78,32 @@ impl Client { Ok(()) } - fn drain_lines(&mut self, state: &Arc>) -> io::Result<()> { - while let Some(pos) = self.buf.iter().position(|&b| b == b'\n') { - let raw: Vec = self.buf.drain(..=pos).collect(); - let line = String::from_utf8_lossy(&raw[..raw.len().saturating_sub(1)]); - let line = line.trim(); - match line { + fn drain_frames(&mut self, state: &Arc>) -> io::Result<()> { + while self.buf.len() >= 4 { + let len = + u32::from_be_bytes(self.buf[..4].try_into().expect("length prefix is 4 bytes")) + as usize; + if len > 1024 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("frame is too large: {len} bytes"), + )); + } + if self.buf.len() < 4 + len { + break; + } + self.buf.drain(..4); + let raw: Vec = self.buf.drain(..len).collect(); + let command = String::from_utf8(raw).map_err(|e| { + io::Error::new(io::ErrorKind::InvalidData, format!("bad UTF-8: {e}")) + })?; + match command.as_str() { "GET_STATUS" => { let g = state .lock() .map_err(|_| io::Error::other("state mutex poisoned"))?; let msg = wire::format_status_line(g.is_on, g.watts_now()); - self.push_response(msg.trim_end()); + self.push_response(&msg); } "SET_ON" => { if let Ok(mut g) = state.lock() { @@ -172,7 +186,7 @@ fn main() -> Result<(), Box> { let c = &mut clients[i]; match c.read_available() { Err(_) => true, - Ok(()) => c.drain_lines(&state).is_err() || c.flush_writes().is_err(), + Ok(()) => c.drain_frames(&state).is_err() || c.flush_writes().is_err(), } }; if remove { diff --git a/src/devices/device.rs b/src/devices/device.rs index f84f55e..d11fc6a 100644 --- a/src/devices/device.rs +++ b/src/devices/device.rs @@ -80,6 +80,12 @@ impl From for Device { } } +impl Default for Device { + fn default() -> Self { + Device::Socket(Socket::default()) + } +} + impl Report for Device { fn report(&self) -> String { format!("{}\n", DeviceInfo::state(self)) diff --git a/src/devices/socket.rs b/src/devices/socket.rs index 6a16fbd..fda51fc 100644 --- a/src/devices/socket.rs +++ b/src/devices/socket.rs @@ -3,6 +3,7 @@ use std::net::SocketAddr; use std::sync::Mutex; use super::device::DeviceInfo; +use crate::report::Report; use crate::types::Power; use crate::wire; @@ -195,6 +196,12 @@ impl Clone for Socket { } } +impl Default for Socket { + fn default() -> Self { + Self::new("Socket".to_string(), false, Power::default()) + } +} + impl PartialEq for Socket { fn eq(&self, other: &Self) -> bool { if self.name != other.name { @@ -254,9 +261,14 @@ impl DeviceInfo for Socket { } } +impl Report for Socket { + fn report(&self) -> String { + format!("{}\n", self.state()) + } +} + #[cfg(test)] mod tests { - use std::io::{Read, Write}; use std::net::TcpListener; use std::sync::mpsc; use std::thread; @@ -283,29 +295,18 @@ mod tests { stream .set_write_timeout(Some(Duration::from_secs(2))) .unwrap(); - let mut line = String::new(); - let mut buf = [0_u8; 1]; - while let Ok(n) = stream.read(&mut buf) { - if n == 0 { - break; - } - if buf[0] == b'\n' { - let l = line.trim(); - if l == "GET_STATUS" { - let on = *flag2.lock().unwrap(); - let w = if on { 99.0 } else { 0.0 }; - let msg = wire::format_status_line(on, w); - let _ = stream.write_all(msg.as_bytes()); - } else if l == "SET_ON" { - *flag2.lock().unwrap() = true; - let _ = stream.write_all(b"OK\n"); - } else if l == "SET_OFF" { - *flag2.lock().unwrap() = false; - let _ = stream.write_all(b"OK\n"); - } - line.clear(); - } else { - line.push(buf[0] as char); + while let Ok(command) = wire::read_frame(&mut stream) { + if command == "GET_STATUS" { + let on = *flag2.lock().unwrap(); + let w = if on { 99.0 } else { 0.0 }; + let msg = wire::format_status_line(on, w); + let _ = wire::write_frame(&mut stream, msg.as_bytes()); + } else if command == "SET_ON" { + *flag2.lock().unwrap() = true; + let _ = wire::write_frame(&mut stream, b"OK"); + } else if command == "SET_OFF" { + *flag2.lock().unwrap() = false; + let _ = wire::write_frame(&mut stream, b"OK"); } } } diff --git a/src/devices/thermometer.rs b/src/devices/thermometer.rs index 049c1bc..90185d3 100644 --- a/src/devices/thermometer.rs +++ b/src/devices/thermometer.rs @@ -7,6 +7,7 @@ use std::thread::JoinHandle; use std::time::Duration; use super::device::DeviceInfo; +use crate::report::Report; use crate::types::Temperature; use crate::wire; @@ -166,6 +167,12 @@ impl Clone for Thermometer { } } +impl Default for Thermometer { + fn default() -> Self { + Self::new("Thermometer".to_string(), Temperature::default()) + } +} + impl PartialEq for Thermometer { fn eq(&self, other: &Self) -> bool { if self.name != other.name { @@ -208,6 +215,12 @@ impl DeviceInfo for Thermometer { } } +impl Report for Thermometer { + fn report(&self) -> String { + format!("{}\n", self.state()) + } +} + #[cfg(test)] mod tests { use std::net::UdpSocket; diff --git a/src/home/builder.rs b/src/home/builder.rs new file mode 100644 index 0000000..4338271 --- /dev/null +++ b/src/home/builder.rs @@ -0,0 +1,88 @@ +use std::collections::HashMap; +use std::marker::PhantomData; + +use crate::devices::Device; + +use super::{Room, SmartHome}; + +/// Builder state before the first room is added. +pub struct NoRoom; + +/// Builder state after at least one room is available. +pub struct WithRoom; + +/// Typestate builder for [`SmartHome`]. +pub struct HomeBuilder { + name: String, + rooms: HashMap, + current_room: Option, + _state: PhantomData, +} + +impl HomeBuilder { + pub fn new() -> Self { + Self { + name: "Smart Home".to_string(), + rooms: HashMap::new(), + current_room: None, + _state: PhantomData, + } + } + + pub fn with_name(name: impl Into) -> Self { + Self { + name: name.into(), + rooms: HashMap::new(), + current_room: None, + _state: PhantomData, + } + } + + pub fn add_room(mut self, name: impl Into) -> HomeBuilder { + let name = name.into(); + self.rooms + .insert(name.clone(), Room::new(name.clone(), HashMap::new())); + HomeBuilder { + name: self.name, + rooms: self.rooms, + current_room: Some(name), + _state: PhantomData, + } + } + + pub fn build(self) -> SmartHome { + SmartHome::new(self.name, self.rooms) + } +} + +impl Default for HomeBuilder { + fn default() -> Self { + Self::new() + } +} + +impl HomeBuilder { + pub fn add_room(mut self, name: impl Into) -> Self { + let name = name.into(); + self.rooms + .insert(name.clone(), Room::new(name.clone(), HashMap::new())); + self.current_room = Some(name); + self + } + + pub fn add_device(mut self, key: impl Into, device: D) -> Self + where + D: Into, + { + if let Some(current_room) = self.current_room.as_deref() + && let Some(room) = self.rooms.get_mut(current_room) + { + room.insert_device(key.into(), device.into()); + } + self + } + + pub fn build(self) -> SmartHome { + SmartHome::new(self.name, self.rooms) + } +} diff --git a/src/home/mod.rs b/src/home/mod.rs index 720bdf0..00e1faa 100644 --- a/src/home/mod.rs +++ b/src/home/mod.rs @@ -1,5 +1,7 @@ +pub mod builder; pub mod room; pub mod smart_home; -pub use room::Room; +pub use builder::{HomeBuilder, NoRoom, WithRoom}; +pub use room::{Room, Subscriber}; pub use smart_home::SmartHome; diff --git a/src/home/room.rs b/src/home/room.rs index 5a17ad0..2614201 100644 --- a/src/home/room.rs +++ b/src/home/room.rs @@ -1,13 +1,28 @@ use std::collections::HashMap; +use std::fmt; use crate::devices::Device; use crate::report::Report; +/// Observer for device additions in a [`Room`]. +pub trait Subscriber { + fn on_event(&mut self, device: &Device); +} + +impl Subscriber for F +where + F: FnMut(&Device), +{ + fn on_event(&mut self, device: &Device) { + self(device); + } +} + /// Room containing smart devices keyed by string identifiers. -#[derive(Debug, Clone, PartialEq)] pub struct Room { name: String, devices: HashMap, + subscribers: Vec>, } impl Room { @@ -28,7 +43,11 @@ impl Room { /// assert_eq!(room.name(), "Living Room"); /// ``` pub fn new(name: String, devices: HashMap) -> Self { - Self { name, devices } + Self { + name, + devices, + subscribers: Vec::new(), + } } /// Reference to a device by key, if present. @@ -43,6 +62,7 @@ impl Room { /// Inserts or replaces a device under `key`. Returns the previous device, if any. pub fn insert_device(&mut self, key: String, device: Device) -> Option { + self.notify_subscribers(&device); self.devices.insert(key, device) } @@ -65,6 +85,52 @@ impl Room { pub fn devices(&self) -> impl Iterator { self.devices.iter() } + + /// Subscribes to device-addition events for this room. + pub fn subscribe(&mut self, subscriber: S) + where + S: Subscriber + 'static, + { + self.subscribers.push(Box::new(subscriber)); + } + + fn notify_subscribers(&mut self, device: &Device) { + for subscriber in &mut self.subscribers { + subscriber.on_event(device); + } + } +} + +impl Default for Room { + fn default() -> Self { + Self::new("Room".to_string(), HashMap::new()) + } +} + +impl Clone for Room { + fn clone(&self) -> Self { + Self { + name: self.name.clone(), + devices: self.devices.clone(), + subscribers: Vec::new(), + } + } +} + +impl PartialEq for Room { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.devices == other.devices + } +} + +impl fmt::Debug for Room { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Room") + .field("name", &self.name) + .field("devices", &self.devices) + .field("subscriber_count", &self.subscribers.len()) + .finish() + } } impl Report for Room { diff --git a/src/home/smart_home.rs b/src/home/smart_home.rs index 6aaeb56..9273493 100644 --- a/src/home/smart_home.rs +++ b/src/home/smart_home.rs @@ -99,6 +99,12 @@ impl SmartHome { } } +impl Default for SmartHome { + fn default() -> Self { + Self::new("Smart Home".to_string(), HashMap::new()) + } +} + impl Report for SmartHome { fn report(&self) -> String { let mut lines = vec![format!("Smart home '{}':\n", self.name)]; diff --git a/src/lib.rs b/src/lib.rs index 972bb59..ab3fc93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,8 +38,8 @@ pub mod wire; pub use devices::{Device, DeviceInfo, Socket, Thermometer}; pub use error::SmartHomeError; -pub use home::{Room, SmartHome}; -pub use report::Report; +pub use home::{HomeBuilder, NoRoom, Room, SmartHome, Subscriber, WithRoom}; +pub use report::{Report, Reporter}; pub use types::{Power, Temperature}; /// Prints a report for any type implementing [`Report`]. diff --git a/src/report.rs b/src/report.rs index b94d8f8..142a8c7 100644 --- a/src/report.rs +++ b/src/report.rs @@ -1,5 +1,7 @@ //! [`Report`] — textual report for devices, rooms, and the whole home. +use std::marker::PhantomData; + /// Types that can produce a human-readable report string. pub trait Report { /// Full report text (typically multiple lines). @@ -10,3 +12,68 @@ pub trait Report { print!("{}", self.report()); } } + +/// Static-polymorphism report composer for heterogeneous [`Report`] values. +pub struct Reporter<'a, Items = ()> { + items: Items, + _marker: PhantomData<&'a ()>, +} + +impl<'a> Reporter<'a, ()> { + pub fn new() -> Self { + Self { + items: (), + _marker: PhantomData, + } + } +} + +impl Default for Reporter<'_, ()> { + fn default() -> Self { + Self::new() + } +} + +impl<'a, Items> Reporter<'a, Items> { + #[allow(clippy::should_implement_trait)] + pub fn add(self, item: &'a T) -> Reporter<'a, (Items, &'a T)> + where + T: Report + 'a, + { + Reporter { + items: (self.items, item), + _marker: PhantomData, + } + } +} + +impl Reporter<'_, Items> +where + Items: ReportItems, +{ + pub fn report(&self) -> String { + let mut output = String::new(); + self.items.append_report(&mut output); + print!("{output}"); + output + } +} + +pub trait ReportItems { + fn append_report(&self, output: &mut String); +} + +impl ReportItems for () { + fn append_report(&self, _output: &mut String) {} +} + +impl ReportItems for (Items, &T) +where + Items: ReportItems, + T: Report, +{ + fn append_report(&self, output: &mut String) { + self.0.append_report(output); + output.push_str(&self.1.report()); + } +} diff --git a/src/wire.rs b/src/wire.rs index 4eecc6a..f5233f3 100644 --- a/src/wire.rs +++ b/src/wire.rs @@ -1,4 +1,4 @@ -//! Line-oriented TCP commands for smart sockets and UDP payload for thermometers. +//! Length-prefixed TCP commands for smart sockets and UDP payload for thermometers. use crate::types::Temperature; use std::io::{self, Read, Write}; @@ -7,6 +7,7 @@ use std::time::Duration; /// Default timeout for synchronous TCP operations from library code. pub const TCP_TIMEOUT: Duration = Duration::from_secs(2); +const TCP_MAX_FRAME_LEN: usize = 1024; pub fn tcp_connect(addr: impl ToSocketAddrs) -> io::Result { let stream = TcpStream::connect_timeout( @@ -21,54 +22,57 @@ pub fn tcp_connect(addr: impl ToSocketAddrs) -> io::Result { Ok(stream) } -pub fn read_line(stream: &mut TcpStream) -> io::Result { - let mut buf = Vec::new(); - let mut byte = [0_u8; 1]; - loop { - let n = stream.read(&mut byte)?; - if n == 0 { - return Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - "connection closed before newline", - )); - } - if byte[0] == b'\n' { - break; - } - buf.push(byte[0]); +pub fn read_frame(stream: &mut TcpStream) -> io::Result { + let mut len_buf = [0_u8; 4]; + stream.read_exact(&mut len_buf)?; + let len = u32::from_be_bytes(len_buf) as usize; + if len > TCP_MAX_FRAME_LEN { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("TCP frame is too large: {len} bytes"), + )); } + let mut buf = vec![0_u8; len]; + stream.read_exact(&mut buf)?; String::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) } -pub fn write_line(stream: &mut TcpStream, line: &[u8]) -> io::Result<()> { - stream.write_all(line)?; - if !line.ends_with(b"\n") { - stream.write_all(b"\n")?; +pub fn write_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { + if payload.len() > TCP_MAX_FRAME_LEN { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("TCP frame is too large: {} bytes", payload.len()), + )); } + let len = u32::try_from(payload.len()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))? + .to_be_bytes(); + stream.write_all(&len)?; + stream.write_all(payload)?; stream.flush() } pub fn socket_get_status(addr: SocketAddr) -> io::Result<(bool, f32)> { let mut stream = tcp_connect(addr)?; - write_line(&mut stream, b"GET_STATUS")?; - let line = read_line(&mut stream)?; + write_frame(&mut stream, b"GET_STATUS")?; + let line = read_frame(&mut stream)?; parse_status_line(&line) } pub fn socket_set_on(addr: SocketAddr) -> io::Result<()> { let mut stream = tcp_connect(addr)?; - write_line(&mut stream, b"SET_ON")?; + write_frame(&mut stream, b"SET_ON")?; expect_ok(&mut stream) } pub fn socket_set_off(addr: SocketAddr) -> io::Result<()> { let mut stream = tcp_connect(addr)?; - write_line(&mut stream, b"SET_OFF")?; + write_frame(&mut stream, b"SET_OFF")?; expect_ok(&mut stream) } fn expect_ok(stream: &mut TcpStream) -> io::Result<()> { - let line = read_line(stream)?; + let line = read_frame(stream)?; if line.trim() == "OK" { Ok(()) } else { @@ -108,7 +112,7 @@ fn parse_status_line(line: &str) -> io::Result<(bool, f32)> { pub fn format_status_line(is_on: bool, watts: f32) -> String { let flag = if is_on { "1" } else { "0" }; - format!("STATUS,{flag},{watts}\n") + format!("STATUS,{flag},{watts}") } /// UDP payload: 4-byte little-endian IEEE754 `f32` (degrees Celsius). @@ -142,6 +146,22 @@ mod tests { assert!((w2 - 123.5).abs() < 0.01); } + #[test] + fn tcp_frame_roundtrip() { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let payload = read_frame(&mut stream).unwrap(); + assert_eq!(payload, "PING"); + write_frame(&mut stream, b"PONG").unwrap(); + }); + + let mut stream = tcp_connect(addr).unwrap(); + write_frame(&mut stream, b"PING").unwrap(); + assert_eq!(read_frame(&mut stream).unwrap(), "PONG"); + } + #[test] fn temperature_udp_roundtrip() { let b = encode_temperature_celsius(-3.5); diff --git a/tests/integration.rs b/tests/integration.rs index b61b51a..45539d9 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,9 +1,12 @@ //! Integration tests: use only the public `smart_home` API (as a downstream crate would). +use std::cell::RefCell; use std::collections::HashMap; +use std::rc::Rc; use smart_home::{ - Device, DeviceInfo, Power, Report, Room, SmartHome, Socket, Temperature, Thermometer, room, + Device, DeviceInfo, HomeBuilder, Power, Report, Reporter, Room, SmartHome, Socket, Subscriber, + Temperature, Thermometer, room, }; #[test] @@ -99,3 +102,86 @@ fn report_contains_expected_sections() { let home = SmartHome::new("H".to_string(), rooms); assert!(home.report().contains("Smart home 'H'")); } + +#[test] +fn home_builder_adds_devices_to_current_room() { + let home = HomeBuilder::new() + .add_room("First room") + .add_device("Socket_1", Socket::default()) + .add_device("Thermo_1", Thermometer::default()) + .add_room("Second room") + .add_device("Socket_2", Socket::default()) + .build(); + + assert_eq!(home.room_count(), 2); + assert_eq!(home.room("First room").unwrap().device_count(), 2); + assert_eq!(home.room("Second room").unwrap().device_count(), 1); + assert!(matches!( + home.device("First room", "Thermo_1").unwrap(), + Device::Thermometer(_) + )); +} + +#[test] +fn reporter_collects_heterogeneous_reports() { + let room = Room::default(); + let device = Device::default(); + let socket = Socket::default(); + let thermometer = Thermometer::default(); + + let report = Reporter::new() + .add(&room) + .add(&device) + .add(&socket) + .add(&thermometer) + .report(); + + assert!(report.contains("Room 'Room'")); + assert!(report.contains("Socket 'Socket'")); + assert!(report.contains("Thermometer 'Thermometer'")); +} + +#[derive(Default)] +struct TestSubscriber { + names: Rc>>, +} + +impl TestSubscriber { + fn new(names: Rc>>) -> Self { + Self { names } + } +} + +impl Subscriber for TestSubscriber { + fn on_event(&mut self, device: &Device) { + self.names.borrow_mut().push(device.name().to_string()); + } +} + +#[test] +fn room_notifies_subscribers_when_device_is_added() { + let mut room = Room::default(); + let object_events = Rc::new(RefCell::new(Vec::new())); + let closure_events = Rc::new(RefCell::new(Vec::new())); + + room.subscribe(TestSubscriber::new(Rc::clone(&object_events))); + + let closure_events_handle = Rc::clone(&closure_events); + room.subscribe(move |device: &Device| { + closure_events_handle + .borrow_mut() + .push(device.name().to_string()); + }); + + room.insert_device("Socket_1".to_string(), Socket::default().into()); + room.insert_device("Thermo_1".to_string(), Thermometer::default().into()); + + assert_eq!( + object_events.borrow().as_slice(), + ["Socket".to_string(), "Thermometer".to_string()] + ); + assert_eq!( + closure_events.borrow().as_slice(), + ["Socket".to_string(), "Thermometer".to_string()] + ); +}