# Первая рабочая версия

Реализовано:
- TUI меню для внесения изменений в пользовательские настройки устройства
- Синхронизация настроек с устройством
This commit is contained in:
Dmitry Akimov 2025-10-02 00:06:16 +03:00
parent 7db52c846f
commit 4880b822df
32 changed files with 3421 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

1500
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

21
Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[workspace]
resolver = "3"
members = [
"config_lib",
"menu_tui",
"protocol_lib",
"rk_nku_sync",
"rk_nku_menu",
]
[workspace.package]
authors = ["MU LLC <info@machunit.com>"]
[workspace.dependencies]
inquire = "0.7.5"
configparser = "3.1.0"
thiserror = "2.0.12"
log = "0.4.27"
env_logger = "0.11.8"
clap = { version = "4.5.41", features = ["derive"] }
serialport = { version = "4.7.2", default-features = false }

7
Cross.toml Normal file
View file

@ -0,0 +1,7 @@
[build]
build-std = false
default-target = "aarch64-unknown-linux-gnu"
[build.zig]
enable = true # Сборка с участием линковщика Zig
version = "2.31" # Версия glibc, установленная на RK3399-ROC-PC-PRO

33
README.md Normal file
View file

@ -0,0 +1,33 @@
# ADAMultitool [rk3399-branch]
> Набор утилит для работы с интерфейсной платой по последовательному порту
## Структура репозитория
### 1. Утилиты
#### 1.1. rk_nku_config
> Утилита для загрузки синхронизации настроек с интерфейсной платой
#### 1.2. rk_nku_menu
> TUI меню для внесения изменений в настройки индикатора
#### 1.3. rk_nku_configs
> Набор конфигов-схем TOML: [настройки индикатора](rk_nku_configs/nku_scheme.toml) и [последовательного порта](rk_nku_configs/rk3399_scheme.toml)
### 2. Библиотеки
#### 2.1. config_lib
> Реализация I/O операций с конфигами-схемами TOML
#### 2.2. protocol_lib
> API для клиентского устройства, используещего [протокол МЮ](protocol_lib/MU%20Protocol.md)
#### 2.3. menu_tui
> Стандартная реализация TUI меню

11
config_lib/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "config_lib"
version = "0.1.0"
edition = "2024"
authors.workspace = true
[dependencies]
rand = "0.9.1"
toml_edit = "0.23.2"
log = { workspace = true }
env_logger = { workspace = true }

View file

@ -0,0 +1,44 @@
use config_lib::device_config::DeviceConfig;
use rand::rng;
use rand::seq::IndexedRandom;
fn main() -> Result<(), String> {
let mut config = DeviceConfig::create_parameter_list("config_lib/examples/simple_config.toml")?;
let parameters = config.get_parameters_names()?;
println!("Полученный список параметров: {:#?}", parameters);
println!("Текущие значения параметров:\n");
for parameter in &parameters {
println!(
"{} = {};\nдиапазон допустимых значений: {:?}\n",
config.get_parameter_description(&parameter)?,
config.get_parameter_value(&parameter)?,
config.get_parameter_possible_values(&parameter)?
);
}
let mut rng = rng();
// Установка случайных значений для параметров в пределах допустимой области
for parameter in &parameters {
let random_value = config
.get_parameter_possible_values(&parameter)?
.choose(&mut rng)
.unwrap()
.clone();
config.set_parameter_value(&parameter, random_value)?;
}
println!("Значения параметров после изменений:\n");
for parameter in &parameters {
println!(
"{} = {};\nдиапазон допустимых значений: {:?}\n",
config.get_parameter_description(&parameter)?,
config.get_parameter_value(&parameter)?,
config.get_parameter_possible_values(&parameter)?
);
}
Ok(())
}

View file

@ -0,0 +1,63 @@
# [Имя таблицы] должно соответствовать названию соответствующего параметра в интерфейсной плате
# name - текстовое название параметра (Используется в меню)
# possible_values - список возможных значений параметра
# default - значение по умолчанию
# current - актуальное текущее значение параметра. Если не указано или некорректно, то используется значение по умолчанию
[groupnumber]
name = "Номер в группе"
possible_values = [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
]
default = "0"
current = "2:1234"
[soundvolume]
name = "Громкость звука"
possible_values = ["0%", "25%", "50%", "75%", "100%"]
default = "50%"
current = "100%"
[musicvolume]
name = "Громкость музыки"
possible_values = ["0%", "25%", "50%", "75%", "100%"]
default = "0%"
current = "0%"
[loadcapacity]
name = "Грузоподъемность"
possible_values = [
"СКРЫТО",
"240кгел.",
"320кгел.",
"400кгел.",
"450кгел.",
"525кгел.",
"630кгел.",
"800кг 10чел.",
"800кг 11чел.",
"1000кг 13чел.",
"1150кг 15чел.",
"1275кг 16чел.",
"1275кг 17чел.",
"1425кг 18чел.",
"1600кг 20чел.",
"1600кг 21чел.",
]
default = "СКРЫТО"
current = "СКРЫТО"

View file

