# Примеры с SysFS (RK3399) и rppal(RPI)

This commit is contained in:
Dmitry Akimov 2026-06-02 23:01:49 +03:00
parent 80b100ad66
commit 71aec0db30
14 changed files with 598 additions and 7 deletions

BIN
.DS_Store vendored

Binary file not shown.

32
Cargo.lock generated
View file

@ -876,6 +876,29 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "pi_nku_menu"
version = "0.1.1"
dependencies = [
"config_lib",
"env_logger",
"log",
"menu_tui",
"rppal",
"thiserror",
]
[[package]]
name = "pi_nku_sync"
version = "0.1.1"
dependencies = [
"clap",
"config_lib",
"env_logger",
"log",
"protocol_lib",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.17" version = "0.2.17"
@ -1035,6 +1058,15 @@ dependencies = [
"protocol_lib", "protocol_lib",
] ]
[[package]]
name = "rppal"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ce3b019009cff02cb6b0e96e7cc2e5c5b90187dc1a490f8ef1521d0596b026"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.44" version = "0.38.44"

View file

@ -5,11 +5,14 @@ members = [
"menu_tui", "menu_tui",
"protocol_lib", "protocol_lib",
"rk_nku_sync", "rk_nku_sync",
"rk_nku_menu", "logger", "pi_nku_sync",
"pi_nku_menu",
"rk_nku_menu",
"logger",
] ]
[workspace.package] [workspace.package]
authors = ["MU LLC <info@machunit.com>"] authors = ["Dmitry Akimov <von.akimov@gmail.com>"]
[workspace.dependencies] [workspace.dependencies]
inquire = "0.7.5" inquire = "0.7.5"

View file

@ -5,8 +5,4 @@ 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
# version = "2.28" # Версия glibc, установленная на RPi Zero W
# [target.aarch64-unknown-linux-gnu]
# pre-build = [
# "apt-get update && apt-get install --assume-yes libxkbcommon"
# ]

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

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

View file

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

13
pi_nku_menu/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "pi_nku_menu"
version = "0.1.1"
edition = "2024"
authors.workspace = true
[dependencies]
thiserror = "2.0"
rppal = "0.22.1"
log = { workspace = true }
env_logger = { workspace = true }
config_lib = { path = "../config_lib" }
menu_tui = { path = "../menu_tui" }

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"

94
pi_nku_menu/src/main.rs Normal file
View file

@ -0,0 +1,94 @@
use std::time::{Duration, Instant};
use log::debug;
use menu_tui::menu_navigation::NavigationManager;
use menu_tui::menu_process::DeviceMenu;
use rppal::gpio::{Gpio, InputPin, Trigger};
/// ### Путь к файлу-схемы параметров устройства
const NKU_DEVICE_CONFIG_PATH: &str = "pi_nku_configs/nku_scheme.toml";
const TUI_APP_CONFIG_PATH: &str = "pi_nku_configs/menu_style.toml";
/// Максимальное время бездействия [c], после достижения которого происходит выход из меню
const IDLE_TIMEOUT_SEC: u64 = 30;
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
// Создаём меню
let mut menu = DeviceMenu::new(NKU_DEVICE_CONFIG_PATH, TUI_APP_CONFIG_PATH);
let nav_manager = menu.get_navigation_manager();
menu.show_main_menu();
// Навигация по меню с помощью кнопок, подключенных к GPIO
let _gpio_buttons = gpio_navigation_handlers(nav_manager)?;
menu.launch_idling_watchdog(IDLE_TIMEOUT_SEC);
menu.run();
menu.quit();
let device_config = menu.get_schema_config()?;
debug!("Итоговая конфигурация:");
for parameter in device_config.get_parameters_names()? {
let value = device_config.get_parameter_value(&parameter).unwrap();
debug!("{}: {}", parameter, value);
}
device_config.save_parameters_values()?;
return Ok(());
}
/// BCM номер порта кнопки ввода
const IN_BTN: u8 = 7;
/// BCM номер порта кнопки выбора
const SEL_BTN: u8 = 8;
fn gpio_navigation_handlers(
nav_manager: NavigationManager,
) -> Result<(InputPin, InputPin), rppal::gpio::Error> {
let gpio = Gpio::new()?;
let mut button_in = gpio.get(IN_BTN)?.into_input_pullup();
let mut button_sel = gpio.get(SEL_BTN)?.into_input_pullup();
button_in.set_reset_on_drop(false);
button_sel.set_reset_on_drop(false);
let nav_down = nav_manager.clone();
let mut last_down = Instant::now() - Duration::from_millis(350);
button_in.set_async_interrupt(
Trigger::FallingEdge,
Some(Duration::from_millis(50)),
move |_event| {
let now = Instant::now();
if now.duration_since(last_down) >= Duration::from_millis(350) {
last_down = now;
log::debug!("IN_BTN -> navigate_down");
nav_down.navigate_down();
}
},
)?;
let nav_sel = nav_manager.clone();
let mut last_sel = Instant::now() - Duration::from_millis(350);
button_sel.set_async_interrupt(
Trigger::FallingEdge,
Some(Duration::from_millis(50)),
move |_event| {
let now = Instant::now();
if now.duration_since(last_sel) >= Duration::from_millis(350) {
last_sel = now;
log::debug!("SEL_BTN -> select_item");
nav_sel.select_item();
}
},
)?;
Ok((button_in, button_sel))
}

