# tui_menu 0.2.0
1. Переход на Cursive 2. Менеджер навигации по меню: вызывающая сторона может осуществлять навигацию по меню вне Cursive event loop, например, в потоке обработчика внешнего прерывания GPIO
This commit is contained in:
parent
4880b822df
commit
0922c81c0a
25 changed files with 1247 additions and 680 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
825
Cargo.lock
generated
825
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
BIN
config_lib/.DS_Store
vendored
Normal file
BIN
config_lib/.DS_Store
vendored
Normal file
Binary file not shown.
|
|
@ -4,17 +4,17 @@ use toml_edit::value;
|
|||
|
||||
use crate::toml_parser::*;
|
||||
|
||||
/// Структура, содержащая набор параметров Parameter
|
||||
/// Структура, содержащая набор параметров` Parameter`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeviceConfig {
|
||||
scheme: TomlScheme,
|
||||
/// Набор параметров
|
||||
/// key - имя параметра, value - структура, описывающая параметр
|
||||
/// `key` - имя параметра, `value` - структура, описывающая параметр
|
||||
parameters: HashMap<String, Parameter>,
|
||||
}
|
||||
|
||||
impl DeviceConfig {
|
||||
/// ## Заполнение струтуры DeviceConfig
|
||||
/// ## Заполнение струтуры `DeviceConfig`
|
||||
/// * `path_to_scheme` - путь к файлу-cхеме
|
||||
pub fn create_parameter_list(path_to_scheme: &str) -> Result<DeviceConfig, String> {
|
||||
let toml_config = TomlScheme::new(path_to_scheme).map_err(|e| e.to_string())?;
|
||||
|
|
@ -43,7 +43,7 @@ impl DeviceConfig {
|
|||
Ok(device_config)
|
||||
}
|
||||
|
||||
/// # Сохранение текущих значений параметров в TOML-файл self.schema_path
|
||||
/// ## Сохранение текущих значений параметров в TOML-файл `self.schema_path`
|
||||
pub fn save_parameters_values(&self) -> Result<(), String> {
|
||||
let mut toml_config =
|
||||
TomlScheme::new(&self.scheme.get_path_to_scheme_file()).map_err(|e| e.to_string())?;
|
||||
|
|
@ -57,7 +57,7 @@ impl DeviceConfig {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// # Получение списка ключей - имен параметров
|
||||
/// ## Получение списка ключей - имен параметров
|
||||
pub fn get_parameters_names(&self) -> Result<Vec<String>, String> {
|
||||
if self.parameters.is_empty() {
|
||||
return Err("Parameters list is empty".to_string());
|
||||
|
|
@ -66,7 +66,7 @@ impl DeviceConfig {
|
|||
return Ok(self.parameters.keys().map(|key| key.clone()).collect());
|
||||
}
|
||||
|
||||
/// # Получение описания параметра соответствующего key
|
||||
/// ## Получение описания параметра соответствующего `key`
|
||||
pub fn get_parameter_description(&self, key: &str) -> Result<String, String> {
|
||||
match self.parameters.get(key) {
|
||||
Some(parameter) => Ok(parameter.get_description()),
|
||||
|
|
@ -74,7 +74,7 @@ impl DeviceConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// # Получение списка возможных значений параметра соответствующего key
|
||||
/// ## Получение списка возможных значений параметра соответствующего `key`
|
||||
pub fn get_parameter_possible_values(&self, key: &str) -> Result<Vec<String>, String> {
|
||||
match self.parameters.get(key) {
|
||||
Some(parameter) => Ok(parameter.get_possible_values()),
|
||||
|
|
@ -82,7 +82,7 @@ impl DeviceConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// # Получение значения параметра соответствующего key
|
||||
/// ## Получение значения параметра соответствующего `key`
|
||||
pub fn get_parameter_value(&self, key: &str) -> Result<String, String> {
|
||||
match self.parameters.get(key) {
|
||||
Some(parameter) => Ok(parameter.get_value()),
|
||||
|
|
@ -90,7 +90,7 @@ impl DeviceConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// # Установка значения параметра соответствующего key
|
||||
/// ## Установка значения параметра соответствующего `key`
|
||||
pub fn set_parameter_value(&mut self, key: &str, value: String) -> Result<(), String> {
|
||||
match self.parameters.get_mut(key) {
|
||||
Some(parameter) => parameter.set_value(value),
|
||||
|
|
@ -99,7 +99,7 @@ impl DeviceConfig {
|
|||
}
|
||||
|
||||
/// ## Получение числового индекса соответствующего текущему значению параметра
|
||||
/// Индекс соответствует положению текущего значения в списке возможных значений possible_values
|
||||
/// Индекс соответствует положению текущего значения в списке возможных значений `possible_values`
|
||||
pub fn get_parameter_index_using_value(&self, key: &str) -> Result<u8, String> {
|
||||
let parameters_list = self.get_parameters_names()?;
|
||||
if parameters_list.contains(&key.to_owned()) {
|
||||
|
|
@ -116,8 +116,8 @@ impl DeviceConfig {
|
|||
Err("Unable to get parameter value using index".to_string())
|
||||
}
|
||||
|
||||
/// ## Присваивание параметру key значения, соответствующего index
|
||||
/// Индекс соответствует положению значения в списке возможных значений possible_values
|
||||
/// ## Присваивание параметру `key` значения, соответствующего `index`
|
||||
/// Индекс соответствует положению значения в списке возможных значений `possible_values`
|
||||
pub fn set_parameter_value_using_index(&mut self, key: &str, index: u8) -> Result<(), String> {
|
||||
let parameters_list = self.get_parameters_names()?;
|
||||
if parameters_list.contains(&key.to_owned()) {
|
||||
|
|
|
|||
BIN
menu_tui/.DS_Store
vendored
Normal file
BIN
menu_tui/.DS_Store
vendored
Normal file
Binary file not shown.
|
|
@ -1,16 +1,14 @@
|
|||
[package]
|
||||
name = "menu_tui"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
config_lib = { path = "../config_lib" }
|
||||
inquire = { workspace = true }
|
||||
enigo = "0.5.0"
|
||||
colored = "3.0.0"
|
||||
terminal-menu = "3.0.0"
|
||||
cursive = {version = "0.21.1", features = ["toml"]}
|
||||
|
||||
crossterm = "0.25.0"
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
|
|
|
|||
9
menu_tui/TODO.md
Normal file
9
menu_tui/TODO.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Рекомендации по улучшению
|
||||
|
||||
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` комментариями и примерами использования
|
||||
25
menu_tui/assets/style.toml
Normal file
25
menu_tui/assets/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"
|
||||
|
|
@ -1,27 +1,66 @@
|
|||
use config_lib::device_config::DeviceConfig;
|
||||
use menu_tui::menu_rendering::{MainMenuStates, show_main_dialog};
|
||||
use menu_tui::menu_navigation::NavigationManager;
|
||||
use menu_tui::menu_process::DeviceMenu;
|
||||
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Запуск меню в режиме имитации нажатия кнопок
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut device_config =
|
||||
DeviceConfig::create_parameter_list("menu_tui/examples/simple_config.toml")?;
|
||||
// Создаём меню
|
||||
let mut menu = DeviceMenu::new(
|
||||
"menu_tui/examples/simple_config.toml",
|
||||
"menu_tui/assets/style.toml",
|
||||
);
|
||||
|
||||
loop {
|
||||
match show_main_dialog(&mut device_config) {
|
||||
Ok(MainMenuStates::ConfigurationState) => {
|
||||
println!("Configuration state");
|
||||
continue;
|
||||
}
|
||||
Ok(MainMenuStates::ExitState) => {
|
||||
println!("The user has chosen to exit...");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Menu error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Получаем менеджер навигации для управления от GPIO
|
||||
let nav_manager = menu.get_navigation_manager();
|
||||
|
||||
menu.show_main_menu();
|
||||
|
||||
menu.launch_idling_watchdog(5);
|
||||
|
||||
// Симуляция GPIO обработчиков в отдельных потоках
|
||||
simulate_gpio_handlers(nav_manager);
|
||||
|
||||
// Запускаем главный цикл обработки событий
|
||||
menu.run();
|
||||
|
||||
let device_config = menu.get_schema_config()?;
|
||||
|
||||
println!("Итоговая конфигурация:");
|
||||
for parameter in device_config.get_parameters_names()? {
|
||||
let value = device_config.get_parameter_value(¶meter).unwrap();
|
||||
println!("{}: {}", parameter, value);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Симуляция обработчиков GPIO кнопок
|
||||
fn simulate_gpio_handlers(nav_manager: NavigationManager) {
|
||||
// Кнопка 1 (GPIO): Навигация вниз
|
||||
let nav_down = nav_manager.clone();
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
|
||||
// Симуляция нажатия кнопки GPIO
|
||||
nav_down.navigate_down();
|
||||
}
|
||||
});
|
||||
|
||||
// Кнопка 2 (GPIO): Выбор/подтверждение
|
||||
let nav_select = nav_manager.clone();
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(3000));
|
||||
|
||||
// Симуляция нажатия кнопки GPIO
|
||||
nav_select.select_item();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,29 @@ possible_values = [
|
|||
"15",
|
||||
]
|
||||
default = "0"
|
||||
current = "6"
|
||||
current = "4"
|
||||
|
||||
[placement]
|
||||
name = "Размещение"
|
||||
possible_values = [
|
||||
"Кабина",
|
||||
"Этаж",
|
||||
"Универсальное",
|
||||
]
|
||||
default = "Кабина"
|
||||
current = "Универсальное"
|
||||
|
||||
|
||||
[station]
|
||||
name = "Станция управления"
|
||||
possible_values = [
|
||||
"Smart",
|
||||
"ШК6000",
|
||||
"НКУ-CAN",
|
||||
"НКУ-SD7",
|
||||
]
|
||||
default = "Smart"
|
||||
current = "Smart"
|
||||
|
||||
[soundvolume]
|
||||
name = "Громкость звука"
|
||||
|
|
@ -38,7 +60,7 @@ current = "50%"
|
|||
name = "Громкость музыки"
|
||||
possible_values = ["0%", "25%", "50%", "75%", "100%"]
|
||||
default = "0%"
|
||||
current = "100%"
|
||||
current = "50%"
|
||||
|
||||
[loadcapacity]
|
||||
name = "Грузоподъемность"
|
||||
|
|
@ -61,4 +83,4 @@ possible_values = [
|
|||
"1600кг 21чел.",
|
||||
]
|
||||
default = "СКРЫТО"
|
||||
current = "240кг 3чел."
|
||||
current = "525кг 7чел."
|
||||
|
|
|
|||
BIN
menu_tui/src/.DS_Store
vendored
Normal file
BIN
menu_tui/src/.DS_Store
vendored
Normal file
Binary file not shown.
|
|
@ -1 +1,4 @@
|
|||
pub mod menu_rendering;
|
||||
pub mod menu_navigation;
|
||||
pub mod menu_process;
|
||||
mod user_parameters;
|
||||
mod views;
|
||||
|
|
|
|||
165
menu_tui/src/menu_navigation.rs
Normal file
165
menu_tui/src/menu_navigation.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
//! # Модуль навигации по меню
|
||||
//!
|
||||
//! > Позволяет имитировать нажатия определенных клавиш в процессе работы с меню.
|
||||
//! Используется если в системе отсутствует клавиатура.
|
||||
//!
|
||||
//!
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
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-функции из любого потока.
|
||||
// Cursive автоматически обрабатывает эти callback’и в своем event loop, обеспечивая потокобезопасность
|
||||
cb_sink: CbSink,
|
||||
current_view: Arc<Mutex<String>>,
|
||||
last_activity: Arc<Mutex<Instant>>,
|
||||
}
|
||||
|
||||
impl NavigationManager {
|
||||
/// Создать новый менеджер навигации
|
||||
pub fn new(cb_sink: CbSink) -> Self {
|
||||
NavigationManager {
|
||||
cb_sink,
|
||||
current_view: Arc::new(Mutex::new("main_menu".to_string())),
|
||||
last_activity: Arc::new(Mutex::new(Instant::now())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить время последнего нажатия клавиши
|
||||
pub fn get_last_activity(&self) -> Instant {
|
||||
self.last_activity.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Установить время последнего нажатия клавиши
|
||||
pub fn set_last_activity(&self, instant: Instant) {
|
||||
let mut last_time = self.last_activity.lock().unwrap();
|
||||
*last_time = instant;
|
||||
}
|
||||
|
||||
/// Установить текущий активный View (Cursive)
|
||||
pub(crate) fn set_current_view(&self, view_name: &str) {
|
||||
let mut current = self.current_view.lock().unwrap();
|
||||
*current = view_name.to_string();
|
||||
}
|
||||
|
||||
/// Получить имя текущего View (Cursive)
|
||||
pub(crate) fn get_current_view(&self) -> String {
|
||||
self.current_view.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Имитация нажатия клавиши вниз
|
||||
pub fn navigate_down(&self) {
|
||||
self.set_last_activity(Instant::now());
|
||||
let view_name = self.get_current_view();
|
||||
self.send_event(&view_name, Event::Key(Key::Down));
|
||||
}
|
||||
|
||||
/// Имитация нажатия клавиши вверх
|
||||
pub fn navigate_up(&self) {
|
||||
self.set_last_activity(Instant::now());
|
||||
let view_name = self.get_current_view();
|
||||
self.send_event(&view_name, Event::Key(Key::Up));
|
||||
}
|
||||
|
||||
/// Имитация нажатия клавиши Enter
|
||||
pub fn select_item(&self) {
|
||||
let mut last_time = self.last_activity.lock().unwrap();
|
||||
*last_time = Instant::now();
|
||||
let view_name = self.get_current_view();
|
||||
self.send_event(&view_name, Event::Key(Key::Enter));
|
||||
}
|
||||
|
||||
/// ## Отправка события `event` текущему рантайму `Cursive` для активного `View`
|
||||
/// Отправка по каналу с помощью `cb_sink: Sender`
|
||||
fn send_event(&self, view_name: &str, event: Event) {
|
||||
let view_name = view_name.to_string();
|
||||
let event_clone = event.clone();
|
||||
|
||||
self.cb_sink
|
||||
.send(Box::new(move |s: &mut Cursive| {
|
||||
Self::dispatch_event_globally(s, &view_name, event_clone);
|
||||
}))
|
||||
.ok(); // Используем .ok() чтобы не паниковать при закрытии
|
||||
}
|
||||
|
||||
// Отправка события `event` в разные типы view
|
||||
fn dispatch_event_globally(siv: &mut cursive::Cursive, view_name: &str, event: Event) {
|
||||
// Сначала пытаемся отправить событие в активный слой (верхний)
|
||||
// Это позволит обработать popup окна
|
||||
let result = siv.screen_mut().on_event(event.clone());
|
||||
|
||||
match result {
|
||||
EventResult::Consumed(maybe_callback) => {
|
||||
// Событие обработано, выполняем callback если есть
|
||||
if let Some(callback) = maybe_callback {
|
||||
callback(siv);
|
||||
}
|
||||
return;
|
||||
}
|
||||
EventResult::Ignored => {
|
||||
|
||||
// Событие проигнорировано верхним слоем
|
||||
// Пытаемся отправить в именованный view
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: отправка в конкретный именованный view
|
||||
// Для CircularFocus<SelectView<i32>> - главное меню
|
||||
if let Some(result) = siv
|
||||
.call_on_name(view_name, |view: &mut CircularFocus<SelectView<i32>>| {
|
||||
view.on_event(event.clone())
|
||||
})
|
||||
{
|
||||
if let EventResult::Consumed(Some(callback)) = result {
|
||||
callback(siv);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Для CircularFocus<ListView> - меню параметров устройства
|
||||
if let Some(result) = siv.call_on_name(view_name, |view: &mut CircularFocus<ListView>| {
|
||||
view.on_event(event.clone())
|
||||
}) {
|
||||
if let EventResult::Consumed(Some(callback)) = result {
|
||||
callback(siv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Реализация клонирования для использования менеджера в разных потоках
|
||||
impl Clone for NavigationManager {
|
||||
fn clone(&self) -> Self {
|
||||
NavigationManager {
|
||||
cb_sink: self.cb_sink.clone(),
|
||||
current_view: Arc::clone(&self.current_view),
|
||||
last_activity: Arc::clone(&self.last_activity),
|
||||
}
|
||||
}
|
||||
}
|
||||
146
menu_tui/src/menu_process.rs
Normal file
146
menu_tui/src/menu_process.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
//! # Модуль отображения меню
|
||||
//!
|
||||
//! ## Навигация
|
||||
//! > Для навигации по меню используется клавиши стрелка вверх/вниз
|
||||
//! Выбор элемента в меню происходит клавишей Enter
|
||||
//! `В случае отсутствия данных клавиш в системе, можно эмулировать их с помощью модуля menu_navigation
|
||||
//!
|
||||
//! ## Параметры меню
|
||||
//! > Параметры меню подтягиваются из файла-схемы TOML с помощью крейта config_lib
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use config_lib::device_config::DeviceConfig;
|
||||
use cursive::{
|
||||
Cursive, CursiveExt,
|
||||
event::{Event, EventResult, EventTrigger},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
menu_navigation::NavigationManager, user_parameters::DeviceParameters,
|
||||
views::main_view::show_main_view,
|
||||
};
|
||||
|
||||
pub struct DeviceMenu {
|
||||
siv: Cursive,
|
||||
nav_manager: NavigationManager,
|
||||
scheme_config: DeviceConfig,
|
||||
}
|
||||
|
||||
pub(crate) struct MenuAppState {
|
||||
pub navigation_manager: NavigationManager,
|
||||
pub inner_config: DeviceParameters,
|
||||
}
|
||||
|
||||
impl DeviceMenu {
|
||||
pub fn new(path_to_scheme: &str, theme_path: &str) -> Self {
|
||||
// Получение конфигурации устройства
|
||||
let device_config = DeviceConfig::create_parameter_list(path_to_scheme)
|
||||
.expect("Ошибка загрузки config_scheme");
|
||||
|
||||
let mut siv = Cursive::default();
|
||||
|
||||
siv.set_global_callback('q', |siv: &mut Cursive| {
|
||||
siv.quit();
|
||||
});
|
||||
// TODO: обработка ошибок
|
||||
let mut device_parameters: DeviceParameters = DeviceParameters::new();
|
||||
|
||||
// Заполнение списка пользовательских параметров данными из `DeviceConfig`
|
||||
device_parameters
|
||||
.load_user_config(&device_config)
|
||||
.expect("Ошибка загрузки пользовательских параметров!");
|
||||
|
||||
let nav_manager = NavigationManager::new(siv.cb_sink().clone());
|
||||
|
||||
// TODO: Cursive logs example
|
||||
if let Err(_) = siv.load_theme_file(theme_path) {
|
||||
println!("{}", format!("Не удалось загрузить тему: {theme_path}"));
|
||||
}
|
||||
|
||||
// Сохраняем и NavigationManager и конфигурацию
|
||||
let app_state = MenuAppState {
|
||||
navigation_manager: nav_manager.clone(),
|
||||
inner_config: device_parameters,
|
||||
};
|
||||
|
||||
// При работе в меню с исподьзованием обычной клавиатуры, обновляем last_activity (активность пользователя)
|
||||
siv.set_on_pre_event_inner(EventTrigger::from_fn(|e| matches!(e, Event::Key(_))), {
|
||||
let nav_manager_clone = nav_manager.clone();
|
||||
move |_event: &Event| -> Option<EventResult> {
|
||||
nav_manager_clone.set_last_activity(Instant::now());
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
siv.set_user_data(app_state);
|
||||
|
||||
DeviceMenu {
|
||||
siv,
|
||||
nav_manager,
|
||||
scheme_config: device_config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Показать главное меню
|
||||
pub fn show_main_menu(&mut self) {
|
||||
self.nav_manager.set_current_view("main_menu");
|
||||
show_main_view(&mut self.siv);
|
||||
}
|
||||
|
||||
/// Получить менеджер навигации для управления извне
|
||||
pub fn get_navigation_manager(&self) -> NavigationManager {
|
||||
self.nav_manager.clone()
|
||||
}
|
||||
|
||||
/// Запустить главный цикл обработки событий
|
||||
pub fn run(&mut self) {
|
||||
self.siv.run();
|
||||
}
|
||||
|
||||
/// ## Запуск таймера, который проверяет активность пользователя
|
||||
/// > Если пользователь не активен в течение `timeout_seconds`, то приложение закрывается.
|
||||
/// > Каждый раз, когда пользователь нажимает одну из кнопок навигации, сбрасывается таймер.
|
||||
pub fn launch_idling_watchdog(&mut self, timeout_seconds: u64) {
|
||||
let nav_manager = self.get_navigation_manager();
|
||||
let cb_sink = self.siv.cb_sink().clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
// Проверяем активность
|
||||
let is_active = {
|
||||
let last = nav_manager.get_last_activity();
|
||||
last.elapsed() < Duration::from_secs(timeout_seconds)
|
||||
};
|
||||
|
||||
if !is_active {
|
||||
cb_sink
|
||||
.send(Box::new(|s: &mut Cursive| {
|
||||
s.quit();
|
||||
}))
|
||||
.ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Закрытие приложения
|
||||
pub fn quit(&mut self) {
|
||||
self.siv.quit();
|
||||
}
|
||||
|
||||
/// Получить текущую конфигурацию
|
||||
pub fn get_schema_config(&mut self) -> Result<DeviceConfig, String> {
|
||||
// TODO: обработка ошибок
|
||||
if let Some(app_state) = self.siv.take_user_data::<MenuAppState>() {
|
||||
app_state
|
||||
.inner_config
|
||||
.update_user_config(&mut self.scheme_config)?;
|
||||
}
|
||||
|
||||
Ok(self.scheme_config.clone())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
//! # Модуль отображения меню
|
||||
//!
|
||||
//! ## Навигация
|
||||
//! > Для навигации по меню используется клавиши стрелка вверх/вниз
|
||||
//! Выбор эелемента в меню происходит клавишей Enter
|
||||
//! `В случае отсутствия данных клавиш в системе, можно эмулировать их с помощью библиотеки enigo`
|
||||
//!
|
||||
//! ## Параметры меню
|
||||
//! > Параметры меню подтягиваются из файла-схемы TOML с помощью крейта config_lib
|
||||
use config_lib::device_config::DeviceConfig;
|
||||
use crossterm::style::Color;
|
||||
use inquire::Select;
|
||||
use terminal_menu::{TerminalMenuItem, button, label, menu, mut_menu, run};
|
||||
|
||||
pub enum MainMenuStates {
|
||||
ConfigurationState,
|
||||
ExitState,
|
||||
}
|
||||
|
||||
/// Отображение главного меню
|
||||
pub fn show_main_dialog(config: &mut DeviceConfig) -> Result<MainMenuStates, String> {
|
||||
// Header
|
||||
let mut menu_structure = generate_menu_header("Меню настроек индикатора");
|
||||
|
||||
// Текущие значения
|
||||
menu_structure.extend(generate_current_values(config)?);
|
||||
|
||||
// Элементы меню
|
||||
menu_structure.extend(generate_menu_items(config)?);
|
||||
|
||||
let main_menu = menu(menu_structure);
|
||||
|
||||
// Отрисовка и навигация по меню
|
||||
run(&main_menu);
|
||||
|
||||
// Получение названия выбранного пункта меню
|
||||
let mut_menu_instance = mut_menu(&main_menu);
|
||||
let selected_menu_item = mut_menu_instance.selected_item_name();
|
||||
|
||||
// Получение ключа параметра в HashMap, соответствующего названию пункта меню
|
||||
let selected_parameter_name = config
|
||||
.get_parameters_names()?
|
||||
.into_iter()
|
||||
.find_map(|par_name| {
|
||||
// Получение ключа параметра в HashMap, в случае если описание параметра совпадает с названием пункта меню
|
||||
if let Ok(par_desc) = config.get_parameter_description(&par_name) {
|
||||
return if par_desc == selected_menu_item {
|
||||
Some(par_name)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
if let Some(key) = selected_parameter_name {
|
||||
show_configuration_dialog(config, &key)?;
|
||||
return Ok(MainMenuStates::ConfigurationState);
|
||||
}
|
||||
|
||||
// Сохранение настроек в TOML-файл схему
|
||||
config.save_parameters_values()?;
|
||||
|
||||
return Ok(MainMenuStates::ExitState);
|
||||
}
|
||||
|
||||
/// Отображение диалога для выбора значения параметра
|
||||
fn show_configuration_dialog(config: &mut DeviceConfig, par_name: &str) -> Result<(), String> {
|
||||
let possible_values = config.get_parameter_possible_values(par_name)?;
|
||||
let answer = Select::new("Выберите значение параметра", possible_values.clone()).prompt();
|
||||
|
||||
match answer {
|
||||
Ok(selection) => {
|
||||
config.set_parameter_value(par_name, selection)?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => return Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Генерация структуры меню: Титул
|
||||
fn generate_menu_header(title: &str) -> Vec<TerminalMenuItem> {
|
||||
vec![
|
||||
label("----------------------").colorize(Color::DarkGreen),
|
||||
label(title).colorize(Color::DarkGreen),
|
||||
label(format!(
|
||||
"{} v{}",
|
||||
env!("CARGO_CRATE_NAME"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
))
|
||||
.colorize(Color::DarkGreen),
|
||||
label(format!("{}", env!("CARGO_PKG_AUTHORS"))).colorize(Color::DarkGreen),
|
||||
label("-----------------------").colorize(Color::DarkGreen),
|
||||
]
|
||||
}
|
||||
|
||||
/// Генерация структуры меню: Текущие значения настраиваемых параметров
|
||||
fn generate_current_values(config: &DeviceConfig) -> Result<Vec<TerminalMenuItem>, String> {
|
||||
let parameter_names = config.get_parameters_names()?;
|
||||
|
||||
let mut menu_structure = vec![];
|
||||
|
||||
for parameter_name in ¶meter_names {
|
||||
let parameter_value = config.get_parameter_value(¶meter_name)?;
|
||||
let parameter_description = config.get_parameter_description(¶meter_name)?;
|
||||
menu_structure.push(
|
||||
label(format!("{}: {}", parameter_description, parameter_value))
|
||||
.colorize(Color::DarkYellow),
|
||||
);
|
||||
}
|
||||
|
||||
menu_structure.push(label("-----------------------").colorize(Color::DarkYellow));
|
||||
|
||||
Ok(menu_structure)
|
||||
}
|
||||
|
||||
/// Генерация структуры меню: Элементы меню
|
||||
fn generate_menu_items(config: &DeviceConfig) -> Result<Vec<TerminalMenuItem>, String> {
|
||||
let parameter_names = config.get_parameters_names()?;
|
||||
|
||||
let mut menu_structure = vec![];
|
||||
|
||||
menu_structure.push(label("-----------------------").colorize(Color::DarkGrey));
|
||||
|
||||
// Элементы меню; имя каждого параметра соответствует полю description
|
||||
let mut menu_structure = menu_structure;
|
||||
for parameter_name in ¶meter_names {
|
||||
let parameter_description = config.get_parameter_description(¶meter_name)?;
|
||||
menu_structure.push(button(parameter_description).colorize(Color::White));
|
||||
}
|
||||
menu_structure.push(button("Выход").colorize(Color::White));
|
||||
|
||||
menu_structure.push(label("-----------------------").colorize(Color::DarkGrey));
|
||||
|
||||
Ok(menu_structure)
|
||||
}
|
||||
89
menu_tui/src/user_parameters.rs
Normal file
89
menu_tui/src/user_parameters.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
use config_lib::device_config::DeviceConfig;
|
||||
|
||||
/// ### Структура для хранения пользовательских параметров
|
||||
/// Дублирует данные из экземпляра `DeviceConfig` (пара ключ-значение из `HashMap<String, Parameter>`)
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Parameter {
|
||||
/// Имя параметра (совпадает с ключом в `HashMap<String, Parameter>`)
|
||||
pub key: String,
|
||||
/// Описание параметра (используется для отображения в интерфейсе)
|
||||
pub description: String,
|
||||
/// Возможные значения параметра
|
||||
pub options: Vec<String>,
|
||||
/// Текущее значение параметра
|
||||
pub selected_value: String,
|
||||
}
|
||||
impl Parameter {
|
||||
fn new(key: String, description: String, options: Vec<String>, selected: String) -> Self {
|
||||
Parameter {
|
||||
key,
|
||||
description,
|
||||
options,
|
||||
selected_value: selected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Конфигурация устройства
|
||||
/// Представляет собой набор `Parameter`, дублирует данные из экземпляра `DeviceConfig`
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DeviceParameters {
|
||||
pub parameters: Vec<Parameter>,
|
||||
}
|
||||
|
||||
impl Default for DeviceParameters {
|
||||
fn default() -> Self {
|
||||
DeviceParameters::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceParameters {
|
||||
pub fn new() -> Self {
|
||||
DeviceParameters {
|
||||
parameters: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Заполнение списка пользовательских параметров данными из `DeviceConfig`
|
||||
pub fn load_user_config(&mut self, parameters_schema: &DeviceConfig) -> Result<(), String> {
|
||||
for parameter_key in parameters_schema.get_parameters_names()? {
|
||||
let parameter_values = parameters_schema
|
||||
.get_parameter_possible_values(¶meter_key)?
|
||||
.clone();
|
||||
let parameter_desc = parameters_schema.get_parameter_description(¶meter_key)?;
|
||||
let parameter_value = parameters_schema
|
||||
.get_parameter_value(¶meter_key)?
|
||||
.clone();
|
||||
|
||||
self.add_parameter(Parameter::new(
|
||||
parameter_key,
|
||||
parameter_desc,
|
||||
parameter_values,
|
||||
parameter_value,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Обновление конфигурации `DeviceConfig` по данным из `DeviceParameters`
|
||||
pub fn update_user_config(&self, parameters_schema: &mut DeviceConfig) -> Result<(), String> {
|
||||
for parameter in self.parameters.iter() {
|
||||
let value = parameter.selected_value.clone();
|
||||
parameters_schema.set_parameter_value(¶meter.key, value)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Добавление нового параметра
|
||||
fn add_parameter(&mut self, param: Parameter) {
|
||||
self.parameters.push(param);
|
||||
}
|
||||
|
||||
/// Обновление значения параметра
|
||||
pub fn update_parameter(&mut self, key: &str, value: String) {
|
||||
if let Some(param) = self.parameters.iter_mut().find(|p| p.key == key) {
|
||||
param.selected_value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
89
menu_tui/src/views/config_view.rs
Normal file
89
menu_tui/src/views/config_view.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
use crate::menu_process::MenuAppState;
|
||||
use cursive::{
|
||||
Cursive,
|
||||
align::HAlign,
|
||||
view::{Nameable, Resizable},
|
||||
views::{Button, CircularFocus, Dialog, ListView, SelectView},
|
||||
};
|
||||
|
||||
/// Отображение диалога выбора значения параметров устройства
|
||||
pub(crate) fn show_config_view(siv: &mut Cursive) {
|
||||
// Получение текущего состояния приложения
|
||||
let app_state: MenuAppState = siv
|
||||
.take_user_data()
|
||||
.expect("Не удалось выполнить take_user_data");
|
||||
|
||||
// Обновляем current_view
|
||||
app_state.navigation_manager.set_current_view("params_menu");
|
||||
|
||||
let mut parameter_list = ListView::new();
|
||||
|
||||
// Разделитель
|
||||
parameter_list.add_delimiter();
|
||||
|
||||
// Создание списка параметров c выпадающими списками возможных значений
|
||||
for parameter in app_state.inner_config.parameters.iter() {
|
||||
let key = parameter.key.clone();
|
||||
let current_value = parameter.selected_value.clone();
|
||||
|
||||
let mut select_view = SelectView::new().popup().h_align(HAlign::Left);
|
||||
|
||||
let mut selected_index = 0;
|
||||
for (index, option) in parameter.options.iter().enumerate() {
|
||||
select_view.add_item(option, option.clone());
|
||||
if option == ¤t_value {
|
||||
selected_index = index;
|
||||
}
|
||||
}
|
||||
// Установка курсора на текущем значении параметра
|
||||
select_view.set_selection(selected_index);
|
||||
|
||||
// Добавление коллбэка для обновления выбранного значения
|
||||
let key_for_callback = key.clone();
|
||||
|
||||
select_view.set_on_submit(move |s, selected_value: &String| {
|
||||
let mut state: MenuAppState = s
|
||||
.take_user_data()
|
||||
.expect("Не удалось выполнить take_user_data");
|
||||
|
||||
state
|
||||
.inner_config
|
||||
.update_parameter(&key_for_callback, selected_value.clone());
|
||||
s.set_user_data(state);
|
||||
});
|
||||
|
||||
let parameter_view = select_view.with_name(key);
|
||||
|
||||
parameter_list.add_child(¶meter.description, parameter_view);
|
||||
}
|
||||
|
||||
// Разделитель
|
||||
parameter_list.add_delimiter();
|
||||
|
||||
// Кнопка возврата в главное меню
|
||||
parameter_list.add_child(
|
||||
"",
|
||||
Button::new("Назад в главное меню", |s| {
|
||||
let state: MenuAppState = s
|
||||
.take_user_data()
|
||||
.expect("Не удалось выполнить take_user_data");
|
||||
state.navigation_manager.set_current_view("main_menu");
|
||||
s.set_user_data(state);
|
||||
s.pop_layer();
|
||||
}),
|
||||
);
|
||||
|
||||
// Обёртка для циклической навигации с помощью одной кнопки
|
||||
let circular_list = CircularFocus::new(parameter_list)
|
||||
.with_wrap_arrows(true)
|
||||
.with_name("params_menu");
|
||||
|
||||
let parameter_dialog = Dialog::around(circular_list)
|
||||
.title("⚙ Параметры устройства")
|
||||
.fixed_width(50);
|
||||
|
||||
// Обновление текущих значений параметров
|
||||
siv.set_user_data(app_state);
|
||||
|
||||
siv.add_layer(parameter_dialog);
|
||||
}
|
||||
6
menu_tui/src/views/exit_view.rs
Normal file
6
menu_tui/src/views/exit_view.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use cursive::Cursive;
|
||||
|
||||
/// Закрытие приложения с сохранением параметров
|
||||
pub(crate) fn show_exit_view(siv: &mut Cursive) {
|
||||
siv.quit();
|
||||
}
|
||||
45
menu_tui/src/views/main_view.rs
Normal file
45
menu_tui/src/views/main_view.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use crate::views::config_view::show_config_view;
|
||||
use crate::views::exit_view::show_exit_view;
|
||||
use crate::views::update_view::show_update_view;
|
||||
use cursive::{
|
||||
Cursive,
|
||||
view::{Nameable, Resizable},
|
||||
views::{CircularFocus, Dialog, LinearLayout, SelectView},
|
||||
};
|
||||
|
||||
/// Отображение главного меню
|
||||
pub(crate) fn show_main_view(siv: &mut Cursive) {
|
||||
siv.pop_layer();
|
||||
|
||||
let main_menu = SelectView::new()
|
||||
.item("Параметры устройства", 1)
|
||||
.item("Обновление прошивки", 2)
|
||||
.item("Сохранение и выход", 3)
|
||||
.h_align(cursive::align::HAlign::Center)
|
||||
.on_submit(|s: &mut Cursive, menu_item_index| menu_item_selected(s, *menu_item_index));
|
||||
|
||||
// Обёртка для циклической навигации с помощью одной кнопки
|
||||
let circular_menu = CircularFocus::new(main_menu).with_wrap_arrows(true);
|
||||
|
||||
let layout = LinearLayout::vertical().child(
|
||||
Dialog::around(circular_menu.with_name("main_menu"))
|
||||
.title(format!(
|
||||
"{} {}",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
))
|
||||
.fixed_size((50, 5)),
|
||||
);
|
||||
|
||||
siv.add_layer(layout);
|
||||
}
|
||||
|
||||
/// Обработка выбора определенного пункта в главном меню
|
||||
fn menu_item_selected(siv: &mut Cursive, menu_item_index: i32) {
|
||||
match menu_item_index {
|
||||
1 => show_config_view(siv),
|
||||
2 => show_update_view(siv),
|
||||
3 => show_exit_view(siv),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
4
menu_tui/src/views/mod.rs
Normal file
4
menu_tui/src/views/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod config_view;
|
||||
pub mod exit_view;
|
||||
pub mod main_view;
|
||||
pub mod update_view;
|
||||
6
menu_tui/src/views/update_view.rs
Normal file
6
menu_tui/src/views/update_view.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use cursive::{Cursive, views::Dialog};
|
||||
|
||||
/// Отображение диалога обновления прошивки
|
||||
pub(crate) fn show_update_view(siv: &mut Cursive) {
|
||||
siv.add_layer(Dialog::info("Обновление прошивки пока не реализовано"));
|
||||
}
|
||||
BIN
rk_nku_menu/.DS_Store
vendored
Normal file
BIN
rk_nku_menu/.DS_Store
vendored
Normal file
Binary file not shown.
|
|
@ -1,12 +1,13 @@
|
|||
[package]
|
||||
name = "rk_nku_menu"
|
||||
name = "rk_smart_menu"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
enigo = "0.5.0"
|
||||
sysfs_gpio = { version = "0.6.2" }
|
||||
thiserror = "2.0"
|
||||
gpiocdev-embedded-hal = "0.1.2"
|
||||
gpiocdev = "0.7.3"
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
config_lib = { path = "../config_lib" }
|
||||
|
|
|
|||
25
rk_nku_menu/assets/style.toml
Normal file
25
rk_nku_menu/assets/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"
|
||||
|
|
@ -1,133 +1,113 @@
|
|||
use config_lib::device_config::DeviceConfig;
|
||||
use enigo::{Direction::Click, Enigo, Key, Keyboard, Settings};
|
||||
use log::{debug, error};
|
||||
use menu_tui::menu_rendering::{MainMenuStates, show_main_dialog};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use gpiocdev::Request;
|
||||
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_nku_configs/nku_scheme.toml";
|
||||
const NKU_DEVICE_CONFIG_PATH: &str = "rk_smart_configs/smart_scheme.toml";
|
||||
const TUI_APP_CONFIG_PATH: &str = "rk_smart_configs/menu_style.toml";
|
||||
|
||||
/// Расчёт номера пина в порядке SYSFS
|
||||
/// Максимальное время бездействия [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
|
||||
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(¶meter).unwrap();
|
||||
debug!("{}: {}", parameter, value);
|
||||
}
|
||||
|
||||
device_config.save_parameters_values()?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
/// ligpiod номер порта кнопки ввода
|
||||
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]
|
||||
/// 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
|
||||
/// Максимальное время бездействия [c], после достижения которого происходит выход из меню
|
||||
const IDLE_TIMEOUT_SEC: u32 = 30;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
let mut device_config = DeviceConfig::create_parameter_list(NKU_DEVICE_CONFIG_PATH)?;
|
||||
|
||||
static BTN_PRESSED: AtomicBool = AtomicBool::new(false);
|
||||
static STOP_FLAG: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// Открытие потоков для обслуживания EXT прерываний
|
||||
let down_irq_thread = std::thread::spawn(move || {
|
||||
let input = Pin::new(S2_BTN as u64);
|
||||
debug!("down_irq_thread launched!");
|
||||
input.with_exported(|| {
|
||||
input.set_direction(Direction::In)?;
|
||||
input.set_edge(Edge::RisingEdge)?;
|
||||
let mut poller = input.get_poller()?;
|
||||
|
||||
while !STOP_FLAG.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
if let Some(pin_value) = poller.poll(1000)? {
|
||||
debug!("{}", format!("Pin value: {}", pin_value));
|
||||
if pin_value == 1 {
|
||||
let mut enigo = Enigo::new(&Settings::default()).unwrap();
|
||||
enigo.key(Key::DownArrow, Click).unwrap();
|
||||
BTN_PRESSED.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
debug!("DownArrow pressed");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
let return_irq_thread = std::thread::spawn(move || {
|
||||
let input = Pin::new(S1_BTN as u64);
|
||||
debug!("return_irq_thread launched!");
|
||||
input.with_exported(|| {
|
||||
input.set_direction(Direction::In)?;
|
||||
input.set_edge(Edge::RisingEdge)?;
|
||||
let mut poller = input.get_poller()?;
|
||||
|
||||
while !STOP_FLAG.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
if let Some(pin_value) = poller.poll(1000)? {
|
||||
debug!("{}", format!("Pin value: {}", pin_value));
|
||||
if pin_value == 1 {
|
||||
let mut enigo = Enigo::new(&Settings::default()).unwrap();
|
||||
enigo.key(Key::Return, Click).unwrap();
|
||||
BTN_PRESSED.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
debug!("Return pressed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
// Поток TUI меню
|
||||
///
|
||||
fn gpio_navigation_handlers(nav_manager: NavigationManager) {
|
||||
// Кнопка 1 (GPIO): Навигация вниз
|
||||
let nav_down = nav_manager.clone();
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match show_main_dialog(&mut device_config) {
|
||||
Ok(MainMenuStates::ConfigurationState) => {
|
||||
debug!("Configuration state");
|
||||
continue;
|
||||
}
|
||||
Ok(MainMenuStates::ExitState) => {
|
||||
debug!("The user has chosen to exit...");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Menu error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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!");
|
||||
|
||||
STOP_FLAG.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
debug!("The user has chosen to exit...");
|
||||
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
|
||||
nav_down.navigate_down();
|
||||
std::thread::sleep(Duration::from_millis(400));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Главный поток - проверка бездействия
|
||||
let mut idle_counter: u32 = 0;
|
||||
while !STOP_FLAG.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
if BTN_PRESSED.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
idle_counter = 0;
|
||||
BTN_PRESSED.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
} else {
|
||||
idle_counter += 1;
|
||||
if idle_counter > IDLE_TIMEOUT_SEC * 10 {
|
||||
STOP_FLAG.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
debug!("Idle timeout has been reached, exiting...");
|
||||
// Кнопка 2 (GPIO): Выбор/подтверждение
|
||||
let nav_select = 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();
|
||||
debug!("return_irq_thread launched!");
|
||||
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));
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
if down_irq_thread.join().is_err() {
|
||||
debug!("Unable to stop down_irq_thread");
|
||||
}
|
||||
|
||||
if return_irq_thread.join().is_err() {
|
||||
debug!("Unable to stop return_irq_thread");
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue