orion-rffa8c856
wip: Phase 1 scaffold — board, drivers, UI, app modules
Add shared-I2C deps (esp-idf-hal, embedded-hal-bus) and flesh out the Rust rewrite skeleton: board/board.rs, display/buttons drivers, ui menu + widgets, and the apps module.
+607 −15
11 files changed
- M Cargo.toml +5 −0
- A src/apps/mod.rs +49 −0
- A src/board/board.rs +55 −0
- M src/board/mod.rs +1 −0
- M src/drivers/buttons.rs +75 −0
- M src/drivers/display.rs +83 −7
- M src/drivers/mod.rs +1 −0
- M src/main.rs +71 −8
- A src/ui/menu.rs +242 −0
- A src/ui/mod.rs +2 −0
- A src/ui/widgets.rs +23 −0
| @@ -26,5 +26,10 @@ embassy-time = { version = "0.5", features = ["generic-queue-8"] } | |||
| 26 | 26 | embedded-graphics = "0.8.2" | |
| 27 | 27 | ssd1306 = "0.10.0" | |
| 28 | 28 | ||
| 29 | + | # Explicit HAL (re-exported by esp-idf-svc, but used directly across the rewrite) | |
| 30 | + | esp-idf-hal = "0.46" | |
| 31 | + | # Shared I2C bus: one I2cDriver behind a RefCell, handed to OLED + (later) PN532 | |
| 32 | + | embedded-hal-bus = "0.3" | |
| 33 | + | ||
| 29 | 34 | [build-dependencies] | |
| 30 | 35 | embuild = "0.33" | |
| @@ -0,0 +1,49 @@ | |||
| 1 | + | //! The `App` abstraction: each menu feature owns its own render/poll loop and | |
| 2 | + | //! returns when the user presses BACK. | |
| 3 | + | //! | |
| 4 | + | //! This replaces the Arduino per-feature blocking `while(1)` loops. An app | |
| 5 | + | //! borrows the shared resources through [`AppCtx`] for the duration of `run` | |
| 6 | + | //! and releases them on return, so the menu regains control afterwards. | |
| 7 | + | ||
| 8 | + | use esp_idf_hal::delay::FreeRtos; | |
| 9 | + | ||
| 10 | + | use crate::board::board::Board; | |
| 11 | + | use crate::drivers::buttons::{debounce, Buttons}; | |
| 12 | + | use crate::drivers::display::Display; | |
| 13 | + | use crate::ui::widgets; | |
| 14 | + | ||
| 15 | + | /// How an app finished. Only BACK exists for now; future apps may add variants | |
| 16 | + | /// (e.g. requesting a restart). | |
| 17 | + | pub enum AppResult { | |
| 18 | + | Back, | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | /// Everything an app may touch, borrowed for the length of `run`. | |
| 22 | + | pub struct AppCtx<'a> { | |
| 23 | + | pub display: &'a mut Display, | |
| 24 | + | pub buttons: &'a mut Buttons, | |
| 25 | + | pub board: &'a mut Board, | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | pub trait App { | |
| 29 | + | fn run(&mut self, ctx: &mut AppCtx) -> AppResult; | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | /// Placeholder app for features awaiting their phase. Shows a name screen and | |
| 33 | + | /// waits for BACK. | |
| 34 | + | pub struct StubApp { | |
| 35 | + | pub name: &'static str, | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | impl App for StubApp { | |
| 39 | + | fn run(&mut self, ctx: &mut AppCtx) -> AppResult { | |
| 40 | + | widgets::stub(ctx.display, self.name); | |
| 41 | + | loop { | |
| 42 | + | if ctx.buttons.back() { | |
| 43 | + | debounce(); | |
| 44 | + | return AppResult::Back; | |
| 45 | + | } | |
| 46 | + | FreeRtos::delay_ms(20); | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | } | |
| @@ -0,0 +1,55 @@ | |||
| 1 | + | //! The one owned `Board`, built in `main` from `Peripherals::take()`. | |
| 2 | + | //! | |
| 3 | + | //! It owns the shared buses and the peripheral/pin handles that later phases | |
| 4 | + | //! consume (radios, CC1101, WiFi/BLE modem). Display and Buttons are built as | |
| 5 | + | //! siblings in `main` and reach apps via `AppCtx`, not through `Board`. | |
| 6 | + | //! | |
| 7 | + | //! The shared I2C lives behind a `'static` `RefCell` so SSD1306 (always-on) and | |
| 8 | + | //! PN532 can both hold `RefCellDevice`s over it without re-initialising the bus. | |
| 9 | + | ||
| 10 | + | use core::cell::RefCell; | |
| 11 | + | ||
| 12 | + | use esp_idf_hal::gpio::AnyIOPin; | |
| 13 | + | use esp_idf_hal::i2c::I2cDriver; | |
| 14 | + | use esp_idf_hal::modem::Modem; | |
| 15 | + | use esp_idf_hal::spi::{SPI2, SPI3}; | |
| 16 | + | ||
| 17 | + | /// Shared handle to the I2C0 bus (OLED + PN532). Leaked to `'static` in `main` | |
| 18 | + | /// because the display is always on for the lifetime of the program. | |
| 19 | + | pub type SharedI2c = &'static RefCell<I2cDriver<'static>>; | |
| 20 | + | ||
| 21 | + | /// NRF24 bus + control pins (SPI2). Consumed in Phase 4. | |
| 22 | + | pub struct NrfPins { | |
| 23 | + | pub sck: AnyIOPin<'static>, | |
| 24 | + | pub miso: AnyIOPin<'static>, | |
| 25 | + | pub mosi: AnyIOPin<'static>, | |
| 26 | + | pub ce1: AnyIOPin<'static>, | |
| 27 | + | pub csn1: AnyIOPin<'static>, | |
| 28 | + | pub ce2: AnyIOPin<'static>, | |
| 29 | + | pub csn2: AnyIOPin<'static>, | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | /// CC1101 bus + control pins (SPI3). Consumed in Phase 4. | |
| 33 | + | pub struct Cc1101Pins { | |
| 34 | + | pub sck: AnyIOPin<'static>, | |
| 35 | + | pub miso: AnyIOPin<'static>, | |
| 36 | + | pub mosi: AnyIOPin<'static>, | |
| 37 | + | pub cs: AnyIOPin<'static>, | |
| 38 | + | pub gdo0: AnyIOPin<'static>, | |
| 39 | + | pub gdo2: AnyIOPin<'static>, | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | /// Owns the peripherals and bus handles not already consumed by the display and | |
| 43 | + | /// buttons. Passed `&mut` into every app via `AppCtx`. | |
| 44 | + | pub struct Board { | |
| 45 | + | /// Shared I2C0 bus (OLED is live; PN532 joins in Phase 4). | |
| 46 | + | pub i2c: SharedI2c, | |
| 47 | + | /// WiFi/BLE radio — acquired/released by one app at a time (Phase 2/3). | |
| 48 | + | pub modem: Modem<'static>, | |
| 49 | + | /// SPI2 host for the two NRF24 radios (Phase 4). | |
| 50 | + | pub spi2: SPI2<'static>, | |
| 51 | + | /// SPI3 host for the CC1101 (Phase 4). | |
| 52 | + | pub spi3: SPI3<'static>, | |
| 53 | + | pub nrf: NrfPins, | |
| 54 | + | pub cc1101: Cc1101Pins, | |
| 55 | + | } | |
| @@ -1 +1,2 @@ | |||
| 1 | + | pub mod board; | |
| 1 | 2 | pub mod pins; | |
| @@ -0,0 +1,75 @@ | |||
| 1 | + | //! Six momentary buttons, active-low with internal pull-ups. | |
| 2 | + | //! | |
| 3 | + | //! Parity reference: `src-arduino/utils/buttons.cpp`. Arduino uses | |
| 4 | + | //! `pinMode(INPUT_PULLUP)` and reports pressed as `!digitalRead(pin)`, i.e. a | |
| 5 | + | //! pressed button reads LOW. We mirror that: `get_level() == Level::Low`. | |
| 6 | + | //! | |
| 7 | + | //! Debounce (the Arduino `delay(150)` after each handled press) lives at the | |
| 8 | + | //! call site via [`debounce`], matching the original per-action delays. | |
| 9 | + | ||
| 10 | + | use esp_idf_hal::delay::FreeRtos; | |
| 11 | + | use esp_idf_hal::gpio::{AnyIOPin, Input, Level, PinDriver, Pull}; | |
| 12 | + | ||
| 13 | + | /// The ~150ms debounce the Arduino firmware applies after each handled press. | |
| 14 | + | pub const DEBOUNCE_MS: u32 = 150; | |
| 15 | + | ||
| 16 | + | /// Block for the standard debounce interval after acting on a press. | |
| 17 | + | pub fn debounce() { | |
| 18 | + | FreeRtos::delay_ms(DEBOUNCE_MS); | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | type Btn = PinDriver<'static, Input>; | |
| 22 | + | ||
| 23 | + | pub struct Buttons { | |
| 24 | + | up: Btn, | |
| 25 | + | down: Btn, | |
| 26 | + | select: Btn, | |
| 27 | + | back: Btn, | |
| 28 | + | right: Btn, | |
| 29 | + | left: Btn, | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | impl Buttons { | |
| 33 | + | /// Configure the six pins as pulled-up inputs. Order matches the physical | |
| 34 | + | /// labels, not GPIO numbers (see `board::pins`). | |
| 35 | + | pub fn new( | |
| 36 | + | up: AnyIOPin<'static>, | |
| 37 | + | down: AnyIOPin<'static>, | |
| 38 | + | select: AnyIOPin<'static>, | |
| 39 | + | back: AnyIOPin<'static>, | |
| 40 | + | right: AnyIOPin<'static>, | |
| 41 | + | left: AnyIOPin<'static>, | |
| 42 | + | ) -> Self { | |
| 43 | + | Self { | |
| 44 | + | up: Self::input(up), | |
| 45 | + | down: Self::input(down), | |
| 46 | + | select: Self::input(select), | |
| 47 | + | back: Self::input(back), | |
| 48 | + | right: Self::input(right), | |
| 49 | + | left: Self::input(left), | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | fn input(pin: AnyIOPin<'static>) -> Btn { | |
| 54 | + | PinDriver::input(pin, Pull::Up).expect("button input") | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | pub fn up(&self) -> bool { | |
| 58 | + | self.up.get_level() == Level::Low | |
| 59 | + | } | |
| 60 | + | pub fn down(&self) -> bool { | |
| 61 | + | self.down.get_level() == Level::Low | |
| 62 | + | } | |
| 63 | + | pub fn select(&self) -> bool { | |
| 64 | + | self.select.get_level() == Level::Low | |
| 65 | + | } | |
| 66 | + | pub fn back(&self) -> bool { | |
| 67 | + | self.back.get_level() == Level::Low | |
| 68 | + | } | |
| 69 | + | pub fn right(&self) -> bool { | |
| 70 | + | self.right.get_level() == Level::Low | |
| 71 | + | } | |
| 72 | + | pub fn left(&self) -> bool { | |
| 73 | + | self.left.get_level() == Level::Low | |
| 74 | + | } | |
| 75 | + | } | |
| @@ -1,18 +1,94 @@ | |||
| 1 | − | use crate::board::pins::*; | |
| 1 | + | //! SSD1306 128x64 OLED over the shared I2C0 bus. | |
| 2 | + | //! | |
| 3 | + | //! Parity reference: `src-arduino/ui/display.cpp` (u8g2 `..._F_HW_I2C`, font | |
| 4 | + | //! `u8g2_font_6x12_tr`). We approximate u8g2 with `embedded-graphics` | |
| 5 | + | //! `MonoTextStyle` + `FONT_6X10`, keeping the Arduino draw coordinates so menu | |
| 6 | + | //! layout matches 1:1. Drawing uses u8g2-style baseline coordinates | |
| 7 | + | //! (`Baseline::Alphabetic`): the given `y` is the text baseline, as in | |
| 8 | + | //! `u8g2.drawStr(x, y, ...)`. | |
| 9 | + | ||
| 10 | + | use embedded_graphics::{ | |
| 11 | + | mono_font::{ | |
| 12 | + | ascii::{FONT_6X10, FONT_9X18_BOLD}, | |
| 13 | + | MonoFont, MonoTextStyle, | |
| 14 | + | }, | |
| 15 | + | pixelcolor::BinaryColor, | |
| 16 | + | prelude::*, | |
| 17 | + | primitives::{PrimitiveStyle, Rectangle}, | |
| 18 | + | text::{Baseline, Text}, | |
| 19 | + | }; | |
| 20 | + | use embedded_hal_bus::i2c::RefCellDevice; | |
| 21 | + | use esp_idf_hal::i2c::I2cDriver; | |
| 22 | + | use ssd1306::{ | |
| 23 | + | mode::{BufferedGraphicsMode, DisplayConfig}, | |
| 24 | + | prelude::*, | |
| 25 | + | I2CDisplayInterface, Ssd1306, | |
| 26 | + | }; | |
| 27 | + | ||
| 28 | + | use crate::board::board::SharedI2c; | |
| 29 | + | ||
| 30 | + | /// The I2C device the OLED talks over: a `RefCellDevice` sharing the `'static` | |
| 31 | + | /// bus so PN532 can later take a second device on the same `RefCell`. | |
| 32 | + | type OledI2c = RefCellDevice<'static, I2cDriver<'static>>; | |
| 33 | + | ||
| 34 | + | type Oled = | |
| 35 | + | Ssd1306<I2CInterface<OledI2c>, DisplaySize128x64, BufferedGraphicsMode<DisplaySize128x64>>; | |
| 2 | 36 | ||
| 3 | 37 | pub struct Display { | |
| 38 | + | raw: Oled, | |
| 4 | 39 | } | |
| 5 | 40 | ||
| 6 | 41 | impl Display { | |
| 7 | − | pub fn new() -> Self { | |
| 8 | − | Self {} | |
| 42 | + | /// Build + initialise the SSD1306 over the shared bus and clear the screen. | |
| 43 | + | pub fn new(bus: SharedI2c) -> Self { | |
| 44 | + | let dev = RefCellDevice::new(bus); | |
| 45 | + | let iface = I2CDisplayInterface::new(dev); | |
| 46 | + | let mut raw = Ssd1306::new(iface, DisplaySize128x64, DisplayRotation::Rotate0) | |
| 47 | + | .into_buffered_graphics_mode(); | |
| 48 | + | raw.init().expect("ssd1306 init"); | |
| 49 | + | ||
| 50 | + | let mut display = Self { raw }; | |
| 51 | + | display.clear(); | |
| 52 | + | display.flush(); | |
| 53 | + | display | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | /// Clear the off-screen buffer (does not touch the panel until `flush`). | |
| 57 | + | pub fn clear(&mut self) { | |
| 58 | + | self.raw.clear_buffer(); | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | /// Push the off-screen buffer to the panel (u8g2 `sendBuffer`). | |
| 62 | + | pub fn flush(&mut self) { | |
| 63 | + | let _ = self.raw.flush(); | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | /// Normal text: foreground pixels on, baseline at `y`. | |
| 67 | + | pub fn text(&mut self, x: i32, y: i32, s: &str) { | |
| 68 | + | self.draw_text(x, y, s, &FONT_6X10, BinaryColor::On); | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | /// Reversed text (off pixels) for drawing over a filled box | |
| 72 | + | /// (u8g2 `setDrawColor(0)`). | |
| 73 | + | pub fn text_rev(&mut self, x: i32, y: i32, s: &str) { | |
| 74 | + | self.draw_text(x, y, s, &FONT_6X10, BinaryColor::Off); | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | /// Large title text (approximates u8g2 `logisoso20`) for the splash screen. | |
| 78 | + | pub fn title(&mut self, x: i32, y: i32, s: &str) { | |
| 79 | + | self.draw_text(x, y, s, &FONT_9X18_BOLD, BinaryColor::On); | |
| 9 | 80 | } | |
| 10 | 81 | ||
| 11 | − | pub fn init(&mut self) { | |
| 12 | − | log::info!("display Initialized!!"); | |
| 82 | + | /// Filled rectangle (u8g2 `drawBox`). | |
| 83 | + | pub fn fill_rect(&mut self, x: i32, y: i32, w: u32, h: u32) { | |
| 84 | + | let _ = Rectangle::new(Point::new(x, y), Size::new(w, h)) | |
| 85 | + | .into_styled(PrimitiveStyle::with_fill(BinaryColor::On)) | |
| 86 | + | .draw(&mut self.raw); | |
| 13 | 87 | } | |
| 14 | 88 | ||
| 15 | − | pub fn draw_text(&mut self, text: &str) { | |
| 16 | − | log::info!("{}", text); | |
| 89 | + | fn draw_text(&mut self, x: i32, y: i32, s: &str, font: &MonoFont<'static>, color: BinaryColor) { | |
| 90 | + | let style = MonoTextStyle::new(font, color); | |
| 91 | + | let _ = Text::with_baseline(s, Point::new(x, y), style, Baseline::Alphabetic) | |
| 92 | + | .draw(&mut self.raw); | |
| 17 | 93 | } | |
| 18 | 94 | } | |
| @@ -1 +1,2 @@ | |||
| 1 | + | pub mod buttons; | |
| 1 | 2 | pub mod display; | |
| @@ -1,21 +1,84 @@ | |||
| 1 | + | // Scaffold phase of the Arduino -> Rust rewrite: later phases consume the | |
| 2 | + | // reserved Board peripherals/pins, so allow the not-yet-read fields for now. | |
| 3 | + | #![allow(dead_code)] | |
| 1 | 4 | ||
| 5 | + | mod apps; | |
| 2 | 6 | mod board; | |
| 3 | 7 | mod drivers; | |
| 8 | + | mod ui; | |
| 4 | 9 | ||
| 10 | + | use core::cell::RefCell; | |
| 11 | + | ||
| 12 | + | use esp_idf_hal::i2c::{config::Config as I2cConfig, I2cDriver}; | |
| 13 | + | use esp_idf_hal::peripherals::Peripherals; | |
| 14 | + | use esp_idf_hal::units::FromValueType; | |
| 15 | + | ||
| 16 | + | use board::board::{Board, Cc1101Pins, NrfPins, SharedI2c}; | |
| 17 | + | use drivers::buttons::Buttons; | |
| 5 | 18 | use drivers::display::Display; | |
| 6 | 19 | ||
| 7 | 20 | fn main() { | |
| 8 | − | // It is necessary to call this function once. Otherwise, some patches to the runtime | |
| 9 | − | // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71 | |
| 21 | + | // Required once so esp-idf-sys runtime patches link. See | |
| 22 | + | // https://github.com/esp-rs/esp-idf-template/issues/71 | |
| 10 | 23 | esp_idf_svc::sys::link_patches(); | |
| 11 | − | ||
| 12 | − | // Bind the log crate to the ESP Logging facilities | |
| 13 | 24 | esp_idf_svc::log::EspLogger::initialize_default(); | |
| 14 | 25 | ||
| 15 | − | log::info!("ORION-RF Booted!!"); | |
| 16 | − | let mut display = Display::new(); | |
| 26 | + | log::info!("ORION-RF booting"); | |
| 27 | + | ||
| 28 | + | let peripherals = Peripherals::take().expect("take peripherals"); | |
| 29 | + | let pins = peripherals.pins; | |
| 30 | + | ||
| 31 | + | // Shared I2C0 bus (OLED + PN532). Leaked to 'static: the display is on for | |
| 32 | + | // the whole program, so this bus never needs to be dropped or re-created. | |
| 33 | + | let i2c = I2cDriver::new( | |
| 34 | + | peripherals.i2c0, | |
| 35 | + | pins.gpio8, // SDA | |
| 36 | + | pins.gpio9, // SCL | |
| 37 | + | &I2cConfig::new().baudrate(400u32.kHz().into()), | |
| 38 | + | ) | |
| 39 | + | .expect("i2c0 init"); | |
| 40 | + | let i2c_bus: SharedI2c = Box::leak(Box::new(RefCell::new(i2c))); | |
| 41 | + | ||
| 42 | + | let mut display = Display::new(i2c_bus); | |
| 43 | + | ||
| 44 | + | // Buttons by physical label (active-low, internal pull-ups). | |
| 45 | + | let mut buttons = Buttons::new( | |
| 46 | + | pins.gpio4.degrade_input_output(), // UP | |
| 47 | + | pins.gpio5.degrade_input_output(), // DOWN | |
| 48 | + | pins.gpio6.degrade_input_output(), // SELECT | |
| 49 | + | pins.gpio7.degrade_input_output(), // BACK | |
| 50 | + | pins.gpio1.degrade_input_output(), // RIGHT | |
| 51 | + | pins.gpio2.degrade_input_output(), // LEFT | |
| 52 | + | ); | |
| 53 | + | ||
| 54 | + | // Everything the radio/NFC phases will consume, owned up front. | |
| 55 | + | let mut board = Board { | |
| 56 | + | i2c: i2c_bus, | |
| 57 | + | modem: peripherals.modem, | |
| 58 | + | spi2: peripherals.spi2, | |
| 59 | + | spi3: peripherals.spi3, | |
| 60 | + | nrf: NrfPins { | |
| 61 | + | sck: pins.gpio18.degrade_input_output(), | |
| 62 | + | miso: pins.gpio16.degrade_input_output(), | |
| 63 | + | mosi: pins.gpio17.degrade_input_output(), | |
| 64 | + | ce1: pins.gpio10.degrade_input_output(), | |
| 65 | + | csn1: pins.gpio11.degrade_input_output(), | |
| 66 | + | ce2: pins.gpio12.degrade_input_output(), | |
| 67 | + | csn2: pins.gpio13.degrade_input_output(), | |
| 68 | + | }, | |
| 69 | + | cc1101: Cc1101Pins { | |
| 70 | + | sck: pins.gpio15.degrade_input_output(), | |
| 71 | + | miso: pins.gpio41.degrade_input_output(), | |
| 72 | + | mosi: pins.gpio35.degrade_input_output(), | |
| 73 | + | cs: pins.gpio40.degrade_input_output(), | |
| 74 | + | gdo0: pins.gpio39.degrade_input_output(), | |
| 75 | + | gdo2: pins.gpio42.degrade_input_output(), | |
| 76 | + | }, | |
| 77 | + | }; | |
| 17 | 78 | ||
| 18 | − | display.init(); | |
| 19 | − | display.draw_text("ORION-RF"); | |
| 79 | + | ui::widgets::splash(&mut display); | |
| 80 | + | esp_idf_hal::delay::FreeRtos::delay_ms(1500); | |
| 20 | 81 | ||
| 82 | + | log::info!("ORION-RF ready"); | |
| 83 | + | ui::menu::run(&mut display, &mut buttons, &mut board); | |
| 21 | 84 | } | |
| @@ -0,0 +1,242 @@ | |||
| 1 | + | //! Scrolling menu + submenu navigation and feature dispatch. | |
| 2 | + | //! | |
| 3 | + | //! Parity reference: `src-arduino/ui/menu.cpp`. Four visible rows, a `>` | |
| 4 | + | //! cursor, `^`/`v` scroll indicators, wrap-around up/down, and the same item | |
| 5 | + | //! order. Selecting BadUSB or NRF Tools enters a submenu; BACK from a submenu | |
| 6 | + | //! returns to the main menu. Restart shows a YES/NO confirm dialog. | |
| 7 | + | //! | |
| 8 | + | //! Phase 1 dispatches every feature except the submenu entries and Restart to | |
| 9 | + | //! [`StubApp`]; later phases swap in the real apps. | |
| 10 | + | ||
| 11 | + | use esp_idf_hal::delay::FreeRtos; | |
| 12 | + | ||
| 13 | + | use crate::apps::{App, AppCtx, StubApp}; | |
| 14 | + | use crate::board::board::Board; | |
| 15 | + | use crate::drivers::buttons::{debounce, Buttons}; | |
| 16 | + | use crate::drivers::display::Display; | |
| 17 | + | ||
| 18 | + | const VISIBLE_ROWS: usize = 4; | |
| 19 | + | ||
| 20 | + | const MAIN: &[&str] = &[ | |
| 21 | + | "BadUSB", | |
| 22 | + | "RF Capture", | |
| 23 | + | "NRF Tools", | |
| 24 | + | "BLE Scan", | |
| 25 | + | "Wifi Scan", | |
| 26 | + | "Wifi Analyzer", | |
| 27 | + | "System Info", | |
| 28 | + | "Device Check", | |
| 29 | + | "Restart", | |
| 30 | + | "Ble Mouse", | |
| 31 | + | "NFC Tools", | |
| 32 | + | ]; | |
| 33 | + | ||
| 34 | + | const NRF_TOOLS: &[&str] = &[ | |
| 35 | + | "BLE Jammer", | |
| 36 | + | "Bluetooth Jammer", | |
| 37 | + | "Wifi Jammer", | |
| 38 | + | "USB Wireless", | |
| 39 | + | "Video TX", | |
| 40 | + | "Zigbee", | |
| 41 | + | "RC", | |
| 42 | + | ]; | |
| 43 | + | ||
| 44 | + | const BADUSB: &[&str] = &[ | |
| 45 | + | "ORION Demo", | |
| 46 | + | "RickRoll", | |
| 47 | + | "Matrix Rain", | |
| 48 | + | "Fake Terminal", | |
| 49 | + | "Wifi Password Extractor", | |
| 50 | + | "Fake Update", | |
| 51 | + | "Fake BSOD", | |
| 52 | + | "Glitch Screen", | |
| 53 | + | "ASCII Spam", | |
| 54 | + | "Hacker Typer", | |
| 55 | + | "PWN Reverse shell", | |
| 56 | + | "Phishing attack", | |
| 57 | + | "Desktop Ghost", | |
| 58 | + | "System Stresser", | |
| 59 | + | ]; | |
| 60 | + | ||
| 61 | + | #[derive(Clone, Copy, PartialEq)] | |
| 62 | + | enum Screen { | |
| 63 | + | Main, | |
| 64 | + | Nrf, | |
| 65 | + | Badusb, | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | struct Nav { | |
| 69 | + | screen: Screen, | |
| 70 | + | index: usize, | |
| 71 | + | offset: usize, | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | impl Nav { | |
| 75 | + | fn items(&self) -> &'static [&'static str] { | |
| 76 | + | match self.screen { | |
| 77 | + | Screen::Main => MAIN, | |
| 78 | + | Screen::Nrf => NRF_TOOLS, | |
| 79 | + | Screen::Badusb => BADUSB, | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | fn enter(&mut self, screen: Screen) { | |
| 84 | + | self.screen = screen; | |
| 85 | + | self.index = 0; | |
| 86 | + | self.offset = 0; | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | /// Run the menu forever. This is the main loop of the device. | |
| 91 | + | pub fn run(display: &mut Display, buttons: &mut Buttons, board: &mut Board) -> ! { | |
| 92 | + | let mut nav = Nav { | |
| 93 | + | screen: Screen::Main, | |
| 94 | + | index: 0, | |
| 95 | + | offset: 0, | |
| 96 | + | }; | |
| 97 | + | draw(display, &mut nav); | |
| 98 | + | ||
| 99 | + | loop { | |
| 100 | + | if buttons.up() { | |
| 101 | + | let len = nav.items().len(); | |
| 102 | + | nav.index = if nav.index == 0 { len - 1 } else { nav.index - 1 }; | |
| 103 | + | draw(display, &mut nav); | |
| 104 | + | debounce(); | |
| 105 | + | } else if buttons.down() { | |
| 106 | + | nav.index += 1; | |
| 107 | + | if nav.index >= nav.items().len() { | |
| 108 | + | nav.index = 0; | |
| 109 | + | } | |
| 110 | + | draw(display, &mut nav); | |
| 111 | + | debounce(); | |
| 112 | + | } else if buttons.select() { | |
| 113 | + | debounce(); | |
| 114 | + | launch(display, buttons, board, &mut nav); | |
| 115 | + | draw(display, &mut nav); | |
| 116 | + | } else if buttons.back() { | |
| 117 | + | if nav.screen != Screen::Main { | |
| 118 | + | nav.enter(Screen::Main); | |
| 119 | + | draw(display, &mut nav); | |
| 120 | + | } | |
| 121 | + | debounce(); | |
| 122 | + | } else { | |
| 123 | + | FreeRtos::delay_ms(20); | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | /// Draw the current menu with scroll handling (parity: `drawMenu`). | |
| 129 | + | fn draw(display: &mut Display, nav: &mut Nav) { | |
| 130 | + | let items = nav.items(); | |
| 131 | + | ||
| 132 | + | if nav.index < nav.offset { | |
| 133 | + | nav.offset = nav.index; | |
| 134 | + | } | |
| 135 | + | if nav.index >= nav.offset + VISIBLE_ROWS { | |
| 136 | + | nav.offset = nav.index - VISIBLE_ROWS + 1; | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | display.clear(); | |
| 140 | + | ||
| 141 | + | for i in 0..VISIBLE_ROWS { | |
| 142 | + | let item = nav.offset + i; | |
| 143 | + | if item >= items.len() { | |
| 144 | + | break; | |
| 145 | + | } | |
| 146 | + | let y = 14 + i as i32 * 14; | |
| 147 | + | if item == nav.index { | |
| 148 | + | display.text(0, y, ">"); | |
| 149 | + | } | |
| 150 | + | display.text(10, y, items[item]); | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | if nav.offset > 0 { | |
| 154 | + | display.text(118, 10, "^"); | |
| 155 | + | } | |
| 156 | + | if nav.offset + VISIBLE_ROWS < items.len() { | |
| 157 | + | display.text(118, 62, "v"); | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | display.flush(); | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | /// Act on SELECT in the current screen (parity: `launchFeature`). | |
| 164 | + | fn launch(display: &mut Display, buttons: &mut Buttons, board: &mut Board, nav: &mut Nav) { | |
| 165 | + | match nav.screen { | |
| 166 | + | Screen::Main => match nav.index { | |
| 167 | + | 0 => nav.enter(Screen::Badusb), | |
| 168 | + | 2 => nav.enter(Screen::Nrf), | |
| 169 | + | 8 => confirm_restart(display, buttons), | |
| 170 | + | other => run_stub(display, buttons, board, MAIN[other]), | |
| 171 | + | }, | |
| 172 | + | Screen::Nrf => run_stub(display, buttons, board, NRF_TOOLS[nav.index]), | |
| 173 | + | Screen::Badusb => run_stub(display, buttons, board, BADUSB[nav.index]), | |
| 174 | + | } | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | fn run_stub(display: &mut Display, buttons: &mut Buttons, board: &mut Board, name: &'static str) { | |
| 178 | + | let mut app = StubApp { name }; | |
| 179 | + | let mut ctx = AppCtx { | |
| 180 | + | display, | |
| 181 | + | buttons, | |
| 182 | + | board, | |
| 183 | + | }; | |
| 184 | + | let _ = app.run(&mut ctx); | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | /// YES/NO restart confirmation (parity: `menu.cpp` case 8). | |
| 188 | + | fn confirm_restart(d: &mut Display, b: &mut Buttons) { | |
| 189 | + | // Wait for the SELECT that opened this dialog to be released. | |
| 190 | + | FreeRtos::delay_ms(200); | |
| 191 | + | while b.select() { | |
| 192 | + | FreeRtos::delay_ms(10); | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | let mut confirm = false; | |
| 196 | + | ||
| 197 | + | loop { | |
| 198 | + | d.clear(); | |
| 199 | + | d.text(18, 18, "Restart Device?"); | |
| 200 | + | ||
| 201 | + | if confirm { | |
| 202 | + | d.fill_rect(10, 35, 45, 15); | |
| 203 | + | d.text_rev(20, 47, "YES"); | |
| 204 | + | d.text(75, 47, "NO"); | |
| 205 | + | } else { | |
| 206 | + | d.text(20, 47, "YES"); | |
| 207 | + | d.fill_rect(65, 35, 45, 15); | |
| 208 | + | d.text_rev(78, 47, "NO"); | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | d.flush(); | |
| 212 | + | ||
| 213 | + | if b.left() || b.up() { | |
| 214 | + | confirm = true; | |
| 215 | + | debounce(); | |
| 216 | + | } | |
| 217 | + | if b.right() || b.down() { | |
| 218 | + | confirm = false; | |
| 219 | + | debounce(); | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | if b.select() { | |
| 223 | + | debounce(); | |
| 224 | + | if confirm { | |
| 225 | + | d.clear(); | |
| 226 | + | d.text(28, 30, "Restarting..."); | |
| 227 | + | d.flush(); | |
| 228 | + | FreeRtos::delay_ms(1000); | |
| 229 | + | esp_idf_hal::reset::restart(); | |
| 230 | + | } else { | |
| 231 | + | break; | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | if b.back() { | |
| 236 | + | debounce(); | |
| 237 | + | break; | |
| 238 | + | } | |
| 239 | + | ||
| 240 | + | FreeRtos::delay_ms(10); | |
| 241 | + | } | |
| 242 | + | } | |
| @@ -0,0 +1,2 @@ | |||
| 1 | + | pub mod menu; | |
| 2 | + | pub mod widgets; | |
| @@ -0,0 +1,23 @@ | |||
| 1 | + | //! Reusable screens drawn with the [`Display`] primitives. | |
| 2 | + | //! | |
| 3 | + | //! Coordinates mirror the Arduino reference so the ported UI looks the same. | |
| 4 | + | ||
| 5 | + | use crate::drivers::display::Display; | |
| 6 | + | ||
| 7 | + | /// Boot splash (parity: `src-arduino/main.cpp` `showSplash`). | |
| 8 | + | pub fn splash(d: &mut Display) { | |
| 9 | + | d.clear(); | |
| 10 | + | d.title(10, 40, "Orion-RF"); | |
| 11 | + | d.text(25, 58, "Initializing..."); | |
| 12 | + | d.flush(); | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | /// Placeholder screen for features not yet ported. Replaced by the real app in | |
| 16 | + | /// its phase; until then every unimplemented menu entry lands here. | |
| 17 | + | pub fn stub(d: &mut Display, name: &str) { | |
| 18 | + | d.clear(); | |
| 19 | + | d.text(0, 14, name); | |
| 20 | + | d.text(0, 34, "Not implemented"); | |
| 21 | + | d.text(0, 54, "BACK to return"); | |
| 22 | + | d.flush(); | |
| 23 | + | } | |