From 221918b3954d20219b16bc52947ebf7d04d0b545 Mon Sep 17 00:00:00 2001 From: Kees Verruijt Date: Mon, 9 Sep 2024 19:33:13 +0200 Subject: [PATCH] Gain control working --- Cargo.lock | 24 +++ Cargo.toml | 2 + src/navico/command.rs | 24 ++- src/navico/data.rs | 168 +++++++++-------- src/navico/report.rs | 213 ++++++++++++--------- src/navico/settings.rs | 199 ++++++++++++-------- src/radar.rs | 71 ++++--- src/settings.rs | 410 ++++++++++++++++++++++++++--------------- src/web.rs | 168 +++++++++-------- web/control.js | 49 +++-- web/style.css | 7 + 11 files changed, 812 insertions(+), 523 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0263f2..825b96c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -975,12 +975,14 @@ dependencies = [ "log", "miette", "network-interface", + "num-derive", "num-traits", "protobuf", "protobuf-codegen", "rust-embed", "serde", "serde_json", + "serde_repr", "serde_with", "socket2", "terminal_size", @@ -1083,6 +1085,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1449,6 +1462,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 9b4e86a..ef80371 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,13 @@ libc = "0.2.156" log = "0.4.22" miette = { version = "7.2.0", features = ["fancy"] } network-interface = "2.0.0" +num-derive = "0.4.2" num-traits = "0.2.19" protobuf = "3.5.1" rust-embed = { version = "8.5.0", features = ["axum"] } serde = { version = "1.0.206", features = ["derive", "serde_derive"] } serde_json = "1.0.125" +serde_repr = "0.1.19" serde_with = { version = "3.9.0", features = ["macros"] } socket2 = "0.5.7" terminal_size = "0.3.0" diff --git a/src/navico/command.rs b/src/navico/command.rs index c7b9c55..46da391 100644 --- a/src/navico/command.rs +++ b/src/navico/command.rs @@ -1,8 +1,10 @@ use log::{debug, trace}; +use std::cmp::min; use std::io; use tokio::net::UdpSocket; -use crate::radar::RadarInfo; +use crate::radar::{RadarError, RadarInfo}; +use crate::settings::{ControlType, ControlValue}; use crate::util::create_multicast_send; pub const REQUEST_03_REPORT: [u8; 2] = [0x04, 0xc2]; // This causes the radar to report Report 3 @@ -25,7 +27,7 @@ impl Command { } } - async fn start_socket(&mut self) -> io::Result<()> { + async fn start_socket(&mut self) -> Result<(), RadarError> { match create_multicast_send(&self.info.send_command_addr, &self.info.nic_addr) { Ok(sock) => { debug!( @@ -41,20 +43,32 @@ impl Command { "{} {} via {}: create multicast failed: {}", self.key, &self.info.send_command_addr, &self.info.nic_addr, e ); - Err(e) + Err(RadarError::Io(e)) } } } - pub async fn send(&mut self, message: &[u8]) -> io::Result<()> { + pub async fn send(&mut self, message: &[u8]) -> Result<(), RadarError> { if self.sock.is_none() { self.start_socket().await?; } if let Some(sock) = &self.sock { - sock.send(message).await?; + sock.send(message).await.map_err(RadarError::Io)?; trace!("{}: sent {:02X?}", self.key, message); } Ok(()) } + + pub async fn set_control(&mut self, cv: &ControlValue) -> Result<(), RadarError> { + match cv.id { + ControlType::Gain => { + let v: i32 = min((cv.value.unwrap_or(0) + 1) * 255 / 100, 255); + let auto: u8 = if cv.auto.unwrap_or(false) { 1 } else { 0 }; + let cmd: [u8;11] = [ 0x06, 0xc1, 0, 0, 0, 0, auto, 0, 0, 0, v as u8]; + self.send(&cmd).await + }, + _ => Err(RadarError::CannotSetControlType(cv.id)) + } + } } diff --git a/src/navico/data.rs b/src/navico/data.rs index e541c6d..a179aa5 100644 --- a/src/navico/data.rs +++ b/src/navico/data.rs @@ -1,9 +1,9 @@ use bincode::deserialize; -use log::{ debug, trace, warn }; +use log::{debug, trace, warn}; use protobuf::Message; use serde::Deserialize; -use std::time::{ SystemTime, UNIX_EPOCH }; -use std::{ io, time::Duration }; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::{io, time::Duration}; use tokio::net::UdpSocket; use tokio::sync::mpsc::Receiver; use tokio::time::sleep; @@ -13,15 +13,11 @@ use crate::locator::LocatorId; use crate::navico::NAVICO_SPOKE_LEN; use crate::protos::RadarMessage::radar_message::Spoke; use crate::protos::RadarMessage::RadarMessage; -use crate::util::{ create_multicast, PrintableSpoke }; -use crate::{ radar::*, Cli }; +use crate::util::{create_multicast, PrintableSpoke}; +use crate::{radar::*, Cli}; use super::{ - DataUpdate, - NAVICO_SPOKES, - NAVICO_SPOKES_RAW, - RADAR_LINE_DATA_LENGTH, - SPOKES_PER_FRAME, + DataUpdate, NAVICO_SPOKES, NAVICO_SPOKES_RAW, RADAR_LINE_DATA_LENGTH, SPOKES_PER_FRAME, }; const BYTE_LOOKUP_LENGTH: usize = (u8::MAX as usize) + 1; @@ -53,31 +49,31 @@ fn extract_heading_value(x: u16) -> Option { #[derive(Deserialize, Debug, Clone, Copy)] #[repr(packed)] struct Br24Header { - header_len: u8, // 1 bytes - status: u8, // 1 bytes + header_len: u8, // 1 bytes + status: u8, // 1 bytes _scan_number: [u8; 2], // 1 byte (HALO and newer), 2 bytes (4G and older) - _mark: [u8; 4], // 4 bytes, on BR24 this is always 0x00, 0x44, 0x0d, 0x0e - angle: [u8; 2], // 2 bytes - heading: [u8; 2], // 2 bytes heading with RI-10/11. See bitmask explanation above. - range: [u8; 4], // 4 bytes - _u01: [u8; 2], // 2 bytes blank - _u02: [u8; 2], // 2 bytes - _u03: [u8; 4], // 4 bytes blank + _mark: [u8; 4], // 4 bytes, on BR24 this is always 0x00, 0x44, 0x0d, 0x0e + angle: [u8; 2], // 2 bytes + heading: [u8; 2], // 2 bytes heading with RI-10/11. See bitmask explanation above. + range: [u8; 4], // 4 bytes + _u01: [u8; 2], // 2 bytes blank + _u02: [u8; 2], // 2 bytes + _u03: [u8; 4], // 4 bytes blank } /* total size = 24 */ #[derive(Deserialize, Debug, Clone, Copy)] #[repr(packed)] struct Br4gHeader { - header_len: u8, // 1 bytes - status: u8, // 1 bytes + header_len: u8, // 1 bytes + status: u8, // 1 bytes _scan_number: [u8; 2], // 1 byte (HALO and newer), 2 bytes (4G and older) - _mark: [u8; 2], // 2 bytes - large_range: [u8; 2], // 2 bytes, on 4G and up - angle: [u8; 2], // 2 bytes - heading: [u8; 2], // 2 bytes heading with RI-10/11. See bitmask explanation above. - small_range: [u8; 2], // 2 bytes or -1 - _rotation: [u8; 2], // 2 bytes or -1 - _u01: [u8; 4], // 4 bytes signed integer, always -1 + _mark: [u8; 2], // 2 bytes + large_range: [u8; 2], // 2 bytes, on 4G and up + angle: [u8; 2], // 2 bytes + heading: [u8; 2], // 2 bytes heading with RI-10/11. See bitmask explanation above. + small_range: [u8; 2], // 2 bytes or -1 + _rotation: [u8; 2], // 2 bytes or -1 + _u01: [u8; 4], // 4 bytes signed integer, always -1 _u02: [u8; 4], // 4 bytes signed integer, mostly -1 (0x80 in last byte) or 0xa0 in last byte } /* total size = 24 */ @@ -151,8 +147,7 @@ impl NavicoDataReceiver { self.sock = Some(sock); debug!( "{} via {}: listening for spoke data", - &self.info.spoke_data_addr, - &self.info.nic_addr + &self.info.spoke_data_addr, &self.info.nic_addr ); Ok(()) } @@ -160,9 +155,7 @@ impl NavicoDataReceiver { sleep(Duration::from_millis(1000)).await; debug!( "{} via {}: create multicast failed: {}", - &self.info.spoke_data_addr, - &self.info.nic_addr, - e + &self.info.spoke_data_addr, &self.info.nic_addr, e ); Ok(()) } @@ -170,10 +163,8 @@ impl NavicoDataReceiver { } fn fill_pixel_to_blob(&mut self, legend: &Legend) { - let mut lookup: [[u8; BYTE_LOOKUP_LENGTH]; LOOKUP_SPOKE_LENGTH] = [ - [0; BYTE_LOOKUP_LENGTH]; - LOOKUP_SPOKE_LENGTH - ]; + let mut lookup: [[u8; BYTE_LOOKUP_LENGTH]; LOOKUP_SPOKE_LENGTH] = + [[0; BYTE_LOOKUP_LENGTH]; LOOKUP_SPOKE_LENGTH]; // Cannot use for() in const expr, so use while instead let mut j: usize = 0; while j < BYTE_LOOKUP_LENGTH { @@ -266,7 +257,10 @@ impl NavicoDataReceiver { let data = &self.buf; if data.len() < FRAME_HEADER_LENGTH + RADAR_LINE_LENGTH { - warn!("UDP data frame with even less than one spoke, len {} dropped", data.len()); + warn!( + "UDP data frame with even less than one spoke, len {} dropped", + data.len() + ); return; } @@ -286,20 +280,24 @@ impl NavicoDataReceiver { let mut offset: usize = FRAME_HEADER_LENGTH; for scanline in 0..scanlines_in_packet { let header_slice = &data[offset..offset + RADAR_LINE_HEADER_LENGTH]; - let spoke_slice = - &data - [ - offset + RADAR_LINE_HEADER_LENGTH..offset + - RADAR_LINE_HEADER_LENGTH + - RADAR_LINE_DATA_LENGTH - ]; - - if let Some((range, angle, heading)) = self.validate_header(header_slice, scanline) { + let spoke_slice = &data[offset + RADAR_LINE_HEADER_LENGTH + ..offset + RADAR_LINE_HEADER_LENGTH + RADAR_LINE_DATA_LENGTH]; + + if let Some((range, angle, heading)) = self.validate_header(header_slice, scanline) + { trace!("range {} angle {} heading {:?}", range, angle, heading); - trace!("Received {:04} spoke {}", scanline, PrintableSpoke::new(spoke_slice)); - message.spokes.push( - self.process_spoke(&pixel_to_blob, range, angle, heading, spoke_slice) + trace!( + "Received {:04} spoke {}", + scanline, + PrintableSpoke::new(spoke_slice) ); + message.spokes.push(self.process_spoke( + &pixel_to_blob, + range, + angle, + heading, + spoke_slice, + )); } else { warn!("Invalid spoke: header {:02X?}", &header_slice); self.statistics.broken_packets += 1; @@ -309,7 +307,9 @@ impl NavicoDataReceiver { } let mut bytes = Vec::new(); - message.write_to_vec(&mut bytes).expect("Cannot write RadarMessage to vec"); + message + .write_to_vec(&mut bytes) + .expect("Cannot write RadarMessage to vec"); match self.info.message_tx.send(bytes) { Err(e) => { @@ -324,39 +324,40 @@ impl NavicoDataReceiver { fn validate_header( &self, header_slice: &[u8], - scanline: usize + scanline: usize, ) -> Option<(u32, u16, Option)> { match self.info.locator_id { - LocatorId::Gen3Plus => - match deserialize::(&header_slice) { - Ok(header) => { - trace!("Received {:04} header {:?}", scanline, header); + LocatorId::Gen3Plus => match deserialize::(&header_slice) { + Ok(header) => { + trace!("Received {:04} header {:?}", scanline, header); - NavicoDataReceiver::validate_4g_header(&header) - } - Err(e) => { - warn!("Illegible spoke: {} header {:02X?}", e, &header_slice); - return None; - } + NavicoDataReceiver::validate_4g_header(&header) } - LocatorId::GenBR24 => - match deserialize::(&header_slice) { - Ok(header) => { - trace!("Received {:04} header {:?}", scanline, header); + Err(e) => { + warn!("Illegible spoke: {} header {:02X?}", e, &header_slice); + return None; + } + }, + LocatorId::GenBR24 => match deserialize::(&header_slice) { + Ok(header) => { + trace!("Received {:04} header {:?}", scanline, header); - NavicoDataReceiver::validate_br24_header(&header) - } - Err(e) => { - warn!("Illegible spoke: {} header {:02X?}", e, &header_slice); - return None; - } + NavicoDataReceiver::validate_br24_header(&header) + } + Err(e) => { + warn!("Illegible spoke: {} header {:02X?}", e, &header_slice); + return None; } + }, } } fn validate_4g_header(header: &Br4gHeader) -> Option<(u32, u16, Option)> { if header.header_len != (RADAR_LINE_HEADER_LENGTH as u8) { - warn!("Spoke with illegal header length ({}) ignored", header.header_len); + warn!( + "Spoke with illegal header length ({}) ignored", + header.header_len + ); return None; } if header.status != 0x02 && header.status != 0x12 { @@ -370,7 +371,11 @@ impl NavicoDataReceiver { let small_range = u16::from_le_bytes(header.small_range); let range = if large_range == 0x80 { - if small_range == 0xffff { 0 } else { (small_range as u32) / 4 } + if small_range == 0xffff { + 0 + } else { + (small_range as u32) / 4 + } } else { ((large_range as u32) * (small_range as u32)) / 512 }; @@ -381,7 +386,10 @@ impl NavicoDataReceiver { fn validate_br24_header(header: &Br24Header) -> Option<(u32, u16, Option)> { if header.header_len != (RADAR_LINE_HEADER_LENGTH as u8) { - warn!("Spoke with illegal header length ({}) ignored", header.header_len); + warn!( + "Spoke with illegal header length ({}) ignored", + header.header_len + ); return None; } if header.status != 0x02 && header.status != 0x12 { @@ -404,7 +412,7 @@ impl NavicoDataReceiver { range: u32, angle: u16, heading: Option, - spoke: &[u8] + spoke: &[u8], ) -> Spoke { // Convert the spoke data to bytes let mut generic_spoke: Vec = Vec::with_capacity(NAVICO_SPOKE_LEN); @@ -434,7 +442,13 @@ impl NavicoDataReceiver { generic_spoke.push(pixel_to_blob[high_nibble_index][pixel]); } - trace!("Spoke {}/{:?}/{} len {}", range, heading, angle, generic_spoke.len()); + trace!( + "Spoke {}/{:?}/{} len {}", + range, + heading, + angle, + generic_spoke.len() + ); let angle = (angle / 2) as u32; // For now, don't send heading in replay mode, signalk-radar-client doesn't diff --git a/src/navico/report.rs b/src/navico/report.rs index ecf3eb1..a2ae13b 100644 --- a/src/navico/report.rs +++ b/src/navico/report.rs @@ -1,21 +1,21 @@ -use anyhow::{ bail, Error }; +use anyhow::{bail, Error}; use enum_primitive_derive::Primitive; -use log::{ debug, error, info, trace }; +use log::{debug, error, info, trace}; use std::mem::transmute; use std::time::Duration; -use std::{ fmt, io }; +use std::{fmt, io}; use tokio::net::UdpSocket; use tokio::sync::mpsc::Sender; -use tokio::time::{ sleep, sleep_until, Instant }; +use tokio::time::{sleep, sleep_until, Instant}; use tokio_graceful_shutdown::SubsystemHandle; -use crate::radar::{ DopplerMode, RadarError, RadarInfo }; -use crate::settings::{ ControlMessage, ControlState, ControlType }; -use crate::util::{ c_string, c_wide_string, create_multicast }; +use crate::radar::{DopplerMode, RadarError, RadarInfo}; +use crate::settings::{ControlMessage, ControlState, ControlType}; +use crate::util::{c_string, c_wide_string, create_multicast}; -use super::command::{ self, Command }; +use super::command::{self, Command}; use super::settings::NavicoControls; -use super::{ DataUpdate, Model, NavicoSettings }; +use super::{DataUpdate, Model, NavicoSettings}; pub struct NavicoReportReceiver { info: RadarInfo, @@ -70,8 +70,8 @@ const REPORT_01_C4_18: u8 = 0x01; struct RadarReport2_99 { _what: u8, _command: u8, - range: [u8; 4], // 2..6 = range - _u00: [u8; 1], // 6 + range: [u8; 4], // 2..6 = range + _u00: [u8; 1], // 6 mode: u8, // 7 = mode, 0 = custom, 1 = harbor, 2 = offshore, 3 = ?, 4 = bird, 5 = weather _u01: [u8; 4], // 8..12 gain: u8, // 12 @@ -107,10 +107,10 @@ const REPORT_02_C4_99: u8 = 0x02; struct RadarReport3_129 { _what: u8, _command: u8, - model: u8, // So far: 01 = 4G and new 3G, 08 = 3G, 0F = BR24, 00 = HALO - _u00: [u8; 31], // Lots of unknown - hours: [u8; 4], // Hours of operation - _u01: [u8; 20], // Lots of unknown + model: u8, // So far: 01 = 4G and new 3G, 08 = 3G, 0F = BR24, 00 = HALO + _u00: [u8; 31], // Lots of unknown + hours: [u8; 4], // Hours of operation + _u01: [u8; 20], // Lots of unknown firmware_date: [u8; 32], // Wide chars, assumed UTF16 firmware_time: [u8; 32], // Wide chars, assumed UTF16 _u02: [u8; 7], @@ -134,13 +134,13 @@ const REPORT_03_C4_129: u8 = 0x03; struct RadarReport4_66 { _what: u8, _command: u8, - _u00: [u8; 4], // 2..6 + _u00: [u8; 4], // 2..6 bearing_alignment: [u8; 2], // 6..8 - _u01: [u8; 2], // 8..10 - antenna_height: [u8; 2], // 10..12 = Antenna height - _u02: [u8; 7], // 12..19 - accent_light: u8, // 19 = Accent light - _u03: [u8; 46], // 20..66 + _u01: [u8; 2], // 8..10 + antenna_height: [u8; 2], // 10..12 = Antenna height + _u02: [u8; 7], // 12..19 + accent_light: u8, // 19 = Accent light + _u03: [u8; 46], // 20..66 } impl RadarReport4_66 { @@ -169,11 +169,11 @@ struct SectorBlankingReport { struct RadarReport6_68 { _what: u8, _command: u8, - _u00: [u8; 4], // 2..6 - name: [u8; 6], // 6..12 - _u01: [u8; 24], // 12..36 + _u00: [u8; 4], // 2..6 + name: [u8; 6], // 6..12 + _u01: [u8; 24], // 12..36 blanking: [SectorBlankingReport; 4], // 36..56 - _u02: [u8; 12], // 56..68 + _u02: [u8; 12], // 56..68 } impl RadarReport6_68 { @@ -191,11 +191,11 @@ impl RadarReport6_68 { struct RadarReport6_74 { _what: u8, _command: u8, - _u00: [u8; 4], // 2..6 - name: [u8; 6], // 6..12 - _u01: [u8; 30], // 12..42 + _u00: [u8; 4], // 2..6 + name: [u8; 6], // 6..12 + _u01: [u8; 30], // 12..42 blanking: [SectorBlankingReport; 4], // 42..52 - _u0: [u8; 12], // 62..74 + _u0: [u8; 12], // 62..74 } impl RadarReport6_74 { @@ -215,23 +215,23 @@ const REPORT_06_C4_68: u8 = 0x06; #[repr(packed)] struct RadarReport8_18 { // 08 c4 length 18 - _what: u8, // 0 0x08 - _command: u8, // 1 0xC4 - sea_state: u8, // 2 + _what: u8, // 0 0x08 + _command: u8, // 1 0xC4 + sea_state: u8, // 2 interference_rejection: u8, // 3 - scan_speed: u8, // 4 - sls_auto: u8, // 5 installation: sidelobe suppression auto - _field6: u8, // 6 - _field7: u8, // 7 - _field8: u8, // 8 - side_lobe_suppression: u8, // 9 installation: sidelobe suppression - _field10: u16, // 10-11 - noise_rejection: u8, // 12 noise rejection - target_sep: u8, // 13 - sea_clutter: u8, // 14 sea clutter on Halo - auto_sea_clutter: u8, // 15 auto sea clutter on Halo - _field13: u8, // 16 - _field14: u8, // 17 + scan_speed: u8, // 4 + sls_auto: u8, // 5 installation: sidelobe suppression auto + _field6: u8, // 6 + _field7: u8, // 7 + _field8: u8, // 8 + side_lobe_suppression: u8, // 9 installation: sidelobe suppression + _field10: u16, // 10-11 + noise_rejection: u8, // 12 noise rejection + target_sep: u8, // 13 + sea_clutter: u8, // 14 sea clutter on Halo + auto_sea_clutter: u8, // 15 auto sea clutter on Halo + _field13: u8, // 16 + _field14: u8, // 17 } #[derive(Debug)] @@ -270,7 +270,7 @@ impl NavicoReportReceiver { pub fn new( info: RadarInfo, // Quick access to our own RadarInfo settings: NavicoSettings, - data_tx: Sender + data_tx: Sender, ) -> NavicoReportReceiver { let key = info.key(); @@ -296,9 +296,7 @@ impl NavicoReportReceiver { self.sock = Some(sock); debug!( "{}: {} via {}: listening for reports", - self.key, - &self.info.report_addr, - &self.info.nic_addr + self.key, &self.info.report_addr, &self.info.nic_addr ); Ok(()) } @@ -306,10 +304,7 @@ impl NavicoReportReceiver { sleep(Duration::from_millis(1000)).await; debug!( "{}: {} via {}: create multicast failed: {}", - self.key, - &self.info.report_addr, - &self.info.nic_addr, - e + self.key, &self.info.report_addr, &self.info.nic_addr, e ); Err(e) } @@ -346,7 +341,7 @@ impl NavicoReportReceiver { }, r = command_rx.recv() => { match r { - Ok(control_message) => { + Ok(control_message) => { self.process_control_message(&control_message).await?; } Err(_e) => {} @@ -357,8 +352,8 @@ impl NavicoReportReceiver { } async fn process_control_message( - &self, - control_message: &ControlMessage + &mut self, + control_message: &ControlMessage, ) -> Result<(), RadarError> { match control_message { ControlMessage::NewClient => { @@ -368,15 +363,19 @@ impl NavicoReportReceiver { } } ControlMessage::Value(v) => { - todo!("Handle control sent to server {:?}", v); + self.command_sender.set_control(v).await?; } } Ok(()) } - async fn send_report_requests(&mut self) -> Result<(), io::Error> { - self.command_sender.send(&command::REQUEST_03_REPORT).await?; - self.command_sender.send(&command::REQUEST_MANY2_REPORT).await?; + async fn send_report_requests(&mut self) -> Result<(), RadarError> { + self.command_sender + .send(&command::REQUEST_03_REPORT) + .await?; + self.command_sender + .send(&command::REQUEST_MANY2_REPORT) + .await?; Ok(()) } @@ -405,7 +404,7 @@ impl NavicoReportReceiver { control_type: &ControlType, value: i32, auto: Option, - state: ControlState + state: ControlState, ) { if let Some(controls) = self.info.controls.as_mut() { match controls.set_all(control_type, value, auto, state) { @@ -419,7 +418,7 @@ impl NavicoReportReceiver { "{}: Control '{}' new value {} state {}", self.key, control_type, - control.value_string(), + control.value(), control.state ); } @@ -442,7 +441,7 @@ impl NavicoReportReceiver { "{}: Control '{}' new value {} {}", self.key, control_type, - control.value_string(), + control.value(), control.item().unit.as_deref().unwrap_or("") ); } @@ -465,7 +464,7 @@ impl NavicoReportReceiver { "{}: Control '{}' new value {} auto {}", self.key, control_type, - control.value_string(), + control.value(), auto ); } @@ -598,18 +597,18 @@ impl NavicoReportReceiver { if self.settings.model != model { info!("{}: Radar is model {}", self.key, model); self.settings.model = model; - self.info.controls = Some( - NavicoControls::new2( - model, - self.info.message_tx.clone(), - self.info.control_tx.clone(), - self.info.command_tx.clone() - ) - ); + self.info.controls = Some(NavicoControls::new2( + model, + self.info.message_tx.clone(), + self.info.control_tx.clone(), + self.info.command_tx.clone(), + )); self.info.set_legend(model == Model::HALO); self.update_radar_info(); - self.data_tx.send(DataUpdate::Legend(self.info.legend.clone())).await?; + self.data_tx + .send(DataUpdate::Legend(self.info.legend.clone())) + .await?; if let Some(serial_number) = self.info.serial_no.as_ref() { self.set_string(&ControlType::SerialNumber, serial_number.to_string()); @@ -634,9 +633,12 @@ impl NavicoReportReceiver { self.set( &ControlType::BearingAlignment, - i16::from_le_bytes(report.bearing_alignment) as i32 + i16::from_le_bytes(report.bearing_alignment) as i32, + ); + self.set( + &ControlType::AntennaHeight, + u16::from_le_bytes(report.antenna_height) as i32, ); - self.set(&ControlType::AntennaHeight, u16::from_le_bytes(report.antenna_height) as i32); if self.settings.model == Model::HALO { self.set(&ControlType::AccentLight, report.accent_light as i32); } @@ -645,10 +647,26 @@ impl NavicoReportReceiver { } const BLANKING_SETS: [(usize, ControlType, ControlType); 4] = [ - (0, ControlType::NoTransmitStart1, ControlType::NoTransmitEnd1), - (1, ControlType::NoTransmitStart2, ControlType::NoTransmitEnd2), - (2, ControlType::NoTransmitStart3, ControlType::NoTransmitEnd3), - (3, ControlType::NoTransmitStart4, ControlType::NoTransmitEnd4), + ( + 0, + ControlType::NoTransmitStart1, + ControlType::NoTransmitEnd1, + ), + ( + 1, + ControlType::NoTransmitStart2, + ControlType::NoTransmitEnd2, + ), + ( + 2, + ControlType::NoTransmitStart3, + ControlType::NoTransmitEnd3, + ), + ( + 3, + ControlType::NoTransmitStart4, + ControlType::NoTransmitEnd4, + ), ]; /// @@ -666,7 +684,11 @@ impl NavicoReportReceiver { let blanking = &report.blanking[i]; let start_angle = i16::from_le_bytes(blanking.start_angle); let end_angle = i16::from_le_bytes(blanking.end_angle); - let state = if blanking.enabled > 0 { ControlState::Manual } else { ControlState::Off }; + let state = if blanking.enabled > 0 { + ControlState::Manual + } else { + ControlState::Off + }; self.set_all(&start, start_angle as i32, None, state); self.set_all(&end, end_angle as i32, None, state); } @@ -689,7 +711,11 @@ impl NavicoReportReceiver { let blanking = &report.blanking[i]; let start_angle = i16::from_le_bytes(blanking.start_angle); let end_angle = i16::from_le_bytes(blanking.end_angle); - let state = if blanking.enabled > 0 { ControlState::Manual } else { ControlState::Off }; + let state = if blanking.enabled > 0 { + ControlState::Manual + } else { + ControlState::Off + }; self.set_all(&start, start_angle as i32, None, state); self.set_all(&end, end_angle as i32, None, state); } @@ -714,10 +740,9 @@ impl NavicoReportReceiver { async fn process_report_08(&mut self) -> Result<(), Error> { let data = &self.buf; - if - data.len() != size_of::() && - data.len() != size_of::() && - data.len() != size_of::() + 1 + if data.len() != size_of::() + && data.len() != size_of::() + && data.len() != size_of::() + 1 { bail!("{}: Report 0x08C4 invalid length {}", self.key, data.len()); } @@ -748,10 +773,17 @@ impl NavicoReportReceiver { let doppler_mode: Result = doppler_state.try_into(); match doppler_mode { Err(_) => { - bail!("{}: Unknown doppler state {}", self.key, report.doppler_state); + bail!( + "{}: Unknown doppler state {}", + self.key, + report.doppler_state + ); } Ok(doppler_mode) => { - debug!("{}: doppler mode={} speed={}", self.key, doppler_mode, doppler_speed); + debug!( + "{}: doppler mode={} speed={}", + self.key, doppler_mode, doppler_speed + ); self.data_tx.send(DataUpdate::Doppler(doppler_mode)).await?; } } @@ -760,12 +792,15 @@ impl NavicoReportReceiver { } self.set(&ControlType::SeaState, sea_state as i32); - self.set(&ControlType::InterferenceRejection, interference_rejection as i32); + self.set( + &ControlType::InterferenceRejection, + interference_rejection as i32, + ); self.set(&ControlType::ScanSpeed, scan_speed as i32); self.set_auto( &ControlType::SideLobeSuppression, sidelobe_suppression, - sidelobe_suppression_auto + sidelobe_suppression_auto, ); self.set(&ControlType::NoiseRejection, noise_reduction); self.set(&ControlType::TargetSeparation, target_sep); diff --git a/src/navico/settings.rs b/src/navico/settings.rs index edc80b4..8e4a4f7 100644 --- a/src/navico/settings.rs +++ b/src/navico/settings.rs @@ -1,12 +1,7 @@ use std::collections::HashMap; use crate::settings::{ - AutomaticValue, - Control, - ControlMessage, - ControlType, - ControlValue, - Controls, + AutomaticValue, Control, ControlMessage, ControlType, ControlValue, Controls, }; use super::Model; @@ -18,7 +13,7 @@ impl NavicoControls { model: Model, protobuf_tx: tokio::sync::broadcast::Sender>, control_tx: tokio::sync::broadcast::Sender, - command_tx: tokio::sync::broadcast::Sender + command_tx: tokio::sync::broadcast::Sender, ) -> Self { let mut controls = HashMap::new(); @@ -27,66 +22,69 @@ impl NavicoControls { ControlType::Mode, Control::new_list( ControlType::Mode, - &["Custom", "Harbor", "Offshore", "Unknown", "Weather", "Bird"] - ) + &["Custom", "Harbor", "Offshore", "Unknown", "Weather", "Bird"], + ), ); controls.insert( ControlType::AccentLight, - Control::new_list(ControlType::AccentLight, &["Off", "Low", "Medium", "High"]) + Control::new_list(ControlType::AccentLight, &["Off", "Low", "Medium", "High"]), ); - controls.insert(ControlType::ModelName, Control::new_string(ControlType::ModelName)); + controls.insert( + ControlType::ModelName, + Control::new_string(ControlType::ModelName), + ); controls.insert( ControlType::NoTransmitStart1, Control::new_numeric(ControlType::NoTransmitStart1, -180, 180) .wire_scale_factor(1800) - .wire_offset(-1) + .wire_offset(-1), ); controls.insert( ControlType::NoTransmitStart2, Control::new_numeric(ControlType::NoTransmitStart2, -180, 180) .wire_scale_factor(1800) - .wire_offset(-1) + .wire_offset(-1), ); controls.insert( ControlType::NoTransmitStart3, Control::new_numeric(ControlType::NoTransmitStart3, -180, 180) .wire_scale_factor(1800) - .wire_offset(-1) + .wire_offset(-1), ); controls.insert( ControlType::NoTransmitStart4, Control::new_numeric(ControlType::NoTransmitStart4, -180, 180) .wire_scale_factor(1800) - .wire_offset(-1) + .wire_offset(-1), ); controls.insert( ControlType::NoTransmitEnd1, Control::new_numeric(ControlType::NoTransmitEnd1, -180, 180) .unit("Deg") .wire_scale_factor(1800) - .wire_offset(-1) + .wire_offset(-1), ); controls.insert( ControlType::NoTransmitEnd2, Control::new_numeric(ControlType::NoTransmitEnd2, -180, 180) .unit("Deg") .wire_scale_factor(1800) - .wire_offset(-1) + .wire_offset(-1), ); controls.insert( ControlType::NoTransmitEnd3, Control::new_numeric(ControlType::NoTransmitEnd3, -180, 180) .unit("Deg") .wire_scale_factor(1800) - .wire_offset(-1) + .wire_offset(-1), ); controls.insert( ControlType::NoTransmitEnd4, Control::new_numeric(ControlType::NoTransmitEnd4, -180, 180) .unit("Deg") .wire_scale_factor(1800) - .wire_offset(-1) + .wire_offset(-1), ); } @@ -94,139 +92,178 @@ impl NavicoControls { ControlType::AntennaHeight, Control::new_numeric(ControlType::AntennaHeight, 0, 9900) .wire_scale_factor(99000) // we report cm but network has mm - .unit("cm") + .unit("cm"), ); controls.insert( ControlType::BearingAlignment, Control::new_numeric(ControlType::BearingAlignment, -180, 180) .unit("Deg") .wire_scale_factor(1800) - .wire_offset(-1) + .wire_offset(-1), ); controls.insert( ControlType::Gain, - Control::new_auto(ControlType::Gain, 0, 100, AutomaticValue { - has_auto: true, - auto_values: 100, - auto_descriptions: None, - has_auto_adjustable: true, - auto_adjust_min_value: -50, - auto_adjust_max_value: 50, - }).wire_scale_factor(255) + Control::new_auto( + ControlType::Gain, + 0, + 100, + AutomaticValue { + has_auto: true, + auto_values: 100, + auto_descriptions: None, + has_auto_adjustable: true, + auto_adjust_min_value: -50, + auto_adjust_max_value: 50, + }, + ) + .wire_scale_factor(255), ); controls.insert( ControlType::InterferenceRejection, - Control::new_list(ControlType::InterferenceRejection, &["Off", "Low", "Medium", "High"]) + Control::new_list( + ControlType::InterferenceRejection, + &["Off", "Low", "Medium", "High"], + ), + ); + controls.insert( + ControlType::Rain, + Control::new_numeric(ControlType::Rain, 0, 100), ); - controls.insert(ControlType::Rain, Control::new_numeric(ControlType::Rain, 0, 100)); - let max_value = - (match model { - Model::Unknown => 0, - Model::BR24 => 24, - Model::Gen3 => 36, - Model::Gen4 => 48, - Model::HALO => 96, - }) * 1852; + let max_value = (match model { + Model::Unknown => 0, + Model::BR24 => 24, + Model::Gen3 => 36, + Model::Gen4 => 48, + Model::HALO => 96, + }) * 1852; controls.insert( ControlType::Range, Control::new_numeric(ControlType::Range, 0, max_value) .unit("m") - .wire_scale_factor(10 * max_value) // Radar sends and receives in decimeters + .wire_scale_factor(10 * max_value), // Radar sends and receives in decimeters ); controls.insert( ControlType::ScanSpeed, - Control::new_list(ControlType::ScanSpeed, if model == Model::HALO { - &["Normal", "Medium", "", "Fast"] - } else { - &["Normal", "Fast"] - }) + Control::new_list( + ControlType::ScanSpeed, + if model == Model::HALO { + &["Normal", "Medium", "", "Fast"] + } else { + &["Normal", "Fast"] + }, + ), ); controls.insert( // TODO: Investigate mapping on 4G ControlType::SeaState, - Control::new_list(ControlType::SeaState, &["Calm", "Moderate", "Rough"]) + Control::new_list(ControlType::SeaState, &["Calm", "Moderate", "Rough"]), ); controls.insert( ControlType::Sea, - Control::new_auto(ControlType::Sea, 0, 100, AutomaticValue { - has_auto: true, - auto_values: 100, - auto_descriptions: None, - has_auto_adjustable: true, - auto_adjust_min_value: -50, - auto_adjust_max_value: 50, - }) + Control::new_auto( + ControlType::Sea, + 0, + 100, + AutomaticValue { + has_auto: true, + auto_values: 100, + auto_descriptions: None, + has_auto_adjustable: true, + auto_adjust_min_value: -50, + auto_adjust_max_value: 50, + }, + ), ); controls.insert( ControlType::SideLobeSuppression, - Control::new_auto(ControlType::SideLobeSuppression, 0, 100, AutomaticValue { - has_auto: true, - auto_values: 100, - auto_descriptions: None, - has_auto_adjustable: true, - auto_adjust_min_value: -50, - auto_adjust_max_value: 50, - }).wire_scale_factor(255) + Control::new_auto( + ControlType::SideLobeSuppression, + 0, + 100, + AutomaticValue { + has_auto: true, + auto_values: 100, + auto_descriptions: None, + has_auto_adjustable: true, + auto_adjust_min_value: -50, + auto_adjust_max_value: 50, + }, + ) + .wire_scale_factor(255), ); controls.insert( ControlType::TargetBoost, - Control::new_list(ControlType::TargetBoost, &["Off", "Low", "High"]) + Control::new_list(ControlType::TargetBoost, &["Off", "Low", "High"]), ); controls.insert( ControlType::TargetExpansion, - Control::new_list(ControlType::TargetExpansion, if model == Model::HALO { - &["Off", "Low", "Medium", "High"] - } else { - &["Off", "On"] - }) + Control::new_list( + ControlType::TargetExpansion, + if model == Model::HALO { + &["Off", "Low", "Medium", "High"] + } else { + &["Off", "On"] + }, + ), ); controls.insert( ControlType::NoiseRejection, - Control::new_list(ControlType::NoiseRejection, if model == Model::HALO { - &["Off", "Low", "Medium", "High"] - } else { - &["Off", "Low", "High"] - }) + Control::new_list( + ControlType::NoiseRejection, + if model == Model::HALO { + &["Off", "Low", "Medium", "High"] + } else { + &["Off", "Low", "High"] + }, + ), ); if model == Model::HALO || model == Model::Gen4 { controls.insert( ControlType::TargetSeparation, - Control::new_list(ControlType::TargetSeparation, &["Off", "Low", "Medium", "High"]) + Control::new_list( + ControlType::TargetSeparation, + &["Off", "Low", "Medium", "High"], + ), ); } if model == Model::HALO { controls.insert( ControlType::Doppler, - Control::new_list(ControlType::Doppler, &["Off", "Normal", "Approaching"]) + Control::new_list(ControlType::Doppler, &["Off", "Normal", "Approaching"]), ); controls.insert( ControlType::DopplerSpeedThreshold, Control::new_numeric(ControlType::DopplerSpeedThreshold, 0, 1594) .wire_scale_factor(1594 * 16) - .unit("cm/s") + .unit("cm/s"), ); } controls.insert( ControlType::OperatingHours, - Control::new_numeric(ControlType::OperatingHours, 0, i32::MAX).read_only().unit("h") + Control::new_numeric(ControlType::OperatingHours, 0, i32::MAX) + .read_only() + .unit("h"), ); - controls.insert(ControlType::SerialNumber, Control::new_string(ControlType::SerialNumber)); + controls.insert( + ControlType::SerialNumber, + Control::new_string(ControlType::SerialNumber), + ); controls.insert( ControlType::FirmwareVersion, - Control::new_string(ControlType::FirmwareVersion) + Control::new_string(ControlType::FirmwareVersion), ); controls.insert( ControlType::Status, Control::new_list( ControlType::Status, - &["Off", "Standby", "Transmit", "", "", "SpinningUp"] - ) + &["Off", "Standby", "Transmit", "", "", "SpinningUp"], + ), ); Controls::new(controls, protobuf_tx, control_tx, command_tx) diff --git a/src/radar.rs b/src/radar.rs index 561bd6a..fc95053 100644 --- a/src/radar.rs +++ b/src/radar.rs @@ -1,31 +1,36 @@ use enum_primitive_derive::Primitive; use log::info; -use serde::ser::{ SerializeMap, Serializer }; +use serde::ser::{SerializeMap, Serializer}; use serde::Serialize; use std::{ collections::HashMap, - fmt::{ self, Display, Write }, - net::{ Ipv4Addr, SocketAddrV4 }, - sync::{ Arc, RwLock }, + fmt::{self, Display, Write}, + net::{Ipv4Addr, SocketAddrV4}, + sync::{Arc, RwLock}, }; use thiserror::Error; use tokio_graceful_shutdown::SubsystemHandle; use crate::locator::LocatorId; -use crate::settings::{ ControlMessage, ControlValue, Controls }; +use crate::settings::{ControlMessage, ControlType, ControlValue, Controls}; use crate::Cli; #[derive(Error, Debug)] pub enum RadarError { - #[error("Socket operation failed")] Io(#[from] std::io::Error), - #[error("Interface '{0}' is not available")] InterfaceNotFound(String), - #[error("Interface '{0}' has no valid IPv4 address")] InterfaceNoV4(String), + #[error("Socket operation failed")] + Io(#[from] std::io::Error), + #[error("Interface '{0}' is not available")] + InterfaceNotFound(String), + #[error("Interface '{0}' has no valid IPv4 address")] + InterfaceNoV4(String), #[error("Cannot detect Ethernet devices")] EnumerationFailed, #[error("Timeout")] Timeout, #[error("Shutdown")] Shutdown, + #[error("Cannot set value for control '{0}'")] + CannotSetControlType(ControlType), } #[derive(Serialize, Clone, Debug)] @@ -47,12 +52,19 @@ struct Color { impl fmt::Display for Color { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, self.a) + write!( + f, + "#{:02x}{:02x}{:02x}{:02x}", + self.r, self.g, self.b, self.a + ) } } impl Serialize for Color { - fn serialize(&self, serializer: S) -> Result where S: Serializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { serializer.serialize_str(&self.to_string()) } } @@ -73,7 +85,10 @@ pub struct Legend { } impl Serialize for Legend { - fn serialize(&self, serializer: S) -> Result where S: Serializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { let mut state = serializer.serialize_map(Some(self.pixels.len()))?; for (n, value) in self.pixels.iter().enumerate() { let key = n.to_string(); @@ -90,18 +105,18 @@ pub struct RadarInfo { pub locator_id: LocatorId, pub brand: String, pub model: Option, - pub serial_no: Option, // Serial # for this radar - pub which: Option, // "A", "B" or None - pub pixel_values: u8, // How many values per pixel, 0..220 or so - pub spokes: u16, // How many spokes per rotation - pub max_spoke_len: u16, // Fixed for some radars, variable for others - pub addr: SocketAddrV4, // The assigned IP address of the radar - pub nic_addr: Ipv4Addr, // IPv4 address of NIC via which radar can be reached - pub spoke_data_addr: SocketAddrV4, // Where the radar will send data spokes - pub report_addr: SocketAddrV4, // Where the radar will send reports + pub serial_no: Option, // Serial # for this radar + pub which: Option, // "A", "B" or None + pub pixel_values: u8, // How many values per pixel, 0..220 or so + pub spokes: u16, // How many spokes per rotation + pub max_spoke_len: u16, // Fixed for some radars, variable for others + pub addr: SocketAddrV4, // The assigned IP address of the radar + pub nic_addr: Ipv4Addr, // IPv4 address of NIC via which radar can be reached + pub spoke_data_addr: SocketAddrV4, // Where the radar will send data spokes + pub report_addr: SocketAddrV4, // Where the radar will send reports pub send_command_addr: SocketAddrV4, // Where displays will send commands to the radar - pub legend: Legend, // What pixel values mean - pub controls: Option, // Which controls there are + pub legend: Legend, // What pixel values mean + pub controls: Option, // Which controls there are // Channels pub message_tx: tokio::sync::broadcast::Sender>, // Serialized RadarMessage @@ -123,7 +138,7 @@ impl RadarInfo { nic_addr: Ipv4Addr, spoke_data_addr: SocketAddrV4, report_addr: SocketAddrV4, - send_command_addr: SocketAddrV4 + send_command_addr: SocketAddrV4, ) -> Self { let (message_tx, _message_rx) = tokio::sync::broadcast::channel(32); let (control_tx, _control_rx) = tokio::sync::broadcast::channel(32); @@ -250,12 +265,10 @@ pub struct Radars { impl Radars { pub fn new(args: Cli) -> Arc> { - Arc::new( - RwLock::new(Radars { - info: HashMap::new(), - args, - }) - ) + Arc::new(RwLock::new(Radars { + info: HashMap::new(), + args, + })) } } impl Radars { diff --git a/src/settings.rs b/src/settings.rs index 6578575..1c8012a 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,9 +1,16 @@ use log::trace; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; use protobuf::Message; -use serde::{ Deserialize, Serialize }; -use std::{ collections::HashMap, fmt::{ self, Display } }; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_repr::*; +use std::{ + collections::HashMap, + fmt::{self, Display}, str::FromStr, +}; use thiserror::Error; + use crate::protos::RadarMessage::RadarMessage; /// @@ -27,11 +34,15 @@ pub enum ControlState { impl Display for ControlState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", match self { - ControlState::Off => "Off", - ControlState::Manual => "Manual", - ControlState::Auto => "Auto", - }) + write!( + f, + "{}", + match self { + ControlState::Off => "Off", + ControlState::Manual => "Manual", + ControlState::Auto => "Auto", + } + ) } } @@ -48,7 +59,7 @@ impl Controls { controls: HashMap, protobuf_tx: tokio::sync::broadcast::Sender>, control_tx: tokio::sync::broadcast::Sender, - command_tx: tokio::sync::broadcast::Sender + command_tx: tokio::sync::broadcast::Sender, ) -> Self { Controls { controls, @@ -59,9 +70,9 @@ impl Controls { } fn get_description(control: &Control) -> Option { - if let Some(descriptions) = &control.item.descriptions { - if control.value >= 0 && control.value < (descriptions.len() as i32) { - return Some(descriptions[control.value as usize].to_string()); + if let (Some(value), Some(descriptions)) = (control.value, &control.item.descriptions) { + if value >= 0 && value < (descriptions.len() as i32) { + return Some(descriptions[value as usize].to_string()); } } return None; @@ -76,24 +87,17 @@ impl Controls { fn broadcast_json(tx: &tokio::sync::broadcast::Sender, control: &Control) { let control_value = crate::settings::ControlValue { id: control.item.control_type, - name: control.item.control_type.to_string(), - value: if control.value == i32::MIN { - None - } else { - Some(control.value) - }, - string_value: control.string_value.clone(), + value: control.value, + description: control.value(), auto: control.auto, - description: Self::get_description(control), }; match tx.send(control_value) { Err(_e) => {} Ok(cnt) => { trace!( - "Sent control value {} value {} to {} JSON clients", + "Sent control value {} to {} JSON clients", control.item.control_type, - &control.value, cnt ); } @@ -101,32 +105,36 @@ impl Controls { } fn broadcast_protobuf(update_tx: &tokio::sync::broadcast::Sender>, control: &Control) { - let mut control_value = crate::protos::RadarMessage::radar_message::ControlValue::new(); - control_value.id = control.item.control_type.to_string(); - control_value.value = control.value; - control_value.auto = control.auto; - control_value.description = Self::get_description(control); - - let mut message = RadarMessage::new(); - message.controls.push(control_value); - - let mut bytes = Vec::new(); - message.write_to_vec(&mut bytes).expect("Cannot write RadarMessage to vec"); - match update_tx.send(bytes) { - Err(_e) => { - trace!( - "Stored control value {} value {}", - control.item.control_type, - &control.value - ); - } - Ok(cnt) => { - trace!( - "Stored control value {} value {} and sent to {} clients", - control.item.control_type, - &control.value, - cnt - ); + if let Some(value) = control.value { + let mut control_value = crate::protos::RadarMessage::radar_message::ControlValue::new(); + control_value.id = control.item.control_type.to_string(); + control_value.value = value; + control_value.auto = control.auto; + control_value.description = Self::get_description(control); + + let mut message = RadarMessage::new(); + message.controls.push(control_value); + + let mut bytes = Vec::new(); + message + .write_to_vec(&mut bytes) + .expect("Cannot write RadarMessage to vec"); + match update_tx.send(bytes) { + Err(_e) => { + trace!( + "Stored control value {} value {}", + control.item.control_type, + &value + ); + } + Ok(cnt) => { + trace!( + "Stored control value {} value {} and sent to {} clients", + control.item.control_type, + &value, + cnt + ); + } } } } @@ -136,7 +144,7 @@ impl Controls { control_type: &ControlType, value: i32, auto: Option, - state: ControlState + state: ControlState, ) -> Result, ControlError> { if let Some(control) = self.controls.get_mut(control_type) { if control.set_all(value, auto, state)?.is_some() { @@ -155,10 +163,13 @@ impl Controls { pub fn set( &mut self, control_type: &ControlType, - value: i32 + value: i32, ) -> Result, ControlError> { if let Some(control) = self.controls.get_mut(control_type) { - if control.set_all(value, None, ControlState::Manual)?.is_some() { + if control + .set_all(value, None, ControlState::Manual)? + .is_some() + { Self::broadcast_protobuf(&self.protobuf_tx, control); Self::broadcast_json(&self.control_tx, control); return Ok(Some(())); @@ -172,10 +183,14 @@ impl Controls { &mut self, control_type: &ControlType, auto: bool, - value: i32 + value: i32, ) -> Result, ControlError> { if let Some(control) = self.controls.get_mut(control_type) { - let state = if auto { ControlState::Auto } else { ControlState::Manual }; + let state = if auto { + ControlState::Auto + } else { + ControlState::Manual + }; if control.set_all(value, Some(auto), state)?.is_some() { Self::broadcast_protobuf(&self.protobuf_tx, control); @@ -190,13 +205,13 @@ impl Controls { pub fn set_string( &mut self, control_type: &ControlType, - value: String + value: String, ) -> Result, ControlError> { if let Some(control) = self.controls.get_mut(control_type) { if control.set_string(value).is_some() { Self::broadcast_protobuf(&self.protobuf_tx, control); Self::broadcast_json(&self.control_tx, control); - return Ok(control.string_value.clone()); + return Ok(control.description.clone()); } Ok(None) } else { @@ -211,19 +226,18 @@ pub enum ControlMessage { NewClient, } +// This is what we send back and forth to clients #[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct ControlValue { + #[serde( deserialize_with = "deserialize_enum_from_string")] pub id: ControlType, - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_option_number_from_string")] pub value: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub string_value: Option, + #[serde(default)] + pub description: String, #[serde(skip_serializing_if = "Option::is_none")] pub auto: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, } #[derive(Clone, Debug, Serialize)] @@ -232,9 +246,9 @@ pub struct Control { #[serde(flatten)] item: ControlDefinition, #[serde(skip)] - value: i32, + value: Option, #[serde(skip)] - string_value: Option, + description: Option, #[serde(skip)] auto: Option, #[serde(skip)] @@ -249,7 +263,7 @@ impl Control { value, auto: None, state: ControlState::Off, - string_value: None, + description: None, } } @@ -266,13 +280,13 @@ impl Control { } pub fn wire_scale_factor(mut self, wire_scale_factor: i32) -> Self { - self.item.wire_scale_factor = wire_scale_factor; + self.item.wire_scale_factor = Some(wire_scale_factor); self } pub fn wire_offset(mut self, wire_offset: i32) -> Self { - self.item.wire_offset = wire_offset; + self.item.wire_offset = Some(wire_offset); self } @@ -284,16 +298,19 @@ impl Control { } pub fn new_numeric(control_type: ControlType, min_value: i32, max_value: i32) -> Self { + let min_value = Some(min_value); + let max_value = Some(max_value); let control = Self::new(ControlDefinition { control_type, + name: control_type.to_string(), automatic: None, has_off: false, default_value: min_value, min_value, max_value, - step_value: 1, + step_value: Some(1), wire_scale_factor: max_value, - wire_offset: 0, + wire_offset: Some(0), unit: None, descriptions: None, is_read_only: false, @@ -306,18 +323,21 @@ impl Control { control_type: ControlType, min_value: i32, max_value: i32, - automatic: AutomaticValue + automatic: AutomaticValue, ) -> Self { + let min_value = Some(min_value); + let max_value = Some(max_value); Self::new(ControlDefinition { control_type, + name: control_type.to_string(), automatic: Some(automatic), has_off: false, default_value: min_value, min_value, max_value, - step_value: 1, + step_value: Some(1), wire_scale_factor: max_value, - wire_offset: 0, + wire_offset: Some(0), unit: None, descriptions: None, is_read_only: false, @@ -328,21 +348,17 @@ impl Control { pub fn new_list(control_type: ControlType, descriptions: &[&str]) -> Self { Self::new(ControlDefinition { control_type, + name: control_type.to_string(), automatic: None, has_off: false, - default_value: 0, - min_value: 0, - max_value: (descriptions.len() as i32) - 1, - step_value: 1, - wire_scale_factor: (descriptions.len() as i32) - 1, - wire_offset: 0, + default_value: Some(0), + min_value: Some(0), + max_value: Some((descriptions.len() as i32) - 1), + step_value: Some(1), + wire_scale_factor: Some((descriptions.len() as i32) - 1), + wire_offset: Some(0), unit: None, - descriptions: Some( - descriptions - .into_iter() - .map(|n| n.to_string()) - .collect() - ), + descriptions: Some(descriptions.into_iter().map(|n| n.to_string()).collect()), is_read_only: false, is_string_value: false, }) @@ -351,14 +367,15 @@ impl Control { pub fn new_string(control_type: ControlType) -> Self { let control = Self::new(ControlDefinition { control_type, + name: control_type.to_string(), automatic: None, has_off: false, - default_value: NOT_USED, - min_value: NOT_USED, - max_value: NOT_USED, - step_value: NOT_USED, - wire_scale_factor: 0, - wire_offset: 0, + default_value: None, + min_value: None, + max_value: None, + step_value: None, + wire_scale_factor: None, + wire_offset: None, unit: None, descriptions: None, is_read_only: true, @@ -372,53 +389,66 @@ impl Control { &self.item } - // pub fn value(&self) -> i32 { - // self.value - // } - // pub fn auto(&self) -> Option { // self.auto // } - pub fn value_string(&self) -> String { - if let Some(descriptions) = &self.item.descriptions { - if let Some(v) = descriptions.get(self.value as usize) { + pub fn value(&self) -> String { + if self.description.is_some() { + return self.description.clone().unwrap(); + } + if let (Some(value), Some(descriptions)) = (self.value, &self.item.descriptions) { + if let Some(v) = descriptions.get(value as usize) { return v.to_string(); } } - return format!("{}", self.value); + + return format!( + "{}", + self.value.unwrap_or(self.item.default_value.unwrap_or(0)) + ); } pub fn set_all( &mut self, - value: i32, + mut value: i32, auto: Option, - state: ControlState + state: ControlState, ) -> Result, ControlError> { - let mut value = if self.item.wire_scale_factor != self.item.max_value { - (((value as i64) * (self.item.max_value as i64)) / - (self.item.wire_scale_factor as i64)) as i32 - } else { - value - }; - if - self.item.wire_offset == -1 && - value > self.item.max_value && - value <= 2 * self.item.max_value + if let (Some(wire_scale_factor), Some(max_value)) = + (self.item.wire_scale_factor, self.item.max_value) { - // debug!("{} value {} -> ", self.item.control_type, value); - value -= 2 * self.item.max_value; - // debug!("{} ..... {}", self.item.control_type, value); + if wire_scale_factor != max_value { + value = (((value as i64) * (max_value as i64)) / (wire_scale_factor as i64)) as i32; + } + } + if let (Some(min_value), Some(max_value)) = (self.item.min_value, self.item.max_value) { + if self.item.wire_offset.is_none() && value > max_value && value <= 2 * max_value { + // debug!("{} value {} -> ", self.item.control_type, value); + value -= 2 * max_value; + // debug!("{} ..... {}", self.item.control_type, value); + } + + if value < min_value { + return Err(ControlError::TooLow( + self.item.control_type, + value, + min_value, + )); + } + if value > max_value { + return Err(ControlError::TooHigh( + self.item.control_type, + value, + max_value, + )); + } } - if value < self.item.min_value { - Err(ControlError::TooLow(self.item.control_type, value, self.item.min_value)) - } else if value > self.item.max_value { - Err(ControlError::TooHigh(self.item.control_type, value, self.item.max_value)) - } else if auto.is_some() && self.item.automatic.is_none() { + if auto.is_some() && self.item.automatic.is_none() { Err(ControlError::NoAuto(self.item.control_type)) - } else if self.value != value || self.auto != auto { - self.value = value; + } else if self.value != Some(value) || self.auto != auto { + self.value = Some(value); self.auto = auto; self.state = state; @@ -429,8 +459,8 @@ impl Control { } pub fn set_string(&mut self, value: String) -> Option<()> { - if self.string_value.is_none() || self.string_value.as_ref().unwrap() != &value { - self.string_value = Some(value); + if self.description.is_none() || self.description.as_ref().unwrap() != &value { + self.description = Some(value); self.state = ControlState::Manual; Some(()) } else { @@ -460,6 +490,7 @@ pub const NOT_USED: i32 = i32::MIN; pub struct ControlDefinition { #[serde(skip)] control_type: ControlType, + name: String, #[serde(skip)] has_off: bool, #[serde(flatten, skip_serializing_if = "Option::is_none")] @@ -467,17 +498,17 @@ pub struct ControlDefinition { #[serde(skip_serializing_if = "is_false")] is_string_value: bool, #[serde(skip)] - default_value: i32, - #[serde(skip_serializing_if = "is_not_used")] - min_value: i32, - #[serde(skip_serializing_if = "is_not_used")] - max_value: i32, - #[serde(skip_serializing_if = "is_not_used")] - step_value: i32, + default_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + min_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + step_value: Option, #[serde(skip)] - wire_scale_factor: i32, + wire_scale_factor: Option, #[serde(skip)] - wire_offset: i32, + wire_offset: Option, #[serde(skip_serializing_if = "Option::is_none")] pub unit: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -500,7 +531,8 @@ fn is_one(v: &i32) -> bool { impl ControlDefinition {} -#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug, Serialize, Deserialize)] +#[derive(Eq, PartialEq, Hash, Copy, Clone, Debug, Serialize_repr, Deserialize_repr, FromPrimitive)] +#[repr(u8)] pub enum ControlType { Status, Range, @@ -515,7 +547,6 @@ pub enum ControlType { Doppler, // DopplerAutoTrack, DopplerSpeedThreshold, - SerialNumber, SideLobeSuppression, // Stc, // StcCurve, @@ -528,15 +559,11 @@ pub enum ControlType { // TrailsMotion, // TuneCoarse, // TuneFine, - AccentLight, - // AntennaForward, - AntennaHeight, - // AntennaStarboard, - BearingAlignment, // ColorGain, // DisplayTiming, // Ftc, InterferenceRejection, + NoiseRejection, // LocalInterferenceRejection, // MainBangSize, // MainBangSuppression, @@ -548,10 +575,15 @@ pub enum ControlType { NoTransmitStart2, NoTransmitStart3, NoTransmitStart4, - NoiseRejection, + AccentLight, + // AntennaForward, + AntennaHeight, + // AntennaStarboard, + BearingAlignment, OperatingHours, - FirmwareVersion, ModelName, + FirmwareVersion, + SerialNumber, // Orientation, } @@ -617,12 +649,98 @@ impl Display for ControlType { #[derive(Error, Debug)] pub enum ControlError { - #[error("Control {0} not supported on this radar")] NotSupported(ControlType), - #[error("Control {0} value {1} is lower than minimum value {2}")] TooLow(ControlType, i32, i32), - #[error("Control {0} value {1} is higher than maximum value {2}")] TooHigh( - ControlType, - i32, - i32, - ), - #[error("Control {0} does not support Auto")] NoAuto(ControlType), + #[error("Control {0} not supported on this radar")] + NotSupported(ControlType), + #[error("Control {0} value {1} is lower than minimum value {2}")] + TooLow(ControlType, i32, i32), + #[error("Control {0} value {1} is higher than maximum value {2}")] + TooHigh(ControlType, i32, i32), + #[error("Control {0} does not support Auto")] + NoAuto(ControlType), +} + +pub fn deserialize_option_number_from_string<'de, T, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, + T: FromStr + serde::Deserialize<'de>, + ::Err: Display, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum NumericOrNull<'a, T> { + Str(&'a str), + String(String), + FromStr(T), + Null, + } + + match NumericOrNull::::deserialize(deserializer)? { + NumericOrNull::Str(s) => match s { + "" => Ok(None), + _ => T::from_str(s).map(Some).map_err(serde::de::Error::custom), + }, + NumericOrNull::String(s) => match s.as_str() { + "" => Ok(None), + _ => T::from_str(&s).map(Some).map_err(serde::de::Error::custom), + }, + NumericOrNull::FromStr(i) => Ok(Some(i)), + NumericOrNull::Null => Ok(None), + } } + +pub fn deserialize_number_from_string<'de, T, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: FromStr + serde::Deserialize<'de>, + ::Err: Display, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrInt { + String(String), + Number(T), + } + + match StringOrInt::::deserialize(deserializer)? { + StringOrInt::String(s) => s.parse::().map_err(serde::de::Error::custom), + StringOrInt::Number(i) => Ok(i), + } +} + +pub fn deserialize_enum_from_string<'de, T, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: FromPrimitive + serde::Deserialize<'de>, +{ + let n = deserialize_number_from_string::(deserializer)?; + + match T::from_usize(n) { + Some(ct) => Ok(ct), + None => Err(serde::de::Error::custom("Invalid valid for enum")) + } +} + +#[cfg(test)] +mod test +{ + use super::*; + + #[test] + fn serialize_control_value() { + let json = r#"{"id":"2","value":"49"}"#; + + match serde_json::from_str::(&json) { + Ok(cv) => { + assert_eq!(cv.id, ControlType::Gain); + assert_eq!(cv.value, Some(49)); + } + Err(e) => { + panic!("Error {e}"); + } + } + + + } +} \ No newline at end of file diff --git a/src/web.rs b/src/web.rs index dc45a9d..b601870 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,26 +1,37 @@ use anyhow::anyhow; use axum::{ - debug_handler, extract::{ ws::{Message, WebSocket}, ConnectInfo, Host, Path, State, WebSocketUpgrade }, http::{ StatusCode, Uri }, response::{ IntoResponse, Response }, routing::get, Json, Router + debug_handler, + extract::{ + ws::{Message, WebSocket}, + ConnectInfo, Host, Path, State, WebSocketUpgrade, + }, + http::{StatusCode, Uri}, + response::{IntoResponse, Response}, + routing::get, + Json, Router, }; +use axum_embed::ServeEmbed; use log::{debug, trace}; +use miette::miette; use miette::{bail, Result}; +use rust_embed::RustEmbed; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, io, - net::{ IpAddr, Ipv4Addr, SocketAddr }, + net::{IpAddr, Ipv4Addr, SocketAddr}, str::FromStr, - sync::{ Arc, RwLock }, + sync::{Arc, RwLock}, }; use thiserror::Error; use tokio::net::TcpListener; use tokio_graceful_shutdown::SubsystemHandle; -use rust_embed::RustEmbed; -use axum_embed::ServeEmbed; -use miette::miette; -use crate::{radar::{ Legend, RadarInfo, Radars }, settings::{Control, ControlMessage, ControlType, ControlValue, Controls}}; use crate::VERSION; +use crate::{ + radar::{Legend, RadarInfo, Radars}, + settings::{Control, ControlMessage, ControlType, ControlValue, Controls}, +}; const RADAR_URI: &str = "/v1/api/radars"; const SPOKES_URI: &str = "/v1/api/spokes/"; @@ -32,7 +43,8 @@ struct Assets; #[derive(Error, Debug)] pub enum WebError { - #[error("Socket operation failed")] Io(#[from] io::Error), + #[error("Socket operation failed")] + Io(#[from] io::Error), } #[derive(Clone, Debug)] @@ -54,9 +66,12 @@ impl Web { } pub async fn run(self, subsys: SubsystemHandle) -> Result<(), WebError> { - let listener = TcpListener::bind( - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), self.port) - ).await.unwrap(); + let listener = TcpListener::bind(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + self.port, + )) + .await + .unwrap(); let serve_assets = ServeEmbed::::new(); let mut shutdown_rx = self.shutdown_tx.subscribe(); @@ -87,7 +102,6 @@ impl Web { } } - #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct RadarApi { @@ -132,7 +146,7 @@ impl RadarApi { async fn get_radars( State(state): State, ConnectInfo(addr): ConnectInfo, - Host(host): Host + Host(host): Host, ) -> Response { debug!("Radar state request from {} for host '{}'", addr, host); @@ -152,8 +166,8 @@ async fn get_radars( let x = &radars.info; let mut api: HashMap = HashMap::new(); for (_key, value) in x.iter() { - if let Some(controls) = &value.controls { - let legend = &value.legend ; + if let Some(controls) = &value.controls { + let legend = &value.legend; let id = format!("radar-{}", value.id); let stream_url = format!("ws://{}{}{}", host, SPOKES_URI, id); let control_url = format!("ws://{}{}{}", host, CONTROL_URI, id); @@ -171,7 +185,8 @@ async fn get_radars( &name, value.spokes, value.max_spoke_len, - stream_url, control_url, + stream_url, + control_url, legend.clone(), controls.controls.clone(), ); @@ -194,28 +209,28 @@ impl IntoResponse for AppError { ( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", self.0), - ).into_response() + ) + .into_response() } } // This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into // `Result<_, AppError>`. That way you don't need to do that manually. -impl From for AppError where E: Into { +impl From for AppError +where + E: Into, +{ fn from(err: E) -> Self { Self(err.into()) } } - #[derive(Deserialize)] struct WebSocketHandlerParameters { key: String, } -fn match_radar_id( - state: &Web, - key: &str, -) -> Result { +fn match_radar_id(state: &Web, key: &str) -> Result { match state.radars.read() { Ok(radars) => { let x = &radars.info; @@ -289,7 +304,6 @@ async fn spokes_stream( } } - #[debug_handler] async fn control_handler( State(state): State, @@ -302,7 +316,7 @@ async fn control_handler( match match_radar_id(&state, ¶ms.key) { Ok(radar) => { let shutdown_rx = state.shutdown_tx.subscribe(); - + // finalize the upgrade process by returning upgrade callback. // we can customize the callback by sending additional info such as address. ws.on_upgrade(move |socket| control_stream(socket, radar, shutdown_rx)) @@ -332,66 +346,66 @@ async fn control_stream( loop { debug!("Loop /control websocket"); tokio::select! { - _ = shutdown_rx.recv() => { - debug!("Shutdown of /control websocket"); - break; - }, - r = control_rx.recv() => { - match r { - Ok(message) => { - let message: String = serde_json::to_string(&message).unwrap(); - trace!("Sending {:?}", message); - let ws_message = Message::Text(message); - - if let Err(e) = socket.send(ws_message).await { - log::error!("send to websocket client: {e}"); - break; - } - - - }, - Err(e) => { - log::error!("Error on Control channel: {e}"); + _ = shutdown_rx.recv() => { + debug!("Shutdown of /control websocket"); + break; + }, + r = control_rx.recv() => { + match r { + Ok(message) => { + let message: String = serde_json::to_string(&message).unwrap(); + trace!("Sending {:?}", message); + let ws_message = Message::Text(message); + + if let Err(e) = socket.send(ws_message).await { + log::error!("send to websocket client: {e}"); break; } + + + }, + Err(e) => { + log::error!("Error on Control channel: {e}"); + break; } - }, - r = socket.recv() => { - match r { - Some(Ok(message)) => { - match message { - Message::Text(message) => { - if let Ok(control_value) = serde_json::from_str(&message) { - trace!("Received ControlValue {:?}", control_value); - - let control_message = ControlMessage::Value(control_value); - - if let Err(e) = command_tx.send(control_message) { - log::error!("send to control channel: {e}"); - break; - } - } else { - log::error!("Unknown JSON string '{}", message); + } + }, + r = socket.recv() => { + match r { + Some(Ok(message)) => { + match message { + Message::Text(message) => { + if let Ok(control_value) = serde_json::from_str(&message) { + trace!("Received ControlValue {:?}", control_value); + + let control_message = ControlMessage::Value(control_value); + + if let Err(e) = command_tx.send(control_message) { + log::error!("send to control channel: {e}"); + break; } - - }, - _ => { - debug!("Dropping unexpected message {:?}", message); + } else { + log::error!("Unknown JSON string '{}", message); } + + }, + _ => { + debug!("Dropping unexpected message {:?}", message); } - - }, - None => { - // Stream has closed - log::debug!("Control websocket closed"); - break; - } - r => { - log::error!("Error reading websocket: {:?}", r); - break; } + + }, + None => { + // Stream has closed + log::debug!("Control websocket closed"); + break; + } + r => { + log::error!("Error reading websocket: {:?}", r); + break; } } } + } } -} \ No newline at end of file +} diff --git a/web/control.js b/web/control.js index ba72617..2d1b37b 100644 --- a/web/control.js +++ b/web/control.js @@ -6,17 +6,24 @@ var radar; var controls; var webSocket; -const StringValue = (id) => +const StringValue = (id, name) => div({class: 'control'}, - label({ for: id }, id), + label({ for: id }, name), input({ type: 'text', id: id, size: 20, readonly: true }) ) -const NumericValue = (id) => +const NumericValue = (id, name) => div({class: 'control'}, - label({ for: id }, id), - input({ type: 'number', id: id, readonly: false }) - ) + label({ for: id }, name), + input({ type: 'number', id: id, onchange: e => change(e) }) + ) + +const RangeValue = (id, name, min, max, def, descriptions) => + div({ class: 'control' }, + label({ for: id }, name), + (descriptions) ? div({ class: 'description' }) : null, + input({ type: 'range', id, min, max, value: def, onchange: e => change(e)}) + ) window.onload = function () { const urlParams = new URLSearchParams(window.location.search); @@ -42,6 +49,7 @@ function radarsLoaded(id, d) { return; } controls = radar.controls; + buildControls(); webSocket = new WebSocket(radar.controlUrl); @@ -56,14 +64,10 @@ function radarsLoaded(id, d) { console.log("websocket message: " + e.data); let v = JSON.parse(e.data); let d = document.getElementById(v.id); - if (d) { - if ('stringValue' in v) { - d.setAttribute('value', v.stringValue); - } else if ('description' in v) { - d.setAttribute('value', v.stringValue); - } else if ('value' in v) { - d.setAttribute('value', v.stringValue); - } + d.setAttribute('value', ('description' in v && d.type == 'text') ? v.description : v.value); + if ('descriptions' in controls[v.id] && d.type == 'range') { + let desc = d.parentNode.querySelector('.description'); + if (desc) desc.innerHTML = v.description; } } @@ -75,11 +79,18 @@ function buildControls() { van.add(c, div(radar.name + " Controls")); for (const [k, v] of Object.entries(controls)) { - if ('isStringValue' in v) { - van.add(c, StringValue(k)); - } else { - van.add(c, NumericValue(k)); - } + van.add(c, ('isStringValue' in v) + ? StringValue(k, v.name) + : ('maxValue' in v && v.maxValue <= 100) + ? RangeValue(k, v.name, v.minValue, v.maxValue, 0, 'descriptions' in v) + : NumericValue(k, v.name)); } console.log(controls); } + +function change(e) { + console.log("change " + e + " " + e.target.id + "=" + e.target.value); + let cv = JSON.stringify({ id: e.target.id, value: e.target.value }); + webSocket.send(cv); + console.log(controls[e.target.id].name + "-> " + cv); +} \ No newline at end of file diff --git a/web/style.css b/web/style.css index 35d21e9..a9ca7be 100644 --- a/web/style.css +++ b/web/style.css @@ -11,14 +11,21 @@ html { div.control { display: block; + border-style: solid; border: 1px; border-radius: 3px; border-color: lightskyblue; } +div.description { + display: block; + text-align: right; +} + label { display: block; } + input { display: block; background-color: darkblue;