13
pi_nku_sync/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "pi_nku_sync"
version = "0.1.1"
edition = "2024"
authors.workspace = true
[dependencies]
protocol_lib = { path = "../protocol_lib" }
config_lib = { path = "../config_lib" }
clap = { workspace = true }
log = { workspace = true }
env_logger = { workspace = true }

130
pi_nku_sync/src/main.rs Normal file
View file

@ -0,0 +1,130 @@
mod nku_client;
mod serial_config;
use std::str::FromStr;
use clap::Parser;
use log::{error, info, warn};
/// Количество попыток выполнить запрос
const REQUEST_ATTEMPTS: u8 = 5;
#[derive(Parser)]
#[command(author = "Akimov Dmitry MU LLC", name = "smart_sync", version = "0.1.0", about, long_about = None)]
struct Args {
/// Тип команды: pull - запрос сохраненных в устройстве настроек, push - отправка новых настроек
#[arg(short = 'm', long = "mode")]
mode: CommandMode,
}
fn main() -> Result<(), String> {
let args = Args::parse();
env_logger::init();
info!("rk_nku_sync> command mode: {:?}", args.mode);
let mut nku_client = nku_client::NkuClient::new()?;
match args.mode {
CommandMode::Pull => pull_parameters(&mut nku_client)?,
CommandMode::Push => push_parameters(&mut nku_client)?,
}
Ok(())
}
/// Отправка настроек на устройство
fn push_parameters(client: &mut nku_client::NkuClient) -> Result<(), String> {
// Цикл попыток установить соединение
let mut attempts: u8 = 1;
'push_request_loop: loop {
warn!("Push request attempt: {attempts}");
if client.push_parameters_to_device().is_ok() {
break 'push_request_loop;
}
let result = client.push_parameters_to_device();
if result.is_ok() {
break 'push_request_loop;
}
error!(
"{}",
format!("Failed to push parameters: {result:?}").as_str()
);
attempts += 1;
if attempts > REQUEST_ATTEMPTS {
return Err("Push request failed!".to_string());
}
}
info!("Parameters have been pushed to device!");
// Запуск стриминга данных от станции
attempts = 1;
'start_streaming_loop: loop {
warn!("Start streaming attempt: {attempts}");
let result = client.start_elevator_data_streaming(nku_client::StreamingMode::OnChangeMode);
if result.is_ok() {
break 'start_streaming_loop;
}
error!(
"{}",
format!("Failed to push parameters: {result:?}").as_str()
);
attempts += 1;
if attempts > REQUEST_ATTEMPTS {
return Err("Start streaming failed!".to_string());
}
}
info!("Data streaming from MCU has been started!");
Ok(())
}
/// Получение настроек, сохраненных в устройстве
fn pull_parameters(client: &mut nku_client::NkuClient) -> Result<(), String> {
let mut attempts: u8 = 1;
// Цикл попыток установить соединение
'pull_request_loop: loop {
warn!("Pull request attempt: {attempts}");
let result = client.pull_parameters_from_device();
if result.is_ok() {
break 'pull_request_loop;
}
error!(
"{}",
format!("Failed to push parameters: {result:?}").as_str()
);
attempts += 1;
if attempts > REQUEST_ATTEMPTS {
return Err("Pull request failed!".to_string());
}
}
info!("Parameters have been pulled from device!");
Ok(())
}
#[derive(Clone, Debug)]
enum CommandMode {
Pull,
Push,
}
impl FromStr for CommandMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"pull" => Ok(CommandMode::Pull),
"push" => Ok(CommandMode::Push),
_ => Err(format!("Unknown command mode: {s}")),
}
}
}

View file