@ -0,0 +1,309 @@
use log::debug;
use std::collections::HashMap;
use toml_edit::value;
use crate::toml_parser::*;
/// Структура, содержащая набор параметров Parameter
#[derive(Debug, Clone)]
pub struct DeviceConfig {
scheme: TomlScheme,
/// Набор параметров
/// key - имя параметра, value - структура, описывающая параметр
parameters: HashMap<String, Parameter>,
}
impl DeviceConfig {
/// ## Заполнение струтуры DeviceConfig
/// * `path_to_scheme` - путь к файлу-cхеме
pub fn create_parameter_list(path_to_scheme: &str) -> Result<DeviceConfig, String> {
let toml_config = TomlScheme::new(path_to_scheme).map_err(|e| e.to_string())?;
let parameter_list = toml_config
.get_list_of_parameters()
.map_err(|e| e.to_string())?;
debug!("Parameter list: {:#?}", parameter_list);
let mut parameter_map = HashMap::new();
for parameter in parameter_list {
let device_parameter = Parameter {
description: toml_config.get_string_value(&parameter, "name")?,
value: toml_config.get_string_value(&parameter, "current")?,
possible_values: toml_config.get_array_value(&parameter, "possible_values")?,
};
parameter_map.insert(parameter, device_parameter);
}
let device_config = DeviceConfig {
scheme: toml_config,
parameters: parameter_map,
};
Ok(device_config)
}
/// # Сохранение текущих значений параметров в TOML-файл self.schema_path
pub fn save_parameters_values(&self) -> Result<(), String> {
let mut toml_config =
TomlScheme::new(&self.scheme.get_path_to_scheme_file()).map_err(|e| e.to_string())?;
for (parameter_name, parameter_object) in &self.parameters {
toml_config.set_parameter_value(
parameter_name,
"current",
value(parameter_object.get_value()),
)?;
}
Ok(())
}
/// # Получение списка ключей - имен параметров
pub fn get_parameters_names(&self) -> Result<Vec<String>, String> {
if self.parameters.is_empty() {
return Err("Parameters list is empty".to_string());
}
return Ok(self.parameters.keys().map(|key| key.clone()).collect());
}
/// # Получение описания параметра соответствующего key
pub fn get_parameter_description(&self, key: &str) -> Result<String, String> {
match self.parameters.get(key) {
Some(parameter) => Ok(parameter.get_description()),
None => Err(format!("Parameter {} not found", key)),
}
}
/// # Получение списка возможных значений параметра соответствующего key
pub fn get_parameter_possible_values(&self, key: &str) -> Result<Vec<String>, String> {
match self.parameters.get(key) {
Some(parameter) => Ok(parameter.get_possible_values()),
None => Err(format!("Parameter {} not found", key)),
}
}
/// # Получение значения параметра соответствующего key
pub fn get_parameter_value(&self, key: &str) -> Result<String, String> {
match self.parameters.get(key) {
Some(parameter) => Ok(parameter.get_value()),
None => Err(format!("Parameter {} not found", key)),
}
}
/// # Установка значения параметра соответствующего key
pub fn set_parameter_value(&mut self, key: &str, value: String) -> Result<(), String> {
match self.parameters.get_mut(key) {
Some(parameter) => parameter.set_value(value),
None => Err(format!("Parameter {} not found", key)),
}
}
/// ## Получение числового индекса соответствующего текущему значению параметра
/// Индекс соответствует положению текущего значения в списке возможных значений possible_values
pub fn get_parameter_index_using_value(&self, key: &str) -> Result<u8, String> {
let parameters_list = self.get_parameters_names()?;
if parameters_list.contains(&key.to_owned()) {
let parameters_possible_values_list = self.get_parameter_possible_values(key)?;
let parameter_current_value = self.get_parameter_value(key)?;
if let Some(index) = parameters_possible_values_list
.iter()
.position(|value| *value == parameter_current_value)
{
return Ok(index as u8);
}
}
Err("Unable to get parameter value using index".to_string())
}
/// ## Присваивание параметру key значения, соответствующего index
/// Индекс соответствует положению значения в списке возможных значений possible_values
pub fn set_parameter_value_using_index(&mut self, key: &str, index: u8) -> Result<(), String> {
let parameters_list = self.get_parameters_names()?;
if parameters_list.contains(&key.to_owned()) {
let parameters_possible_values_list = self.get_parameter_possible_values(key)?;
if let Some(value) = parameters_possible_values_list.get(index as usize) {
return self.set_parameter_value(key, value.clone());
}
}
Err("Unable to get parameter value using index".to_string())
}
}
/// Структура, описывающая параметр
#[derive(Debug, Clone)]
struct Parameter {
/// Описание параметра (для использования в меню)
description: String,
/// Текущее значение параметра
value: String,
/// Список допустимых значений параметра
possible_values: Vec<String>,
}
impl Parameter {
/// Получение имени параметра (для использования в меню)
fn get_description(&self) -> String {
self.description.clone()
}
/// Получение текущего значения параметра
fn get_value(&self) -> String {
self.value.clone()
}
/// Установка текущего значения параметра
fn set_value(&mut self, value: String) -> Result<(), String> {
if !self.possible_values.contains(&value) {
return Err(format!(
"Parameter {} cannot be set to {}!",
self.description, value
));
}
self.value = value;
Ok(())
}
/// Получение списка возможных значений параметра
fn get_possible_values(&self) -> Vec<String> {
self.possible_values.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_io_operations() {
let mut device_config =
DeviceConfig::create_parameter_list("examples/simple_config.toml").unwrap();
let names = device_config.get_parameters_names().unwrap();
assert!(names.contains(&"groupnumber".to_string()));
assert!(
device_config
.set_parameter_value("groupnumber", "19".to_string())
.is_err()
);
assert!(
device_config
.set_parameter_value("groupnumber", "11".to_string())
.is_ok()
);
assert_eq!(
device_config.get_parameter_value("groupnumber").unwrap(),
"11"
);
}
#[test]
fn test_parameter_saving() {
let mut device_config =
DeviceConfig::create_parameter_list("examples/simple_config.toml").unwrap();
let names = device_config.get_parameters_names().unwrap();
assert!(names.contains(&"soundvolume".to_string()));
assert!(
device_config
.set_parameter_value("soundvolume", "100".to_string())
.is_err()
);
assert!(
device_config
.set_parameter_value("soundvolume", "100%".to_string())
.is_ok()
);
assert!(device_config.save_parameters_values().is_ok());
let device_config =
DeviceConfig::create_parameter_list("examples/simple_config.toml").unwrap();
assert_eq!(
device_config.get_parameter_value("soundvolume").unwrap(),
"100%"
);
}
#[test]
fn test_get_parameter_index_using_value() {
let mut device_config =
DeviceConfig::create_parameter_list("examples/simple_config.toml").unwrap();
for index in 0..device_config
.get_parameter_possible_values("groupnumber")
.unwrap()
.len()
{
device_config
.set_parameter_value("groupnumber", index.to_string())
.unwrap();
assert_eq!(
device_config
.get_parameter_index_using_value("groupnumber")
.unwrap(),
index as u8
);
}
let capacity_values = device_config
.get_parameter_possible_values("loadcapacity")
.unwrap();
for index in 0..capacity_values.len() {
device_config
.set_parameter_value("loadcapacity", capacity_values[index].clone())
.unwrap();
assert_eq!(
device_config
.get_parameter_index_using_value("loadcapacity")
.unwrap(),
index as u8
);
}
}
#[test]
fn test_set_parameter_value_using_index() {
let mut device_config =
DeviceConfig::create_parameter_list("examples/simple_config.toml").unwrap();
for index in 0..device_config
.get_parameter_possible_values("groupnumber")
.unwrap()
.len()
{
device_config
.set_parameter_value_using_index("groupnumber", index as u8)
.unwrap();
assert_eq!(
device_config.get_parameter_value("groupnumber").unwrap(),
index.to_string()
);
}
for index in 0..device_config
.get_parameter_possible_values("loadcapacity")
.unwrap()
.len()
{
device_config
.set_parameter_value_using_index("loadcapacity", index as u8)
.unwrap();
assert_eq!(
device_config.get_parameter_value("loadcapacity").unwrap(),
device_config
.get_parameter_possible_values("loadcapacity")
.unwrap()[index]
);
}
}
}

2
config_lib/src/lib.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod device_config;
mod toml_parser;

View file

@ -0,0 +1,148 @@
use toml_edit::{DocumentMut, Item, Value};
#[derive(Debug, Clone)]
/// Структура для работы с TOML-конфигом
pub(crate) struct TomlScheme {
path_to_scheme: String,
document: DocumentMut,
tables: Vec<String>,
}
impl TomlScheme {
pub(crate) fn new(path_to_scheme: &str) -> Result<Self, String> {
let toml_str = std::fs::read_to_string(path_to_scheme).map_err(|e| e.to_string())?;
let doc = toml_str.parse::<DocumentMut>().map_err(|e| e.to_string())?;
let table = doc.as_table();
// Получение списка вложенных в файл таблиц-параметров
let sub_tables: Vec<String> = table
.iter()
.filter_map(|(k, v)| {
if v.is_table() {
Some(k.to_string())
} else {
None
}
})
.collect();
if sub_tables.is_empty() {
return Err("Parameters list is empty".to_string());
}
Ok(TomlScheme {
path_to_scheme: path_to_scheme.to_string(),
document: doc,
tables: sub_tables,
})
}
/// Получение пути к конфиг-файлу
pub(crate) fn get_path_to_scheme_file(&self) -> String {
self.path_to_scheme.clone()
}
/// Получение списка вложенных в файл параметров (TOML-таблицы)
pub(crate) fn get_list_of_parameters(&self) -> Result<Vec<String>, String> {
Ok(self.tables.clone())
}
/// Получение значения для `key`(подпараметр) у параметра `parameter_name`
fn get_parameter_value(&self, parameter_name: &str, key: &str) -> Result<Value, String> {
if let Some(table) = self.document.get(parameter_name) {
if let Some(sub_table) = table.as_table() {
if let Some(value) = sub_table.get(key) {
if let Some(value) = value.as_value() {
return Ok(value.clone());
}
}
}
}
return Err(format!("Unable to get {} value", key));
}
/// Получение строкового значения для `key`(подпараметр) у параметра `parameter_name`
pub(crate) fn get_string_value(
&self,
parameter_name: &str,
key: &str,
) -> Result<String, String> {
let value = self.get_parameter_value(parameter_name, key)?;
if let Some(value) = value.as_str() {
return Ok(value.to_string());
}
Err(format!("Unable to get {} value as string", key))
}
/// Получение значения - массива строк для `key`(подпараметр) у параметра `parameter_name`
pub(crate) fn get_array_value(
&self,
parameter_name: &str,
key: &str,
) -> Result<Vec<String>, String> {
let value = self.get_parameter_value(parameter_name, key)?;
if let Some(array) = value.as_array() {
return Ok(array
.iter()
.map(|x| x.as_str().expect("Unable to get array value").to_string())
.collect());
}
Err(format!("Unable to get {} value as array", key))
}
/// Установка значения `value` для `key`(подпараметр) у параметра `parameter_name`
pub(crate) fn set_parameter_value(
&mut self,
parameter_name: &str,
key: &str,
val: Item,
) -> Result<(), String> {
if let Some(table) = self.document.get_mut(parameter_name) {
if let Some(sub_table) = table.as_table() {
if let Some(_) = sub_table.get(key) {
self.document[parameter_name][key] = val;
let new_toml = self.document.to_string();
std::fs::write(&self.path_to_scheme, new_toml)
.expect("Failed to write TOML file");
return Ok(());
}
}
}
return Err(format!(
"Unable to set {}'s {} value to {:?}",
parameter_name, key, val
));
}
}
#[cfg(test)]
mod tests {
use super::*;
use toml_edit::value;
#[test]
fn test_get_list_of_parameters() {
let toml_scheme = TomlScheme::new("examples/simple_config.toml").unwrap();
let parameters = toml_scheme.get_list_of_parameters().unwrap();
assert_eq!(parameters.len(), 4);
}
#[test]
fn test_set_parameter_value() {
let mut toml_scheme = TomlScheme::new("examples/simple_config.toml").unwrap();
toml_scheme
.set_parameter_value("groupnumber", "current", value("2:1234"))
.unwrap();
let value = toml_scheme
.get_string_value("groupnumber", "current")
.unwrap();
assert_eq!(value.as_str(), "2:1234");
}
}

View file

@ -0,0 +1,20 @@
# Настройка рабочего окружения
## Кросскомпиляция
- [cargo-zigbuild](https://github.com/rust-cross/cargo-zigbuild)
- [cross-rs](https://github.com/cross-rs/cross)
### Сборка
> Важно: необходимо использовать версию линковщика, совпадающую с версией, установленной на целевом устройстве
- cross-rs: `cross build --target=arm-unknown-linux-gnueabihf --bin <crate> --release`
- zigbuild: `cargo zigbuild --target=arm-unknown-linux-gnueabihf.[версия линкера на целевом устройстве] --bin <crate> --release`
> Для кросскомпиляции `serialport` (libudev не поддерживается на MacOS & WIN64) указать фича-флаг `default-features = false`
## Загрузка на целевое устройство
`scp -P [port_number] [path/to/bin] [hostname]@[ip_address]:/home/[user-name]`

5
docs/vscode/Install.md Normal file
View file

@ -0,0 +1,5 @@
# Установка необходимых расширений
```bash
cat vs_code_extensions_list.txt | xargs -n 1 code --install-extension
```

Binary file not shown.

16
menu_tui/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "menu_tui"
version = "0.1.0"
edition = "2024"
authors.workspace = true
[dependencies]
clap = { workspace = true }
config_lib = { path = "../config_lib" }
inquire = { workspace = true }
enigo = "0.5.0"
colored = "3.0.0"
terminal-menu = "3.0.0"
crossterm = "0.25.0"
log = { workspace = true }
env_logger = { workspace = true }

View file

@ -0,0 +1,27 @@
use config_lib::device_config::DeviceConfig;
use menu_tui::menu_rendering::{MainMenuStates, show_main_dialog};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut device_config =
DeviceConfig::create_parameter_list("menu_tui/examples/simple_config.toml")?;
loop {
match show_main_dialog(&mut device_config) {
Ok(MainMenuStates::ConfigurationState) => {
println!("Configuration state");
continue;
}
Ok(MainMenuStates::ExitState) => {
println!("The user has chosen to exit...");
break;
}
Err(e) => {
println!("Menu error: {}", e);
break;
}
}
}
return Ok(());
}

View file

@ -0,0 +1,64 @@
# [Имя таблицы] должно соответствовать названию соответствующего параметра в интерфейсной плате
# name - текстовое название параметра (Используется в меню)
# possible_values - список возможных значений параметра
# default - значение по умолчанию
# current - актуальное текущее значение параметра. Если не указано или некорректно, то используется значение по умолчанию
[groupnumber]
name = "Номер в группе"
possible_values = [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
]
default = "0"
current = "6"
[soundvolume]
name = "Громкость звука"
possible_values = ["0%", "25%", "50%", "75%", "100%"]
default = "50%"
current = "50%"
[musicvolume]
name = "Громкость музыки"
possible_values = ["0%", "25%", "50%", "75%", "100%"]
default = "0%"
current = "100%"
[loadcapacity]
name = "Грузоподъемность"
possible_values = [
"СКРЫТО",
"240кгел.",
"320кгел.",
"400кгел.",
"450кгел.",
"525кгел.",
"630кгел.",
"800кг 10чел.",
"800кг 11чел.",
"1000кг 13чел.",
"1150кг 15чел.",
"1275кг 16чел.",
"1275кг 17чел.",
"1425кг 18чел.",
"1600кг 20чел.",
"1600кг 21чел.",
]
default = "СКРЫТО"
current = "240кгел."

1
menu_tui/src/lib.rs Normal file
View file

@ -0,0 +1 @@
pub mod menu_rendering;

View file

@ -0,0 +1,136 @@
//! # Модуль отображения меню
//!
//! ## Навигация
//! > Для навигации по меню используется клавиши стрелка вверх/вниз
//! Выбор эелемента в меню происходит клавишей Enter
//! `В случае отсутствия данных клавиш в системе, можно эмулировать их с помощью библиотеки enigo`
//!
//! ## Параметры меню
//! > Параметры меню подтягиваются из файла-схемы TOML с помощью крейта config_lib
use config_lib::device_config::DeviceConfig;
use crossterm::style::Color;
use inquire::Select;
use terminal_menu::{TerminalMenuItem, button, label, menu, mut_menu, run};
pub enum MainMenuStates {
ConfigurationState,
ExitState,
}
/// Отображение главного меню
pub fn show_main_dialog(config: &mut DeviceConfig) -> Result<MainMenuStates, String> {
// Header
let mut menu_structure = generate_menu_header("Меню настроек индикатора");
// Текущие значения
menu_structure.extend(generate_current_values(config)?);
// Элементы меню
menu_structure.extend(generate_menu_items(config)?);
let main_menu = menu(menu_structure);
// Отрисовка и навигация по меню
run(&main_menu);
// Получение названия выбранного пункта меню
let mut_menu_instance = mut_menu(&main_menu);
let selected_menu_item = mut_menu_instance.selected_item_name();
// Получение ключа параметра в HashMap, соответствующего названию пункта меню
let selected_parameter_name = config
.get_parameters_names()?
.into_iter()
.find_map(|par_name| {
// Получение ключа параметра в HashMap, в случае если описание параметра совпадает с названием пункта меню
if let Ok(par_desc) = config.get_parameter_description(&par_name) {
return if par_desc == selected_menu_item {
Some(par_name)
} else {
None
};
}
None
});
if let Some(key) = selected_parameter_name {
show_configuration_dialog(config, &key)?;
return Ok(MainMenuStates::ConfigurationState);
}
// Сохранение настроек в TOML-файл схему
config.save_parameters_values()?;
return Ok(MainMenuStates::ExitState);
}
/// Отображение диалога для выбора значения параметра
fn show_configuration_dialog(config: &mut DeviceConfig, par_name: &str) -> Result<(), String> {
let possible_values = config.get_parameter_possible_values(par_name)?;
let answer = Select::new("Выберите значение параметра", possible_values.clone()).prompt();
match answer {
Ok(selection) => {
config.set_parameter_value(par_name, selection)?;
Ok(())
}
Err(e) => return Err(e.to_string()),
}
}
/// Генерация структуры меню: Титул
fn generate_menu_header(title: &str) -> Vec<TerminalMenuItem> {
vec![
label("----------------------").colorize(Color::DarkGreen),
label(title).colorize(Color::DarkGreen),
label(format!(
"{} v{}",
env!("CARGO_CRATE_NAME"),
env!("CARGO_PKG_VERSION")
))
.colorize(Color::DarkGreen),
label(format!("{}", env!("CARGO_PKG_AUTHORS"))).colorize(Color::DarkGreen),
label("-----------------------").colorize(Color::DarkGreen),
]
}
/// Генерация структуры меню: Текущие значения настраиваемых параметров
fn generate_current_values(config: &DeviceConfig) -> Result<Vec<TerminalMenuItem>, String> {
let parameter_names = config.get_parameters_names()?;
let mut menu_structure = vec![];
for parameter_name in &parameter_names {
let parameter_value = config.get_parameter_value(&parameter_name)?;
let parameter_description = config.get_parameter_description(&parameter_name)?;
menu_structure.push(
label(format!("{}: {}", parameter_description, parameter_value))
.colorize(Color::DarkYellow),
);
}
menu_structure.push(label("-----------------------").colorize(Color::DarkYellow));
Ok(menu_structure)
}
/// Генерация структуры меню: Элементы меню
fn generate_menu_items(config: &DeviceConfig) -> Result<Vec<TerminalMenuItem>, String> {
let parameter_names = config.get_parameters_names()?;
let mut menu_structure = vec![];
menu_structure.push(label("-----------------------").colorize(Color::DarkGrey));
// Элементы меню; имя каждого параметра соответствует полю description
let mut menu_structure = menu_structure;
for parameter_name in &parameter_names {
let parameter_description = config.get_parameter_description(&parameter_name)?;
menu_structure.push(button(parameter_description).colorize(Color::White));
}
menu_structure.push(button("Выход").colorize(Color::White));
menu_structure.push(label("-----------------------").colorize(Color::DarkGrey));
Ok(menu_structure)
}

10
protocol_lib/Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "protocol_lib"
version = "0.1.1"
edition = "2024"
authors.workspace = true
[dependencies]
log = { workspace = true }
crc = "3.3.0"
serialport = { workspace = true }

View file

@ -0,0 +1,93 @@
# **MU_protocol — Протокол обмена данными между микроконтроллером и одноплатным компьютером**
## **1. Общее описание**
Протокол предназначен для передачи данных между **Python Server App** и **одноплатным компьютером** по UART. Протокол обеспечивает:
- Синхронизацию пакетов
- Целостность данных (через CRC16)
- Гибкость в передаче данных разных типов (определяется через `opcode`)
---
## **2. Структура пакета**
Каждый пакет состоит из следующих полей:
| Поле | Размер | Тип | Описание |
|------------|--------|----------|----------|
| `sync1` | 1 байт | uint8 | Стартовый байт синхронизации, значение `0xAA` |
| `size` | 1 байт | uint8 | Размер поля `data` в байтах (от 0 до 255) |
| `opcode` | 1 байт | uint8 | Код операции (например, 0xDA для передачи данных о состоянии лифта) |
| `data` | N байт | массив | Полезная нагрузка |
| `crc16` | 2 байта| uint16 | CRC-16 контрольная сумма |
| `sync2` | 1 байт | uint8 | Конечный байт синхронизации, значение `0xBB` |
### **Общая длина пакета**:
```
Длина = 1 (sync1) + 1 (size) + 1 (opcode) + N (data) + 2 (crc16) + 1 (sync2) = 6 + N байт
```
---
## **3. Поля пакета**
### **3.1. `sync1` и `sync2`**
- Используются для синхронизации начала и конца пакета
- Значения: `0xAA` и `0xBB` соответственно
### **3.2. `size`**
- Определяет длину поля `data`
- Допустимые значения: от 0 до 255
### **3.3. `opcode`**
- Код операции, определяющий тип данных в поле `data`
- Примеры:
- `0xDA` — данные о состоянии лифта
- `0xDB` — команда от компьютера к микроконтроллеру
- `0xDC` — подтверждение приёма
- `0xDD` — запрос состояния
- и т.д.
### **3.4. `data`**
- Полезная нагрузка, формат которой зависит от `opcode`
- Для `opcode: 0xDA` — строка вида `#STM:L%d:R%d:A%d:S%d:M%d:E#\r\n`
### **3.5. `crc16`**
- CRC-16 контрольная сумма, рассчитанная по следующим параметрам:
| Параметр | Значение |
|------------|------------------|
| Название | CRC-16 CCITT |
| Полином | `0x1021` (`x^16 + x^12 + x^5 + 1`) |
| Начальное значение | `0xFFFF` |
| Реверс входных данных | Нет |
| Реверс результата | Нет |
| Выходное XOR (`XorOut`)| `0x0000` |
| Контрольное значение (`"123456789"`) | `0x29B1` |
| Максимальная длина блока | 4095 байт (32767 бит) |
---
## **4. Пример пакета**
### **4.1. Исходные данные**
```c
opcode = 0xDA
data = "#STM:L10:R3:A1:S4:M0:E#\r\n"
```
### **4.2. Шаги формирования пакета**
1. Вычислить длину `data`: `len(data) = 24`
2. Установить `size = 24`
3. Вычислить CRC16 от последовательности: `opcode + data`
4. Сформировать пакет:
```
[0xAA] [0x18] [0xDA] [data bytes...] [CRC16_H] [CRC16_L] [0xBB]
```
---

View file

@ -0,0 +1,74 @@
use log::{debug, error, warn};
use crate::mu_frame::MUFrame;
use std::time::Duration;
/// Количество попыток установить соединение c MCU
const HANDSHAKE_ATTEMPTS: u8 = 5;
pub struct HostClient {
serial_port: Box<dyn serialport::SerialPort + 'static>,
}
impl HostClient {
/// Создание соединения host - интерфейсная плата over UART
pub fn connect(
port_name: &str,
baudrate: u32,
timeout: Duration,
) -> Result<HostClient, String> {
let serial_port = serialport::new(port_name, baudrate)
.timeout(timeout)
.open()
.unwrap_or_else(|_| panic!("Unable to open serial port: {port_name}"));
Self::try_handshake(serial_port)
}
/// Попытка установить соединение с устройством
fn try_handshake(instance: Box<dyn serialport::SerialPort + 'static>) -> Result<Self, String> {
let mut attempts: u8 = 1;
let mut client_connection = HostClient {
serial_port: instance,
};
// Цикл попыток установить соединение
'handshake_loop: loop {
warn!("Handshake attempt: {attempts}");
let answer = client_connection.send_request("hello");
if let Ok(response) = answer {
warn!("Response from device: {response}");
if response.as_bytes() == b"Hi!\r\n" {
return Ok(client_connection);
}
} else {
error!("Handshake: {}", answer.clone().unwrap_err());
}
attempts += 1;
if attempts > HANDSHAKE_ATTEMPTS {
break 'handshake_loop;
}
}
Err("Handshake failed!".to_string())
}
/// Отправка запроса на устройство, возврат полученного отклика
pub fn send_request(&mut self, request: &str) -> Result<String, String> {
let mut frame = MUFrame::new();
frame
.set_data(format!("{}{}", request, "\n").as_bytes().to_vec())
.map_err(|e| e.to_string())?;
crate::send_proto_message(frame, &mut self.serial_port)?;
let new_frame =
crate::recv_proto_message(&mut self.serial_port).map_err(|e| e.to_string())?;
Ok(String::from_utf8(new_frame.get_data().to_vec()).map_err(|e| e.to_string())?)
}
}

84
protocol_lib/src/lib.rs Normal file
View file

@ -0,0 +1,84 @@
pub mod client;
mod mu_frame;
use crate::mu_frame::MUFrame;
use log::trace;
use std::{
io::{Read, Write},
thread,
};
/// Задержка перед приемом отклика от интерфейсной платы
const ANSWER_DELAY_MS: u64 = 500;
/// Отправка сообщения
fn send_proto_message<Writer: Write>(data: MUFrame, mut writer: Writer) -> Result<(), String> {
let bytes = data.serialize();
writer.write_all(&bytes).map_err(|e| e.to_string())?;
// Время для составления ответа
thread::sleep(std::time::Duration::from_millis(ANSWER_DELAY_MS));
Ok(())
}
/// Прием сообщения
fn recv_proto_message<Reader: Read>(mut reader: Reader) -> Result<MUFrame, String> {
let mut raw_frame = Vec::new();
let mut read_buffer = [0; 256];
// Чтение отклика от интерфейсной платы
reader.read(&mut read_buffer).map_err(|e| e.to_string())?;
trace!("Received raw message: {read_buffer:?}");
// Сборка сообщения
let prefix = read_buffer[0];
raw_frame.push(prefix);
let payload_length = read_buffer[1];
raw_frame.push(payload_length);
let opcode = read_buffer[2];
raw_frame.push(opcode);
// 3[prefix, payload_length, opcode] + payload_length
let end_of_data_idx = 3 + payload_length as usize;
let payload = &read_buffer[3..end_of_data_idx];
raw_frame.extend_from_slice(payload);
let crc_low = read_buffer[end_of_data_idx];
raw_frame.push(crc_low);
let crc_high = read_buffer[1 + end_of_data_idx];
raw_frame.push(crc_high);
let postfix = read_buffer[2 + end_of_data_idx];
raw_frame.push(postfix);
let frame = MUFrame::deserialize(&raw_frame).map_err(|e| e.to_string())?;
Ok(frame)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_send_and_recv() {
let mut frame_to_send = MUFrame::new();
frame_to_send
.set_data(b"get server_info\n".to_vec())
.unwrap();
let mut buf = Vec::new();
send_proto_message(frame_to_send.clone(), &mut buf).unwrap();
let received_frame = recv_proto_message(&buf[..]).unwrap();
assert_eq!(received_frame.get_data(), frame_to_send.get_data());
assert_eq!(received_frame, frame_to_send);
}
}

View file

@ -0,0 +1,225 @@
use std::fmt::Display;
const SYNC1: u8 = 0xAA;
const SYNC2: u8 = 0xBB;
const MAX_DATA_SIZE: u8 = u8::MAX;
const CONSOLE_OPCODE: u8 = 0xC0;
/// Пакет данных протокола "МЮ"
///
///
/// ## Пример
/// ```ignore
/// let mut frame_to_send = MUFrame::new();
/// frame_to_send
/// .set_data(b"get server_info\n".to_vec())?;
/// let raw_bytes = frame_to_send.serialize();
/// ```
///
#[derive(Debug, PartialEq, Clone)]
pub struct MUFrame {
prefix: u8,
length: u8,
opcode: u8,
data: Vec<u8>,
crc_low: u8,
crc_high: u8,
suffix: u8,
}
impl MUFrame {
pub fn new() -> Self {
Self {
prefix: SYNC1,
length: 0,
opcode: CONSOLE_OPCODE,
data: Vec::with_capacity(MAX_DATA_SIZE as usize),
crc_low: 0x00,
crc_high: 0x00,
suffix: SYNC2,
}
}
pub(crate) fn get_data(&self) -> &Vec<u8> {
&self.data
}
/// Загрузка данных в фрейм, вычисление CRC и длины
pub(crate) fn set_data(&mut self, data: Vec<u8>) -> Result<(), String> {
if data.len() > MAX_DATA_SIZE as usize || data.is_empty() {
return Err("Data too long".to_string());
}
if !data.is_ascii() {
return Err("Bad encoding".to_string());
}
self.length = data.len() as u8;
self.data = data;
let crc_value = self.calculate_src();
self.crc_low = crc_value as u8;
self.crc_high = (crc_value >> 8) as u8;
Ok(())
}
/// Десериализация данных из буфера
pub(crate) fn deserialize(data: &[u8]) -> Result<Self, String> {
let mut frame = Self::new();
frame.prefix = data[0];
frame.length = data[1];
frame.opcode = data[2];
frame.data = data[3..3 + frame.length as usize].to_vec();
frame.crc_low = data[3 + frame.length as usize];
frame.crc_high = data[4 + frame.length as usize];
frame.suffix = data[5 + frame.length as usize];
frame.invalidate_frame()?;
Ok(frame)
}
/// Сериализация данных
pub(crate) fn serialize(&self) -> Vec<u8> {
let mut result = Vec::with_capacity(5 + self.length as usize);
result.push(self.prefix);
result.push(self.length);
result.push(self.opcode);
result.extend(self.data.iter());
result.push(self.crc_low);
result.push(self.crc_high);
result.push(self.suffix);
result
}
/// Проверка валидности фрейма
fn invalidate_frame(&self) -> Result<(), String> {
if !self.is_prefix_correct() {
return Err("Bad prefix".to_string());
}
if !self.is_postfix_correct() {
return Err("Bad postfix".to_string());
}
let crc_value = (self.crc_high as u16) << 8 | self.crc_low as u16;
if !self.is_crc_valid(crc_value) {
return Err("Bad CRC".to_string());
}
if !self.data.is_ascii() {
return Err("Bad encoding".to_string());
}
Ok(())
}
/// Вычисление CRC
fn calculate_src(&self) -> u16 {
const CRC16_MU: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_3740);
let mut crc_data = Vec::with_capacity(self.length as usize + 1);
crc_data.push(self.opcode);
crc_data.extend(self.data.iter());
CRC16_MU.checksum(&crc_data)
}
/// Проверка соответствия фактического CRC посылки с принятым
fn is_crc_valid(&self, crc: u16) -> bool {
let calculated_crc = self.calculate_src();
calculated_crc == crc
}
/// Проверка корректности префикса (SYNC1)
fn is_prefix_correct(&self) -> bool {
self.prefix == SYNC1
}
/// Проверка корректности постфикса (SYNC2)
fn is_postfix_correct(&self) -> bool {
self.suffix == SYNC2
}
}
impl Display for MUFrame {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"MU Message: \n 1.Opcode={}, \n 2.Data length={} \n 3.Payload={:?} \n 4.CRC={}",
self.opcode,
self.length,
self.data,
(self.crc_high as u16) << 8 | self.crc_low as u16
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_crc_calculation() {
let mut frame = MUFrame::new();
frame
.set_data(b"#STM:L16:R16:A0:S0:M0:E#\r\n\0".to_vec())
.unwrap();
assert!(frame.is_crc_valid(0x2D52));
}
#[test]
fn test_serialize() {
let mut frame = MUFrame::new();
frame
.set_data(b"#STM:L0:R16:A1:S0:M0:E#\r\n\0".to_vec())
.unwrap();
let serialized_vec = frame.serialize();
assert_eq!(serialized_vec.len(), 6 + frame.length as usize);
assert_eq!(
serialized_vec,
vec![
0xAA, 0x1A, 0xC0, 0x23, 0x53, 0x54, 0x4D, 0x3A, 0x4C, 0x30, 0x3A, 0x52, 0x31, 0x36,
0x3A, 0x41, 0x31, 0x3A, 0x53, 0x30, 0x3A, 0x4D, 0x30, 0x3A, 0x45, 0x23, 0x0D, 0x0A,
0x00, 0x80, 0x77, 0xBB
]
);
}
#[test]
fn test_deserialize() {
let serialized_vec = vec![
0xAA, 0x1A, 0xC0, 0x23, 0x53, 0x54, 0x4D, 0x3A, 0x4C, 0x30, 0x3A, 0x52, 0x31, 0x36,
0x3A, 0x41, 0x31, 0x3A, 0x53, 0x30, 0x3A, 0x4D, 0x30, 0x3A, 0x45, 0x23, 0x0D, 0x0A,
0x00, 0x80, 0x77, 0xBB,
];
let frame = MUFrame::deserialize(&serialized_vec).unwrap();
assert!(frame.is_prefix_correct());
assert!(frame.is_postfix_correct());
assert!(frame.is_crc_valid(0x7780));
assert_eq!(frame.opcode, CONSOLE_OPCODE);
assert_eq!(frame.data, b"#STM:L0:R16:A1:S0:M0:E#\r\n\0");
}
#[test]
fn test_frame_invalidation() {
let serialized_vec = vec![
0xAA, 0x1B, 0xC0, 0x23, 0x53, 0x54, 0x4D, 0x3A, 0x4C, 0x31, 0x36, 0x3A, 0x52, 0x31,
0x36, 0x3A, 0x41, 0x31, 0x3A, 0x53, 0x32, 0x3A, 0x4D, 0x30, 0x3A, 0x45, 0x23, 0x0D,
0x0A, 0x00, 0xBB, 0xB6, 0xBB,
];
let frame = MUFrame::deserialize(&serialized_vec).unwrap();
assert!(frame.is_prefix_correct());
assert!(frame.is_postfix_correct());
assert!(frame.is_crc_valid(0xB6BB));
assert_eq!(frame.opcode, CONSOLE_OPCODE);
assert_eq!(frame.data, b"#STM:L16:R16:A1:S2:M0:E#\r\n\0");
frame.invalidate_frame().unwrap();
}
}

