# Первая рабочая версия
Реализовано: - TUI меню для внесения изменений в пользовательские настройки устройства - Синхронизация настроек с устройством
This commit is contained in:
parent
7db52c846f
commit
4880b822df
32 changed files with 3421 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
1500
Cargo.lock
generated
Normal file
1500
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal 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
7
Cross.toml
Normal 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
33
README.md
Normal 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
11
config_lib/Cargo.toml
Normal 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 }
|
||||
44
config_lib/examples/simple.rs
Normal file
44
config_lib/examples/simple.rs
Normal 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 ¶meters {
|
||||
println!(
|
||||
"{} = {};\nдиапазон допустимых значений: {:?}\n",
|
||||
config.get_parameter_description(¶meter)?,
|
||||
config.get_parameter_value(¶meter)?,
|
||||
config.get_parameter_possible_values(¶meter)?
|
||||
);
|
||||
}
|
||||
|
||||
let mut rng = rng();
|
||||
|
||||
// Установка случайных значений для параметров в пределах допустимой области
|
||||
for parameter in ¶meters {
|
||||
let random_value = config
|
||||
.get_parameter_possible_values(¶meter)?
|
||||
.choose(&mut rng)
|
||||
.unwrap()
|
||||
.clone();
|
||||
config.set_parameter_value(¶meter, random_value)?;
|
||||
}
|
||||
|
||||
println!("Значения параметров после изменений:\n");
|
||||
for parameter in ¶meters {
|
||||
println!(
|
||||
"{} = {};\nдиапазон допустимых значений: {:?}\n",
|
||||
config.get_parameter_description(¶meter)?,
|
||||
config.get_parameter_value(¶meter)?,
|
||||
config.get_parameter_possible_values(¶meter)?
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
63
config_lib/examples/simple_config.toml
Normal file
63
config_lib/examples/simple_config.toml
Normal 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кг 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 = "СКРЫТО"
|
||||
309
config_lib/src/device_config.rs
Normal file
309
config_lib/src/device_config.rs
Normal 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(¶meter, "name")?,
|
||||
value: toml_config.get_string_value(¶meter, "current")?,
|
||||
possible_values: toml_config.get_array_value(¶meter, "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
2
config_lib/src/lib.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod device_config;
|
||||
mod toml_parser;
|
||||
148
config_lib/src/toml_parser.rs
Normal file
148
config_lib/src/toml_parser.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
20
docs/build'n'deploy/README.md
Normal file
20
docs/build'n'deploy/README.md
Normal 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
5
docs/vscode/Install.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Установка необходимых расширений
|
||||
|
||||
```bash
|
||||
cat vs_code_extensions_list.txt | xargs -n 1 code --install-extension
|
||||
```
|
||||
BIN
docs/vscode/vs_code_extensions_list.txt
Normal file
BIN
docs/vscode/vs_code_extensions_list.txt
Normal file
Binary file not shown.
16
menu_tui/Cargo.toml
Normal file
16
menu_tui/Cargo.toml
Normal 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 }
|
||||
27
menu_tui/examples/simple.rs
Normal file
27
menu_tui/examples/simple.rs
Normal 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(());
|
||||
}
|
||||
64
menu_tui/examples/simple_config.toml
Normal file
64
menu_tui/examples/simple_config.toml
Normal 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кг 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 = "240кг 3чел."
|
||||
1
menu_tui/src/lib.rs
Normal file
1
menu_tui/src/lib.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod menu_rendering;
|
||||
136
menu_tui/src/menu_rendering.rs
Normal file
136
menu_tui/src/menu_rendering.rs
Normal 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 ¶meter_names {
|
||||
let parameter_value = config.get_parameter_value(¶meter_name)?;
|
||||
let parameter_description = config.get_parameter_description(¶meter_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 ¶meter_names {
|
||||
let parameter_description = config.get_parameter_description(¶meter_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
10
protocol_lib/Cargo.toml
Normal 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 }
|
||||
93
protocol_lib/MU Protocol.md
Normal file
93
protocol_lib/MU Protocol.md
Normal 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]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
74
protocol_lib/src/client.rs
Normal file
74
protocol_lib/src/client.rs
Normal 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
84
protocol_lib/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
225
protocol_lib/src/mu_frame.rs
Normal file
225
protocol_lib/src/mu_frame.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
63
rk_nku_configs/nku_scheme.toml
Normal file
63
rk_nku_configs/nku_scheme.toml
Normal 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кг 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 = "240кг 3чел."
|
||||
17
rk_nku_configs/rk3399_scheme.toml
Normal file
17
rk_nku_configs/rk3399_scheme.toml
Normal 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
13
rk_nku_menu/Cargo.toml
Normal 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
133
rk_nku_menu/src/main.rs
Normal 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
12
rk_nku_sync/Cargo.toml
Normal 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
128
rk_nku_sync/src/main.rs
Normal 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}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
124
rk_nku_sync/src/nku_client.rs
Normal file
124
rk_nku_sync/src/nku_client.rs
Normal 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(¶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<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(¶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(())
|
||||
}
|
||||
}
|
||||
38
rk_nku_sync/src/serial_config.rs
Normal file
38
rk_nku_sync/src/serial_config.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue