# tui_menu 0.2.2 - переход на Cursive

This commit is contained in:
Dmitry Akimov 2026-06-02 22:55:03 +03:00
parent 0922c81c0a
commit 80b100ad66
35 changed files with 1325 additions and 584 deletions

BIN
.DS_Store vendored

Binary file not shown.

820
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@ members = [
"menu_tui", "menu_tui",
"protocol_lib", "protocol_lib",
"rk_nku_sync", "rk_nku_sync",
"rk_nku_menu", "rk_nku_menu", "logger",
] ]
[workspace.package] [workspace.package]

View file

@ -5,3 +5,8 @@ default-target = "aarch64-unknown-linux-gnu"
[build.zig] [build.zig]
enable = true # Сборка с участием линковщика Zig enable = true # Сборка с участием линковщика Zig
version = "2.31" # Версия glibc, установленная на RK3399-ROC-PC-PRO 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

Binary file not shown.

View file

@ -1,6 +1,6 @@
[package] [package]
name = "config_lib" name = "config_lib"
version = "0.1.0" version = "0.1.1"
edition = "2024" edition = "2024"
authors.workspace = true authors.workspace = true

View file

@ -26,12 +26,24 @@ impl DeviceConfig {
debug!("Parameter list: {:#?}", parameter_list); debug!("Parameter list: {:#?}", parameter_list);
let mut parameter_map = HashMap::new(); 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(&parameter, "should_sync") {
match should_sync.as_str() {
"false" => {
continue 'mapify;
}
_ => (),
}
}
let device_parameter = Parameter { let device_parameter = Parameter {
description: toml_config.get_string_value(&parameter, "name")?, description: toml_config.get_string_value(&parameter, "name")?,
value: toml_config.get_string_value(&parameter, "current")?, value: toml_config.get_string_value(&parameter, "current")?,
possible_values: toml_config.get_array_value(&parameter, "possible_values")?, possible_values: toml_config.get_array_value(&parameter, "possible_values")?,
}; };
parameter_map.insert(parameter, device_parameter); parameter_map.insert(parameter, device_parameter);
} }
@ -44,6 +56,7 @@ impl DeviceConfig {
} }
/// ## Сохранение текущих значений параметров в TOML-файл `self.schema_path` /// ## Сохранение текущих значений параметров в TOML-файл `self.schema_path`
/// TODO: параметры с `should_sync = false` не сохраняются???
pub fn save_parameters_values(&self) -> Result<(), String> { pub fn save_parameters_values(&self) -> Result<(), String> {
let mut toml_config = let mut toml_config =
TomlScheme::new(&self.scheme.get_path_to_scheme_file()).map_err(|e| e.to_string())?; TomlScheme::new(&self.scheme.get_path_to_scheme_file()).map_err(|e| e.to_string())?;

View file

@ -1,2 +1,3 @@
pub mod device_config; pub mod device_config;
pub mod serial_config;
mod toml_parser; mod toml_parser;

View 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
}
}

View file

@ -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]`

View file

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

14
logger/Cargo.toml Normal file
View 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
View 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
View 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(())
}

View 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

View 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

View file

@ -1,6 +1,6 @@
[package] [package]
name = "menu_tui" name = "menu_tui"
version = "0.2.0" version = "0.2.2"
edition = "2024" edition = "2024"
authors.workspace = true authors.workspace = true

View file

@ -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` комментариями и примерами использования

View file

