# tui_menu 0.2.2 - переход на Cursive
This commit is contained in:
parent
0922c81c0a
commit
80b100ad66
35 changed files with 1325 additions and 584 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
820
Cargo.lock
generated
820
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,7 @@ members = [
|
|||
"menu_tui",
|
||||
"protocol_lib",
|
||||
"rk_nku_sync",
|
||||
"rk_nku_menu",
|
||||
"rk_nku_menu", "logger",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
|
|||
|
|
@ -5,3 +5,8 @@ default-target = "aarch64-unknown-linux-gnu"
|
|||
[build.zig]
|
||||
enable = true # Сборка с участием линковщика Zig
|
||||
version = "2.31" # Версия glibc, установленная на RK3399-ROC-PC-PRO
|
||||
|
||||
# [target.aarch64-unknown-linux-gnu]
|
||||
# pre-build = [
|
||||
# "apt-get update && apt-get install --assume-yes libxkbcommon"
|
||||
# ]
|
||||
BIN
config_lib/.DS_Store
vendored
BIN
config_lib/.DS_Store
vendored
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "config_lib"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -26,12 +26,24 @@ impl DeviceConfig {
|
|||
debug!("Parameter list: {:#?}", parameter_list);
|
||||
let mut parameter_map = HashMap::new();
|
||||
|
||||
for parameter in parameter_list {
|
||||
'mapify: for parameter in parameter_list {
|
||||
// Пропускаем параметры, которые не требуют синхронизации с внешним MCU
|
||||
// Актуально для {protocol_name}_scheme.toml
|
||||
if let Ok(should_sync) = toml_config.get_string_value(¶meter, "should_sync") {
|
||||
match should_sync.as_str() {
|
||||
"false" => {
|
||||
continue 'mapify;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +56,7 @@ impl DeviceConfig {
|
|||
}
|
||||
|
||||
/// ## Сохранение текущих значений параметров в TOML-файл `self.schema_path`
|
||||
/// TODO: параметры с `should_sync = false` не сохраняются???
|
||||
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())?;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod device_config;
|
||||
pub mod serial_config;
|
||||
mod toml_parser;
|
||||
|
|
|
|||
38
config_lib/src/serial_config.rs
Normal file
38
config_lib/src/serial_config.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use crate::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 smart_serial_parameters = DeviceConfig::create_parameter_list(path_to_scheme)?;
|
||||
|
||||
let serial_name = smart_serial_parameters.get_parameter_value("device")?;
|
||||
let serial_baudrate = smart_serial_parameters
|
||||
.get_parameter_value("baudrate")?
|
||||
.parse::<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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# Настройка рабочего окружения
|
||||
|
||||
## Кросскомпиляция
|
||||
|
||||
- [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]`
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Установка необходимых расширений
|
||||
|
||||
```bash
|
||||
cat vs_code_extensions_list.txt | xargs -n 1 code --install-extension
|
||||
```
|
||||
Binary file not shown.
14
logger/Cargo.toml
Normal file
14
logger/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "logger"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
protocol_lib = { path = "../protocol_lib" }
|
||||
config_lib = { path = "../config_lib" }
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
serialport = { workspace = true }
|
||||
signal-hook = "0.3"
|
||||
179
logger/src/logger.rs
Normal file
179
logger/src/logger.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
/// ### Модуль логгирования в файл
|
||||
/// - Предоставляет функционал для записи логов в файл с временными метками
|
||||
/// - Каждые сутки создает новый файл для записи
|
||||
/// - Предоставляет статистику: количество корректных сообщений, количество ошибок, количество созданных файлов
|
||||
use chrono::{Local, NaiveDate};
|
||||
use log::{trace, warn};
|
||||
use protocol_lib::mu_frame::MUFrame;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufWriter, Write};
|
||||
pub struct RollingFileLogger {
|
||||
/// Директория для хранения логов
|
||||
log_dir: String,
|
||||
/// Текущее время с начала старта
|
||||
current_date: NaiveDate,
|
||||
/// Текущий файл, в который ведётся запись
|
||||
current_file: Option<BufWriter<File>>,
|
||||
/// Буфер для промежуточного хранения логов
|
||||
buffer: Vec<String>,
|
||||
/// Емкость буфера (хранит логи перед записью в файл)
|
||||
max_buffer_size: usize,
|
||||
/// Статистика логгера
|
||||
stats: LoggerStats,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
/// Статистика текущей сессии логгирования
|
||||
pub struct LoggerStats {
|
||||
/// Корректно принятые, распакованные сообщения
|
||||
pub total_messages: u64,
|
||||
/// Количество возникших ошибок (парсинг, таймаут приема)
|
||||
pub total_errors: u64,
|
||||
/// Количество созданных файлов
|
||||
pub files_rotated: u64,
|
||||
}
|
||||
|
||||
impl RollingFileLogger {
|
||||
pub fn new(log_dir: &str, max_buffer_size: usize) -> io::Result<Self> {
|
||||
std::fs::create_dir_all(log_dir)?;
|
||||
|
||||
let logger = Self {
|
||||
log_dir: log_dir.to_string(),
|
||||
current_date: Local::now().date_naive(),
|
||||
current_file: None,
|
||||
buffer: Vec::new(),
|
||||
max_buffer_size,
|
||||
stats: LoggerStats::default(),
|
||||
};
|
||||
|
||||
Ok(logger)
|
||||
}
|
||||
|
||||
/// Логгирование распакованного сообщения
|
||||
pub fn log_frame(&mut self, opcode: u8, payload: &[u8]) -> io::Result<()> {
|
||||
let payload_str = String::from_utf8_lossy(payload);
|
||||
let message = format!(
|
||||
"СООБЩЕНИЕ | ТИП: {} | ДАННЫЕ: {}",
|
||||
MUFrame::get_opcode_description(opcode),
|
||||
payload_str
|
||||
);
|
||||
|
||||
trace!("{}", message);
|
||||
|
||||
self.log(message)?;
|
||||
self.stats.total_messages += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Логгирование ошибки
|
||||
pub fn log_error(&mut self, error: &str) -> io::Result<()> {
|
||||
let message = format!("ОШИБКА | {}", error);
|
||||
self.log(message)?;
|
||||
self.stats.total_errors += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn log(&mut self, message: String) -> io::Result<()> {
|
||||
let now = Local::now();
|
||||
let new_date = now.date_naive();
|
||||
|
||||
// Прошли сутки - создаем новый файл для записи
|
||||
if new_date != self.current_date {
|
||||
self.flush()?;
|
||||
self.current_file = None;
|
||||
self.current_date = new_date;
|
||||
self.stats.files_rotated += 1;
|
||||
|
||||
warn!("[LOG] Date changed, new log file created: {}", new_date);
|
||||
}
|
||||
if self.current_file.is_none() {
|
||||
let file = self.open_log_file()?;
|
||||
self.current_file = Some(BufWriter::new(file));
|
||||
}
|
||||
|
||||
let timestamped = format!("[{}] {}", now.format("%Y-%m-%d %H:%M:%S%.3f"), message);
|
||||
|
||||
self.buffer.push(timestamped);
|
||||
|
||||
// Запись в файл после заполнения буфера
|
||||
if self.buffer.len() >= self.max_buffer_size {
|
||||
self.flush()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// **Открытие файла для записи**
|
||||
/// > Файл будет создан, если он не существует
|
||||
fn open_log_file(&self) -> io::Result<File> {
|
||||
let filename = format!(
|
||||
"{}/uart_log_{}.txt",
|
||||
self.log_dir,
|
||||
self.current_date.format("%Y-%m-%d")
|
||||
);
|
||||
|
||||
OpenOptions::new().create(true).append(true).open(&filename)
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
if let Some(ref mut file) = self.current_file {
|
||||
for line in self.buffer.drain(..) {
|
||||
writeln!(file, "{}", line)?;
|
||||
}
|
||||
file.flush()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Получение статистики текущей сессии логгирования
|
||||
pub fn stats(&self) -> &LoggerStats {
|
||||
&self.stats
|
||||
}
|
||||
|
||||
/// Получение имени файла, в который в данный момент ведётся запись
|
||||
pub fn current_log_file(&self) -> String {
|
||||
format!(
|
||||
"{}/uart_log_{}.txt",
|
||||
self.log_dir,
|
||||
self.current_date.format("%Y-%m-%d")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RollingFileLogger {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.flush();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_log_error() {
|
||||
let mut logger = RollingFileLogger::new("tests_output", 10).unwrap();
|
||||
logger.log_error("Test error").unwrap();
|
||||
assert_eq!(logger.stats.total_errors, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_frame() {
|
||||
let mut logger = RollingFileLogger::new("tests_output", 10).unwrap();
|
||||
logger
|
||||
.log_frame(0x01, "That was test frame #1".as_bytes())
|
||||
.unwrap();
|
||||
assert_eq!(logger.stats.total_messages, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flush() {
|
||||
let mut logger = RollingFileLogger::new("tests_output", 10).unwrap();
|
||||
logger
|
||||
.log_frame(0x01, "That was test frame #2".as_bytes())
|
||||
.unwrap();
|
||||
logger.flush().unwrap();
|
||||
assert_eq!(logger.stats.total_messages, 1);
|
||||
}
|
||||
}
|
||||
135
logger/src/main.rs
Normal file
135
logger/src/main.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
|
||||
use crate::logger::RollingFileLogger;
|
||||
use config_lib::serial_config::SerialPortConfig;
|
||||
use log::{error, info, warn};
|
||||
use protocol_lib::inc_parser::{FrameParser, ParseResult};
|
||||
use signal_hook::consts::{SIGINT, SIGTERM};
|
||||
use signal_hook::iterator::Signals;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Путь до файла-схемы параметров Serial порта устройства
|
||||
const SERIAL_PORT_CONFIG_PATH: &str = "rk_nku_configs/rk3399_scheme.toml";
|
||||
/// Максимальное время ожидания ответа от устройства (`Heartbeat устройства - 60 секунд`)
|
||||
const BOARD_RESPONSE_TIMEOUT_MS: std::time::Duration = std::time::Duration::from_secs(65);
|
||||
/// Размер буфера для хранения принятых данных перед записью в файл
|
||||
const BUFFER_SIZE: usize = 20;
|
||||
/// Путь директории для хранения логов
|
||||
const LOG_DIR: &str = "logs";
|
||||
/// Интервал между записями в лог файл
|
||||
const FLUSH_INTERVAL_MS: u64 = 100;
|
||||
|
||||
mod logger;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
|
||||
// Инициализация обработчиков сигналов ОС
|
||||
let mut signals = Signals::new(&[SIGINT, SIGTERM])?;
|
||||
let handle = signals.handle();
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let running_clone = Arc::clone(&running);
|
||||
|
||||
// Создание потока для обработки сигналов
|
||||
let signal_thread = thread::spawn(move || {
|
||||
for sig in signals.forever() {
|
||||
match sig {
|
||||
SIGINT => {
|
||||
warn!("\n[*] Получен сигнал SIGINT.");
|
||||
running_clone.store(false, Ordering::SeqCst);
|
||||
break;
|
||||
}
|
||||
SIGTERM => {
|
||||
warn!("\n[*] Получен сигнал SIGTERM.");
|
||||
running_clone.store(false, Ordering::SeqCst);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Настройка последовательного порта
|
||||
let serial_port_config = SerialPortConfig::new(SERIAL_PORT_CONFIG_PATH)?;
|
||||
let port_name = serial_port_config.get_serial_name();
|
||||
let baudrate = serial_port_config.get_serial_baudrate();
|
||||
|
||||
info!("{}: {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
||||
info!("Порт: {:<30}", port_name);
|
||||
info!("Скорость: {:<24}", baudrate);
|
||||
info!("Место хранения логов: {:<27}", LOG_DIR);
|
||||
|
||||
let mut serial_port = serialport::new(port_name, baudrate)
|
||||
.timeout(BOARD_RESPONSE_TIMEOUT_MS)
|
||||
.open()?;
|
||||
|
||||
let mut logger = RollingFileLogger::new(LOG_DIR, BUFFER_SIZE)?;
|
||||
let mut parser = FrameParser::new();
|
||||
|
||||
let mut buffer = [0u8; 512];
|
||||
let mut last_flush = std::time::Instant::now();
|
||||
let mut frame_counter = 0;
|
||||
|
||||
while running.load(Ordering::SeqCst) {
|
||||
match serial_port.read(&mut buffer) {
|
||||
Ok(n) if n > 0 => {
|
||||
// Обработка принятого сообщения побайтно
|
||||
for &byte in &buffer[0..n] {
|
||||
match parser.process_raw_byte(byte) {
|
||||
ParseResult::FrameReady(frame) => {
|
||||
logger.log_frame(frame.get_opcode(), frame.get_data().as_slice())?;
|
||||
|
||||
frame_counter += 1;
|
||||
|
||||
if frame_counter % 10 == 0 {
|
||||
info!("[*] Записано {} сообщений", frame_counter);
|
||||
}
|
||||
}
|
||||
ParseResult::Error(e) => {
|
||||
logger.log_error(&e)?;
|
||||
error!("[!] Ошибка парсинга: {}", e);
|
||||
}
|
||||
ParseResult::Incomplete => {}
|
||||
}
|
||||
}
|
||||
last_flush = std::time::Instant::now();
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Ошибка при чтении порта: {}", e);
|
||||
logger.log_error(&error_msg)?;
|
||||
error!("[!] {}", error_msg);
|
||||
|
||||
// Try to reconnect
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic flush
|
||||
if last_flush.elapsed() > Duration::from_millis(FLUSH_INTERVAL_MS) {
|
||||
logger.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
warn!("\n[*] Ожидания закрытие потока чтения сигналов ОС...");
|
||||
handle.close();
|
||||
let _ = signal_thread.join();
|
||||
|
||||
// Final flush
|
||||
logger.flush()?;
|
||||
|
||||
let stats = logger.stats();
|
||||
|
||||
info!("########################################################");
|
||||
info!(". Статистика сессии .");
|
||||
info!("Всего сообщений: {:<26}в•‘", stats.total_messages);
|
||||
info!("Всего ошибок: {:<26}в•‘", stats.total_errors);
|
||||
info!("Создано файлов с логами: {:<25}в•‘", stats.files_rotated);
|
||||
info!("Текущий файл: {:<30}в•‘", logger.current_log_file());
|
||||
info!("########################################################");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
5
logger/tests_output/uart_log_2025-12-10.txt
Normal file
5
logger/tests_output/uart_log_2025-12-10.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[2025-12-10 14:34:29.234] ERROR | Test error
|
||||
[2025-12-10 14:34:29.234] FRAME | OPCODE: 0x01 | CRC: 0x1234 | PAYLOAD: That was test frame
|
||||
[2025-12-10 14:36:07.753] ERROR | Test error
|
||||
[2025-12-10 14:36:07.753] FRAME | OPCODE: 0x01 | PAYLOAD: That was test frame #1
|
||||
[2025-12-10 14:36:07.753] FRAME | OPCODE: 0x01 | PAYLOAD: That was test frame #2
|
||||
3
logger/tests_output/uart_log_2025-12-11.txt
Normal file
3
logger/tests_output/uart_log_2025-12-11.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[2025-12-11 11:26:46.696] СООБЩЕНИЕ | ТИП: ОШИБКА | ДАННЫЕ: That was test frame #2
|
||||
[2025-12-11 11:26:46.696] ОШИБКА | Test error
|
||||
[2025-12-11 11:26:46.696] СООБЩЕНИЕ | ТИП: ОШИБКА | ДАННЫЕ: That was test frame #1
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "menu_tui"
|
||||
version = "0.2.0"
|
||||
version = "0.2.2"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
# Рекомендации по улучшению
|
||||
|
||||
1.Рефакторинг обработки ошибок: Заменить все `unwrap()` / `expect()` на proper error handling с кастомным error enum
|
||||
2.Добавить логирование: Интегрировать `log` / `defmt` с выводом на UART для embedded-отладки
|
||||
3.Типобезопасные идентификаторы: Использовать `enum` вместо строк для имен `view`
|
||||
4.Оптимизация памяти: Рассмотреть `Rc<RefCell<...>>` для state management
|
||||
5.Unit-тесты: Покрыть тестами критичные модули (`DeviceParameters`, `NavigationManager`)
|
||||
6.Реализовать `update_view`: Добавить функционал обновления прошивки через HTTP/UART
|
||||
7.Документация: Дополнить `rustdoc` комментариями и примерами использования
|
||||
|
|
@ -57,7 +57,7 @@ fn simulate_gpio_handlers(nav_manager: NavigationManager) {
|
|||
thread::sleep(Duration::from_secs(3));
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(3000));
|
||||
thread::sleep(Duration::from_millis(4000));
|
||||
|
||||
// Симуляция нажатия кнопки GPIO
|
||||
nav_select.select_item();
|
||||
|
|
|
|||
|
|
@ -12,26 +12,6 @@ use cursive::event::{Event, EventResult, Key};
|
|||
use cursive::views::{CircularFocus, ListView, SelectView};
|
||||
use cursive::{CbSink, Cursive, View};
|
||||
|
||||
// TODO: Перенести в отдельный модуль
|
||||
enum ActiveMenuView {
|
||||
MainView,
|
||||
ConfigView,
|
||||
ExitView,
|
||||
UpdateView,
|
||||
}
|
||||
|
||||
impl From<&str> for ActiveMenuView {
|
||||
fn from(value: &str) -> Self {
|
||||
match value {
|
||||
"main_menu" => ActiveMenuView::MainView,
|
||||
"config_menu" => ActiveMenuView::ConfigView,
|
||||
"exit_menu" => ActiveMenuView::ExitView,
|
||||
"update_menu" => ActiveMenuView::UpdateView,
|
||||
_ => ActiveMenuView::MainView,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Менеджер навигации для управления меню c вызывающей стороны
|
||||
pub struct NavigationManager {
|
||||
// CbSink — это thread-safe канал, позволяющий отправлять callback-функции из любого потока.
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ pub(crate) fn show_config_view(siv: &mut Cursive) {
|
|||
// Кнопка возврата в главное меню
|
||||
parameter_list.add_child(
|
||||
"",
|
||||
Button::new("Назад в главное меню", |s| {
|
||||
Button::new("Назад", |s| {
|
||||
let state: MenuAppState = s
|
||||
.take_user_data()
|
||||
.expect("Не удалось выполнить take_user_data");
|
||||
|
|
|
|||
BIN
protocol_lib/.DS_Store
vendored
Normal file
BIN
protocol_lib/.DS_Store
vendored
Normal file
Binary file not shown.
|
|
@ -1,93 +0,0 @@
|
|||
# **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]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use log::{debug, error, warn};
|
||||
use log::{error, warn};
|
||||
|
||||
use crate::mu_frame::MUFrame;
|
||||
use std::time::Duration;
|
||||
|
|
|
|||
158
protocol_lib/src/inc_parser.rs
Normal file
158
protocol_lib/src/inc_parser.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
use crate::{
|
||||
MUFrame,
|
||||
mu_frame::{SYNC1, SYNC2},
|
||||
};
|
||||
|
||||
/// Состояния парсера фреймов
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ParserState {
|
||||
WaitingForSync1,
|
||||
WaitingForLength,
|
||||
WaitingForOpcode,
|
||||
WaitingForPayload { length: u8, read: u8 },
|
||||
WaitingForCrcLow,
|
||||
WaitingForCrcHigh,
|
||||
WaitingForSync2,
|
||||
}
|
||||
|
||||
/// Контекст парсера фрейма
|
||||
pub struct FrameParser {
|
||||
state: ParserState,
|
||||
frame_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Результат парсинга данных принятых по UART
|
||||
pub enum ParseResult {
|
||||
/// Фрейм полностью принят
|
||||
FrameReady(MUFrame),
|
||||
/// Ошибка парсинга
|
||||
Error(String),
|
||||
/// Фрейм принят не полностью
|
||||
Incomplete,
|
||||
}
|
||||
|
||||
impl FrameParser {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: ParserState::WaitingForSync1,
|
||||
frame_buffer: Vec::with_capacity(256),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_raw_byte(&mut self, byte: u8) -> ParseResult {
|
||||
match self.state {
|
||||
// Чтение SYNC1 байта
|
||||
ParserState::WaitingForSync1 => {
|
||||
if byte == SYNC1 {
|
||||
self.frame_buffer.clear();
|
||||
self.frame_buffer.push(byte);
|
||||
self.state = ParserState::WaitingForLength;
|
||||
}
|
||||
ParseResult::Incomplete
|
||||
}
|
||||
// Чтение длины payload
|
||||
ParserState::WaitingForLength => {
|
||||
if byte == 0 {
|
||||
self.state = ParserState::WaitingForSync1;
|
||||
return ParseResult::Error("Invalid payload length".to_string());
|
||||
}
|
||||
self.frame_buffer.push(byte);
|
||||
self.state = ParserState::WaitingForOpcode;
|
||||
ParseResult::Incomplete
|
||||
}
|
||||
// Чтение opcode
|
||||
ParserState::WaitingForOpcode => {
|
||||
if !MUFrame::is_opcode_correct(byte) {
|
||||
self.state = ParserState::WaitingForSync1;
|
||||
return ParseResult::Error("Invalid opcode".to_string());
|
||||
}
|
||||
self.frame_buffer.push(byte);
|
||||
let length = self.frame_buffer[1];
|
||||
self.state = ParserState::WaitingForPayload { length, read: 0 };
|
||||
ParseResult::Incomplete
|
||||
}
|
||||
// Чтение payload
|
||||
ParserState::WaitingForPayload { length, read } => {
|
||||
self.frame_buffer.push(byte);
|
||||
let new_read = read + 1;
|
||||
|
||||
self.state = if new_read >= length {
|
||||
ParserState::WaitingForCrcLow
|
||||
} else {
|
||||
ParserState::WaitingForPayload {
|
||||
length,
|
||||
read: new_read,
|
||||
}
|
||||
};
|
||||
ParseResult::Incomplete
|
||||
}
|
||||
// Чтение CRC - LSB
|
||||
ParserState::WaitingForCrcLow => {
|
||||
self.frame_buffer.push(byte);
|
||||
self.state = ParserState::WaitingForCrcHigh;
|
||||
ParseResult::Incomplete
|
||||
}
|
||||
// Чтение CRC - MSB
|
||||
ParserState::WaitingForCrcHigh => {
|
||||
self.frame_buffer.push(byte);
|
||||
self.state = ParserState::WaitingForSync2;
|
||||
ParseResult::Incomplete
|
||||
}
|
||||
// Чтение SYNC2
|
||||
ParserState::WaitingForSync2 => {
|
||||
self.frame_buffer.push(byte);
|
||||
|
||||
if byte != SYNC2 {
|
||||
self.state = ParserState::WaitingForSync1;
|
||||
return ParseResult::Error("Invalid SYNC2 byte".to_string());
|
||||
}
|
||||
|
||||
// Попытка десериализации в MuFrame
|
||||
let result = match MUFrame::deserialize(&self.frame_buffer) {
|
||||
Ok(frame) => ParseResult::FrameReady(frame),
|
||||
Err(e) => ParseResult::Error(e),
|
||||
};
|
||||
self.state = ParserState::WaitingForSync1;
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Сброс состояния парсера в начальное
|
||||
pub fn reset(&mut self) {
|
||||
self.state = ParserState::WaitingForSync1;
|
||||
self.frame_buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FrameParser {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::MUFrame;
|
||||
|
||||
#[test]
|
||||
fn test_parsing() {
|
||||
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 mut parser = FrameParser::new();
|
||||
let deserilized_frame = MUFrame::deserialize(&serialized_vec).unwrap();
|
||||
assert!(deserilized_frame.validate_frame().is_ok());
|
||||
|
||||
for byte in serialized_vec {
|
||||
let result = parser.process_raw_byte(byte);
|
||||
if let ParseResult::FrameReady(frame) = result {
|
||||
assert_eq!(frame, deserilized_frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
pub mod client;
|
||||
mod mu_frame;
|
||||
pub mod inc_parser;
|
||||
pub mod mu_frame;
|
||||
|
||||
use crate::inc_parser::{FrameParser, ParseResult};
|
||||
use crate::mu_frame::MUFrame;
|
||||
|
||||
use log::trace;
|
||||
use log::{trace, warn};
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
thread,
|
||||
|
|
@ -23,6 +25,35 @@ fn send_proto_message<Writer: Write>(data: MUFrame, mut writer: Writer) -> Resul
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Прием сообщения
|
||||
fn _recv_proto_messages<Reader: Read>(mut reader: Reader) -> Result<Vec<MUFrame>, String> {
|
||||
let mut read_buffer = [0; 512];
|
||||
let mut parser = FrameParser::new();
|
||||
let mut result = Vec::new();
|
||||
|
||||
match reader.read(&mut read_buffer) {
|
||||
Ok(n) if n > 0 => {
|
||||
trace!("Received raw message: {read_buffer:?}");
|
||||
for byte in &read_buffer[0..n] {
|
||||
match parser.process_raw_byte(*byte) {
|
||||
ParseResult::FrameReady(frame) => {
|
||||
result.push(frame);
|
||||
parser.reset();
|
||||
}
|
||||
ParseResult::Error(e) => {
|
||||
warn!("Error parsing frame: {}", e);
|
||||
}
|
||||
ParseResult::Incomplete => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => return Err(e.to_string()),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Прием сообщения
|
||||
fn recv_proto_message<Reader: Read>(mut reader: Reader) -> Result<MUFrame, String> {
|
||||
let mut raw_frame = Vec::new();
|
||||
|
|
@ -81,4 +112,28 @@ mod tests {
|
|||
assert_eq!(received_frame.get_data(), frame_to_send.get_data());
|
||||
assert_eq!(received_frame, frame_to_send);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_and_multiple_recv() {
|
||||
let mut frame_to_send_1 = MUFrame::new();
|
||||
frame_to_send_1
|
||||
.set_data(b"get server_info\n".to_vec())
|
||||
.unwrap();
|
||||
|
||||
let mut frame_to_send_2 = MUFrame::new();
|
||||
frame_to_send_2
|
||||
.set_data(b"Just for test\n".to_vec())
|
||||
.unwrap();
|
||||
|
||||
let mut buf_1 = Vec::new();
|
||||
let mut buf_2 = Vec::new();
|
||||
send_proto_message(frame_to_send_1.clone(), &mut buf_1).unwrap();
|
||||
send_proto_message(frame_to_send_2.clone(), &mut buf_2).unwrap();
|
||||
|
||||
let doubled_buf = [buf_1.clone(), buf_2.clone()].concat();
|
||||
let received_frames = _recv_proto_messages(&doubled_buf[..]).unwrap();
|
||||
assert_eq!(received_frames.len(), 2);
|
||||
assert_eq!(received_frames[0], frame_to_send_1);
|
||||
assert_eq!(received_frames[1], frame_to_send_2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
const SYNC1: u8 = 0xAA;
|
||||
const SYNC2: u8 = 0xBB;
|
||||
const MAX_DATA_SIZE: u8 = u8::MAX;
|
||||
const CONSOLE_OPCODE: u8 = 0xC0;
|
||||
pub const SYNC1: u8 = 0xAA;
|
||||
pub const SYNC2: u8 = 0xBB;
|
||||
pub const MAX_DATA_SIZE: u8 = u8::MAX;
|
||||
|
||||
pub const OPCODE_ERROR: u8 = 0x01;
|
||||
pub const OPCODE_LOG: u8 = 0x19;
|
||||
pub const OPCODE_DATA: u8 = 0xDA;
|
||||
pub const OPCODE_CONSOLE: u8 = 0xC0;
|
||||
pub const OPCODE_BOOTLOADER: u8 = 0xB1;
|
||||
/// Пакет данных протокола "МЮ"
|
||||
///
|
||||
///
|
||||
|
|
@ -33,7 +37,7 @@ impl MUFrame {
|
|||
Self {
|
||||
prefix: SYNC1,
|
||||
length: 0,
|
||||
opcode: CONSOLE_OPCODE,
|
||||
opcode: OPCODE_CONSOLE,
|
||||
data: Vec::with_capacity(MAX_DATA_SIZE as usize),
|
||||
crc_low: 0x00,
|
||||
crc_high: 0x00,
|
||||
|
|
@ -41,10 +45,30 @@ impl MUFrame {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_data(&self) -> &Vec<u8> {
|
||||
pub fn get_data(&self) -> &Vec<u8> {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn get_opcode(&self) -> u8 {
|
||||
self.opcode
|
||||
}
|
||||
|
||||
/// Проверка корректности опкода (ERROR, LOG, DATA, CONSOLE)
|
||||
pub fn is_opcode_correct(opcode: u8) -> bool {
|
||||
return [OPCODE_ERROR, OPCODE_LOG, OPCODE_DATA, OPCODE_CONSOLE].contains(&opcode);
|
||||
}
|
||||
|
||||
/// Получение текстового описания опкода фрейма
|
||||
pub fn get_opcode_description(opcode: u8) -> String {
|
||||
match opcode {
|
||||
OPCODE_ERROR => "ОШИБКА".to_string(),
|
||||
OPCODE_LOG => "СТАТУС".to_string(),
|
||||
OPCODE_DATA => "ДАННЫЕ".to_string(),
|
||||
OPCODE_CONSOLE => "КОНСОЛЬ".to_string(),
|
||||
_ => "UNKNOWN".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Загрузка данных в фрейм, вычисление CRC и длины
|
||||
pub(crate) fn set_data(&mut self, data: Vec<u8>) -> Result<(), String> {
|
||||
if data.len() > MAX_DATA_SIZE as usize || data.is_empty() {
|
||||
|
|
@ -78,7 +102,7 @@ impl MUFrame {
|
|||
|
||||
frame.suffix = data[5 + frame.length as usize];
|
||||
|
||||
frame.invalidate_frame()?;
|
||||
frame.validate_frame()?;
|
||||
|
||||
Ok(frame)
|
||||
}
|
||||
|
|
@ -98,7 +122,7 @@ impl MUFrame {
|
|||
}
|
||||
|
||||
/// Проверка валидности фрейма
|
||||
fn invalidate_frame(&self) -> Result<(), String> {
|
||||
pub(crate) fn validate_frame(&self) -> Result<(), String> {
|
||||
if !self.is_prefix_correct() {
|
||||
return Err("Bad prefix".to_string());
|
||||
}
|
||||
|
|
@ -203,7 +227,7 @@ mod tests {
|
|||
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.opcode, OPCODE_CONSOLE);
|
||||
assert_eq!(frame.data, b"#STM:L0:R16:A1:S0:M0:E#\r\n\0");
|
||||
}
|
||||
|
||||
|
|
@ -218,8 +242,8 @@ mod tests {
|
|||
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.opcode, OPCODE_CONSOLE);
|
||||
assert_eq!(frame.data, b"#STM:L16:R16:A1:S2:M0:E#\r\n\0");
|
||||
frame.invalidate_frame().unwrap();
|
||||
frame.validate_frame().unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
rk_nku_configs/menu_style.toml
Normal file
25
rk_nku_configs/menu_style.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# First come some various options
|
||||
shadow = false # Don't draw shadows around stacked views
|
||||
borders = "simple" # Alternatives are "none" and "outset"
|
||||
|
||||
# Here we define the color palette.
|
||||
[colors]
|
||||
background = "#000000"
|
||||
# If the value is an array, the first valid color will be used.
|
||||
# If the terminal doesn't support custom color,
|
||||
# non-base colors will be skipped.
|
||||
shadow = ["#000000", "black"]
|
||||
view = "#d3d7cf"
|
||||
|
||||
# Array and simple values have the same effect.
|
||||
primary = ["#111111"]
|
||||
secondary = "#EEEEEE"
|
||||
tertiary = "#444444"
|
||||
|
||||
# Hex values can use lower or uppercase.
|
||||
title_primary = "#0000FF"
|
||||
title_secondary = "#ffff55"
|
||||
|
||||
# Lower precision values can use only 3 digits.
|
||||
highlight = "#00F"
|
||||
highlight_inactive = "#5555FF"
|
||||
|
|
@ -3,61 +3,100 @@
|
|||
# possible_values - список возможных значений параметра
|
||||
# default - значение по умолчанию (одно из possible_values)
|
||||
# current - актуальное текущее значение параметра (одно из possible_values). Если не указано или некорректно, то используется значение по умолчанию
|
||||
# should_sync - true, если параметр необходимо синхронизировать (pull & push) с внешним микроконтроллером
|
||||
|
||||
[groupnumber]
|
||||
name = "Номер в группе"
|
||||
possible_values = [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
"13",
|
||||
"14",
|
||||
"15",
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
"13",
|
||||
"14",
|
||||
"15",
|
||||
]
|
||||
default = "0"
|
||||
current = "6"
|
||||
default = "1"
|
||||
current = "1"
|
||||
should_sync = "true"
|
||||
|
||||
[soundvolume]
|
||||
name = "Громкость звука"
|
||||
possible_values = ["0%", "25%", "50%", "75%", "100%"]
|
||||
possible_values = [
|
||||
"0%",
|
||||
"25%",
|
||||
"50%",
|
||||
"75%",
|
||||
"100%"
|
||||
]
|
||||
default = "50%"
|
||||
current = "100%"
|
||||
current = "50%"
|
||||
should_sync = "true"
|
||||
|
||||
[musicvolume]
|
||||
name = "Громкость музыки"
|
||||
possible_values = ["0%", "25%", "50%", "75%", "100%"]
|
||||
possible_values = [
|
||||
"0%",
|
||||
"25%",
|
||||
"50%",
|
||||
"75%",
|
||||
"100%"
|
||||
]
|
||||
default = "0%"
|
||||
current = "100%"
|
||||
current = "25%"
|
||||
should_sync = "true"
|
||||
|
||||
[loadcapacity]
|
||||
name = "Грузоподъемность"
|
||||
possible_values = [
|
||||
"СКРЫТО",
|
||||
"240кг 3чел.",
|
||||
"320кг 4чел.",
|
||||
"400кг 5чел.",
|
||||
"450кг 6чел.",
|
||||
"525кг 7чел.",
|
||||
"630кг 8чел.",
|
||||
"800кг 10чел.",
|
||||
"800кг 11чел.",
|
||||
"1000кг 13чел.",
|
||||
"1150кг 15чел.",
|
||||
"1275кг 16чел.",
|
||||
"1275кг 17чел.",
|
||||
"1425кг 18чел.",
|
||||
"1600кг 20чел.",
|
||||
"1600кг 21чел.",
|
||||
"СКРЫТО",
|
||||
"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чел."
|
||||
current = "400кг 5чел."
|
||||
should_sync = "true"
|
||||
|
||||
[cubicmedia]
|
||||
name = "Cubic Media"
|
||||
possible_values = [
|
||||
"ВЫКЛ",
|
||||
"ВКЛ"
|
||||
]
|
||||
default = "ВЫКЛ"
|
||||
current = "ВКЛ"
|
||||
should_sync = "true"
|
||||
|
||||
[network_indicator_poll_interval]
|
||||
name = "Опрос статуса сети"
|
||||
possible_values = [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"60s"
|
||||
]
|
||||
default = "5s"
|
||||
current = "5s"
|
||||
should_sync = "false"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
[package]
|
||||
name = "rk_smart_menu"
|
||||
version = "0.1.0"
|
||||
name = "rk_nku_menu"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = "2.0"
|
||||
gpiocdev-embedded-hal = "0.1.2"
|
||||
gpiocdev = "0.7.3"
|
||||
sysfs_gpio = { version = "0.6.2" }
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
config_lib = { path = "../config_lib" }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use gpiocdev::Request;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use log::debug;
|
||||
use menu_tui::menu_navigation::NavigationManager;
|
||||
use menu_tui::menu_process::DeviceMenu;
|
||||
use std::time::Duration;
|
||||
use sysfs_gpio::{Direction, Edge, Pin};
|
||||
/// ### Путь к файлу-схемы параметров устройства
|
||||
const NKU_DEVICE_CONFIG_PATH: &str = "rk_smart_configs/smart_scheme.toml";
|
||||
const TUI_APP_CONFIG_PATH: &str = "rk_smart_configs/menu_style.toml";
|
||||
const NKU_DEVICE_CONFIG_PATH: &str = "rk_nku_configs/nku_scheme.toml";
|
||||
const TUI_APP_CONFIG_PATH: &str = "rk_nku_configs/menu_style.toml";
|
||||
|
||||
/// Максимальное время бездействия [c], после достижения которого происходит выход из меню
|
||||
const IDLE_TIMEOUT_SEC: u64 = 30;
|
||||
|
|
@ -42,72 +43,75 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
/// ligpiod номер порта кнопки ввода
|
||||
const S1_BTN: (&str, u8) = ("/dev/gpiochip3", 16);
|
||||
/// ligpiod номер порта кнопки выбора
|
||||
const S2_BTN: (&str, u8) = ("/dev/gpiochip2", 1);
|
||||
/// ## Симуляция обработчиков GPIO кнопок
|
||||
/// ### Расчёт номера пина в порядке libgpiod
|
||||
/// Расчёт номера пина в порядке 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;
|
||||
///
|
||||
/// gpiochip_number = pin_number / 32; // Номер банка
|
||||
/// line_offset = pin_number % 32; // Номер пина в банке
|
||||
/// ```
|
||||
///
|
||||
|
||||
/// SYSFS номер порта кнопки ввода
|
||||
const S1_BTN: u8 = 124; //124
|
||||
/// SYSFS номер порта кнопки выбора
|
||||
const S2_BTN: u8 = 125; //125
|
||||
|
||||
fn gpio_navigation_handlers(nav_manager: NavigationManager) {
|
||||
// Кнопка 1 (GPIO): Навигация вниз
|
||||
let nav_down = nav_manager.clone();
|
||||
std::thread::spawn(move || {
|
||||
let down_btn_req = Request::builder()
|
||||
.on_chip(S1_BTN.0)
|
||||
.with_line(S1_BTN.1 as u32)
|
||||
.as_input()
|
||||
.with_edge_detection(gpiocdev::line::EdgeDetection::RisingEdge)
|
||||
.request()
|
||||
.unwrap();
|
||||
debug!("down_irq_thread launched!");
|
||||
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()?;
|
||||
|
||||
let mut last_press = Instant::now() - Duration::from_millis(1000);
|
||||
let debounce = Duration::from_millis(350);
|
||||
|
||||
loop {
|
||||
if down_btn_req.has_edge_event().unwrap() {
|
||||
if let Ok(edge) = down_btn_req.read_edge_event() {
|
||||
if edge.kind == gpiocdev::line::EdgeKind::Rising {
|
||||
debug!("DownArrow pressed");
|
||||
// Симуляция нажатия кнопки DOWN
|
||||
if let Some(pin_value) = poller.poll(1000)? {
|
||||
if pin_value == 1 {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(last_press) >= debounce {
|
||||
last_press = now;
|
||||
nav_down.navigate_down();
|
||||
std::thread::sleep(Duration::from_millis(400));
|
||||
} else {
|
||||
// Событие проигнорировано как дребезг
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Кнопка 2 (GPIO): Выбор/подтверждение
|
||||
let nav_select = nav_manager.clone();
|
||||
let nav_down = nav_manager.clone();
|
||||
std::thread::spawn(move || {
|
||||
let return_btn_req = Request::builder()
|
||||
.on_chip(S2_BTN.0)
|
||||
.with_line(S2_BTN.1 as u32)
|
||||
.as_input()
|
||||
.with_edge_detection(gpiocdev::line::EdgeDetection::RisingEdge)
|
||||
.request()
|
||||
.unwrap();
|
||||
let input = Pin::new(S2_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()?;
|
||||
|
||||
let mut last_press = Instant::now() - Duration::from_millis(1000);
|
||||
let debounce = Duration::from_millis(350);
|
||||
loop {
|
||||
if return_btn_req.has_edge_event().unwrap() {
|
||||
if let Ok(edge) = return_btn_req.read_edge_event() {
|
||||
if edge.kind == gpiocdev::line::EdgeKind::Rising {
|
||||
debug!("Return pressed");
|
||||
// Симуляция нажатия кнопки ENTER
|
||||
nav_select.select_item();
|
||||
std::thread::sleep(Duration::from_millis(400));
|
||||
if let Some(pin_value) = poller.poll(1000)? {
|
||||
if pin_value == 1 {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(last_press) >= debounce {
|
||||
last_press = now;
|
||||
nav_down.select_item();
|
||||
} else {
|
||||
// Событие проигнорировано как дребезг
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "rk_nku_sync"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
|
||||
|
|
@ -10,3 +10,4 @@ config_lib = { path = "../config_lib" }
|
|||
clap = { workspace = true }
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ mod serial_config;
|
|||
use std::str::FromStr;
|
||||
|
||||
use clap::Parser;
|
||||
use log::{error, warn};
|
||||
use log::{error, info, 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)]
|
||||
#[command(author = "Akimov Dmitry MU LLC", name = "smart_sync", version = "0.1.0", about, long_about = None)]
|
||||
struct Args {
|
||||
/// Тип команды: pull - запрос сохраненных в устройстве настроек, push - отправка новых настроек
|
||||
#[arg(short = 'm', long = "mode")]
|
||||
|
|
@ -21,7 +21,7 @@ fn main() -> Result<(), String> {
|
|||
let args = Args::parse();
|
||||
|
||||
env_logger::init();
|
||||
warn!("rk_nku_sync> command mode: {:?}", args.mode);
|
||||
info!("rk_nku_sync> command mode: {:?}", args.mode);
|
||||
|
||||
let mut nku_client = nku_client::NkuClient::new()?;
|
||||
|
||||
|
|
@ -60,6 +60,8 @@ fn push_parameters(client: &mut nku_client::NkuClient) -> Result<(), String> {
|
|||
}
|
||||
}
|
||||
|
||||
info!("Parameters have been pushed to device!");
|
||||
|
||||
// Запуск стриминга данных от станции
|
||||
attempts = 1;
|
||||
'start_streaming_loop: loop {
|
||||
|
|
@ -80,7 +82,7 @@ fn push_parameters(client: &mut nku_client::NkuClient) -> Result<(), String> {
|
|||
return Err("Start streaming failed!".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
info!("Data streaming from MCU has been started!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +108,7 @@ fn pull_parameters(client: &mut nku_client::NkuClient) -> Result<(), String> {
|
|||
return Err("Pull request failed!".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
info!("Parameters have been pulled from device!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ pub struct SerialPortConfig {
|
|||
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 smart_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
|
||||
let serial_name = smart_serial_parameters.get_parameter_value("device")?;
|
||||
let serial_baudrate = smart_serial_parameters
|
||||
.get_parameter_value("baudrate")?
|
||||
.parse::<u32>()
|
||||
.map_err(|e: std::num::ParseIntError| e.to_string())?;
|
||||
|
|
|
|||
Loading…
Reference in a new issue