@ -0,0 +1,124 @@
use crate::serial_config::SerialPortConfig;
use config_lib::device_config::DeviceConfig;
use log::{debug, info};
use protocol_lib::client::HostClient;
const BOARD_RESPONSE_TIMEOUT_MS: std::time::Duration = std::time::Duration::from_millis(5000);
const SERIAL_PORT_CONFIG_PATH: &str = "pi_nku_configs/pi_scheme.toml";
const NKU_DEVICE_CONFIG_PATH: &str = "pi_nku_configs/nku_scheme.toml";
#[derive(Clone)]
/// Режимы стриминга данных от устройства
pub enum StreamingMode {
/// Без стриминга
__SilentMode = 0,
/// Отправка данных, в случае изменения их состояния (и наличия)
OnChangeMode = 1,
/// Отправка данных периодически, с заданным в настройках (periodicity) периодом
__PeriodicMode = 2,
/// Отправка данных по требованию
__OnDemandMode = 3,
}
pub struct NkuClient {
nku_client: HostClient,
nku_config: DeviceConfig,
}
impl NkuClient {
pub fn new() -> Result<Self, String> {
// Чтение параметров последовательного порта
let serial_config = SerialPortConfig::new(SERIAL_PORT_CONFIG_PATH)?;
let nku_config = DeviceConfig::create_parameter_list(NKU_DEVICE_CONFIG_PATH)?;
debug!("Parameters list: {:#?}", nku_config.get_parameters_names());
let port_name = serial_config.get_serial_name();
let baudrate = serial_config.get_serial_baudrate();
debug!("Serial config: {port_name}, {baudrate}");
let nku_client = HostClient::connect(&port_name, baudrate, BOARD_RESPONSE_TIMEOUT_MS)?;
info!("Connection with IMv has been established!");
Ok(Self {
nku_client,
nku_config,
})
}
/// ### Запрос начала стриминга данных со станции управления
pub fn start_elevator_data_streaming(&mut self, mode: StreamingMode) -> Result<String, String> {
let response_from_mcu = self
.nku_client
.send_request(format!("set mode {}", mode.clone() as u8).as_str())?;
if !response_from_mcu.contains(format!("mode: {}", mode as u8).as_str()) {
return Err("START STREAMING> invalid response".into());
}
Ok(response_from_mcu)
}
/// ### Запрос сохраненных в устройстве настроек
pub fn pull_parameters_from_device(&mut self) -> Result<(), String> {
let parameters_list = self.nku_config.get_parameters_names()?;
for parameter in parameters_list {
let request_string = format!("get {parameter}");
debug!("PULL> sending request: {request_string}");
let response_from_mcu = self.nku_client.send_request(&request_string)?;
debug!("PULL> response from MCU: {response_from_mcu}");
let parameter_value =
NkuClient::extract_parameter_value(&parameter, response_from_mcu)?;
self.nku_config
.set_parameter_value_using_index(&parameter, parameter_value)?;
}
self.nku_config.save_parameters_values()?;
Ok(())
}
/// Извлечение значения параметра из отклика от MCU
fn extract_parameter_value(
parameter_name: &str,
response_from_mcu: String,
) -> Result<u8, String> {
// Ожидается ответ в формате "parameter_name:value"
let tokens = response_from_mcu.split(':').collect::<Vec<&str>>().to_vec();
if tokens.len() != 2 {
return Err(format!(
"Failed to parse response from MCU: {response_from_mcu}"
));
}
if tokens[0] != parameter_name {
return Err(format!(
"Failed to parse response from MCU: {response_from_mcu}"
));
}
tokens[1].trim().parse::<u8>().map_err(|e| e.to_string())
}
/// Отправка новых настроек на устройство для последующего сохранения
pub fn push_parameters_to_device(&mut self) -> Result<(), String> {
let parameters_list = self.nku_config.get_parameters_names()?;
for parameter in parameters_list {
let parameter_value = self
.nku_config
.get_parameter_index_using_value(&parameter)?;
let request_string = format!("set {parameter} {parameter_value}");
debug!("PUSH> sending request: {request_string}");
let response_from_mcu = self.nku_client.send_request(&request_string)?;
debug!("PUSH> response from MCU: {response_from_mcu}");
if !response_from_mcu.contains(format!("{parameter}: {parameter_value}").as_str()) {
return Err("PUSH> invalid response".into());
}
}
Ok(())
}
}

View file

@ -0,0 +1,38 @@
use config_lib::device_config::DeviceConfig;
use log::debug;
pub struct SerialPortConfig {
serial_name: String,
serial_baudrate: u32,
}
impl SerialPortConfig {
/// Чтение параметров последовательного порта из файла-схемы TOML
pub fn new(path_to_scheme: &str) -> Result<Self, String> {
let 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
}
}