diff --git a/.DS_Store b/.DS_Store index 254e579..b570e07 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Cargo.lock b/Cargo.lock index b5f20f6..dbfc9ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -876,6 +876,29 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pi_nku_menu" +version = "0.1.1" +dependencies = [ + "config_lib", + "env_logger", + "log", + "menu_tui", + "rppal", + "thiserror", +] + +[[package]] +name = "pi_nku_sync" +version = "0.1.1" +dependencies = [ + "clap", + "config_lib", + "env_logger", + "log", + "protocol_lib", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1035,6 +1058,15 @@ dependencies = [ "protocol_lib", ] +[[package]] +name = "rppal" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ce3b019009cff02cb6b0e96e7cc2e5c5b90187dc1a490f8ef1521d0596b026" +dependencies = [ + "libc", +] + [[package]] name = "rustix" version = "0.38.44" diff --git a/Cargo.toml b/Cargo.toml index a226c4a..ff8ed87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,14 @@ members = [ "menu_tui", "protocol_lib", "rk_nku_sync", - "rk_nku_menu", "logger", + "pi_nku_sync", + "pi_nku_menu", + "rk_nku_menu", + "logger", ] [workspace.package] -authors = ["MU LLC "] +authors = ["Dmitry Akimov "] [workspace.dependencies] inquire = "0.7.5" diff --git a/Cross.toml b/Cross.toml index 9b152cb..74c499f 100644 --- a/Cross.toml +++ b/Cross.toml @@ -5,8 +5,4 @@ default-target = "aarch64-unknown-linux-gnu" [build.zig] enable = true # Сборка с участием линковщика Zig version = "2.31" # Версия glibc, установленная на RK3399-ROC-PC-PRO - -# [target.aarch64-unknown-linux-gnu] -# pre-build = [ -# "apt-get update && apt-get install --assume-yes libxkbcommon" -# ] \ No newline at end of file +# version = "2.28" # Версия glibc, установленная на RPi Zero W diff --git a/pi_nku_configs/menu_style.toml b/pi_nku_configs/menu_style.toml new file mode 100644 index 0000000..89086e9 --- /dev/null +++ b/pi_nku_configs/menu_style.toml @@ -0,0 +1,25 @@ +# First come some various options +shadow = false # Don't draw shadows around stacked views +borders = "simple" # Alternatives are "none" and "outset" + +# Here we define the color palette. +[colors] + background = "#000000" + # If the value is an array, the first valid color will be used. + # If the terminal doesn't support custom color, + # non-base colors will be skipped. + shadow = ["#000000", "black"] + view = "#d3d7cf" + + # Array and simple values have the same effect. + primary = ["#111111"] + secondary = "#EEEEEE" + tertiary = "#444444" + + # Hex values can use lower or uppercase. + title_primary = "#0000FF" + title_secondary = "#ffff55" + + # Lower precision values can use only 3 digits. + highlight = "#00F" + highlight_inactive = "#5555FF" \ No newline at end of file diff --git a/pi_nku_configs/nku_scheme.toml b/pi_nku_configs/nku_scheme.toml new file mode 100644 index 0000000..e014b40 --- /dev/null +++ b/pi_nku_configs/nku_scheme.toml @@ -0,0 +1,81 @@ +# [table name] должно соответствовать названию соответствующего параметра в ПО для интерфейсной платы +# name - текстовое название параметра (используется в TUI меню) +# possible_values - список возможных значений параметра +# default - значение по умолчанию (одно из possible_values) +# current - актуальное текущее значение параметра (одно из possible_values). Если не указано или некорректно, то используется значение по умолчанию +# should_sync - true, если параметр необходимо синхронизировать (pull & push) с внешним микроконтроллером + +[groupnumber] +name = "Номер в группе" +possible_values = [ +"0", +"1", +"2", +"3", +"4", +"5", +"6", +"7", +"8", +"9", +"10", +"11", +"12", +"13", +"14", +"15", +] +default = "1" +current = "1" +should_sync = "true" + +[soundvolume] +name = "Громкость звука" +possible_values = [ +"0%", +"25%", +"50%", +"75%", +"100%" +] +default = "50%" +current = "50%" +should_sync = "true" + +[musicvolume] +name = "Громкость музыки" +possible_values = [ +"0%", +"25%", +"50%", +"75%", +"100%" +] +default = "0%" +current = "25%" +should_sync = "true" + +[loadcapacity] +name = "Грузоподъемность" +possible_values = [ +"СКРЫТО", +"240кг 3чел.", +"320кг 4чел.", +"400кг 5чел.", +"450кг 6чел.", +"525кг 7чел.", +"630кг 8чел.", +"800кг 10чел.", +"800кг 11чел.", +"1000кг 13чел.", +"1150кг 15чел.", +"1275кг 16чел.", +"1275кг 17чел.", +"1425кг 18чел.", +"1600кг 20чел.", +"1600кг 21чел.", +] +default = "СКРЫТО" +current = "400кг 5чел." +should_sync = "true" + diff --git a/pi_nku_configs/pi_scheme.toml b/pi_nku_configs/pi_scheme.toml new file mode 100644 index 0000000..f54b1e4 --- /dev/null +++ b/pi_nku_configs/pi_scheme.toml @@ -0,0 +1,17 @@ +# [table name] должно соответствовать названию соответствующего параметра в ПО для интерфейсной платы +# name - текстовое название параметра (используется в TUI меню) +# possible_values - список возможных значений параметра +# default - значение по умолчанию (одно из possible_values) +# current - актуальное текущее значение параметра (одно из possible_values). Если не указано или некорректно, то используется значение по умолчанию + +[device] +name = "Имя порта" +possible_values = ["/dev/serial0"] +default = "/dev/serial0" +current = "/dev/serial0" + +[baudrate] +name = "Скорость передачи данных" +possible_values = ["9600", "38400", "115200"] +default = "115200" +current = "115200" diff --git a/pi_nku_menu/Cargo.toml b/pi_nku_menu/Cargo.toml new file mode 100644 index 0000000..2c30dca --- /dev/null +++ b/pi_nku_menu/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pi_nku_menu" +version = "0.1.1" +edition = "2024" +authors.workspace = true + +[dependencies] +thiserror = "2.0" +rppal = "0.22.1" +log = { workspace = true } +env_logger = { workspace = true } +config_lib = { path = "../config_lib" } +menu_tui = { path = "../menu_tui" } diff --git a/pi_nku_menu/assets/style.toml b/pi_nku_menu/assets/style.toml new file mode 100644 index 0000000..89086e9 --- /dev/null +++ b/pi_nku_menu/assets/style.toml @@ -0,0 +1,25 @@ +# First come some various options +shadow = false # Don't draw shadows around stacked views +borders = "simple" # Alternatives are "none" and "outset" + +# Here we define the color palette. +[colors] + background = "#000000" + # If the value is an array, the first valid color will be used. + # If the terminal doesn't support custom color, + # non-base colors will be skipped. + shadow = ["#000000", "black"] + view = "#d3d7cf" + + # Array and simple values have the same effect. + primary = ["#111111"] + secondary = "#EEEEEE" + tertiary = "#444444" + + # Hex values can use lower or uppercase. + title_primary = "#0000FF" + title_secondary = "#ffff55" + + # Lower precision values can use only 3 digits. + highlight = "#00F" + highlight_inactive = "#5555FF" \ No newline at end of file diff --git a/pi_nku_menu/src/main.rs b/pi_nku_menu/src/main.rs new file mode 100644 index 0000000..c20cc1b --- /dev/null +++ b/pi_nku_menu/src/main.rs @@ -0,0 +1,94 @@ +use std::time::{Duration, Instant}; + +use log::debug; +use menu_tui::menu_navigation::NavigationManager; +use menu_tui::menu_process::DeviceMenu; +use rppal::gpio::{Gpio, InputPin, Trigger}; + +/// ### Путь к файлу-схемы параметров устройства +const NKU_DEVICE_CONFIG_PATH: &str = "pi_nku_configs/nku_scheme.toml"; +const TUI_APP_CONFIG_PATH: &str = "pi_nku_configs/menu_style.toml"; + +/// Максимальное время бездействия [c], после достижения которого происходит выход из меню +const IDLE_TIMEOUT_SEC: u64 = 30; + +fn main() -> Result<(), Box> { + env_logger::init(); + + // Создаём меню + let mut menu = DeviceMenu::new(NKU_DEVICE_CONFIG_PATH, TUI_APP_CONFIG_PATH); + + let nav_manager = menu.get_navigation_manager(); + + menu.show_main_menu(); + + // Навигация по меню с помощью кнопок, подключенных к GPIO + let _gpio_buttons = gpio_navigation_handlers(nav_manager)?; + + menu.launch_idling_watchdog(IDLE_TIMEOUT_SEC); + + menu.run(); + + menu.quit(); + + let device_config = menu.get_schema_config()?; + + debug!("Итоговая конфигурация:"); + for parameter in device_config.get_parameters_names()? { + let value = device_config.get_parameter_value(¶meter).unwrap(); + debug!("{}: {}", parameter, value); + } + + device_config.save_parameters_values()?; + + return Ok(()); +} + +/// BCM номер порта кнопки ввода +const IN_BTN: u8 = 7; +/// BCM номер порта кнопки выбора +const SEL_BTN: u8 = 8; + +fn gpio_navigation_handlers( + nav_manager: NavigationManager, +) -> Result<(InputPin, InputPin), rppal::gpio::Error> { + let gpio = Gpio::new()?; + + let mut button_in = gpio.get(IN_BTN)?.into_input_pullup(); + let mut button_sel = gpio.get(SEL_BTN)?.into_input_pullup(); + + button_in.set_reset_on_drop(false); + button_sel.set_reset_on_drop(false); + + let nav_down = nav_manager.clone(); + let mut last_down = Instant::now() - Duration::from_millis(350); + button_in.set_async_interrupt( + Trigger::FallingEdge, + Some(Duration::from_millis(50)), + move |_event| { + let now = Instant::now(); + if now.duration_since(last_down) >= Duration::from_millis(350) { + last_down = now; + log::debug!("IN_BTN -> navigate_down"); + nav_down.navigate_down(); + } + }, + )?; + + let nav_sel = nav_manager.clone(); + let mut last_sel = Instant::now() - Duration::from_millis(350); + button_sel.set_async_interrupt( + Trigger::FallingEdge, + Some(Duration::from_millis(50)), + move |_event| { + let now = Instant::now(); + if now.duration_since(last_sel) >= Duration::from_millis(350) { + last_sel = now; + log::debug!("SEL_BTN -> select_item"); + nav_sel.select_item(); + } + }, + )?; + + Ok((button_in, button_sel)) +} diff --git a/pi_nku_sync/Cargo.toml b/pi_nku_sync/Cargo.toml new file mode 100644 index 0000000..2b1e950 --- /dev/null +++ b/pi_nku_sync/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pi_nku_sync" +version = "0.1.1" +edition = "2024" +authors.workspace = true + +[dependencies] +protocol_lib = { path = "../protocol_lib" } +config_lib = { path = "../config_lib" } +clap = { workspace = true } +log = { workspace = true } +env_logger = { workspace = true } + diff --git a/pi_nku_sync/src/main.rs b/pi_nku_sync/src/main.rs new file mode 100644 index 0000000..1d9d73c --- /dev/null +++ b/pi_nku_sync/src/main.rs @@ -0,0 +1,130 @@ +mod nku_client; +mod serial_config; + +use std::str::FromStr; + +use clap::Parser; +use log::{error, info, warn}; + +/// Количество попыток выполнить запрос +const REQUEST_ATTEMPTS: u8 = 5; + +#[derive(Parser)] +#[command(author = "Akimov Dmitry MU LLC", name = "smart_sync", version = "0.1.0", about, long_about = None)] +struct Args { + /// Тип команды: pull - запрос сохраненных в устройстве настроек, push - отправка новых настроек + #[arg(short = 'm', long = "mode")] + mode: CommandMode, +} + +fn main() -> Result<(), String> { + let args = Args::parse(); + + env_logger::init(); + info!("rk_nku_sync> command mode: {:?}", args.mode); + + let mut nku_client = nku_client::NkuClient::new()?; + + match args.mode { + CommandMode::Pull => pull_parameters(&mut nku_client)?, + CommandMode::Push => push_parameters(&mut nku_client)?, + } + + Ok(()) +} + +/// Отправка настроек на устройство +fn push_parameters(client: &mut nku_client::NkuClient) -> Result<(), String> { + // Цикл попыток установить соединение + let mut attempts: u8 = 1; + 'push_request_loop: loop { + warn!("Push request attempt: {attempts}"); + if client.push_parameters_to_device().is_ok() { + break 'push_request_loop; + } + let result = client.push_parameters_to_device(); + + if result.is_ok() { + break 'push_request_loop; + } + + error!( + "{}", + format!("Failed to push parameters: {result:?}").as_str() + ); + + attempts += 1; + + if attempts > REQUEST_ATTEMPTS { + return Err("Push request failed!".to_string()); + } + } + + info!("Parameters have been pushed to device!"); + + // Запуск стриминга данных от станции + attempts = 1; + 'start_streaming_loop: loop { + warn!("Start streaming attempt: {attempts}"); + + let result = client.start_elevator_data_streaming(nku_client::StreamingMode::OnChangeMode); + + if result.is_ok() { + break 'start_streaming_loop; + } + error!( + "{}", + format!("Failed to push parameters: {result:?}").as_str() + ); + attempts += 1; + + if attempts > REQUEST_ATTEMPTS { + return Err("Start streaming failed!".to_string()); + } + } + info!("Data streaming from MCU has been started!"); + Ok(()) +} + +/// Получение настроек, сохраненных в устройстве +fn pull_parameters(client: &mut nku_client::NkuClient) -> Result<(), String> { + let mut attempts: u8 = 1; + // Цикл попыток установить соединение + 'pull_request_loop: loop { + warn!("Pull request attempt: {attempts}"); + + let result = client.pull_parameters_from_device(); + if result.is_ok() { + break 'pull_request_loop; + } + error!( + "{}", + format!("Failed to push parameters: {result:?}").as_str() + ); + + attempts += 1; + + if attempts > REQUEST_ATTEMPTS { + return Err("Pull request failed!".to_string()); + } + } + info!("Parameters have been pulled from device!"); + Ok(()) +} + +#[derive(Clone, Debug)] +enum CommandMode { + Pull, + Push, +} + +impl FromStr for CommandMode { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "pull" => Ok(CommandMode::Pull), + "push" => Ok(CommandMode::Push), + _ => Err(format!("Unknown command mode: {s}")), + } + } +} diff --git a/pi_nku_sync/src/nku_client.rs b/pi_nku_sync/src/nku_client.rs new file mode 100644 index 0000000..06b2b9c --- /dev/null +++ b/pi_nku_sync/src/nku_client.rs @@ -0,0 +1,124 @@ +use crate::serial_config::SerialPortConfig; +use config_lib::device_config::DeviceConfig; +use log::{debug, info}; +use protocol_lib::client::HostClient; + +const BOARD_RESPONSE_TIMEOUT_MS: std::time::Duration = std::time::Duration::from_millis(5000); +const SERIAL_PORT_CONFIG_PATH: &str = "pi_nku_configs/pi_scheme.toml"; +const NKU_DEVICE_CONFIG_PATH: &str = "pi_nku_configs/nku_scheme.toml"; + +#[derive(Clone)] +/// Режимы стриминга данных от устройства +pub enum StreamingMode { + /// Без стриминга + __SilentMode = 0, + /// Отправка данных, в случае изменения их состояния (и наличия) + OnChangeMode = 1, + /// Отправка данных периодически, с заданным в настройках (periodicity) периодом + __PeriodicMode = 2, + /// Отправка данных по требованию + __OnDemandMode = 3, +} + +pub struct NkuClient { + nku_client: HostClient, + nku_config: DeviceConfig, +} + +impl NkuClient { + pub fn new() -> Result { + // Чтение параметров последовательного порта + let serial_config = SerialPortConfig::new(SERIAL_PORT_CONFIG_PATH)?; + + let nku_config = DeviceConfig::create_parameter_list(NKU_DEVICE_CONFIG_PATH)?; + debug!("Parameters list: {:#?}", nku_config.get_parameters_names()); + + let port_name = serial_config.get_serial_name(); + let baudrate = serial_config.get_serial_baudrate(); + + debug!("Serial config: {port_name}, {baudrate}"); + + let nku_client = HostClient::connect(&port_name, baudrate, BOARD_RESPONSE_TIMEOUT_MS)?; + + info!("Connection with IMv has been established!"); + + Ok(Self { + nku_client, + nku_config, + }) + } + + /// ### Запрос начала стриминга данных со станции управления + pub fn start_elevator_data_streaming(&mut self, mode: StreamingMode) -> Result { + let response_from_mcu = self + .nku_client + .send_request(format!("set mode {}", mode.clone() as u8).as_str())?; + if !response_from_mcu.contains(format!("mode: {}", mode as u8).as_str()) { + return Err("START STREAMING> invalid response".into()); + } + Ok(response_from_mcu) + } + + /// ### Запрос сохраненных в устройстве настроек + pub fn pull_parameters_from_device(&mut self) -> Result<(), String> { + let parameters_list = self.nku_config.get_parameters_names()?; + + for parameter in parameters_list { + let request_string = format!("get {parameter}"); + debug!("PULL> sending request: {request_string}"); + let response_from_mcu = self.nku_client.send_request(&request_string)?; + debug!("PULL> response from MCU: {response_from_mcu}"); + + let parameter_value = + NkuClient::extract_parameter_value(¶meter, response_from_mcu)?; + self.nku_config + .set_parameter_value_using_index(¶meter, parameter_value)?; + } + self.nku_config.save_parameters_values()?; + Ok(()) + } + + /// Извлечение значения параметра из отклика от MCU + fn extract_parameter_value( + parameter_name: &str, + response_from_mcu: String, + ) -> Result { + // Ожидается ответ в формате "parameter_name:value" + let tokens = response_from_mcu.split(':').collect::>().to_vec(); + + if tokens.len() != 2 { + return Err(format!( + "Failed to parse response from MCU: {response_from_mcu}" + )); + } + + if tokens[0] != parameter_name { + return Err(format!( + "Failed to parse response from MCU: {response_from_mcu}" + )); + } + + tokens[1].trim().parse::().map_err(|e| e.to_string()) + } + + /// Отправка новых настроек на устройство для последующего сохранения + pub fn push_parameters_to_device(&mut self) -> Result<(), String> { + let parameters_list = self.nku_config.get_parameters_names()?; + + for parameter in parameters_list { + let parameter_value = self + .nku_config + .get_parameter_index_using_value(¶meter)?; + let request_string = format!("set {parameter} {parameter_value}"); + debug!("PUSH> sending request: {request_string}"); + let response_from_mcu = self.nku_client.send_request(&request_string)?; + + debug!("PUSH> response from MCU: {response_from_mcu}"); + if !response_from_mcu.contains(format!("{parameter}: {parameter_value}").as_str()) { + return Err("PUSH> invalid response".into()); + } + } + + Ok(()) + } +} diff --git a/pi_nku_sync/src/serial_config.rs b/pi_nku_sync/src/serial_config.rs new file mode 100644 index 0000000..1f87bf0 --- /dev/null +++ b/pi_nku_sync/src/serial_config.rs @@ -0,0 +1,38 @@ +use config_lib::device_config::DeviceConfig; +use log::debug; + +pub struct SerialPortConfig { + serial_name: String, + serial_baudrate: u32, +} + +impl SerialPortConfig { + /// Чтение параметров последовательного порта из файла-схемы TOML + pub fn new(path_to_scheme: &str) -> Result { + let smart_serial_parameters = DeviceConfig::create_parameter_list(path_to_scheme)?; + + let serial_name = smart_serial_parameters.get_parameter_value("device")?; + let serial_baudrate = smart_serial_parameters + .get_parameter_value("baudrate")? + .parse::() + .map_err(|e: std::num::ParseIntError| e.to_string())?; + + debug!("serial port_name: {}", serial_name); + debug!("serial baudrate: {}", serial_baudrate); + + Ok(SerialPortConfig { + serial_name, + serial_baudrate, + }) + } + + /// Чтение имени последовательного порта из конфига + pub fn get_serial_name(&self) -> String { + self.serial_name.clone() + } + + /// Чтение скорости последовательного порта из конфига + pub fn get_serial_baudrate(&self) -> u32 { + self.serial_baudrate + } +}