@ -57,7 +57,7 @@ fn simulate_gpio_handlers(nav_manager: NavigationManager) {
thread::sleep(Duration::from_secs(3)); thread::sleep(Duration::from_secs(3));
loop { loop {
thread::sleep(Duration::from_millis(3000)); thread::sleep(Duration::from_millis(4000));
// Симуляция нажатия кнопки GPIO // Симуляция нажатия кнопки GPIO
nav_select.select_item(); nav_select.select_item();

View file

@ -12,26 +12,6 @@ use cursive::event::{Event, EventResult, Key};
use cursive::views::{CircularFocus, ListView, SelectView}; use cursive::views::{CircularFocus, ListView, SelectView};
use cursive::{CbSink, Cursive, View}; 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 вызывающей стороны /// Менеджер навигации для управления меню c вызывающей стороны
pub struct NavigationManager { pub struct NavigationManager {
// CbSink  — это thread-safe канал, позволяющий отправлять callback-функции из любого потока. // CbSink  — это thread-safe канал, позволяющий отправлять callback-функции из любого потока.

View file

@ -63,7 +63,7 @@ pub(crate) fn show_config_view(siv: &mut Cursive) {
// Кнопка возврата в главное меню // Кнопка возврата в главное меню
parameter_list.add_child( parameter_list.add_child(
"", "",
Button::new("Назад в главное меню", |s| { Button::new("Назад", |s| {
let state: MenuAppState = s let state: MenuAppState = s
.take_user_data() .take_user_data()
.expect("Не удалось выполнить take_user_data"); .expect("Не удалось выполнить take_user_data");

BIN
protocol_lib/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -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]
```
---

View file

@ -1,4 +1,4 @@
use log::{debug, error, warn}; use log::{error, warn};
use crate::mu_frame::MUFrame; use crate::mu_frame::MUFrame;
use std::time::Duration; use std::time::Duration;

View 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);
}
}
}
}

View file

@ -1,9 +1,11 @@
pub mod client; 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 crate::mu_frame::MUFrame;
use log::trace; use log::{trace, warn};
use std::{ use std::{
io::{Read, Write}, io::{Read, Write},
thread, thread,
@ -23,6 +25,35 @@ fn send_proto_message<Writer: Write>(data: MUFrame, mut writer: Writer) -> Resul
Ok(()) 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> { fn recv_proto_message<Reader: Read>(mut reader: Reader) -> Result<MUFrame, String> {
let mut raw_frame = Vec::new(); 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.get_data(), frame_to_send.get_data());
assert_eq!(received_frame, frame_to_send); 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);
}
} }

View file

@ -1,10 +1,14 @@
use std::fmt::Display; use std::fmt::Display;
const SYNC1: u8 = 0xAA; pub const SYNC1: u8 = 0xAA;
const SYNC2: u8 = 0xBB; pub const SYNC2: u8 = 0xBB;
const MAX_DATA_SIZE: u8 = u8::MAX; pub const MAX_DATA_SIZE: u8 = u8::MAX;
const CONSOLE_OPCODE: u8 = 0xC0;
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 { Self {
prefix: SYNC1, prefix: SYNC1,
length: 0, length: 0,
opcode: CONSOLE_OPCODE, opcode: OPCODE_CONSOLE,
data: Vec::with_capacity(MAX_DATA_SIZE as usize), data: Vec::with_capacity(MAX_DATA_SIZE as usize),
crc_low: 0x00, crc_low: 0x00,
crc_high: 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 &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 и длины /// Загрузка данных в фрейм, вычисление CRC и длины
pub(crate) fn set_data(&mut self, data: Vec<u8>) -> Result<(), String> { pub(crate) fn set_data(&mut self, data: Vec<u8>) -> Result<(), String> {
if data.len() > MAX_DATA_SIZE as usize || data.is_empty() { 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.suffix = data[5 + frame.length as usize];
frame.invalidate_frame()?; frame.validate_frame()?;
Ok(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() { if !self.is_prefix_correct() {
return Err("Bad prefix".to_string()); return Err("Bad prefix".to_string());
} }
@ -203,7 +227,7 @@ mod tests {
assert!(frame.is_prefix_correct()); assert!(frame.is_prefix_correct());
assert!(frame.is_postfix_correct()); assert!(frame.is_postfix_correct());
assert!(frame.is_crc_valid(0x7780)); 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"); 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_prefix_correct());
assert!(frame.is_postfix_correct()); assert!(frame.is_postfix_correct());
assert!(frame.is_crc_valid(0xB6BB)); 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"); assert_eq!(frame.data, b"#STM:L16:R16:A1:S2:M0:E#\r\n\0");
frame.invalidate_frame().unwrap(); frame.validate_frame().unwrap();
} }
} }

View 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"

View file

@ -3,6 +3,7 @@
# possible_values - список возможных значений параметра # possible_values - список возможных значений параметра
# default - значение по умолчанию (одно из possible_values) # default - значение по умолчанию (одно из possible_values)
# current - актуальное текущее значение параметра (одно из possible_values). Если не указано или некорректно, то используется значение по умолчанию # current - актуальное текущее значение параметра (одно из possible_values). Если не указано или некорректно, то используется значение по умолчанию
# should_sync - true, если параметр необходимо синхронизировать (pull & push) с внешним микроконтроллером
[groupnumber] [groupnumber]
name = "Номер в группе" name = "Номер в группе"
@ -24,20 +25,35 @@ possible_values = [
"14", "14",
"15", "15",
] ]
default = "0" default = "1"
current = "6" current = "1"
should_sync = "true"
[soundvolume] [soundvolume]
name = "Громкость звука" name = "Громкость звука"
possible_values = ["0%", "25%", "50%", "75%", "100%"] possible_values = [
"0%",
"25%",
"50%",
"75%",
"100%"
]
default = "50%" default = "50%"
current = "100%" current = "50%"
should_sync = "true"
[musicvolume] [musicvolume]
name = "Громкость музыки" name = "Громкость музыки"
possible_values = ["0%", "25%", "50%", "75%", "100%"] possible_values = [
"0%",
"25%",
"50%",
"75%",
"100%"
]
default = "0%" default = "0%"
current = "100%" current = "25%"
should_sync = "true"
[loadcapacity] [loadcapacity]
name = "Грузоподъемность" name = "Грузоподъемность"
@ -60,4 +76,27 @@ possible_values = [
"1600кг 21чел.", "1600кг 21чел.",
] ]
default = "СКРЫТО" default = "СКРЫТО"
current = "240кгел." current = "400кгел."
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"

View file

@ -1,13 +1,12 @@
[package] [package]
name = "rk_smart_menu" name = "rk_nku_menu"
version = "0.1.0" version = "0.1.1"
edition = "2024" edition = "2024"
authors.workspace = true authors.workspace = true
[dependencies] [dependencies]
thiserror = "2.0" thiserror = "2.0"
gpiocdev-embedded-hal = "0.1.2" sysfs_gpio = { version = "0.6.2" }
gpiocdev = "0.7.3"
log = { workspace = true } log = { workspace = true }
env_logger = { workspace = true } env_logger = { workspace = true }
config_lib = { path = "../config_lib" } config_lib = { path = "../config_lib" }

View file

@ -1,11 +1,12 @@
use gpiocdev::Request; use std::time::{Duration, Instant};
use log::debug; use log::debug;
use menu_tui::menu_navigation::NavigationManager; use menu_tui::menu_navigation::NavigationManager;
use menu_tui::menu_process::DeviceMenu; 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 NKU_DEVICE_CONFIG_PATH: &str = "rk_nku_configs/nku_scheme.toml";
const TUI_APP_CONFIG_PATH: &str = "rk_smart_configs/menu_style.toml"; const TUI_APP_CONFIG_PATH: &str = "rk_nku_configs/menu_style.toml";
/// Максимальное время бездействия [c], после достижения которого происходит выход из меню /// Максимальное время бездействия [c], после достижения которого происходит выход из меню
const IDLE_TIMEOUT_SEC: u64 = 30; const IDLE_TIMEOUT_SEC: u64 = 30;
@ -42,72 +43,75 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(()); return Ok(());
} }
/// ligpiod номер порта кнопки ввода /// Расчёт номера пина в порядке SYSFS
const S1_BTN: (&str, u8) = ("/dev/gpiochip3", 16);
/// ligpiod номер порта кнопки выбора
const S2_BTN: (&str, u8) = ("/dev/gpiochip2", 1);
/// ## Симуляция обработчиков GPIO кнопок
/// ### Расчёт номера пина в порядке libgpiod
/// ``` /// ```
/// bank = 0; // GPIO0_B5 => 0, bank ∈ [0,4] /// bank = 0; // GPIO0_B5 => 0, bank ∈ [0,4]
/// group = 1; // GPIO0_B5 => 1, group ∈ {(A=0), (B=1), (C=2), (D=3)} /// group = 1; // GPIO0_B5 => 1, group ∈ {(A=0), (B=1), (C=2), (D=3)}
/// X = 5; // GPIO0_B5 => 5, X ∈ [0,7] /// X = 5; // GPIO0_B5 => 5, X ∈ [0,7]
/// number = group * 8 + X = 1 * 8 + 5 = 13; /// number = group * 8 + X = 1 * 8 + 5 = 13;
/// pin = bank * 32 + number = 0 * 32 + 13 = 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) { fn gpio_navigation_handlers(nav_manager: NavigationManager) {
// Кнопка 1 (GPIO): Навигация вниз // Кнопка 1 (GPIO): Навигация вниз
let nav_down = nav_manager.clone(); let nav_down = nav_manager.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let down_btn_req = Request::builder() let input = Pin::new(S1_BTN as u64);
.on_chip(S1_BTN.0) debug!("return_irq_thread launched!");
.with_line(S1_BTN.1 as u32) input.with_exported(|| {
.as_input() input.set_direction(Direction::In)?;
.with_edge_detection(gpiocdev::line::EdgeDetection::RisingEdge) input.set_edge(Edge::RisingEdge)?;
.request() let mut poller = input.get_poller()?;
.unwrap();
debug!("down_irq_thread launched!"); let mut last_press = Instant::now() - Duration::from_millis(1000);
let debounce = Duration::from_millis(350);
loop { loop {
if down_btn_req.has_edge_event().unwrap() { if let Some(pin_value) = poller.poll(1000)? {
if let Ok(edge) = down_btn_req.read_edge_event() { if pin_value == 1 {
if edge.kind == gpiocdev::line::EdgeKind::Rising { let now = Instant::now();
debug!("DownArrow pressed"); if now.duration_since(last_press) >= debounce {
// Симуляция нажатия кнопки DOWN last_press = now;
nav_down.navigate_down(); nav_down.navigate_down();
std::thread::sleep(Duration::from_millis(400)); } else {
// Событие проигнорировано как дребезг
} }
} }
} }
} }
})
}); });
// Кнопка 2 (GPIO): Выбор/подтверждение // Кнопка 2 (GPIO): Выбор/подтверждение
let nav_select = nav_manager.clone(); let nav_down = nav_manager.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let return_btn_req = Request::builder() let input = Pin::new(S2_BTN as u64);
.on_chip(S2_BTN.0)
.with_line(S2_BTN.1 as u32)
.as_input()
.with_edge_detection(gpiocdev::line::EdgeDetection::RisingEdge)
.request()
.unwrap();
debug!("return_irq_thread launched!"); 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 { loop {
if return_btn_req.has_edge_event().unwrap() { if let Some(pin_value) = poller.poll(1000)? {
if let Ok(edge) = return_btn_req.read_edge_event() { if pin_value == 1 {
if edge.kind == gpiocdev::line::EdgeKind::Rising { let now = Instant::now();
debug!("Return pressed"); if now.duration_since(last_press) >= debounce {
// Симуляция нажатия кнопки ENTER last_press = now;
nav_select.select_item(); nav_down.select_item();
std::thread::sleep(Duration::from_millis(400)); } else {
// Событие проигнорировано как дребезг
} }
} }
} }
} }
})
}); });
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "rk_nku_sync" name = "rk_nku_sync"
version = "0.1.0" version = "0.1.1"
edition = "2024" edition = "2024"
authors.workspace = true authors.workspace = true
@ -10,3 +10,4 @@ config_lib = { path = "../config_lib" }
clap = { workspace = true } clap = { workspace = true }
log = { workspace = true } log = { workspace = true }
env_logger = { workspace = true } env_logger = { workspace = true }

View file

@ -4,13 +4,13 @@ mod serial_config;
use std::str::FromStr; use std::str::FromStr;
use clap::Parser; use clap::Parser;
use log::{error, warn}; use log::{error, info, warn};
/// Количество попыток выполнить запрос /// Количество попыток выполнить запрос
const REQUEST_ATTEMPTS: u8 = 5; const REQUEST_ATTEMPTS: u8 = 5;
#[derive(Parser)] #[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 { struct Args {
/// Тип команды: pull - запрос сохраненных в устройстве настроек, push - отправка новых настроек /// Тип команды: pull - запрос сохраненных в устройстве настроек, push - отправка новых настроек
#[arg(short = 'm', long = "mode")] #[arg(short = 'm', long = "mode")]
@ -21,7 +21,7 @@ fn main() -> Result<(), String> {
let args = Args::parse(); let args = Args::parse();
env_logger::init(); 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()?; 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; attempts = 1;
'start_streaming_loop: loop { '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()); return Err("Start streaming failed!".to_string());
} }
} }
info!("Data streaming from MCU has been started!");
Ok(()) Ok(())
} }
@ -106,7 +108,7 @@ fn pull_parameters(client: &mut nku_client::NkuClient) -> Result<(), String> {
return Err("Pull request failed!".to_string()); return Err("Pull request failed!".to_string());
} }
} }
info!("Parameters have been pulled from device!");
Ok(()) Ok(())
} }

View file

@ -9,10 +9,10 @@ pub struct SerialPortConfig {
impl SerialPortConfig { impl SerialPortConfig {
/// Чтение параметров последовательного порта из файла-схемы TOML /// Чтение параметров последовательного порта из файла-схемы TOML
pub fn new(path_to_scheme: &str) -> Result<Self, String> { 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_name = smart_serial_parameters.get_parameter_value("device")?;
let serial_baudrate = nku_serial_parameters let serial_baudrate = smart_serial_parameters
.get_parameter_value("baudrate")? .get_parameter_value("baudrate")?
.parse::<u32>() .parse::<u32>()
.map_err(|e: std::num::ParseIntError| e.to_string())?; .map_err(|e: std::num::ParseIntError| e.to_string())?;