View file

@ -0,0 +1,63 @@
# [table name] должно соответствовать названию соответствующего параметра в ПО для интерфейсной платы
# name - текстовое название параметра (используется в TUI меню)
# possible_values - список возможных значений параметра
# default - значение по умолчанию (одно из possible_values)
# current - актуальное текущее значение параметра (одно из possible_values). Если не указано или некорректно, то используется значение по умолчанию
[groupnumber]
name = "Номер в группе"
possible_values = [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
]
default = "0"
current = "6"
[soundvolume]
name = "Громкость звука"
possible_values = ["0%", "25%", "50%", "75%", "100%"]
default = "50%"
current = "100%"
[musicvolume]
name = "Громкость музыки"
possible_values = ["0%", "25%", "50%", "75%", "100%"]
default = "0%"
current = "100%"
[loadcapacity]
name = "Грузоподъемность"
possible_values = [
"СКРЫТО",
"240кгел.",
"320кгел.",
"400кгел.",
"450кгел.",
"525кгел.",
"630кгел.",
"800кг 10чел.",
"800кг 11чел.",
"1000кг 13чел.",
"1150кг 15чел.",
"1275кг 16чел.",
"1275кг 17чел.",
"1425кг 18чел.",
"1600кг 20чел.",
"1600кг 21чел.",
]
default = "СКРЫТО"
current = "240кгел."

View file

@ -0,0 +1,17 @@
# [table name] должно соответствовать названию соответствующего параметра в ПО для интерфейсной платы
# name - текстовое название параметра (используется в TUI меню)
# possible_values - список возможных значений параметра
# default - значение по умолчанию (одно из possible_values)
# current - актуальное текущее значение параметра (одно из possible_values). Если не указано или некорректно, то используется значение по умолчанию
[device]
name = "Имя порта"
possible_values = ["/dev/ttyS4"]
default = "/dev/ttyS4"
current = "/dev/ttyS4"
[baudrate]
name = "Скорость передачи данных"
possible_values = ["9600", "38400", "115200"]
default = "115200"
current = "115200"

13
rk_nku_menu/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "rk_nku_menu"
version = "0.1.0"
edition = "2024"
authors.workspace = true
[dependencies]
enigo = "0.5.0"
sysfs_gpio = { version = "0.6.2" }
log = { workspace = true }
env_logger = { workspace = true }
config_lib = { path = "../config_lib" }
menu_tui = { path = "../menu_tui" }

133
rk_nku_menu/src/main.rs Normal file
View file

@ -0,0 +1,133 @@
use config_lib::device_config::DeviceConfig;
use enigo::{Direction::Click, Enigo, Key, Keyboard, Settings};
use log::{debug, error};
use menu_tui::menu_rendering::{MainMenuStates, show_main_dialog};
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use sysfs_gpio::{Direction, Edge, Pin};
/// ### Путь к файлу-схемы параметров устройства
const NKU_DEVICE_CONFIG_PATH: &str = "rk_nku_configs/nku_scheme.toml";
/// Расчёт номера пина в порядке SYSFS
/// ```
/// bank = 0; // GPIO0_B5 => 0, bank ∈ [0,4]
/// group = 1; // GPIO0_B5 => 1, group ∈ {(A=0), (B=1), (C=2), (D=3)}
/// X = 5; // GPIO0_B5 => 5, X ∈ [0,7]
/// number = group * 8 + X = 1 * 8 + 5 = 13;
/// pin = bank * 32 + number = 0 * 32 + 13 = 13;
/// ```
/// SYSFS номер порта кнопки ввода
const S1_BTN: u8 = 124; //124
/// SYSFS номер порта кнопки выбора
const S2_BTN: u8 = 125; //125
/// Максимальное время бездействия [c], после достижения которого происходит выход из меню
const IDLE_TIMEOUT_SEC: u32 = 30;
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let mut device_config = DeviceConfig::create_parameter_list(NKU_DEVICE_CONFIG_PATH)?;
static BTN_PRESSED: AtomicBool = AtomicBool::new(false);
static STOP_FLAG: AtomicBool = AtomicBool::new(false);
// Открытие потоков для обслуживания EXT прерываний
let down_irq_thread = std::thread::spawn(move || {
let input = Pin::new(S2_BTN as u64);
debug!("down_irq_thread launched!");
input.with_exported(|| {
input.set_direction(Direction::In)?;
input.set_edge(Edge::RisingEdge)?;
let mut poller = input.get_poller()?;
while !STOP_FLAG.load(std::sync::atomic::Ordering::SeqCst) {
if let Some(pin_value) = poller.poll(1000)? {
debug!("{}", format!("Pin value: {}", pin_value));
if pin_value == 1 {
let mut enigo = Enigo::new(&Settings::default()).unwrap();
enigo.key(Key::DownArrow, Click).unwrap();
BTN_PRESSED.store(true, std::sync::atomic::Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(500));
debug!("DownArrow pressed");
}
}
}
Ok(())
})
});
let return_irq_thread = std::thread::spawn(move || {
let input = Pin::new(S1_BTN as u64);
debug!("return_irq_thread launched!");
input.with_exported(|| {
input.set_direction(Direction::In)?;
input.set_edge(Edge::RisingEdge)?;
let mut poller = input.get_poller()?;
while !STOP_FLAG.load(std::sync::atomic::Ordering::SeqCst) {
if let Some(pin_value) = poller.poll(1000)? {
debug!("{}", format!("Pin value: {}", pin_value));
if pin_value == 1 {
let mut enigo = Enigo::new(&Settings::default()).unwrap();
enigo.key(Key::Return, Click).unwrap();
BTN_PRESSED.store(true, std::sync::atomic::Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(500));
debug!("Return pressed");
}
}
}
Ok(())
})
});
// Поток TUI меню
std::thread::spawn(move || {
loop {
match show_main_dialog(&mut device_config) {
Ok(MainMenuStates::ConfigurationState) => {
debug!("Configuration state");
continue;
}
Ok(MainMenuStates::ExitState) => {
debug!("The user has chosen to exit...");
break;
}
Err(e) => {
error!("Menu error: {}", e);
break;
}
}
}
STOP_FLAG.store(true, std::sync::atomic::Ordering::SeqCst);
debug!("The user has chosen to exit...");
});
// Главный поток - проверка бездействия
let mut idle_counter: u32 = 0;
while !STOP_FLAG.load(std::sync::atomic::Ordering::SeqCst) {
if BTN_PRESSED.load(std::sync::atomic::Ordering::SeqCst) {
idle_counter = 0;
BTN_PRESSED.store(false, std::sync::atomic::Ordering::SeqCst);
} else {
idle_counter += 1;
if idle_counter > IDLE_TIMEOUT_SEC * 10 {
STOP_FLAG.store(true, std::sync::atomic::Ordering::SeqCst);
debug!("Idle timeout has been reached, exiting...");
}
}
std::thread::sleep(Duration::from_millis(100));
}
if down_irq_thread.join().is_err() {
debug!("Unable to stop down_irq_thread");
}
if return_irq_thread.join().is_err() {
debug!("Unable to stop return_irq_thread");
}
return Ok(());
}

12
rk_nku_sync/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "rk_nku_sync"
version = "0.1.0"
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 }

128
rk_nku_sync/src/main.rs Normal file
View file

@ -0,0 +1,128 @@
mod nku_client;
mod serial_config;
use std::str::FromStr;
use clap::Parser;
use log::{error, warn};
/// Количество попыток выполнить запрос
const REQUEST_ATTEMPTS: u8 = 5;
#[derive(Parser)]
#[command(author = "Akimov Dmitry MU LLC", name = "nku_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();
warn!("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());
}
}
// Запуск стриминга данных от станции
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());
}
}
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());
}
}
Ok(())
}
#[derive(Clone, Debug)]
enum CommandMode {
Pull,
Push,
}
impl FromStr for CommandMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"pull" => Ok(CommandMode::Pull),
"push" => Ok(CommandMode::Push),
_ => Err(format!("Unknown command mode: {s}")),
}
}
}

View file

@ -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 = "rk_nku_configs/rk3399_scheme.toml";
const NKU_DEVICE_CONFIG_PATH: &str = "rk_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<Self, String> {
// Чтение параметров последовательного порта
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<String, String> {
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(&parameter, response_from_mcu)?;
self.nku_config
.set_parameter_value_using_index(&parameter, parameter_value)?;
}
self.nku_config.save_parameters_values()?;
Ok(())
}
/// Извлечение значения параметра из отклика от MCU
fn extract_parameter_value(
parameter_name: &str,
response_from_mcu: String,
) -> Result<u8, String> {
// Ожидается ответ в формате "parameter_name:value"
let tokens = response_from_mcu.split(':').collect::<Vec<&str>>().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::<u8>().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(&parameter)?;
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(())
}
}

View file

@ -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<Self, String> {
let nku_serial_parameters = DeviceConfig::create_parameter_list(path_to_scheme)?;
let serial_name = nku_serial_parameters.get_parameter_value("device")?;
let serial_baudrate = nku_serial_parameters
.get_parameter_value("baudrate")?
.parse::<u32>()
.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
}
}