├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md ├── app ├── Makefile ├── admin-cli │ ├── Cargo.toml │ └── src │ │ ├── main.rs │ │ ├── subcmds.rs │ │ └── subcmds │ │ └── onebutton.rs ├── app-core │ ├── Cargo.toml │ ├── a.mid │ ├── build.rs │ ├── docs │ │ └── 设计预期.md │ ├── music │ │ ├── Fontaine__HOYO-MiX.mid │ │ ├── If_I_Can_Stop_One_Heart_From_Breaking.mid │ │ ├── Le Souvenir avec le crepuscule.mid │ │ └── 我不曾忘记.mid │ ├── src │ │ ├── adapter.rs │ │ ├── adapter │ │ │ └── route.rs │ │ ├── lib.rs │ │ ├── node.rs │ │ ├── node │ │ │ ├── page.rs │ │ │ ├── page │ │ │ │ ├── boot.rs │ │ │ │ ├── home.rs │ │ │ │ ├── menu.rs │ │ │ │ ├── music.rs │ │ │ │ └── weather.rs │ │ │ ├── service.rs │ │ │ ├── service │ │ │ │ ├── midiplayer.rs │ │ │ │ ├── onebutton.rs │ │ │ │ ├── router.rs │ │ │ │ ├── storage.rs │ │ │ │ ├── system.rs │ │ │ │ ├── timer.rs │ │ │ │ ├── weather.rs │ │ │ │ ├── weather │ │ │ │ │ ├── common.rs │ │ │ │ │ ├── geo.rs │ │ │ │ │ └── weather.rs │ │ │ │ └── wifi.rs │ │ │ ├── topview.rs │ │ │ └── topview │ │ │ │ ├── alarm.rs │ │ │ │ ├── canvas.rs │ │ │ │ └── notication.rs │ │ ├── scheduler.rs │ │ └── ui.rs │ ├── tools │ │ └── merge_midi.py │ └── ui │ │ ├── app.slint │ │ ├── common │ │ └── route.slint │ │ ├── components │ │ ├── index.slint │ │ ├── qweather-icon │ │ │ ├── icons │ │ │ │ ├── 100.svg │ │ │ │ ├── 101.svg │ │ │ │ ├── 102.svg │ │ │ │ ├── 103.svg │ │ │ │ ├── 104.svg │ │ │ │ ├── 150.svg │ │ │ │ ├── 151.svg │ │ │ │ ├── 152.svg │ │ │ │ ├── 153.svg │ │ │ │ ├── 300.svg │ │ │ │ ├── 301.svg │ │ │ │ ├── 302.svg │ │ │ │ ├── 303.svg │ │ │ │ ├── 304.svg │ │ │ │ ├── 305.svg │ │ │ │ ├── 306.svg │ │ │ │ ├── 307.svg │ │ │ │ ├── 308.svg │ │ │ │ ├── 309.svg │ │ │ │ ├── 310.svg │ │ │ │ ├── 311.svg │ │ │ │ ├── 312.svg │ │ │ │ ├── 313.svg │ │ │ │ ├── 314.svg │ │ │ │ ├── 315.svg │ │ │ │ ├── 316.svg │ │ │ │ ├── 317.svg │ │ │ │ ├── 318.svg │ │ │ │ ├── 350.svg │ │ │ │ ├── 351.svg │ │ │ │ ├── 399.svg │ │ │ │ ├── 400.svg │ │ │ │ ├── 401.svg │ │ │ │ ├── 402.svg │ │ │ │ ├── 403.svg │ │ │ │ ├── 404.svg │ │ │ │ ├── 405.svg │ │ │ │ ├── 406.svg │ │ │ │ ├── 407.svg │ │ │ │ ├── 408.svg │ │ │ │ ├── 409.svg │ │ │ │ ├── 410.svg │ │ │ │ ├── 456.svg │ │ │ │ ├── 457.svg │ │ │ │ ├── 499.svg │ │ │ │ ├── 500.svg │ │ │ │ ├── 501.svg │ │ │ │ ├── 502.svg │ │ │ │ ├── 503.svg │ │ │ │ ├── 504.svg │ │ │ │ ├── 507.svg │ │ │ │ ├── 508.svg │ │ │ │ ├── 509.svg │ │ │ │ ├── 510.svg │ │ │ │ ├── 511.svg │ │ │ │ ├── 512.svg │ │ │ │ ├── 513.svg │ │ │ │ ├── 514.svg │ │ │ │ ├── 515.svg │ │ │ │ ├── 900.svg │ │ │ │ ├── 901.svg │ │ │ │ └── 999.svg │ │ │ └── index.slint │ │ └── string-listview.slint │ │ ├── pages │ │ ├── boot │ │ │ ├── assets │ │ │ │ ├── gate.jpg │ │ │ │ ├── genshin.png │ │ │ │ ├── genshin.png:Zone.Identifier │ │ │ │ ├── mihoyo.png │ │ │ │ └── mihoyo.png:Zone.Identifier │ │ │ └── index.slint │ │ ├── home │ │ │ ├── Klee.png │ │ │ ├── Nahida.png │ │ │ ├── assets │ │ │ │ ├── icons │ │ │ │ │ ├── thermometer.svg │ │ │ │ │ └── water-level.svg │ │ │ │ └── index.slint │ │ │ └── index.slint │ │ ├── index.slint │ │ ├── menu │ │ │ ├── assets │ │ │ │ ├── icons │ │ │ │ │ ├── home.svg │ │ │ │ │ └── sun.svg │ │ │ │ └── index.slint │ │ │ └── index.slint │ │ ├── music │ │ │ └── index.slint │ │ └── weather │ │ │ └── index.slint │ │ └── topviews │ │ ├── alertdialog.slint │ │ ├── canvas.slint │ │ ├── index.slint │ │ └── performance.slint ├── desktop-impl │ ├── .gitignore │ ├── Cargo.toml │ ├── msg_graph.py │ └── src │ │ ├── http_client.rs │ │ ├── http_server.rs │ │ ├── json_storage.rs │ │ ├── main.rs │ │ └── midi_player.rs ├── esp32c3-impl │ ├── .cargo │ │ └── config.toml │ ├── .gitignore │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ ├── build.rs │ ├── partitions.csv │ ├── rust-toolchain.toml │ ├── sdkconfig.defaults │ └── src │ │ ├── main.rs │ │ ├── node.rs │ │ ├── node │ │ ├── buzzer.rs │ │ ├── canvas.rs │ │ ├── httpclient.rs │ │ ├── httpserver.rs │ │ ├── onebutton.rs │ │ ├── sntp.rs │ │ ├── storage.rs │ │ ├── system.rs │ │ └── wifi.rs │ │ ├── peripherals.rs │ │ └── peripherals │ │ └── pyclock.rs ├── proto │ ├── Cargo.toml │ └── src │ │ ├── ipc.rs │ │ ├── ipc │ │ ├── buzzer.rs │ │ ├── httpclient.rs │ │ ├── midi.rs │ │ ├── notifaction.rs │ │ ├── storage.rs │ │ ├── system.rs │ │ ├── useralarm.rs │ │ └── weather.rs │ │ ├── lib.rs │ │ ├── message.rs │ │ ├── message │ │ ├── bootpage.rs │ │ ├── buzzer.rs │ │ ├── canvas.rs │ │ ├── common.rs │ │ ├── http.rs │ │ ├── lifecycle.rs │ │ ├── midi.rs │ │ ├── notifaction.rs │ │ ├── onebutton.rs │ │ ├── router.rs │ │ ├── sntp.rs │ │ ├── storage.rs │ │ ├── system.rs │ │ ├── timer.rs │ │ ├── useralarm.rs │ │ ├── weather.rs │ │ └── wifi.rs │ │ ├── node.rs │ │ ├── storage.rs │ │ ├── storage │ │ ├── music.rs │ │ ├── system.rs │ │ ├── useralarm.rs │ │ ├── weather.rs │ │ └── wifi.rs │ │ └── topic.rs └── wasm-impl │ ├── Cargo.toml │ ├── Makefile │ ├── index.html │ ├── index.js │ ├── index.module.js │ ├── js │ └── midi.min.js │ └── src │ ├── console.rs │ ├── http.rs │ ├── lib.rs │ ├── midiplayer.rs │ └── storage.rs ├── libs ├── embedded-graphics-mux │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── embedded-software-slint-backend │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── embedded-tone │ ├── Cargo.toml │ ├── examples │ └── desktop-tone │ │ ├── Cargo.toml │ │ ├── dsa.mid │ │ ├── liyue.mid │ │ ├── ql.mid │ │ └── src │ │ └── main.rs │ └── src │ ├── guitar.rs │ ├── lib.rs │ ├── note.rs │ └── player.rs ├── screen-projector ├── Cargo.toml └── src │ ├── main.rs │ └── screen.rs ├── server ├── .gitignore ├── Cargo.toml └── src │ ├── error.rs │ ├── main.rs │ ├── service.rs │ └── service │ ├── openwrt.rs │ ├── openwrt │ └── client.rs │ ├── photo.rs │ ├── ping.rs │ └── weather.rs └── vue-console ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public └── vite.svg ├── src ├── App.vue ├── api │ ├── common.ts │ ├── index.ts │ └── onebutton.ts ├── main.ts ├── router │ └── index.ts ├── store │ ├── admin.ts │ └── index.ts ├── style.css ├── views │ └── Login.vue └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: {} 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Setup Rust 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: nightly 19 | components: rustfmt, clippy 20 | override: true 21 | 22 | - name: Setup Node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | 27 | - name: Build app for desktop 28 | run: | 29 | sudo apt update 30 | sudo apt install libudev-dev libasound2-dev libsdl2-dev 31 | cargo install ldproxy 32 | git submodule update --init 33 | cargo build 34 | cd app/esp32c3-impl && cargo build && cd - -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libs/button-driver"] 2 | path = libs/button-driver 3 | url = https://github.com/zhangzqs/button-driver 4 | branch = platform-interface 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "app/proto", 6 | "app/app-core", 7 | "libs/button-driver", 8 | "app/desktop-impl", 9 | "app/admin-cli", 10 | "app/wasm-impl", 11 | ] 12 | exclude = ["app/esp32c3-impl"] 13 | 14 | [profile.release] 15 | opt-level = "s" 16 | 17 | [profile.dev] 18 | debug = true 19 | opt-level = "z" 20 | -------------------------------------------------------------------------------- /app/Makefile: -------------------------------------------------------------------------------- 1 | fmt: 2 | cd proto && cargo fmt && cd - 3 | cd app-core && cargo fmt && cd - 4 | cd desktop-impl && cargo fmt && cd - 5 | cd desktop-software-renderer-impl && cargo fmt && cd - 6 | cd esp32c3-impl && cargo fmt && cd - 7 | cd wasm-impl && cargo fmt && cd - 8 | 9 | clippy: 10 | cd proto && cargo clippy --fix --allow-dirty && cd - 11 | cd app-core && cargo clippy --fix --allow-dirty && cd - 12 | cd desktop-impl && cargo clippy --fix --allow-dirty && cd - 13 | cd desktop-software-renderer-impl && cargo clippy --fix --allow-dirty && cd - 14 | cd esp32c3-impl && cargo clippy --fix --allow-dirty && cd - 15 | cd wasm-impl && cargo clippy --fix --allow-dirty && cd - -------------------------------------------------------------------------------- /app/admin-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "admin-cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | reqwest = { version = "0.11.23", features = ["blocking", "json"] } 8 | serde = { version = "1.0.193", features = ["derive"] } 9 | serde_json = "1.0.108" 10 | clap = { version = "4.4.11", features = ["derive"] } 11 | env_logger = "0.10.1" 12 | log = "0.4.20" 13 | anyhow = "1.0.86" 14 | proto = { path = "../proto"} 15 | termion = "4.0.0" 16 | tui = "0.19.0" 17 | crossterm = "0.27.0" 18 | 19 | -------------------------------------------------------------------------------- /app/admin-cli/src/subcmds/onebutton.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | use crossterm::{ 5 | event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, 6 | execute, 7 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 8 | }; 9 | use tui::{ 10 | backend::CrosstermBackend, 11 | widgets::{Block, Borders}, 12 | Terminal, 13 | }; 14 | 15 | pub fn run() -> Result<()> { 16 | enable_raw_mode()?; 17 | let mut stdout = std::io::stdout(); 18 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 19 | let backend = CrosstermBackend::new(stdout); 20 | let mut terminal = Terminal::new(backend)?; 21 | 22 | let handle_events = || -> Result { 23 | if event::poll(Duration::from_millis(50))? { 24 | if let Event::Key(key) = event::read()? { 25 | if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') { 26 | return Ok(true); 27 | } 28 | } 29 | } 30 | Ok(false) 31 | }; 32 | 33 | let mut should_quit = false; 34 | while !should_quit { 35 | terminal.draw(|f| { 36 | let size = f.size(); 37 | let block = Block::default().title("Block").borders(Borders::ALL); 38 | f.render_widget(block, size); 39 | })?; 40 | should_quit = handle_events()?; 41 | } 42 | 43 | // restore terminal 44 | disable_raw_mode()?; 45 | execute!( 46 | terminal.backend_mut(), 47 | LeaveAlternateScreen, 48 | DisableMouseCapture 49 | )?; 50 | terminal.show_cursor()?; 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /app/app-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app-core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.83" 10 | log = "0.4.21" 11 | slint = { version = "1.6.0", default-features = false } 12 | time = { version = "0.3.36" } 13 | button-driver = { path = "../../libs/button-driver" } 14 | serde = { version = "1.0.202", features = ["derive"] } 15 | serde_json = "1.0.117" 16 | embedded-tone = { path = "../../libs/embedded-tone" } 17 | midly = { version = "0.5.3", default-features = false, features = ["alloc"] } 18 | base64 = "0.22.1" 19 | proto = { path = "../proto" } 20 | embedded-graphics = "0.8.1" 21 | 22 | [build-dependencies] 23 | slint-build = { version = "1.6.0" } 24 | cfg-if = "1.0.0" 25 | 26 | [features] 27 | default = ["slint/default"] 28 | software-renderer = ["slint/compat-1-2", "slint/unsafe-single-threaded"] 29 | dev-config = [] 30 | -------------------------------------------------------------------------------- /app/app-core/a.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/a.mid -------------------------------------------------------------------------------- /app/app-core/build.rs: -------------------------------------------------------------------------------- 1 | use slint_build::CompilerConfiguration; 2 | 3 | fn build_slint() -> Result<(), Box> { 4 | cfg_if::cfg_if! { 5 | if #[cfg(feature = "software-renderer")] { 6 | use slint_build::EmbedResourcesKind; 7 | let slint_cfg = CompilerConfiguration::new().embed_resources(EmbedResourcesKind::EmbedForSoftwareRenderer); 8 | slint_build::compile_with_config("ui/app.slint", slint_cfg)?; 9 | } else { 10 | let slint_cfg = CompilerConfiguration::new(); 11 | slint_build::compile_with_config("ui/app.slint", slint_cfg)?; 12 | } 13 | } 14 | Ok(()) 15 | } 16 | 17 | fn main() -> Result<(), Box> { 18 | build_slint()?; 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /app/app-core/docs/设计预期.md: -------------------------------------------------------------------------------- 1 | # 启动界面 2 | 3 | 原神启动 4 | 5 | # 桌面页 6 | 7 | 显示日期时间,当前天气,空气质量 8 | 9 | 单击进入菜单页 10 | 11 | # 菜单页 12 | 13 | 长按 2s 后,每 1s 进行切换 app 功能选择,直到松手 14 | 单机进入指定 app 15 | 16 | # 天气页 17 | 18 | 长按 3s 后返回桌面 19 | 20 | # 音乐页 21 | 22 | 单击可切换音乐 23 | 长按 3s 后返回桌面 24 | -------------------------------------------------------------------------------- /app/app-core/music/Fontaine__HOYO-MiX.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/music/Fontaine__HOYO-MiX.mid -------------------------------------------------------------------------------- /app/app-core/music/If_I_Can_Stop_One_Heart_From_Breaking.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/music/If_I_Can_Stop_One_Heart_From_Breaking.mid -------------------------------------------------------------------------------- /app/app-core/music/Le Souvenir avec le crepuscule.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/music/Le Souvenir avec le crepuscule.mid -------------------------------------------------------------------------------- /app/app-core/music/我不曾忘记.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/music/我不曾忘记.mid -------------------------------------------------------------------------------- /app/app-core/src/adapter.rs: -------------------------------------------------------------------------------- 1 | mod route; 2 | pub use route::{proto_route_table_to_slint_route_table, slint_route_table_to_proto_route_table}; 3 | -------------------------------------------------------------------------------- /app/app-core/src/adapter/route.rs: -------------------------------------------------------------------------------- 1 | use crate::{proto::RoutePage, ui::PageRouteTable}; 2 | 3 | pub fn proto_route_table_to_slint_route_table(r: RoutePage) -> PageRouteTable { 4 | match r { 5 | RoutePage::Boot => PageRouteTable::Boot, 6 | RoutePage::Home => PageRouteTable::Home, 7 | RoutePage::Menu => PageRouteTable::Menu, 8 | RoutePage::Weather => PageRouteTable::Weather, 9 | RoutePage::Music => PageRouteTable::Music, 10 | } 11 | } 12 | 13 | pub fn slint_route_table_to_proto_route_table(r: PageRouteTable) -> RoutePage { 14 | match r { 15 | PageRouteTable::Boot => RoutePage::Boot, 16 | PageRouteTable::Home => RoutePage::Home, 17 | PageRouteTable::Menu => RoutePage::Menu, 18 | PageRouteTable::Weather => RoutePage::Weather, 19 | PageRouteTable::Music => RoutePage::Music, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/app-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use node::*; 4 | 5 | mod adapter; 6 | mod node; 7 | mod scheduler; 8 | mod ui; 9 | 10 | pub use proto; 11 | pub use proto::storage; 12 | pub use scheduler::Scheduler; 13 | pub use ui::get_app_window; 14 | 15 | static mut SCHEDULER: Option> = None; 16 | 17 | pub fn get_scheduler() -> Rc { 18 | unsafe { 19 | SCHEDULER 20 | .get_or_insert_with(|| { 21 | let s = Scheduler::new(); 22 | register_default_nodes(&s); 23 | Rc::new(s) 24 | }) 25 | .clone() 26 | } 27 | } 28 | 29 | fn register_default_nodes(sche: &Scheduler) { 30 | sche.register_node(HomePage::new()); 31 | sche.register_node(WeatherPage::new()); 32 | sche.register_node(MenuPage::new()); 33 | sche.register_node(BootPage::new()); 34 | sche.register_node(AlertDialog::new()); 35 | sche.register_node(MusicPage::new()); 36 | 37 | sche.register_node(RouterService::new()); 38 | sche.register_node(TouchOneButtonAdapterService::new()); 39 | sche.register_node(WeatherService::new()); 40 | sche.register_node(MockStorageService::new()); 41 | sche.register_node(MockSystemService {}); 42 | sche.register_node(TimerService::new()); 43 | 44 | sche.register_node(MockWiFiService::new()); 45 | sche.register_node(MidiPlayerService::new()); 46 | sche.register_node(CanvasView::new()); 47 | sche.register_node(UserAlarmService::new()); 48 | } 49 | -------------------------------------------------------------------------------- /app/app-core/src/node.rs: -------------------------------------------------------------------------------- 1 | mod page; 2 | mod service; 3 | mod topview; 4 | 5 | pub use {page::*, service::*, topview::*}; 6 | -------------------------------------------------------------------------------- /app/app-core/src/node/page.rs: -------------------------------------------------------------------------------- 1 | mod boot; 2 | mod home; 3 | mod menu; 4 | mod music; 5 | mod weather; 6 | 7 | pub use {boot::BootPage, home::HomePage, menu::MenuPage, music::MusicPage, weather::WeatherPage}; 8 | -------------------------------------------------------------------------------- /app/app-core/src/node/service.rs: -------------------------------------------------------------------------------- 1 | mod midiplayer; 2 | mod onebutton; 3 | mod router; 4 | mod storage; 5 | mod system; 6 | mod timer; 7 | mod weather; 8 | mod wifi; 9 | 10 | pub use { 11 | midiplayer::MidiPlayerService, onebutton::TouchOneButtonAdapterService, router::RouterService, 12 | storage::MockStorageService, system::MockSystemService, timer::TimerService, 13 | weather::WeatherService, wifi::MockWiFiService, 14 | }; 15 | -------------------------------------------------------------------------------- /app/app-core/src/node/service/router.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use slint::ComponentHandle; 4 | 5 | use crate::get_app_window; 6 | use crate::proto::{ 7 | Context, HandleResult, LifecycleMessage, Message, MessageWithHeader, Node, NodeName, 8 | RouterMessage, 9 | }; 10 | use crate::{adapter, proto::RoutePage, ui::PageRouter}; 11 | 12 | pub struct RouterService {} 13 | 14 | impl RouterService { 15 | pub fn new() -> Self { 16 | Self {} 17 | } 18 | fn goto_page(r: RoutePage) { 19 | if let Some(ui) = get_app_window().upgrade() { 20 | let slint_route = adapter::proto_route_table_to_slint_route_table(r); 21 | let router = ui.global::(); 22 | router.set_current_page(slint_route); 23 | } 24 | } 25 | 26 | fn get_current_page() -> RoutePage { 27 | let slint_route = get_app_window() 28 | .upgrade() 29 | .unwrap() 30 | .global::() 31 | .get_current_page(); 32 | adapter::slint_route_table_to_proto_route_table(slint_route) 33 | } 34 | } 35 | 36 | impl Node for RouterService { 37 | fn node_name(&self) -> NodeName { 38 | NodeName::Router 39 | } 40 | 41 | fn handle_message(&self, ctx: Rc, msg: MessageWithHeader) -> HandleResult { 42 | match msg.body { 43 | Message::Router(RouterMessage::GotoPage(r)) => { 44 | ctx.sync_call( 45 | Self::get_current_page().map_to_node_name(), 46 | Message::Lifecycle(LifecycleMessage::Hide), 47 | ); 48 | ctx.sync_call( 49 | r.map_to_node_name(), 50 | Message::Lifecycle(LifecycleMessage::Show), 51 | ); 52 | Self::goto_page(r); 53 | return HandleResult::Finish(Message::Empty); 54 | } 55 | _ => {} 56 | } 57 | HandleResult::Discard 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/app-core/src/node/service/storage.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap}; 2 | 3 | use crate::proto::*; 4 | 5 | pub struct MockStorageService { 6 | data: RefCell>, 7 | } 8 | 9 | impl MockStorageService { 10 | pub fn new() -> Self { 11 | Self { 12 | data: RefCell::new(HashMap::new()), 13 | } 14 | } 15 | } 16 | 17 | impl Node for MockStorageService { 18 | fn node_name(&self) -> NodeName { 19 | NodeName::Storage 20 | } 21 | 22 | fn handle_message( 23 | &self, 24 | _ctx: std::rc::Rc, 25 | msg: MessageWithHeader, 26 | ) -> HandleResult { 27 | if let Message::Storage(sm) = msg.body { 28 | let mut data = self.data.borrow_mut(); 29 | return HandleResult::Finish(Message::Storage(match sm { 30 | StorageMessage::GetRequest(k) => { 31 | StorageMessage::GetResponse(data.get(&k).cloned().unwrap_or(StorageValue::None)) 32 | } 33 | StorageMessage::SetRequest(k, v) => { 34 | match v { 35 | StorageValue::None => { 36 | data.remove(&k); 37 | } 38 | v => { 39 | data.insert(k, v); 40 | } 41 | } 42 | StorageMessage::SetResponse 43 | } 44 | StorageMessage::ListKeysRequest(prefix) => StorageMessage::ListKeysResponse( 45 | data.keys() 46 | .filter(|x| x.starts_with(&prefix)) 47 | .map(|x| x.into()) 48 | .collect(), 49 | ), 50 | m => panic!("unexcepted message {m:?}"), 51 | })); 52 | } 53 | HandleResult::Discard 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/app-core/src/node/service/system.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::proto::*; 4 | pub struct MockSystemService {} 5 | 6 | impl Node for MockSystemService { 7 | fn node_name(&self) -> NodeName { 8 | NodeName::System 9 | } 10 | 11 | fn handle_message(&self, _ctx: Rc, msg: MessageWithHeader) -> HandleResult { 12 | if let Message::System(pm) = msg.body { 13 | return HandleResult::Finish(Message::System(match pm { 14 | SystemMessage::GetFreeHeapSizeRequest => { 15 | SystemMessage::GetFreeHeapSizeResponse(666) 16 | } 17 | SystemMessage::GetLargestFreeBlock => { 18 | SystemMessage::GetLargestFreeBlockResponse(999) 19 | } 20 | SystemMessage::GetFpsRequest => SystemMessage::GetFpsResponse(60), 21 | m => panic!("unexpected message {m:?}"), 22 | })); 23 | } 24 | HandleResult::Discard 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/app-core/src/node/service/timer.rs: -------------------------------------------------------------------------------- 1 | use std::{rc::Rc, time::Duration}; 2 | 3 | use crate::proto::*; 4 | 5 | pub struct TimerService {} 6 | 7 | impl TimerService { 8 | pub fn new() -> Self { 9 | Self {} 10 | } 11 | } 12 | 13 | impl Node for TimerService { 14 | fn node_name(&self) -> NodeName { 15 | NodeName::Timer 16 | } 17 | 18 | fn handle_message(&self, ctx: Rc, msg: MessageWithHeader) -> HandleResult { 19 | if let Message::Timer(TimerMessage::Request(x)) = msg.body { 20 | slint::Timer::single_shot(Duration::from_millis(x as _), move || { 21 | ctx.async_ready(msg.seq, Message::Timer(TimerMessage::Response)); 22 | }); 23 | return HandleResult::Pending; 24 | } 25 | HandleResult::Discard 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/app-core/src/node/service/weather/common.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, str::FromStr}; 2 | 3 | use crate::proto::*; 4 | use serde::{Deserialize, Serialize}; 5 | use time::{format_description::well_known::Iso8601, OffsetDateTime}; 6 | 7 | #[derive(Debug, Clone, Copy, Serialize)] 8 | pub struct Number(T); 9 | 10 | impl<'de, T: FromStr, E: Display> Deserialize<'de> for Number { 11 | fn deserialize(deserializer: D) -> Result 12 | where 13 | D: ::serde::Deserializer<'de>, 14 | { 15 | Ok(Self( 16 | String::deserialize(deserializer)? 17 | .parse::() 18 | .map_err(serde::de::Error::custom)?, 19 | )) 20 | } 21 | } 22 | 23 | impl ToString for Number { 24 | fn to_string(&self) -> String { 25 | self.0.to_string() 26 | } 27 | } 28 | 29 | impl Number { 30 | pub fn take(self) -> T { 31 | self.0 32 | } 33 | } 34 | 35 | #[derive(Debug, Clone, Copy)] 36 | pub struct UtcDateTime(OffsetDateTime); 37 | 38 | impl<'de> Deserialize<'de> for UtcDateTime { 39 | fn deserialize(deserializer: D) -> Result 40 | where 41 | D: serde::Deserializer<'de>, 42 | { 43 | let s = String::deserialize(deserializer)?; 44 | let date = OffsetDateTime::parse(&s, &Iso8601::DATE_TIME_OFFSET) 45 | .map_err(serde::de::Error::custom)?; 46 | Ok(Self(date)) 47 | } 48 | } 49 | 50 | impl From for OffsetDateTime { 51 | fn from(val: UtcDateTime) -> Self { 52 | val.0 53 | } 54 | } 55 | 56 | #[derive(Debug, Clone, Copy, Deserialize)] 57 | pub struct ErrorCode(Number); 58 | 59 | impl ErrorCode { 60 | pub fn detect_error(self) -> Result<(), WeatherError> { 61 | match self.0.take() { 62 | 200 | 206 => Ok(()), 63 | err_code => Err(WeatherError::ApiError(err_code)), 64 | } 65 | } 66 | } 67 | 68 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 69 | pub struct RgbColor { 70 | pub red: u8, 71 | pub green: u8, 72 | pub blue: u8, 73 | pub alpha: u8, 74 | } 75 | -------------------------------------------------------------------------------- /app/app-core/src/node/service/weather/geo.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::proto::*; 4 | use ipc::HttpClient; 5 | use serde::Deserialize; 6 | 7 | use super::common::*; 8 | 9 | #[derive(Deserialize, Debug, Clone)] 10 | pub struct GeoCityLookupInput { 11 | pub location: String, 12 | pub key: String, 13 | pub number: Option, 14 | } 15 | 16 | impl GeoCityLookupInput { 17 | fn to_url(&self) -> String { 18 | format!( 19 | "https://geoapi.qweather.com/v2/city/lookup?gzip=n&lang=en&key={}&location={}&number={}", 20 | self.key, 21 | self.location, 22 | self.number.unwrap_or(5), 23 | ) 24 | } 25 | 26 | pub fn request( 27 | &self, 28 | ctx: Rc, 29 | callback: Box)>, 30 | ) { 31 | HttpClient(ctx).request( 32 | HttpRequest { 33 | method: HttpRequestMethod::Get, 34 | url: self.to_url(), 35 | }, 36 | Box::new(|r| { 37 | callback(match r { 38 | Ok(x) => x 39 | .body 40 | .deserialize_by_json() 41 | .map_err(|e| WeatherError::SerdeError(format!("{e}"))), 42 | Err(e) => Err(WeatherError::HttpError(e)), 43 | }); 44 | }), 45 | ); 46 | } 47 | } 48 | 49 | #[derive(Deserialize, Debug, Clone)] 50 | pub struct GeoCityLookupItem { 51 | pub name: String, 52 | pub id: String, 53 | pub country: String, 54 | } 55 | 56 | impl From for proto::CityLookUpItem { 57 | fn from(val: GeoCityLookupItem) -> Self { 58 | proto::CityLookUpItem { 59 | name: val.name, 60 | id: val.id, 61 | country: val.country, 62 | } 63 | } 64 | } 65 | 66 | #[derive(Deserialize, Debug, Clone)] 67 | pub struct GeoCityLookupOutput { 68 | pub code: ErrorCode, 69 | pub location: Vec, 70 | } 71 | 72 | impl TryInto> for GeoCityLookupOutput { 73 | type Error = WeatherError; 74 | 75 | fn try_into(self) -> Result, Self::Error> { 76 | self.code.detect_error()?; 77 | Ok(self.location.into_iter().map(Into::into).collect()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/app-core/src/node/service/wifi.rs: -------------------------------------------------------------------------------- 1 | use std::{net::Ipv4Addr, rc::Rc, time::Duration}; 2 | 3 | use crate::proto::*; 4 | 5 | pub struct MockWiFiService {} 6 | 7 | impl MockWiFiService { 8 | pub fn new() -> Self { 9 | Self {} 10 | } 11 | } 12 | 13 | impl Node for MockWiFiService { 14 | fn node_name(&self) -> NodeName { 15 | NodeName::WiFi 16 | } 17 | 18 | fn handle_message(&self, ctx: Rc, msg: MessageWithHeader) -> HandleResult { 19 | let seq = msg.seq; 20 | if let Message::WiFi(msg) = msg.body { 21 | match msg { 22 | WiFiMessage::ConnectRequest(_) => { 23 | slint::Timer::single_shot(Duration::from_secs(9), move || { 24 | ctx.async_ready(seq, Message::WiFi(WiFiMessage::ConnectResponse)); 25 | }); 26 | return HandleResult::Pending; 27 | } 28 | WiFiMessage::StartAPRequest => { 29 | return HandleResult::Finish(Message::WiFi(WiFiMessage::StartAPResponse)); 30 | } 31 | WiFiMessage::GetIpInfoRequest => { 32 | return HandleResult::Finish(Message::WiFi(WiFiMessage::GetIpInfoResponse( 33 | NetIpInfo { 34 | ip: Ipv4Addr::new(127, 0, 0, 1), 35 | }, 36 | ))) 37 | } 38 | m => panic!("unexpected request message: {m:?}"), 39 | } 40 | } 41 | HandleResult::Discard 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/app-core/src/node/topview.rs: -------------------------------------------------------------------------------- 1 | mod alarm; 2 | mod canvas; 3 | mod notication; 4 | 5 | pub use alarm::UserAlarmService; 6 | pub use canvas::CanvasView; 7 | pub use notication::AlertDialog; 8 | -------------------------------------------------------------------------------- /app/app-core/src/node/topview/canvas.rs: -------------------------------------------------------------------------------- 1 | use proto::*; 2 | use slint::{ComponentHandle, Image, Rgb8Pixel, SharedPixelBuffer}; 3 | use std::{cell::RefCell, rc::Rc}; 4 | 5 | use crate::{get_app_window, ui}; 6 | pub struct CanvasView { 7 | spb: RefCell>>, 8 | } 9 | 10 | impl CanvasView { 11 | pub fn new() -> Self { 12 | Self { 13 | spb: Default::default(), 14 | } 15 | } 16 | 17 | fn show(&self) { 18 | if let Some(ui) = get_app_window().upgrade() { 19 | let vm = ui.global::(); 20 | vm.set_show(true); 21 | } 22 | } 23 | 24 | fn close(&self) { 25 | if let Some(ui) = get_app_window().upgrade() { 26 | let vm = ui.global::(); 27 | vm.set_show(false); 28 | } 29 | } 30 | 31 | fn update_frame(&self) { 32 | if let Some(ui) = get_app_window().upgrade() { 33 | let vm = ui.global::(); 34 | vm.set_image(Image::from_rgb8( 35 | self.spb.borrow().as_ref().unwrap().clone(), 36 | )) 37 | } 38 | } 39 | } 40 | 41 | impl Node for CanvasView { 42 | fn node_name(&self) -> NodeName { 43 | NodeName::Canvas 44 | } 45 | 46 | fn handle_message(&self, _ctx: Rc, msg: MessageWithHeader) -> HandleResult { 47 | match msg.body { 48 | Message::Lifecycle(LifecycleMessage::Init) => { 49 | *self.spb.borrow_mut() = Some(SharedPixelBuffer::new(240, 240)); 50 | } 51 | Message::Canvas(cm) => match cm { 52 | CanvasMessage::Open => { 53 | self.show(); 54 | } 55 | CanvasMessage::Close => { 56 | self.close(); 57 | } 58 | CanvasMessage::Clear((r, g, b)) => { 59 | if let Some(x) = self.spb.borrow_mut().as_mut() { 60 | x.make_mut_slice().iter_mut().for_each(|x| { 61 | (x.r, x.g, x.b) = (r, g, b); 62 | }); 63 | } 64 | self.update_frame(); 65 | } 66 | CanvasMessage::DrawLine(_) => todo!(), 67 | CanvasMessage::DrawCircle(_) => todo!(), 68 | CanvasMessage::DrawRectangle(_) => todo!(), 69 | CanvasMessage::DrawPixels(_) => todo!(), 70 | CanvasMessage::BatchCommand(_) => todo!(), 71 | }, 72 | _ => {} 73 | } 74 | HandleResult::Discard 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/app-core/src/ui.rs: -------------------------------------------------------------------------------- 1 | use slint::ComponentHandle; 2 | use slint::Weak; 3 | 4 | slint::include_modules!(); 5 | 6 | static mut APP: Option = None; 7 | 8 | pub fn get_app_window() -> Weak { 9 | unsafe { 10 | APP.get_or_insert_with(|| AppWindow::new().unwrap()) 11 | .as_weak() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/app-core/ui/app.slint: -------------------------------------------------------------------------------- 1 | import { PageRouteTable, PageRouter } from "common/route.slint"; 2 | import { RouteView, HomeViewModel, MenuViewModel, WeatherPageViewModel, MusicPageViewModel, BootPageViewModel } from "pages/index.slint"; 3 | import { TopView, AlertDialogViewModel, PerformanceViewModel, CanvasViewModel } from "topviews/index.slint"; 4 | 5 | export global OneButtenAdapter { 6 | callback pressed(); 7 | callback release(); 8 | } 9 | 10 | export component AppWindow inherits Window { 11 | width: 240px; 12 | height: 240px; 13 | 14 | TouchArea { 15 | pointer-event(e) => { 16 | if (e.button == PointerEventButton.left) { 17 | if (e.kind == PointerEventKind.down) { 18 | OneButtenAdapter.pressed() 19 | } else if (e.kind == PointerEventKind.up) { 20 | OneButtenAdapter.release() 21 | } 22 | } 23 | } 24 | } 25 | 26 | FocusScope { 27 | key-pressed(e) => { 28 | if (e.text == Key.Space) { 29 | OneButtenAdapter.pressed(); 30 | } 31 | accept 32 | } 33 | key-released(e) => { 34 | OneButtenAdapter.release(); 35 | accept 36 | } 37 | } 38 | 39 | TopView { 40 | z: 1; 41 | width: 100%; 42 | height: 100%; 43 | } 44 | 45 | RouteView { 46 | z: 0; 47 | width: 100%; 48 | height: 100%; 49 | } 50 | } 51 | 52 | export { 53 | HomeViewModel, 54 | MenuViewModel, 55 | PageRouter, 56 | WeatherPageViewModel, 57 | MusicPageViewModel, 58 | BootPageViewModel, 59 | AlertDialogViewModel, 60 | PerformanceViewModel, 61 | CanvasViewModel 62 | } -------------------------------------------------------------------------------- /app/app-core/ui/common/route.slint: -------------------------------------------------------------------------------- 1 | export enum PageRouteTable { 2 | boot, 3 | home, 4 | menu, 5 | weather, 6 | music, 7 | } 8 | 9 | export global PageRouter { 10 | in-out property current-page: PageRouteTable.boot; 11 | } 12 | -------------------------------------------------------------------------------- /app/app-core/ui/components/index.slint: -------------------------------------------------------------------------------- 1 | import { QWeatherIcon } from "qweather-icon/index.slint"; 2 | import { StringListView } from "string-listview.slint"; 3 | 4 | export { QWeatherIcon, StringListView } -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/100.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/101.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/102.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/103.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/104.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/150.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/151.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/152.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/153.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/300.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/301.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/302.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/303.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/304.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/305.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/306.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/307.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/308.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/309.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/310.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/311.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/312.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/313.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/314.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/315.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/316.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/317.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/318.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/350.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/351.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/399.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/400.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/401.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/402.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/403.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/404.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/405.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/406.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/407.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/408.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/456.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/457.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/499.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/500.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/501.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/502.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/503.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/504.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/507.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/508.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/509.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/510.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/511.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/512.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/513.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/514.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/515.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/900.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/901.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/components/qweather-icon/icons/999.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/app-core/ui/pages/boot/assets/gate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/ui/pages/boot/assets/gate.jpg -------------------------------------------------------------------------------- /app/app-core/ui/pages/boot/assets/genshin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/ui/pages/boot/assets/genshin.png -------------------------------------------------------------------------------- /app/app-core/ui/pages/boot/assets/genshin.png:Zone.Identifier: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/ui/pages/boot/assets/genshin.png:Zone.Identifier -------------------------------------------------------------------------------- /app/app-core/ui/pages/boot/assets/mihoyo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/ui/pages/boot/assets/mihoyo.png -------------------------------------------------------------------------------- /app/app-core/ui/pages/boot/assets/mihoyo.png:Zone.Identifier: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/ui/pages/boot/assets/mihoyo.png:Zone.Identifier -------------------------------------------------------------------------------- /app/app-core/ui/pages/home/Klee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/ui/pages/home/Klee.png -------------------------------------------------------------------------------- /app/app-core/ui/pages/home/Nahida.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/app/app-core/ui/pages/home/Nahida.png -------------------------------------------------------------------------------- /app/app-core/ui/pages/home/assets/icons/thermometer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/app-core/ui/pages/home/assets/icons/water-level.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/app-core/ui/pages/home/assets/index.slint: -------------------------------------------------------------------------------- 1 | export global HomeIcons { 2 | out property water-level: @image-url("icons/water-level.svg"); 3 | out property thermometer: @image-url("icons/thermometer.svg"); 4 | } 5 | -------------------------------------------------------------------------------- /app/app-core/ui/pages/index.slint: -------------------------------------------------------------------------------- 1 | import { HomePage, HomeViewModel } from "home/index.slint"; 2 | import { MenuPage, MenuViewModel } from "menu/index.slint"; 3 | import { PageRouter, PageRouteTable } from "../common/route.slint"; 4 | import { WeatherPage, WeatherPageViewModel } from "weather/index.slint"; 5 | import { MusicPage, MusicPageViewModel } from "music/index.slint"; 6 | import { BootPage, BootPageViewModel } from "boot/index.slint"; 7 | 8 | export component RouteView inherits Rectangle { 9 | BootPage { 10 | visible: PageRouter.current-page == PageRouteTable.boot; 11 | } 12 | 13 | HomePage { 14 | visible: PageRouter.current-page == PageRouteTable.home; 15 | } 16 | 17 | MenuPage { 18 | visible: PageRouter.current-page == PageRouteTable.menu; 19 | } 20 | 21 | WeatherPage { 22 | visible: PageRouter.current-page == PageRouteTable.weather; 23 | } 24 | 25 | MusicPage { 26 | visible: PageRouter.current-page == PageRouteTable.music; 27 | } 28 | } 29 | 30 | export { 31 | BootPageViewModel, 32 | HomeViewModel, 33 | MenuViewModel, 34 | WeatherPageViewModel, 35 | MusicPageViewModel 36 | } -------------------------------------------------------------------------------- /app/app-core/ui/pages/menu/assets/icons/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/app-core/ui/pages/menu/assets/icons/sun.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/app-core/ui/pages/menu/assets/index.slint: -------------------------------------------------------------------------------- 1 | export global MenuIcons { 2 | out property home: @image-url("icons/home.svg"); 3 | out property sun: @image-url("icons/sun.svg"); 4 | } -------------------------------------------------------------------------------- /app/app-core/ui/pages/music/index.slint: -------------------------------------------------------------------------------- 1 | import { Button, ScrollView } from "std-widgets.slint"; 2 | import { StringListView } from "../../components/string-listview.slint"; 3 | 4 | export global MusicPageViewModel { 5 | in property <[string]> music-list; 6 | in-out property select-id; 7 | } 8 | 9 | export component MusicPage inherits Rectangle { 10 | width: 240px; 11 | height: 240px; 12 | background: black; 13 | 14 | if MusicPageViewModel.music-list.length == 0: Text { 15 | text: "No music"; 16 | font-size: 30px; 17 | color: white; 18 | } 19 | 20 | if MusicPageViewModel.music-list.length > 0: StringListView { 21 | string-list: MusicPageViewModel.music-list; 22 | select-id: MusicPageViewModel.select-id; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/app-core/ui/topviews/alertdialog.slint: -------------------------------------------------------------------------------- 1 | 2 | export global AlertDialogViewModel { 3 | in-out property width: 80%; 4 | in-out property height: 80%; 5 | in-out property text; 6 | in-out property image; 7 | in-out property show; 8 | } 9 | 10 | export component AlertDialogView inherits Rectangle { 11 | visible: AlertDialogViewModel.show; 12 | Rectangle { 13 | border-color: gray; 14 | border-radius: 20px; 15 | border-width: 2px; 16 | background: black; 17 | width: AlertDialogViewModel.width; 18 | height: AlertDialogViewModel.height; 19 | HorizontalLayout { 20 | alignment: center; 21 | VerticalLayout { 22 | padding: 10px; 23 | alignment: center; 24 | Text { 25 | text: AlertDialogViewModel.text; 26 | font-size: 20px; 27 | color: red; 28 | wrap: word-wrap; 29 | } 30 | 31 | Image { 32 | source: AlertDialogViewModel.image; 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/app-core/ui/topviews/canvas.slint: -------------------------------------------------------------------------------- 1 | export global CanvasViewModel { 2 | in-out property image; 3 | in-out property show; 4 | } 5 | 6 | export component CanvasView inherits Rectangle { 7 | visible: CanvasViewModel.show; 8 | Image { 9 | width: 100%; 10 | height: 100%; 11 | source: CanvasViewModel.image; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/app-core/ui/topviews/index.slint: -------------------------------------------------------------------------------- 1 | import { AlertDialogView, AlertDialogViewModel } from "alertdialog.slint"; 2 | import { PerformanceView, PerformanceViewModel } from "performance.slint"; 3 | import { CanvasView, CanvasViewModel } from "canvas.slint"; 4 | export component TopView { 5 | CanvasView { 6 | z: 15; 7 | } 8 | 9 | PerformanceView { 10 | z: 10; 11 | } 12 | 13 | AlertDialogView { 14 | z: 5; 15 | } 16 | } 17 | 18 | export { 19 | AlertDialogViewModel, 20 | PerformanceViewModel, 21 | CanvasViewModel 22 | } -------------------------------------------------------------------------------- /app/app-core/ui/topviews/performance.slint: -------------------------------------------------------------------------------- 1 | 2 | export global PerformanceViewModel { 3 | in property is-show; 4 | in property fps; 5 | in property memory; 6 | in property largest-free-block; 7 | in property cpu; 8 | } 9 | 10 | export component PerformanceView inherits Rectangle { 11 | visible: PerformanceViewModel.is-show; 12 | in property fps <=> PerformanceViewModel.fps; 13 | in property memory <=> PerformanceViewModel.memory; 14 | in property largest-free-block <=> PerformanceViewModel.largest-free-block; 15 | in property cpu <=> PerformanceViewModel.cpu; 16 | 17 | VerticalLayout { 18 | alignment: start; 19 | HorizontalLayout { 20 | HorizontalLayout { 21 | alignment: start; 22 | Rectangle { 23 | background: gray.with-alpha(0.5); 24 | Text { 25 | color: #ff2a00; 26 | font-size: 20px; 27 | text: "memory: \{memory}\n" + "free block: \{largest-free-block}"; 28 | } 29 | } 30 | } 31 | 32 | HorizontalLayout { 33 | alignment: end; 34 | Rectangle { 35 | background: gray.with-alpha(0.5); 36 | Text { 37 | color: #ff2a00; 38 | font-size: 20px; 39 | text: "fps: \{fps}\n" + "cpu: \{cpu}"; 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/desktop-impl/.gitignore: -------------------------------------------------------------------------------- 1 | config.json -------------------------------------------------------------------------------- /app/desktop-impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "desktop-impl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | default = [] 8 | software-renderer = ["app-core/software-renderer"] 9 | 10 | [dependencies] 11 | slint = { version = "1.6.0" } 12 | anyhow = "1.0.44" 13 | env_logger = "0.10.1" 14 | log = "0.4.20" 15 | app-core = { path = "../app-core", default-features = false } 16 | reqwest = { version = "0.12.4", features = ["blocking", "gzip"] } 17 | tiny_http = "0.12.0" 18 | serde = "1.0.202" 19 | serde_json = "1.0.117" 20 | 21 | embedded-software-slint-backend = { path = "../../libs/embedded-software-slint-backend" } 22 | embedded-graphics = { version = "0.8.0" } 23 | embedded-graphics-simulator = { version = "0.5.0" } 24 | -------------------------------------------------------------------------------- /app/desktop-impl/msg_graph.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Dict, Tuple, Set 3 | import sys 4 | 5 | args = sys.argv 6 | if len(args) != 3: 7 | print("invalid argument") 8 | exit(1) 9 | 10 | input_file, output_file = args[1], args[2] 11 | 12 | first_pattern = r"from node: (\w+), to node: (\w+), msg: (.+)" 13 | second_pattern = r"handle message result: (\w+)" 14 | 15 | graph: Dict[Tuple[str, str], Set[str]] = {} 16 | first_node = 'Scheduler' 17 | 18 | with open(input_file) as f: 19 | for line in f: 20 | if 'handle message from' not in line: 21 | continue 22 | matches = re.search(first_pattern, line) 23 | from_node, to_node, msg = matches.group(1), matches.group(2), matches.group(3) 24 | 25 | matches = re.search(second_pattern, next(f)) 26 | result = matches.group(1) 27 | if result == 'Discard': 28 | continue 29 | 30 | k = (from_node, to_node) 31 | if k not in graph.keys(): 32 | graph[k] = set() 33 | graph[k].add(msg) 34 | 35 | with open(output_file, mode='w') as f: 36 | f.write(f'@startuml 消息图\n[*] --> {first_node}\n') 37 | for (edge, msgs) in graph.items(): 38 | for msg in msgs: 39 | f.write(f'{edge[0]} --> {edge[1]}: {msg}\n') 40 | f.write('@enduml') -------------------------------------------------------------------------------- /app/desktop-impl/src/http_server.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use app_core::proto::*; 4 | 5 | use log::error; 6 | use serde::{Deserialize, Serialize}; 7 | use tiny_http::Response; 8 | 9 | #[derive(Debug, Clone, Serialize, Deserialize)] 10 | struct Req { 11 | to: MessageTo, 12 | body: Message, 13 | } 14 | 15 | pub struct HttpServer { 16 | h: tiny_http::Server, 17 | } 18 | 19 | impl HttpServer { 20 | pub fn new() -> Self { 21 | let h = tiny_http::Server::http("127.0.0.1:38080").unwrap(); 22 | Self { h } 23 | } 24 | 25 | fn handle(&self, ctx: Rc) { 26 | match self.h.try_recv() { 27 | Ok(Some(mut raw_req)) => match serde_json::from_reader::<_, Req>(raw_req.as_reader()) { 28 | Ok(req_msg) => match req_msg.to { 29 | MessageTo::Broadcast => { 30 | ctx.broadcast_global(req_msg.body); 31 | } 32 | MessageTo::Topic(topic) => { 33 | ctx.broadcast_topic(topic, req_msg.body); 34 | } 35 | MessageTo::Point(p) => { 36 | ctx.async_call( 37 | p, 38 | req_msg.body, 39 | Box::new(|r| { 40 | let bs = serde_json::to_vec(&r).unwrap(); 41 | if let Err(e) = raw_req.respond(Response::from_data(bs)) { 42 | error!("http server write err: {e:?}"); 43 | } 44 | }), 45 | ); 46 | } 47 | }, 48 | Err(e) => { 49 | if let Err(e) = 50 | raw_req.respond(Response::from_string(e.to_string()).with_status_code(400)) 51 | { 52 | error!("http server write err: {e:?}"); 53 | } 54 | } 55 | }, 56 | _ => {} 57 | } 58 | } 59 | } 60 | 61 | impl Node for HttpServer { 62 | fn node_name(&self) -> NodeName { 63 | NodeName::Other("HttpServer".into()) 64 | } 65 | 66 | fn handle_message(&self, ctx: Rc, msg: MessageWithHeader) -> HandleResult { 67 | match msg.body { 68 | Message::Lifecycle(LifecycleMessage::Init) => ctx.subscribe_topic(TopicName::Scheduler), 69 | Message::Empty => self.handle(ctx.clone()), 70 | _ => {} 71 | } 72 | HandleResult::Discard 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/desktop-impl/src/midi_player.rs: -------------------------------------------------------------------------------- 1 | use app_core::proto::*; 2 | use std::rc::Rc; 3 | pub struct MidiPlayer {} 4 | 5 | impl MidiPlayer { 6 | pub fn new() -> Self { 7 | Self {} 8 | } 9 | } 10 | 11 | impl Node for MidiPlayer { 12 | fn node_name(&self) -> NodeName { 13 | NodeName::MidiPlayer 14 | } 15 | 16 | fn handle_message(&self, _ctx: Rc, msg: MessageWithHeader) -> HandleResult { 17 | match msg.body { 18 | Message::Midi(MidiMessage::PlayRequest(r)) => { 19 | println!("{r:?}"); 20 | return HandleResult::Finish(Message::Midi(MidiMessage::PlayResponse(false))); 21 | } 22 | _ => {} 23 | } 24 | HandleResult::Discard 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/esp32c3-impl/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "riscv32imc-esp-espidf" 3 | # target = "xtensa-esp32s3-espidf" 4 | 5 | [target.xtensa-esp32s3-espidf] 6 | linker = "ldproxy" 7 | runner = "espflash flash --monitor" # Select this runner for espflash v2.x.x 8 | rustflags = [ "--cfg", "espidf_time64"] # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110 9 | 10 | [target.riscv32imc-esp-espidf] 11 | linker = "ldproxy" 12 | runner = "espflash flash --monitor --partition-table=partitions.csv -s=4mb" # Select this runner for espflash v2.x.x 13 | rustflags = [ 14 | "--cfg","espidf_time64", 15 | "-C","default-linker-libraries", 16 | ] 17 | 18 | [unstable] 19 | build-std = ["std", "panic_abort"] 20 | 21 | [env] 22 | # MCU="esp32s3" 23 | ESP_IDF_VERSION = "v5.1.3" 24 | ESP_IDF_PATH_ISSUES = 'warn' -------------------------------------------------------------------------------- /app/esp32c3-impl/.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /.embuild 3 | /target 4 | /Cargo.lock 5 | /cfg.toml 6 | 7 | output-debug.bin 8 | output-release.bin -------------------------------------------------------------------------------- /app/esp32c3-impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "esp32c3-impl" 3 | version = "0.1.0" 4 | authors = ["zzq "] 5 | edition = "2021" 6 | resolver = "2" 7 | rust-version = "1.66" 8 | 9 | [profile.release] 10 | strip = true 11 | lto = true 12 | panic = "abort" 13 | opt-level = "z" 14 | 15 | [profile.dev] 16 | debug = true # Symbols are nice and they don't increase the size on Flash 17 | opt-level = "z" 18 | 19 | [features] 20 | pyclock = [] 21 | default = ["std", "embassy", "esp-idf-svc/native", "pyclock"] 22 | pio = ["esp-idf-svc/pio"] 23 | std = ["alloc", "esp-idf-svc/binstart", "esp-idf-svc/std"] 24 | alloc = ["esp-idf-svc/alloc"] 25 | nightly = ["esp-idf-svc/nightly"] 26 | experimental = ["esp-idf-svc/experimental"] 27 | embassy = [ 28 | "esp-idf-svc/embassy-sync", 29 | "esp-idf-svc/critical-section", 30 | "esp-idf-svc/embassy-time-driver", 31 | ] 32 | 33 | [dependencies] 34 | # embedded crates 35 | esp-idf-svc = { version = "0.48", default-features = false } 36 | esp-idf-sys = { version = "0.34" } 37 | esp-idf-hal = { version = "0.43" } 38 | embedded-svc = { version = "0.27" } 39 | embedded-graphics = "0.8.0" 40 | embedded-io-adapters = { version = "0.6.1", features = ["std"] } 41 | display-interface-spi = "0.4.1" 42 | display-interface = "0.4.1" 43 | mipidsi = { version = "0.7.1", default-features = false } 44 | heapless = "0.8.0" 45 | 46 | # common crates 47 | log = { version = "0.4.17", default-features = false } 48 | anyhow = "1.0.71" 49 | slint = { version = "1.6.0", default-features = false } 50 | serde_json = "1.0.117" 51 | serde = "1.0.203" 52 | libflate = "2.1.0" 53 | 54 | # internal crates 55 | embedded-software-slint-backend = { path = "../../libs/embedded-software-slint-backend" } 56 | embedded-graphics-mux = { path = "../../libs/embedded-graphics-mux" } 57 | app-core = { path = "../app-core", default-features = false, features = [ 58 | "software-renderer", 59 | ] } 60 | button-driver = { path = "../../libs/button-driver", features = [ 61 | "std", 62 | "embedded_hal", 63 | ] } 64 | 65 | [build-dependencies] 66 | embuild = "0.31.2" 67 | anyhow = "1.0.71" 68 | toml-cfg = "=0.1.3" 69 | -------------------------------------------------------------------------------- /app/esp32c3-impl/Makefile: -------------------------------------------------------------------------------- 1 | save-image: 2 | cargo build -r 3 | espflash save-image --chip esp32c3 target/riscv32imc-esp-espidf/release/esp32c3-impl output-release.bin 4 | 5 | save-debug-image: 6 | cargo build 7 | espflash save-image --chip esp32c3 target/riscv32imc-esp-espidf/debug/esp32c3-impl output-debug.bin -------------------------------------------------------------------------------- /app/esp32c3-impl/README.md: -------------------------------------------------------------------------------- 1 | # 硬件方案 2 | 3 | 硬件方案使用现成的 pyClock 4 | 5 | 具体 PCB 布局在以下仓库: 6 | https://github.com/01studio-lab/pyClock 7 | 8 | # 引脚布局 9 | 10 | esp32c3 模组引出了以下 GPIO 外设: 11 | 12 | ## 已被占用的 GPIO: 13 | 14 | | GPIO 编号 | 已连接的外设 | 15 | | --------- | ------------------- | 16 | | GPIO0 | Sensor 外部接口引出 | 17 | | GPIO2 | 内部蓝色 LED | 18 | | GPIO4 | LCD D/C | 19 | | GPIO5 | LCD CS | 20 | | GPIO6 | LCD SCL | 21 | | GPIO7 | LCD SDA | 22 | | GPIO8 | LCD RST | 23 | | GPIO9 | 外部按键 | 24 | | GPIO18 | USB- | 25 | | GPIO19 | USB+ | 26 | | GPIO20 | RX0 | 27 | | GPIO21 | TX0 | 28 | 29 | ## 剩余可用的 GPIO 30 | 31 | 根据官方手册,以下未使用的 GPIO 支持如下功能: 32 | 33 | | GPIO 编号 | 支持 | 34 | | --------- | --------------------------- | 35 | | GPIO1 | GPIO1, ADC1_CH1, XTAL_32K_N | 36 | | GPIO3 | GPIO3, ADC1_CH3, LED PWM | 37 | | GPIO10 | GPIO10, FSPICS0, LED PWM | 38 | 39 | # 魔改 40 | 41 | | GPIO 编号 | 使用 | 42 | | --------- | --------------------------------- | 43 | | GPIO0 | 连接蜂鸣器 | 44 | | GPIO10 | 连接屏幕背光,可 PWM 调节屏幕亮度 | 45 | -------------------------------------------------------------------------------- /app/esp32c3-impl/build.rs: -------------------------------------------------------------------------------- 1 | fn main() -> anyhow::Result<()> { 2 | embuild::espidf::sysenv::output(); 3 | Ok(()) 4 | } 5 | -------------------------------------------------------------------------------- /app/esp32c3-impl/partitions.csv: -------------------------------------------------------------------------------- 1 | # Name, Type, SubType, Offset, Size, Flags 2 | # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap 3 | # 384KB 4 | nvs, data, nvs, , 0x60000, 5 | # 4KB 6 | phy_init, data, phy, , 0x1000, 7 | # 3MB 8 | factory, app, factory, , 3M, -------------------------------------------------------------------------------- /app/esp32c3-impl/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = ["rust-src"] 4 | -------------------------------------------------------------------------------- /app/esp32c3-impl/sdkconfig.defaults: -------------------------------------------------------------------------------- 1 | # Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) 2 | CONFIG_ESP_MAIN_TASK_STACK_SIZE=30000 3 | 4 | # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). 5 | # This allows to use 1 ms granuality for thread sleeps (10 ms by default). 6 | CONFIG_FREERTOS_HZ=1000 7 | 8 | # Workaround for https://github.com/espressif/esp-idf/issues/7631 9 | # CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n 10 | # CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n 11 | 12 | CONFIG_LOG_DEFAULT_LEVEL_INFO=y 13 | CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT=y 14 | CONFIG_LOG_COLORS=y 15 | 16 | # 蓝牙相关 17 | # CONFIG_BT_ENABLED=y 18 | # CONFIG_BT_BLE_ENABLED=y 19 | # CONFIG_BT_BLUEDROID_ENABLED=n 20 | # CONFIG_BT_NIMBLE_ENABLED=y 21 | # CONFIG_BT_NIMBLE_EXT_ADV=y 22 | # CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y 23 | # CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n 24 | # CONFIG_BTDM_CTRL_MODE_BTDM=n 25 | # CONFIG_BT_NIMBLE_EXT_ADV=y 26 | 27 | # 证书 28 | # CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y 29 | 30 | # 启用WebSocket 31 | CONFIG_HTTPD_WS_SUPPORT=y -------------------------------------------------------------------------------- /app/esp32c3-impl/src/node.rs: -------------------------------------------------------------------------------- 1 | mod buzzer; 2 | mod canvas; 3 | mod httpclient; 4 | mod httpserver; 5 | mod onebutton; 6 | mod sntp; 7 | mod storage; 8 | mod system; 9 | mod wifi; 10 | 11 | pub use buzzer::BuzzerService; 12 | pub use canvas::CanvasView; 13 | pub use httpclient::HttpClientService; 14 | pub use httpserver::HttpServerService; 15 | pub use onebutton::OneButtonService; 16 | pub use sntp::SntpService; 17 | pub use storage::NvsStorageService; 18 | pub use system::SystemService; 19 | pub use wifi::WiFiService; 20 | -------------------------------------------------------------------------------- /app/esp32c3-impl/src/node/onebutton.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | rc::Rc, 3 | sync::mpsc::{sync_channel, Receiver, TryRecvError}, 4 | thread, 5 | time::Duration, 6 | }; 7 | 8 | use app_core::proto::*; 9 | use esp_idf_hal::gpio::{Input, Pin, PinDriver}; 10 | use esp_idf_sys as _; 11 | use log::info; 12 | 13 | pub struct OneButtonService { 14 | rx: Receiver, 15 | } 16 | 17 | impl OneButtonService { 18 | pub fn new(pin: PinDriver<'static, P, Input>) -> Self { 19 | let (tx, rx) = sync_channel(1); 20 | let mut button = button_driver::Button::new(pin, Default::default()); 21 | 22 | thread::spawn(move || loop { 23 | button.tick(); 24 | if button.clicks() > 0 { 25 | let clicks = button.clicks(); 26 | if clicks == 1 { 27 | tx.send(OneButtonMessage::Click).unwrap(); 28 | } else { 29 | tx.send(OneButtonMessage::Clicks(clicks)).unwrap(); 30 | } 31 | } else if let Some(dur) = button.current_holding_time() { 32 | info!("Held for {dur:?}"); 33 | tx.send(OneButtonMessage::LongPressHolding(dur.as_millis() as _)) 34 | .unwrap(); 35 | } else if let Some(dur) = button.held_time() { 36 | info!("Total holding time {dur:?}"); 37 | tx.send(OneButtonMessage::LongPressHeld(dur.as_millis() as _)) 38 | .unwrap(); 39 | } 40 | button.reset(); 41 | thread::sleep(Duration::from_millis(10)); 42 | }); 43 | Self { rx } 44 | } 45 | } 46 | 47 | impl Node for OneButtonService { 48 | fn node_name(&self) -> NodeName { 49 | NodeName::Other("EspOneButton".into()) 50 | } 51 | 52 | fn handle_message(&self, ctx: Rc, msg: MessageWithHeader) -> HandleResult { 53 | match msg.body { 54 | Message::Lifecycle(LifecycleMessage::Init) => { 55 | ctx.subscribe_topic(TopicName::Scheduler); 56 | } 57 | Message::Empty => match self.rx.try_recv() { 58 | Ok(x) => ctx.broadcast_topic(TopicName::OneButton, Message::OneButton(x)), 59 | Err(TryRecvError::Empty) => {} 60 | Err(TryRecvError::Disconnected) => unreachable!(), 61 | }, 62 | _ => {} 63 | } 64 | HandleResult::Discard 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/esp32c3-impl/src/node/sntp.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use app_core::proto::*; 4 | use esp_idf_svc::sntp::{EspSntp, OperatingMode, SntpConf, SyncMode, SyncStatus}; 5 | use esp_idf_sys as _; 6 | use log::info; 7 | 8 | pub struct SntpService { 9 | sntp: RefCell>>, 10 | } 11 | 12 | impl SntpService { 13 | pub fn new() -> Self { 14 | Self { 15 | sntp: RefCell::new(None), 16 | } 17 | } 18 | } 19 | 20 | impl Node for SntpService { 21 | fn node_name(&self) -> NodeName { 22 | NodeName::Other("EspSntpTime".into()) 23 | } 24 | 25 | fn poll(&self, ctx: Rc, seq: usize) { 26 | let s = self.sntp.borrow(); 27 | let sntp = s.as_ref().unwrap(); 28 | match sntp.get_sync_status() { 29 | SyncStatus::Reset => {} 30 | SyncStatus::Completed => { 31 | info!("时间同步完成"); 32 | ctx.broadcast_topic(TopicName::Sntp, Message::Sntp(SntpMessage::SyncCompleted)); 33 | ctx.async_ready(seq, Message::Empty); 34 | } 35 | SyncStatus::InProgress => {} 36 | } 37 | } 38 | 39 | fn handle_message(&self, ctx: Rc, msg: MessageWithHeader) -> HandleResult { 40 | match msg.body { 41 | Message::Lifecycle(LifecycleMessage::Init) => { 42 | ctx.subscribe_topic(TopicName::WiFi); 43 | } 44 | Message::WiFi(WiFiMessage::ConnectedBroadcast) => { 45 | let ntp_server = ipc::StorageClient(ctx) 46 | .get("sntp/server".into()) 47 | .unwrap() 48 | .as_str() 49 | .unwrap_or("0.pool.ntp.org".into()); 50 | let sntp = EspSntp::new(&SntpConf { 51 | servers: [&ntp_server], 52 | sync_mode: SyncMode::Immediate, 53 | operating_mode: OperatingMode::Poll, 54 | }) 55 | .unwrap(); 56 | *self.sntp.borrow_mut() = Some(sntp); 57 | return HandleResult::Pending; 58 | } 59 | _ => {} 60 | } 61 | HandleResult::Discard 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/esp32c3-impl/src/peripherals.rs: -------------------------------------------------------------------------------- 1 | use esp_idf_hal::ledc::{LedcChannel, LedcTimer}; 2 | use esp_idf_hal::rmt::RmtChannel; 3 | use esp_idf_svc::hal::gpio::{AnyInputPin, AnyOutputPin, PinDriver}; 4 | use esp_idf_svc::hal::modem::Modem; 5 | 6 | #[cfg(feature = "pyclock")] 7 | mod pyclock; 8 | #[cfg(feature = "pyclock")] 9 | use pyclock as _; 10 | 11 | pub struct SystemPeripherals< 12 | SPI, 13 | BuzzerRmtChannel, 14 | BacklightLedcChannel, 15 | BacklightLedcTimer, 16 | BoardLedLedcChannel, 17 | BoardLedLedcTimer, 18 | > { 19 | pub buzzer: Option>, 20 | pub board_led: Option>, 21 | pub button: Option, 22 | pub display: DisplaySpiPeripherals, 23 | pub modem: Modem, 24 | } 25 | 26 | pub struct BuzzerPeripherals { 27 | pub pin: AnyOutputPin, 28 | pub rmt_channel: C, 29 | } 30 | 31 | pub struct LedcPeripherals { 32 | pub pin: AnyOutputPin, 33 | pub ledc_channel: C, 34 | pub ledc_timer: T, 35 | } 36 | 37 | pub struct DisplayControlPeripherals { 38 | pub backlight: Option>, 39 | pub dc: AnyOutputPin, 40 | pub rst: AnyOutputPin, 41 | } 42 | 43 | pub struct DisplaySpiPeripherals { 44 | pub control: DisplayControlPeripherals, 45 | pub spi: SPI, 46 | pub sclk: AnyOutputPin, 47 | pub sdo: AnyOutputPin, 48 | pub cs: AnyOutputPin, 49 | } 50 | -------------------------------------------------------------------------------- /app/esp32c3-impl/src/peripherals/pyclock.rs: -------------------------------------------------------------------------------- 1 | use esp_idf_hal::{gpio, ledc, rmt, spi}; 2 | use esp_idf_svc::hal::peripherals::Peripherals; 3 | 4 | use super::{ 5 | BuzzerPeripherals, DisplayControlPeripherals, DisplaySpiPeripherals, LedcPeripherals, 6 | SystemPeripherals, 7 | }; 8 | 9 | impl 10 | SystemPeripherals< 11 | spi::SPI2, 12 | rmt::CHANNEL0, 13 | ledc::CHANNEL0, 14 | ledc::TIMER0, 15 | ledc::CHANNEL1, 16 | ledc::TIMER1, 17 | > 18 | { 19 | pub fn take() -> Self { 20 | let peripherals = Peripherals::take().unwrap(); 21 | 22 | SystemPeripherals { 23 | button: Some(peripherals.pins.gpio9.into()), 24 | board_led: Some(LedcPeripherals { 25 | pin: peripherals.pins.gpio2.into(), 26 | ledc_channel: peripherals.ledc.channel1, 27 | ledc_timer: peripherals.ledc.timer1, 28 | }), 29 | display: DisplaySpiPeripherals { 30 | control: DisplayControlPeripherals { 31 | backlight: Some(LedcPeripherals { 32 | pin: peripherals.pins.gpio10.into(), 33 | ledc_channel: peripherals.ledc.channel0, 34 | ledc_timer: peripherals.ledc.timer0, 35 | }), 36 | dc: peripherals.pins.gpio4.into(), 37 | rst: peripherals.pins.gpio8.into(), 38 | }, 39 | spi: peripherals.spi2, 40 | sclk: peripherals.pins.gpio6.into(), 41 | sdo: peripherals.pins.gpio7.into(), 42 | cs: peripherals.pins.gpio5.into(), 43 | }, 44 | modem: peripherals.modem, 45 | buzzer: Some(BuzzerPeripherals { 46 | pin: peripherals.pins.gpio0.into(), 47 | rmt_channel: peripherals.rmt.channel0, 48 | }), 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/proto/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proto" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "1.0.202", features = ["derive"] } 8 | serde_json = "1.0.117" 9 | base64 = "0.22.1" 10 | time = { version = "0.3.36", features = [ 11 | "parsing", 12 | "serde", 13 | "macros", 14 | "formatting", 15 | ] } 16 | -------------------------------------------------------------------------------- /app/proto/src/ipc.rs: -------------------------------------------------------------------------------- 1 | type AsyncCallback = Box; 2 | type AsyncResultCallback = Box)>; 3 | 4 | mod buzzer; 5 | mod httpclient; 6 | mod midi; 7 | mod notifaction; 8 | mod storage; 9 | mod system; 10 | mod useralarm; 11 | mod weather; 12 | 13 | pub use { 14 | buzzer::BuzzerClient, httpclient::HttpClient, midi::MidiPlayerClient, 15 | notifaction::NotifactionClient, storage::StorageClient, system::SystemClient, 16 | useralarm::UserAlarmClient, weather::WeatherClient, 17 | }; 18 | -------------------------------------------------------------------------------- /app/proto/src/ipc/buzzer.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::{Context, Message, NodeName}; 4 | 5 | use crate::message::{BuzzerMessage, ToneFrequency, ToneSeries}; 6 | 7 | use super::AsyncCallback; 8 | 9 | #[derive(Clone)] 10 | pub struct BuzzerClient(pub Rc); 11 | 12 | impl BuzzerClient { 13 | pub fn tone(&self, freq: ToneFrequency) { 14 | self.0.sync_call( 15 | NodeName::Buzzer, 16 | Message::Buzzer(BuzzerMessage::ToneForever(freq)), 17 | ); 18 | } 19 | 20 | pub fn tone_series(&self, series: ToneSeries, callback: AsyncCallback) { 21 | self.0.async_call( 22 | NodeName::Buzzer, 23 | Message::Buzzer(BuzzerMessage::ToneSeriesRequest(series)), 24 | Box::new(|r| { 25 | callback(match r.unwrap() { 26 | Message::Buzzer(BuzzerMessage::ToneSeriesResponse(is_finished)) => is_finished, 27 | m => panic!("unexpected response, {:?}", m), 28 | }) 29 | }), 30 | ); 31 | } 32 | 33 | pub fn off(&self) { 34 | self.0 35 | .sync_call(NodeName::Buzzer, Message::Buzzer(BuzzerMessage::Off)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/proto/src/ipc/httpclient.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::{Context, Message, NodeName}; 4 | 5 | use super::AsyncResultCallback; 6 | use crate::message::{HttpError, HttpMessage, HttpRequest, HttpResponse}; 7 | 8 | #[derive(Clone)] 9 | pub struct HttpClient(pub Rc); 10 | 11 | impl HttpClient { 12 | pub fn request( 13 | &self, 14 | request: HttpRequest, 15 | callback: AsyncResultCallback, 16 | ) { 17 | self.0.async_call( 18 | NodeName::HttpClient, 19 | Message::Http(HttpMessage::Request(request)), 20 | Box::new(|r| { 21 | callback(match r.unwrap() { 22 | Message::Http(HttpMessage::Response(resp)) => Ok(resp), 23 | Message::Http(HttpMessage::Error(e)) => Err(e), 24 | m => panic!("unexpected HandleResult {:?}", m), 25 | }); 26 | }), 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/proto/src/ipc/midi.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::{Context, Message, NodeName}; 4 | 5 | use crate::message::{Bytes, MidiError, MidiMessage}; 6 | 7 | use super::AsyncResultCallback; 8 | 9 | #[derive(Clone)] 10 | pub struct MidiPlayerClient(pub Rc); 11 | 12 | impl MidiPlayerClient { 13 | pub fn play(&self, mid: Vec, callback: AsyncResultCallback) { 14 | self.0.async_call( 15 | NodeName::MidiPlayer, 16 | Message::Midi(MidiMessage::PlayRequest(Bytes(mid))), 17 | Box::new(|r| { 18 | callback(match r.unwrap() { 19 | Message::Midi(msg) => match msg { 20 | MidiMessage::PlayResponse(is_finished) => Ok(is_finished), 21 | MidiMessage::Error(e) => Err(e), 22 | m => panic!("unexpected response, {:?}", m), 23 | }, 24 | m => panic!("unexpected response, {:?}", m), 25 | }); 26 | }), 27 | ); 28 | } 29 | 30 | pub fn off(&self) { 31 | self.0 32 | .sync_call(NodeName::MidiPlayer, Message::Midi(MidiMessage::Off)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/proto/src/ipc/notifaction.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::{Context, Message, NodeName, NotifactionContent, NotifactionMessage}; 4 | 5 | use super::AsyncCallback; 6 | #[derive(Clone)] 7 | pub struct NotifactionClient(pub Rc); 8 | 9 | impl NotifactionClient { 10 | pub fn show(&self, duration: usize, content: NotifactionContent, on_close: AsyncCallback<()>) { 11 | self.0.async_call( 12 | NodeName::Notifaction, 13 | Message::Notifaction(NotifactionMessage::ShowRequest { duration, content }), 14 | Box::new(move |r| match r.unwrap() { 15 | Message::Notifaction(NotifactionMessage::ShowResponse) => { 16 | on_close(()); 17 | } 18 | m => panic!("unexcepted msg: {:?}", m), 19 | }), 20 | ) 21 | } 22 | 23 | pub fn close(&self) { 24 | self.0.sync_call( 25 | NodeName::Notifaction, 26 | Message::Notifaction(NotifactionMessage::Close), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/proto/src/ipc/storage.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, rc::Rc}; 2 | 3 | use crate::{Context, Message, NodeName}; 4 | 5 | use crate::message::{StorageError, StorageMessage, StorageValue}; 6 | #[derive(Clone)] 7 | pub struct StorageClient(pub Rc); 8 | 9 | impl StorageClient { 10 | pub fn set(&self, key: String, value: StorageValue) -> Result<(), StorageError> { 11 | let r = self.0.sync_call( 12 | NodeName::Storage, 13 | Message::Storage(StorageMessage::SetRequest(key, value)), 14 | ); 15 | match r.unwrap() { 16 | Message::Storage(StorageMessage::SetResponse) => Ok(()), 17 | Message::Storage(StorageMessage::Error(e)) => Err(e), 18 | m => panic!("unexpected message {:?}", m), 19 | } 20 | } 21 | pub fn get(&self, key: String) -> Result { 22 | let r = self.0.sync_call( 23 | NodeName::Storage, 24 | Message::Storage(StorageMessage::GetRequest(key)), 25 | ); 26 | match r.unwrap() { 27 | Message::Storage(StorageMessage::GetResponse(r)) => Ok(r), 28 | Message::Storage(StorageMessage::Error(e)) => Err(e), 29 | m => panic!("unexpected message {:?}", m), 30 | } 31 | } 32 | 33 | pub fn list(&self, prefix: String) -> Result, StorageError> { 34 | let r = self.0.sync_call( 35 | NodeName::Storage, 36 | Message::Storage(StorageMessage::ListKeysRequest(prefix)), 37 | ); 38 | match r.unwrap() { 39 | Message::Storage(StorageMessage::ListKeysResponse(r)) => Ok(r), 40 | Message::Storage(StorageMessage::Error(e)) => Err(e), 41 | m => panic!("unexpected message {:?}", m), 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/proto/src/ipc/system.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::{Context, Message, NodeName}; 4 | 5 | use crate::message::SystemMessage; 6 | 7 | #[derive(Clone)] 8 | pub struct SystemClient(pub Rc); 9 | 10 | impl SystemClient { 11 | pub fn get_free_heap_size(&self) -> usize { 12 | let r = self.0.sync_call( 13 | NodeName::System, 14 | Message::System(SystemMessage::GetFreeHeapSizeRequest), 15 | ); 16 | match r.unwrap() { 17 | Message::System(SystemMessage::GetFreeHeapSizeResponse(s)) => s, 18 | m => panic!("unexpected response, {:?}", m), 19 | } 20 | } 21 | 22 | pub fn get_largeest_free_block(&self) -> usize { 23 | let r = self.0.sync_call( 24 | NodeName::System, 25 | Message::System(SystemMessage::GetLargestFreeBlock), 26 | ); 27 | match r.unwrap() { 28 | Message::System(SystemMessage::GetLargestFreeBlockResponse(s)) => s, 29 | m => panic!("unexpected response, {:?}", m), 30 | } 31 | } 32 | 33 | pub fn get_fps(&self) -> usize { 34 | let r = self.0.sync_call( 35 | NodeName::System, 36 | Message::System(SystemMessage::GetFpsRequest), 37 | ); 38 | match r.unwrap() { 39 | Message::System(SystemMessage::GetFpsResponse(s)) => s, 40 | m => panic!("unexpected response, {:?}", m), 41 | } 42 | } 43 | 44 | pub fn restart(&self) { 45 | self.0 46 | .sync_call(NodeName::System, Message::System(SystemMessage::Restart)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/proto/src/message/bootpage.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] 4 | pub enum BootPageMessage { 5 | EnableSystemMonitor(bool), 6 | } 7 | -------------------------------------------------------------------------------- /app/proto/src/message/buzzer.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, time::Duration}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub type ToneFrequency = u16; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct ToneDuration(pub u16); 9 | 10 | impl From for ToneDuration { 11 | fn from(value: Duration) -> Self { 12 | Self(value.as_millis() as u16) 13 | } 14 | } 15 | 16 | impl From for Duration { 17 | fn from(val: ToneDuration) -> Self { 18 | Duration::from_millis(val.0 as _) 19 | } 20 | } 21 | 22 | #[derive(Clone, Serialize, Deserialize)] 23 | pub struct ToneSeries(pub Vec<(ToneFrequency, ToneDuration)>); 24 | 25 | impl fmt::Debug for ToneSeries { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | let l = self.0.len(); 28 | f.debug_tuple("ToneSeries") 29 | .field(&format!("[(ToneFrequency, ToneDuration);{l}]")) 30 | .finish() 31 | } 32 | } 33 | 34 | #[derive(Clone, Debug, Serialize, Deserialize)] 35 | pub enum BuzzerMessage { 36 | /// 不停地播放一个固定音符 37 | ToneForever(ToneFrequency), 38 | /// 播放一系列音符 39 | ToneSeriesRequest(ToneSeries), 40 | /// true: 播放正常结束 41 | /// false: 播放Off调用中断结束 42 | ToneSeriesResponse(bool), 43 | /// 关闭蜂鸣器播放 44 | Off, 45 | } 46 | -------------------------------------------------------------------------------- /app/proto/src/message/canvas.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] 4 | pub struct DrawLineInput { 5 | start: (u16, u16), 6 | end: (u16, u16), 7 | color: (u8, u8, u8), 8 | width: u16, 9 | } 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize)] 12 | pub struct DrawRectangleInput { 13 | top_left: (u16, u16), 14 | size: (u16, u16), 15 | color: (u8, u8, u8), 16 | } 17 | 18 | #[derive(Debug, Clone, Serialize, Deserialize)] 19 | pub struct DrawCircleInput { 20 | center: (u16, u16), 21 | radius: (u16, u16), 22 | color: (u8, u8, u8), 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Deserialize)] 26 | pub struct DrawPixelsInput { 27 | top_left: (u16, u16), 28 | width: u16, 29 | pixels: Vec<(u8, u8, u8)>, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | pub enum CanvasMessage { 34 | Open, 35 | Close, 36 | Clear((u8, u8, u8)), 37 | DrawLine(DrawLineInput), 38 | DrawCircle(DrawCircleInput), 39 | DrawRectangle(DrawRectangleInput), 40 | DrawPixels(DrawPixelsInput), 41 | BatchCommand(Vec), 42 | } 43 | -------------------------------------------------------------------------------- /app/proto/src/message/common.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use base64::{prelude::BASE64_STANDARD, Engine}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Clone)] 7 | pub struct Bytes(pub Vec); 8 | 9 | impl From for Vec { 10 | fn from(val: Bytes) -> Self { 11 | val.0 12 | } 13 | } 14 | 15 | impl Bytes { 16 | pub fn to_base64(&self) -> String { 17 | let mut s = String::new(); 18 | BASE64_STANDARD.encode_string(&self.0, &mut s); 19 | s 20 | } 21 | } 22 | 23 | impl Serialize for Bytes { 24 | fn serialize(&self, serializer: S) -> Result 25 | where 26 | S: serde::Serializer, 27 | { 28 | let mut s = String::new(); 29 | BASE64_STANDARD.encode_string(&self.0, &mut s); 30 | serializer.serialize_str(&s) 31 | } 32 | } 33 | 34 | impl<'de> Deserialize<'de> for Bytes { 35 | fn deserialize(deserializer: D) -> Result 36 | where 37 | D: ::serde::Deserializer<'de>, 38 | { 39 | let mut v = Vec::new(); 40 | BASE64_STANDARD 41 | .decode_vec(String::deserialize(deserializer)?, &mut v) 42 | .map_err(serde::de::Error::custom)?; 43 | Ok(Self(v)) 44 | } 45 | } 46 | 47 | impl fmt::Debug for Bytes { 48 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 49 | if self.0.len() < 64 { 50 | f.debug_tuple("Bytes").field(&self.0).finish() 51 | } else { 52 | f.debug_tuple("Bytes") 53 | .field(&format!("[u8;{}]", self.0.len())) 54 | .finish() 55 | } 56 | } 57 | } 58 | 59 | pub type Rgb888Color = (u8, u8, u8); 60 | 61 | #[derive(Clone, Debug, Serialize, Deserialize)] 62 | pub enum ImageColorMode { 63 | BinaryColor { on: Rgb888Color, off: Rgb888Color }, 64 | Rgb565, 65 | Rgb888, 66 | } 67 | 68 | #[derive(Clone, Debug, Serialize, Deserialize)] 69 | pub struct Image { 70 | pub width: u16, 71 | pub height: u16, 72 | pub color_mode: ImageColorMode, 73 | pub data: Bytes, 74 | } 75 | -------------------------------------------------------------------------------- /app/proto/src/message/http.rs: -------------------------------------------------------------------------------- 1 | use serde::{de, Deserialize, Serialize}; 2 | 3 | use super::Bytes; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub enum HttpBody { 7 | Bytes(Bytes), 8 | Stream, 9 | } 10 | 11 | impl HttpBody { 12 | pub fn deserialize_by_json<'a, T>(&'a self) -> serde_json::Result 13 | where 14 | T: de::Deserialize<'a>, 15 | { 16 | match self { 17 | HttpBody::Bytes(bs) => serde_json::from_slice::(&bs.0), 18 | HttpBody::Stream => { 19 | unimplemented!("not implement"); 20 | } 21 | } 22 | } 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Deserialize)] 26 | pub enum HttpRequestMethod { 27 | Get, 28 | } 29 | 30 | #[derive(Debug, Clone, Serialize, Deserialize)] 31 | pub struct HttpRequest { 32 | pub method: HttpRequestMethod, 33 | pub url: String, 34 | } 35 | 36 | #[derive(Debug, Clone, Serialize, Deserialize)] 37 | pub struct HttpResponse { 38 | pub body: HttpBody, 39 | } 40 | 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | pub enum HttpError { 43 | Timeout, 44 | Other(String), 45 | } 46 | 47 | #[derive(Debug, Clone, Serialize, Deserialize)] 48 | pub enum HttpMessage { 49 | Error(HttpError), 50 | Request(HttpRequest), 51 | Response(HttpResponse), 52 | } 53 | -------------------------------------------------------------------------------- /app/proto/src/message/lifecycle.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] 4 | pub enum LifecycleMessage { 5 | // 调度器首次调度向所有组件发送一个初始化消息 6 | Init, 7 | // 当组件可见时 8 | Show, 9 | // 当组件不可见时 10 | Hide, 11 | } 12 | -------------------------------------------------------------------------------- /app/proto/src/message/midi.rs: -------------------------------------------------------------------------------- 1 | use super::Bytes; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt::Debug; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub enum MidiError { 7 | Other(String), 8 | } 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | pub enum MidiMessage { 12 | Error(MidiError), 13 | PlayRequest(Bytes), 14 | PlayResponse(bool), 15 | Off, 16 | } 17 | -------------------------------------------------------------------------------- /app/proto/src/message/notifaction.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::Image; 4 | 5 | #[derive(Clone, Debug, Serialize, Deserialize)] 6 | pub struct NotifactionContent { 7 | pub title: Option, 8 | pub text: Option, 9 | pub icon: Option, 10 | } 11 | 12 | #[derive(Clone, Debug, Serialize, Deserialize)] 13 | pub enum NotifactionMessage { 14 | ShowRequest { 15 | /// 持续时间,0表示永久 16 | #[serde(default)] 17 | duration: usize, 18 | 19 | /// 内容 20 | content: NotifactionContent, 21 | }, 22 | ShowResponse, 23 | Close, 24 | } 25 | -------------------------------------------------------------------------------- /app/proto/src/message/onebutton.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] 4 | pub enum OneButtonMessage { 5 | // 单击 6 | Click, 7 | // 点击超过一次 8 | Clicks(usize), 9 | // 长按持续的毫秒数 10 | LongPressHolding(usize), 11 | // 长按松手 12 | LongPressHeld(usize), 13 | } 14 | -------------------------------------------------------------------------------- /app/proto/src/message/router.rs: -------------------------------------------------------------------------------- 1 | use crate::NodeName; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | pub enum RoutePage { 6 | Boot, 7 | Home, 8 | Menu, 9 | Weather, 10 | Music, 11 | } 12 | 13 | impl RoutePage { 14 | pub fn map_to_node_name(&self) -> NodeName { 15 | match *self { 16 | RoutePage::Boot => NodeName::BootPage, 17 | RoutePage::Home => NodeName::HomePage, 18 | RoutePage::Menu => NodeName::MenuPage, 19 | RoutePage::Weather => NodeName::WeatherPage, 20 | RoutePage::Music => NodeName::MusicPage, 21 | } 22 | } 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Deserialize)] 26 | pub enum RouterMessage { 27 | GotoPage(RoutePage), 28 | } 29 | -------------------------------------------------------------------------------- /app/proto/src/message/sntp.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] 4 | pub enum SntpMessage { 5 | SyncCompleted, 6 | } 7 | -------------------------------------------------------------------------------- /app/proto/src/message/storage.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashSet; 3 | use time::OffsetDateTime; 4 | 5 | use super::Bytes; 6 | 7 | #[derive(Debug, Clone, Deserialize, Serialize)] 8 | pub enum StorageError { 9 | IOError(String), 10 | TypeError(String), 11 | Other(String), 12 | } 13 | 14 | #[derive(Debug, Clone, Deserialize, Serialize)] 15 | pub enum StorageValue { 16 | None, 17 | Bytes(Bytes), 18 | String(String), 19 | } 20 | 21 | impl StorageValue { 22 | pub fn as_str(self) -> Option { 23 | match self { 24 | Self::String(x) => { 25 | return Some(x); 26 | } 27 | Self::Bytes(bs) => { 28 | if let Ok(x) = String::from_utf8(bs.0) { 29 | return Some(x); 30 | } 31 | } 32 | _ => {} 33 | } 34 | None 35 | } 36 | } 37 | 38 | impl From for String { 39 | fn from(val: StorageValue) -> Self { 40 | match val { 41 | StorageValue::String(x) => x, 42 | m => panic!("type unmatch err: {m:?}"), 43 | } 44 | } 45 | } 46 | 47 | impl From for Bytes { 48 | fn from(val: StorageValue) -> Self { 49 | match val { 50 | StorageValue::Bytes(x) => x, 51 | m => panic!("type unmatch err: {m:?}"), 52 | } 53 | } 54 | } 55 | 56 | impl From for StorageValue { 57 | fn from(value: String) -> Self { 58 | StorageValue::String(value) 59 | } 60 | } 61 | 62 | impl From for StorageValue { 63 | fn from(value: OffsetDateTime) -> Self { 64 | StorageValue::String(value.to_string()) 65 | } 66 | } 67 | 68 | #[derive(Debug, Clone, Deserialize, Serialize)] 69 | pub enum StorageMessage { 70 | /// 错误定义 71 | Error(StorageError), 72 | 73 | /// 获取 74 | GetRequest(String), 75 | GetResponse(StorageValue), 76 | 77 | /// 设置,设置为None表示删除 78 | SetRequest(String, StorageValue), 79 | SetResponse, 80 | 81 | /// 根据给定一个前缀,列举出所有的keys 82 | ListKeysRequest(String), 83 | ListKeysResponse(HashSet), 84 | } 85 | -------------------------------------------------------------------------------- /app/proto/src/message/system.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] 4 | pub enum SystemMessage { 5 | GetFreeHeapSizeRequest, 6 | GetFreeHeapSizeResponse(usize), 7 | GetLargestFreeBlock, 8 | GetLargestFreeBlockResponse(usize), 9 | GetFpsRequest, 10 | GetFpsResponse(usize), 11 | Restart, 12 | } 13 | -------------------------------------------------------------------------------- /app/proto/src/message/timer.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] 4 | pub enum TimerMessage { 5 | Request(usize), 6 | Response, 7 | } 8 | -------------------------------------------------------------------------------- /app/proto/src/message/useralarm.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use time::Weekday; 3 | 4 | use crate::StorageError; 5 | 6 | #[derive(Debug, Serialize, Clone, Deserialize)] 7 | pub enum UserAlarmRingTone { 8 | None, 9 | Default, 10 | Music(String), 11 | } 12 | 13 | #[derive(Debug, Serialize, Clone, Deserialize)] 14 | pub struct UserAlarmRepeatCustom { 15 | pub monday: bool, 16 | pub tuesday: bool, 17 | pub wednesday: bool, 18 | pub thursday: bool, 19 | pub friday: bool, 20 | pub saturday: bool, 21 | pub sunday: bool, 22 | } 23 | 24 | #[derive(Debug, Serialize, Clone, Deserialize)] 25 | pub enum UserAlarmRepeatMode { 26 | /// 只响铃一次,然后自己删除 27 | Once, 28 | /// 每天 29 | Everyday, 30 | /// 仅工作日(周一至周五) 31 | MonToFri, 32 | /// 自定义每周重复类型 33 | Custom(UserAlarmRepeatCustom), 34 | } 35 | 36 | impl UserAlarmRepeatMode { 37 | pub fn is_active(&self, weekday: Weekday) -> bool { 38 | match self { 39 | UserAlarmRepeatMode::Once | UserAlarmRepeatMode::Everyday => true, 40 | UserAlarmRepeatMode::MonToFri => { 41 | ![Weekday::Saturday, Weekday::Sunday].contains(&weekday) 42 | } 43 | UserAlarmRepeatMode::Custom(c) => [ 44 | (Weekday::Monday, c.monday), 45 | (Weekday::Tuesday, c.tuesday), 46 | (Weekday::Wednesday, c.wednesday), 47 | (Weekday::Thursday, c.thursday), 48 | (Weekday::Friday, c.friday), 49 | (Weekday::Saturday, c.saturday), 50 | (Weekday::Sunday, c.sunday), 51 | ] 52 | .iter() 53 | .any(|(d, enable)| *enable && *d == weekday), 54 | } 55 | } 56 | } 57 | 58 | #[derive(Debug, Serialize, Clone, Deserialize)] 59 | pub struct UserAlarmBody { 60 | /// 闹铃 61 | pub ring_tone: UserAlarmRingTone, 62 | /// 重复模式 63 | pub repeat_mode: UserAlarmRepeatMode, 64 | /// 响铃时间 hh:mm 65 | pub time: (u8, u8), 66 | /// 闹钟响铃时将显示该备注 67 | pub comment: String, 68 | } 69 | 70 | #[derive(Debug, Serialize, Clone, Deserialize)] 71 | pub enum UserAlarmError { 72 | StorageError(StorageError), 73 | NotFound, 74 | } 75 | 76 | #[derive(Debug, Serialize, Clone, Deserialize)] 77 | pub enum UserAlarmMessage { 78 | Error(UserAlarmError), 79 | 80 | AddRequest(UserAlarmBody), 81 | AddResponse(usize), 82 | 83 | DeleteRequest(usize), 84 | DeleteResponse(UserAlarmBody), 85 | 86 | GetRequest(usize), 87 | GetResponse(UserAlarmBody), 88 | 89 | ListRequest, 90 | ListResponse(Vec), 91 | } 92 | -------------------------------------------------------------------------------- /app/proto/src/message/wifi.rs: -------------------------------------------------------------------------------- 1 | use std::net::Ipv4Addr; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | pub struct WiFiStorageConfiguration { 6 | pub ssid: String, 7 | pub password: Option, 8 | } 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | pub struct NetIpInfo { 12 | pub ip: Ipv4Addr, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | pub enum WiFiError { 17 | NotStarted, 18 | NotFoundAP, 19 | ApNeedPassword, 20 | Other(String), 21 | } 22 | 23 | #[derive(Debug, Clone, Serialize, Deserialize)] 24 | pub enum WiFiMessage { 25 | Error(WiFiError), 26 | 27 | // 开启WiFi热点 28 | StartAPRequest, 29 | StartAPResponse, 30 | 31 | // 根据指定配置连接wifi 32 | ConnectRequest(WiFiStorageConfiguration), 33 | ConnectResponse, 34 | 35 | // 获取ip信息 36 | GetIpInfoRequest, 37 | GetIpInfoResponse(NetIpInfo), 38 | 39 | ConnectedBroadcast, 40 | APStartedBroadcast, 41 | } 42 | -------------------------------------------------------------------------------- /app/proto/src/node.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Hash, PartialEq, Eq, Clone, Serialize, Deserialize)] 4 | pub enum NodeName { 5 | // App框架调度器 6 | Scheduler, 7 | // Http客户端 8 | HttpClient, 9 | // 天气查询器 10 | Weather, 11 | // 定时器服务 12 | Timer, 13 | // 单个操作按钮事件产生器,可产生一些按键事件 14 | OneButton, 15 | // 页面ui路由器 16 | Router, 17 | // 本地存储 18 | Storage, 19 | // WiFi 20 | WiFi, 21 | // 蜂鸣器 22 | Buzzer, 23 | // MIDI播放器 24 | MidiPlayer, 25 | // 系统相关 26 | System, 27 | // 全局Alert对话框 28 | Notifaction, 29 | // 画板 30 | Canvas, 31 | // 闹钟 32 | Alarm, 33 | // 启动页ui与逻辑 34 | BootPage, 35 | // 首页ui与逻辑 36 | HomePage, 37 | // 菜单页ui与逻辑 38 | MenuPage, 39 | // 天气页ui与逻辑 40 | WeatherPage, 41 | // 音乐播放器 42 | MusicPage, 43 | // 其他扩展节点 44 | Other(String), 45 | } 46 | -------------------------------------------------------------------------------- /app/proto/src/storage.rs: -------------------------------------------------------------------------------- 1 | mod music; 2 | mod system; 3 | mod useralarm; 4 | mod weather; 5 | mod wifi; 6 | 7 | pub use { 8 | music::MusicStorage, system::SystemStorage, useralarm::UserAlarmStorage, 9 | weather::WeatherStorage, wifi::WiFiStorage, 10 | }; 11 | 12 | use crate::StorageError; 13 | type Result = std::result::Result; 14 | -------------------------------------------------------------------------------- /app/proto/src/storage/music.rs: -------------------------------------------------------------------------------- 1 | use crate::{ipc::StorageClient, Bytes, StorageValue}; 2 | 3 | pub struct MusicStorage(pub StorageClient); 4 | impl MusicStorage { 5 | fn update_list(&self, list: Vec) { 6 | self.0 7 | .set( 8 | "music/list".into(), 9 | StorageValue::String(serde_json::to_string(&list).unwrap()), 10 | ) 11 | .expect("update music list error"); 12 | } 13 | 14 | pub fn get_list(&self) -> Vec { 15 | self.0 16 | .get("music/list".into()) 17 | .unwrap() 18 | .as_str() 19 | .map(|x| serde_json::from_str(&x).unwrap_or_default()) 20 | .unwrap_or_default() 21 | } 22 | 23 | pub fn get_data(&self, filename: String) -> Vec { 24 | match self 25 | .0 26 | .get(format!("music/data/{filename}")) 27 | .expect("not found music data") 28 | { 29 | StorageValue::Bytes(bs) => bs.0, 30 | m => panic!("unexpected storage value {m:?}"), 31 | } 32 | } 33 | 34 | pub fn upload(&self, filename: String, data: Vec) { 35 | // 上传文件内容 36 | self.0 37 | .set( 38 | format!("music/data/{filename}"), 39 | StorageValue::Bytes(Bytes(data)), 40 | ) 41 | .unwrap(); 42 | // 更新元数据 43 | let mut list = self 44 | .get_list() 45 | .into_iter() 46 | .filter(|x| x != &filename) // 重复元素移除 47 | .collect::>(); 48 | list.push(filename); 49 | self.update_list(list); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/proto/src/storage/system.rs: -------------------------------------------------------------------------------- 1 | use crate::{ipc::StorageClient, StorageValue}; 2 | 3 | pub struct SystemStorage(pub StorageClient); 4 | 5 | impl SystemStorage { 6 | pub fn get_monitor_enable(&self) -> bool { 7 | self.0 8 | .get("system/monitor-enable".into()) 9 | .map(|x| match x { 10 | StorageValue::String(x) => &x == "1", 11 | _ => false, 12 | }) 13 | .unwrap_or_default() 14 | } 15 | 16 | pub fn set_monitor_enable(&self, enable: bool) { 17 | self.0 18 | .set( 19 | "system/monitor-enable".into(), 20 | StorageValue::String(if enable { "1" } else { "0" }.into()), 21 | ) 22 | .unwrap(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/proto/src/storage/useralarm.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | use crate::{ipc::StorageClient, StorageValue, UserAlarmBody}; 3 | pub struct UserAlarmStorage(pub StorageClient); 4 | 5 | impl UserAlarmStorage { 6 | fn update_id_list(&self, list: Vec) -> Result<()> { 7 | self.0.set( 8 | "useralarm/list".into(), 9 | StorageValue::String(serde_json::to_string(&list).unwrap()), 10 | ) 11 | } 12 | 13 | pub fn get_id_list(&self) -> Result> { 14 | Ok(self 15 | .0 16 | .get("useralarm/list".into())? 17 | .as_str() 18 | .map(|x| serde_json::from_str(&x).unwrap_or_default()) 19 | .unwrap_or_default()) 20 | } 21 | 22 | pub fn get(&self, id: usize) -> Result { 23 | match self.0.get(format!("useralarm/data/{id}"))? { 24 | StorageValue::String(x) => Ok(serde_json::from_str(&x).unwrap()), 25 | m => panic!("unexpected storage value {m:?}"), 26 | } 27 | } 28 | 29 | pub fn delete(&self, id: usize) -> Result { 30 | let bakup = self.get(id)?; 31 | // 删除元数据 32 | let list = self 33 | .get_id_list()? 34 | .into_iter() 35 | .filter(|x| *x != id) 36 | .collect(); 37 | 38 | self.update_id_list(list)?; 39 | self.0 40 | .set(format!("useralarm/data/{id}"), StorageValue::None)?; 41 | Ok(bakup) 42 | } 43 | 44 | pub fn add(&self, body: UserAlarmBody) -> Result { 45 | // 申请一个id 46 | let mut id_list = self.get_id_list()?; 47 | let id = id_list.iter().copied().max().unwrap_or(0) + 1; 48 | 49 | // 设置数据 50 | self.0.set( 51 | format!("useralarm/data/{id}"), 52 | StorageValue::String(serde_json::to_string(&body).unwrap()), 53 | )?; 54 | 55 | // 更新元数据 56 | id_list.push(id); 57 | self.update_id_list(id_list)?; 58 | Ok(id) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/proto/src/storage/weather.rs: -------------------------------------------------------------------------------- 1 | use crate::{ipc::StorageClient, Location, StorageValue, WeatherError}; 2 | 3 | type Result = std::result::Result; 4 | 5 | pub struct WeatherStorage(pub StorageClient); 6 | 7 | impl WeatherStorage { 8 | pub fn set_key(&self, key: String) -> Result<()> { 9 | self.0 10 | .set("weather/key".into(), StorageValue::String(key)) 11 | .map_err(WeatherError::StorageError)?; 12 | Ok(()) 13 | } 14 | 15 | pub fn set_location(&self, location_id: u32, location: String) -> Result<()> { 16 | self.0 17 | .set( 18 | "weather/location".into(), 19 | StorageValue::String( 20 | serde_json::to_string(&Location { 21 | location_id, 22 | location, 23 | }) 24 | .unwrap(), 25 | ), 26 | ) 27 | .map_err(WeatherError::StorageError)?; 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/proto/src/storage/wifi.rs: -------------------------------------------------------------------------------- 1 | use crate::{ipc::StorageClient, StorageValue}; 2 | 3 | pub struct WiFiStorage(pub StorageClient); 4 | 5 | impl WiFiStorage { 6 | pub fn get_ssid(&self) -> Option { 7 | self.0.get("wifi/ssid".into()).unwrap().as_str() 8 | } 9 | 10 | pub fn get_password(&self) -> Option { 11 | self.0.get("wifi/password".into()).unwrap().as_str() 12 | } 13 | 14 | pub fn set_ssid(&self, val: Option) { 15 | self.0 16 | .set( 17 | "wifi/ssid".into(), 18 | match val { 19 | Some(x) => StorageValue::String(x), 20 | None => StorageValue::None, 21 | }, 22 | ) 23 | .unwrap(); 24 | } 25 | 26 | pub fn set_password(&self, val: Option) { 27 | self.0 28 | .set( 29 | "wifi/password".into(), 30 | match val { 31 | Some(x) => StorageValue::String(x), 32 | None => StorageValue::None, 33 | }, 34 | ) 35 | .unwrap(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/proto/src/topic.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Hash, PartialEq, Eq, Clone, Serialize, Deserialize)] 4 | pub enum TopicName { 5 | OneButton, 6 | Scheduler, 7 | Sntp, 8 | WiFi, 9 | } 10 | -------------------------------------------------------------------------------- /app/wasm-impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-impl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | wasm-bindgen = { version = "0.2" } 8 | slint = "1.6.0" 9 | log = "0.4.6" 10 | app-core = { path = "../app-core" } 11 | wasm-logger = "0.2.0" 12 | time = { version = "0.3.36" } 13 | web-sys = { version = "0.3.69", features = ["Storage", "Window"] } 14 | reqwest = "0.12.4" 15 | wasm-bindgen-futures = "0.4.42" 16 | serde = "1.0.202" 17 | serde_json = "1.0.117" 18 | base64 = "0.22.1" 19 | 20 | [lib] 21 | path = "src/lib.rs" 22 | crate-type = ["cdylib"] 23 | 24 | [features] 25 | default = ["time/wasm-bindgen"] 26 | -------------------------------------------------------------------------------- /app/wasm-impl/Makefile: -------------------------------------------------------------------------------- 1 | all: debug 2 | 3 | serve: 4 | python3 -m http.server 51808 5 | 6 | watch: 7 | cargo watch -i .gitignore -i "pkg/*" -s "wasm-pack build --target web --dev" 8 | 9 | debug: 10 | wasm-pack build --target web --dev 11 | 12 | release: 13 | wasm-pack build --target web --release -------------------------------------------------------------------------------- /app/wasm-impl/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | clock 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 |
14 | 15 | 16 | 17 |

18 | 首页单击进入菜单
19 | 菜单单击滚动切换app
20 | 菜单双击进入对应app
21 | 天气app长按进入首页
22 |

23 | 24 | 25 | -------------------------------------------------------------------------------- /app/wasm-impl/index.js: -------------------------------------------------------------------------------- 1 | export function loadFile(b64) { 2 | MIDI.loadPlugin({ 3 | soundfontUrl: "https://gleitz.github.io/midi-js-soundfonts/MusyngKite/", 4 | onsuccess: function () { 5 | console.log("player..."); 6 | MIDI.Player.loadFile("data:audio/midi;base64," + b64, function () { 7 | MIDI.Player.start(); 8 | MIDI.Player.BPM = 60; 9 | }); 10 | }, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /app/wasm-impl/index.module.js: -------------------------------------------------------------------------------- 1 | // import the generated file. 2 | import init from "./pkg/wasm_impl.js"; 3 | import { send_message } from "./pkg/wasm_impl.js"; 4 | init(); 5 | 6 | function call_message(msg) { 7 | return new Promise((resolve, _) => send_message(msg, resolve)); 8 | } 9 | 10 | (() => { 11 | document 12 | .getElementById("msg-send-btn") 13 | .addEventListener("click", async () => { 14 | let content = document.getElementById("msg-input").value; 15 | let ret = await call_message(content); 16 | console.log(ret); 17 | }); 18 | })(); 19 | -------------------------------------------------------------------------------- /app/wasm-impl/src/http.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use app_core::proto::{ 4 | Bytes, Context, HandleResult, HttpBody, HttpError, HttpMessage, HttpRequestMethod, 5 | HttpResponse, Message, MessageWithHeader, Node, NodeName, 6 | }; 7 | 8 | fn convert(method: HttpRequestMethod) -> reqwest::Method { 9 | use reqwest::Method; 10 | match method { 11 | HttpRequestMethod::Get => Method::GET, 12 | } 13 | } 14 | 15 | pub struct HttpClient {} 16 | 17 | impl HttpClient { 18 | pub fn new() -> Self { 19 | Self {} 20 | } 21 | } 22 | 23 | impl Node for HttpClient { 24 | fn node_name(&self) -> NodeName { 25 | NodeName::HttpClient 26 | } 27 | 28 | fn handle_message(&self, ctx: Rc, msg: MessageWithHeader) -> HandleResult { 29 | match msg.body { 30 | Message::Http(HttpMessage::Request(req)) => { 31 | // 否则为新消息 32 | let req = req.clone(); 33 | wasm_bindgen_futures::spawn_local(async move { 34 | let x = async { 35 | let req = reqwest::Client::new() 36 | .request(convert(req.method.clone()), req.url.clone()) 37 | .build() 38 | .map_err(|x| HttpError::Other(x.to_string()))?; 39 | let resp = reqwest::Client::new() 40 | .execute(req) 41 | .await 42 | .map_err(|x| HttpError::Other(x.to_string()))?; 43 | let body = resp 44 | .bytes() 45 | .await 46 | .map_err(|x| HttpError::Other(x.to_string()))? 47 | .to_vec(); 48 | Ok(HttpResponse { 49 | body: HttpBody::Bytes(Bytes(body)), 50 | }) 51 | } 52 | .await; 53 | ctx.async_ready( 54 | msg.seq, 55 | Message::Http(match x { 56 | Ok(x) => HttpMessage::Response(x), 57 | Err(e) => HttpMessage::Error(e), 58 | }), 59 | ); 60 | }); 61 | return HandleResult::Pending; 62 | } 63 | _ => {} 64 | } 65 | HandleResult::Discard 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/wasm-impl/src/lib.rs: -------------------------------------------------------------------------------- 1 | use app_core::{get_app_window, get_scheduler}; 2 | use slint::ComponentHandle; 3 | use std::time::Duration; 4 | 5 | mod console; 6 | mod http; 7 | mod midiplayer; 8 | mod storage; 9 | 10 | #[wasm_bindgen::prelude::wasm_bindgen(start)] 11 | pub fn main() { 12 | wasm_logger::init(wasm_logger::Config::default()); 13 | let app = get_app_window(); 14 | let sche = get_scheduler(); 15 | sche.register_node(http::HttpClient::new()); 16 | sche.register_node(storage::LocalStorageService::new()); 17 | sche.register_node(midiplayer::MidiPlayerService::new()); 18 | sche.register_node(console::ConsoleNode::new()); 19 | let sche_timer = slint::Timer::default(); 20 | sche_timer.start( 21 | slint::TimerMode::Repeated, 22 | Duration::from_millis(16), 23 | move || { 24 | sche.schedule_once(); 25 | }, 26 | ); 27 | if let Some(x) = app.upgrade() { 28 | x.run().unwrap(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/wasm-impl/src/midiplayer.rs: -------------------------------------------------------------------------------- 1 | use app_core::proto::*; 2 | use base64::{prelude::BASE64_STANDARD, Engine}; 3 | 4 | use wasm_bindgen::prelude::*; 5 | #[wasm_bindgen(raw_module = "../index.js")] 6 | extern "C" { 7 | fn loadFile(b64: String); 8 | } 9 | 10 | pub struct MidiPlayerService {} 11 | 12 | impl MidiPlayerService { 13 | pub fn new() -> Self { 14 | Self {} 15 | } 16 | } 17 | 18 | impl Node for MidiPlayerService { 19 | fn node_name(&self) -> NodeName { 20 | NodeName::MidiPlayer 21 | } 22 | 23 | fn handle_message( 24 | &self, 25 | _ctx: std::rc::Rc, 26 | msg: MessageWithHeader, 27 | ) -> HandleResult { 28 | match msg.body { 29 | Message::Midi(MidiMessage::PlayRequest(Bytes(bs))) => { 30 | let mut s = String::new(); 31 | BASE64_STANDARD.encode_string(bs, &mut s); 32 | loadFile(s); 33 | return HandleResult::Finish(Message::Midi(MidiMessage::PlayResponse(false))); 34 | } 35 | _ => {} 36 | } 37 | HandleResult::Discard 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /libs/embedded-graphics-mux/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "embedded-graphics-mux" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | embedded-graphics = "0.8.1" 8 | log = "0.4.21" 9 | -------------------------------------------------------------------------------- /libs/embedded-software-slint-backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "embedded-software-slint-backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | embedded-graphics = "0.8.1" 10 | log = "0.4.20" 11 | slint = { version = "1.3.0", default-features = false, features = [ 12 | "compat-1-2", 13 | "unsafe-single-threaded", 14 | "renderer-software", 15 | ] } 16 | euclid = "0.22.9" # slint will build error if don't have this 17 | -------------------------------------------------------------------------------- /libs/embedded-tone/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "embedded-tone" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /libs/embedded-tone/examples/desktop-tone/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "desktop-tone" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | embedded-tone = { path = "../.." } 10 | midly = "0.5.3" 11 | rodio = "0.17.3" 12 | -------------------------------------------------------------------------------- /libs/embedded-tone/examples/desktop-tone/dsa.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/libs/embedded-tone/examples/desktop-tone/dsa.mid -------------------------------------------------------------------------------- /libs/embedded-tone/examples/desktop-tone/liyue.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/libs/embedded-tone/examples/desktop-tone/liyue.mid -------------------------------------------------------------------------------- /libs/embedded-tone/examples/desktop-tone/ql.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangzqs/esp-clock-rs/9a0962cbeb7440dc7c0ebe7d951a7011481a4990/libs/embedded-tone/examples/desktop-tone/ql.mid -------------------------------------------------------------------------------- /libs/embedded-tone/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod guitar; 2 | mod note; 3 | mod player; 4 | 5 | pub use guitar::*; 6 | pub use note::*; 7 | pub use player::*; -------------------------------------------------------------------------------- /libs/embedded-tone/src/player.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | 3 | use crate::{ 4 | note::{Note, NoteDuration, Rest}, 5 | SlideNote, 6 | }; 7 | 8 | pub trait RawTonePlayer { 9 | fn tone(&mut self, freq: u32); 10 | fn off(&mut self); 11 | } 12 | 13 | pub struct TonePlayer 14 | where 15 | P: RawTonePlayer, 16 | D: Fn(Duration), 17 | { 18 | tone_player: P, 19 | whole_duration: Duration, // 一个全音符的时间 20 | slide_note_samples: usize, 21 | delay: D, 22 | } 23 | 24 | impl TonePlayer 25 | where 26 | P: RawTonePlayer, 27 | D: Fn(Duration), 28 | { 29 | pub fn new(player: P, delay: D) -> Self { 30 | Self { 31 | tone_player: player, 32 | whole_duration: Duration::from_secs(4), 33 | slide_note_samples: 50, 34 | delay, 35 | } 36 | } 37 | 38 | /// bpm: beat per minute 39 | pub fn set_beat_duration_from_bpm(&mut self, bpm: u32, note_duration_as_beat: NoteDuration) { 40 | // 一分钟有60秒 41 | // 以给定的音符时值为一拍 42 | // 一拍的时长为60/bpm秒 43 | let d: f32 = note_duration_as_beat.into(); 44 | self.whole_duration = Duration::from_secs_f32(60.0 / bpm as f32 / d); 45 | } 46 | 47 | pub fn play_note(&mut self, note: Note) { 48 | let pitch = note.pitch.frequency(); 49 | let duration = self.whole_duration.mul_f32(note.duration.into()); 50 | self.tone_player.tone(pitch); 51 | (self.delay)(duration); 52 | self.tone_player.off(); 53 | } 54 | pub fn play_rest(&mut self, rest: Rest) { 55 | let dur = self.whole_duration.mul_f32(rest.duration.into()); 56 | (self.delay)(dur); 57 | } 58 | pub fn play_slide(&mut self, slide_note: SlideNote) { 59 | let t = self.whole_duration.mul_f32(slide_note.duration.into()); 60 | let n = (t.as_secs_f32() * self.slide_note_samples as f32) as usize; 61 | let start_freq = slide_note.start_pitch.frequency() as f32; 62 | let end_freq = slide_note.end_pitch.frequency() as f32; 63 | let freq_step = (end_freq - start_freq) / n as f32; 64 | 65 | for i in 0..n { 66 | let freq = (start_freq + freq_step * i as f32) as u32; 67 | let t = t / n as u32; 68 | self.tone_player.tone(freq); 69 | (self.delay)(t); 70 | self.tone_player.off(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /screen-projector/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screen-projector" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | image = "0.24.7" 10 | scrap = "0.5.0" 11 | -------------------------------------------------------------------------------- /screen-projector/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::Write, 3 | sync::{Arc, Mutex}, 4 | thread, 5 | time::Duration, error::Error, 6 | }; 7 | 8 | use crate::screen::Capturer; 9 | use image::{imageops::resize, imageops::FilterType}; 10 | mod screen; 11 | fn main() -> Result<(), Box> { 12 | let mut args = std::env::args(); 13 | if args.len() <= 1 { 14 | println!("usage: {} ", args.next().unwrap()); 15 | return Ok(()); 16 | } 17 | let addr = args.nth(1).unwrap(); 18 | let mut stream = std::net::TcpStream::connect(addr)?; 19 | println!("connect success"); 20 | 21 | let mut cap = Capturer::new(); 22 | let (w, h) = cap.size(); 23 | let (w, h) = (w as u32, h as u32); 24 | 25 | let fps = Arc::new(Mutex::new(0)); 26 | let fps_ref = fps.clone(); 27 | thread::spawn(move || loop { 28 | thread::sleep(Duration::from_secs(1)); 29 | let mut fps_ref = fps_ref.lock().unwrap(); 30 | println!("fps: {}", *fps_ref); 31 | *fps_ref = 0; 32 | drop(fps_ref); 33 | }); 34 | loop { 35 | let buf = cap.capture(); 36 | let img = image::ImageBuffer::from_fn(w, h, |x, y| { 37 | let idx = 4 * (y * w + x) as usize; 38 | image::Rgba([buf[idx + 2], buf[idx + 1], buf[idx], buf[idx + 3]]) 39 | }); 40 | let img = resize(&img, 240, 240, FilterType::Nearest); 41 | let buf = img 42 | .enumerate_pixels() 43 | .flat_map(|(_, _, p)| { 44 | let p = p.0; 45 | [p[0], p[1], p[2]] 46 | }) 47 | .collect::>(); 48 | *fps.lock().unwrap() += 1; 49 | // 连接到tcp投屏服务器 50 | stream.write_all(&buf)?; 51 | stream.flush()?; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /screen-projector/src/screen.rs: -------------------------------------------------------------------------------- 1 | use scrap::Display; 2 | use std::io::ErrorKind::WouldBlock; 3 | use std::slice::from_raw_parts; 4 | use std::time::Duration; 5 | 6 | pub struct Capturer { 7 | w: usize, 8 | h: usize, 9 | capturer: Option, 10 | sleep: Duration, 11 | } 12 | impl Capturer { 13 | pub fn new() -> Capturer { 14 | let display = Display::primary().unwrap(); 15 | let capturer = scrap::Capturer::new(display).unwrap(); 16 | let (w, h) = (capturer.width(), capturer.height()); 17 | Capturer { 18 | w, 19 | h, 20 | capturer: Some(capturer), 21 | sleep: Duration::new(1, 0) / 120, 22 | } 23 | } 24 | fn reload(&mut self) { 25 | println!("Reload capturer"); 26 | drop(self.capturer.take()); 27 | let display = match Display::primary() { 28 | Ok(display) => display, 29 | Err(_) => { 30 | return; 31 | } 32 | }; 33 | 34 | let capturer = match scrap::Capturer::new(display) { 35 | Ok(capturer) => capturer, 36 | Err(_) => return, 37 | }; 38 | self.capturer = Some(capturer); 39 | } 40 | pub fn size(&self) -> (usize, usize) { 41 | (self.w, self.h) 42 | } 43 | 44 | pub fn capture(&mut self) -> &[u8] { 45 | loop { 46 | match &mut self.capturer { 47 | Some(capturer) => { 48 | let cp = capturer.frame(); 49 | let buffer = match cp { 50 | Ok(buffer) => buffer, 51 | Err(error) => { 52 | std::thread::sleep(self.sleep); 53 | if error.kind() != WouldBlock { 54 | std::thread::sleep(std::time::Duration::from_millis(20)); 55 | self.reload(); 56 | } 57 | continue; 58 | } 59 | }; 60 | return unsafe { from_raw_parts(buffer.as_ptr(), buffer.len()) }; 61 | } 62 | None => { 63 | std::thread::sleep(std::time::Duration::from_millis(20)); 64 | self.reload(); 65 | continue; 66 | } 67 | }; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | config.yaml -------------------------------------------------------------------------------- /server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.75" 10 | env_logger = "0.10.1" 11 | http = "1.0.0" 12 | image = "0.24.7" 13 | log = "0.4.20" 14 | num-complex = "0.4.4" 15 | poem = "1.3.58" 16 | poem-openapi = { version = "3.0.5", features = ["swagger-ui"] } 17 | qweather-http-client = "0.2.1" 18 | qweather-service = "0.2.1" 19 | reqwest = { version = "0.11.23", features = ["json", "cookies"] } 20 | serde = "1.0.188" 21 | serde_json = "1.0.107" 22 | serde_yaml = "0.9.27" 23 | thiserror = "1.0.56" 24 | tokio = { version = "1.32.0", features = ["rt-multi-thread"] } 25 | redis = { version = "0.24.0", features = ["tokio-comp", "json"] } 26 | -------------------------------------------------------------------------------- /server/src/error.rs: -------------------------------------------------------------------------------- 1 | use poem::error::ResponseError; 2 | use reqwest::StatusCode; 3 | use serde_json::json; 4 | 5 | #[derive(Debug, thiserror::Error)] 6 | pub enum ApiError { 7 | #[error("reource not found: {resource:?}")] 8 | NotFound { resource: Option }, 9 | #[error("reqwest error: {0}")] 10 | Reqwest(#[from] reqwest::Error), 11 | #[error("serde_json error: {0}")] 12 | SerdeJson(#[from] serde_json::Error), 13 | #[error("redis error: {0}")] 14 | Redis(#[from] redis::RedisError), 15 | #[error("other error")] 16 | OtherError { 17 | code: Option, 18 | msg: Option, 19 | }, 20 | } 21 | 22 | impl ResponseError for ApiError { 23 | fn status(&self) -> StatusCode { 24 | match self { 25 | ApiError::NotFound { .. } => StatusCode::NOT_FOUND, 26 | ApiError::Reqwest(err) => err.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), 27 | ApiError::SerdeJson(_) => StatusCode::INTERNAL_SERVER_ERROR, 28 | ApiError::Redis(_) => StatusCode::INTERNAL_SERVER_ERROR, 29 | ApiError::OtherError { code, .. } => code 30 | .map(|x| StatusCode::from_u16(x).unwrap()) 31 | .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), 32 | } 33 | } 34 | 35 | fn as_response(&self) -> poem::Response 36 | where 37 | Self: std::error::Error + Send + Sync + 'static, 38 | { 39 | poem::Response::builder() 40 | .status(self.status()) 41 | .header("Content-Type", "application/json") 42 | .body( 43 | json!({ 44 | "error": self.to_string(), 45 | }) 46 | .to_string(), 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/src/main.rs: -------------------------------------------------------------------------------- 1 | use poem::{listener::TcpListener, Result, Route, Server}; 2 | use poem_openapi::OpenApiService; 3 | 4 | use serde::Deserialize; 5 | 6 | mod error; 7 | mod service; 8 | 9 | #[derive(Debug, Deserialize)] 10 | struct ServiceConfig { 11 | openwrt: service::OpenWrtServiceConfig, 12 | weather: service::WeatherServiceConfig, 13 | } 14 | 15 | #[derive(Debug, Deserialize)] 16 | struct Config { 17 | bind_addr: String, 18 | redis_addr: String, 19 | service: ServiceConfig, 20 | } 21 | 22 | #[tokio::main] 23 | async fn main() -> Result<(), Box> { 24 | env_logger::init(); 25 | let config = std::fs::read_to_string("config.yaml")?; 26 | let config = serde_yaml::from_str::(&config)?; 27 | let redis_cli = redis::Client::open(config.redis_addr)?; 28 | let service = OpenApiService::new( 29 | ( 30 | service::PingService, 31 | service::PhotoService, 32 | service::OpenWrt::new(config.service.openwrt), 33 | service::WeatherService::new(config.service.weather, redis_cli.clone()), 34 | ), 35 | "Esp Clock Server Api", 36 | "1.0", 37 | ) 38 | .server("/api"); 39 | let swagger_ui = service.swagger_ui(); 40 | Server::new(TcpListener::bind(config.bind_addr)) 41 | .run(Route::new().nest("/api", service).nest("/ui", swagger_ui)) 42 | .await?; 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /server/src/service.rs: -------------------------------------------------------------------------------- 1 | mod openwrt; 2 | pub use openwrt::{OpenWrt, OpenWrtServiceConfig}; 3 | 4 | mod ping; 5 | pub use ping::PingService; 6 | 7 | mod photo; 8 | pub use photo::PhotoService; 9 | 10 | mod weather; 11 | pub use weather::{WeatherService, WeatherServiceConfig}; -------------------------------------------------------------------------------- /server/src/service/openwrt.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::{payload::Json, OpenApi}; 2 | use serde::Deserialize; 3 | use serde_json::json; 4 | 5 | mod client; 6 | 7 | #[derive(Debug, Deserialize)] 8 | pub struct OpenWrtServiceConfig { 9 | client: client::ClientConfig, 10 | } 11 | 12 | pub struct OpenWrt { 13 | client: client::Client, 14 | } 15 | 16 | impl OpenWrt { 17 | pub fn new(config: OpenWrtServiceConfig) -> Self { 18 | OpenWrt { 19 | client: client::Client::new(config.client), 20 | } 21 | } 22 | } 23 | 24 | #[OpenApi] 25 | impl OpenWrt { 26 | #[oai(path = "/openwrt/network_info", method = "get")] 27 | async fn network_info(&self) -> Json { 28 | let info = self.client.get_all_client_network_info().await; 29 | let text = format!("{:?}", info); 30 | Json(json!({ 31 | "text": text, 32 | })) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/service/photo.rs: -------------------------------------------------------------------------------- 1 | use image::{imageops::FilterType, GenericImageView}; 2 | use poem_openapi::{ 3 | payload::{Attachment, AttachmentType}, 4 | OpenApi, 5 | }; 6 | 7 | pub struct PhotoService; 8 | 9 | #[OpenApi] 10 | impl PhotoService { 11 | #[oai(path = "/photo", method = "get")] 12 | async fn photo(&self) -> Attachment> { 13 | let resp = reqwest::get("http://little-paimon.zzq:5000/img") 14 | .await 15 | .unwrap(); 16 | let bytes = resp.bytes().await.unwrap(); 17 | let img = image::load_from_memory(&bytes).unwrap(); 18 | // 将图片缩放为 240x240 19 | let img = img.resize(240, 240, FilterType::Nearest); 20 | let mut bytes = Vec::new(); 21 | // 写入一个字节的宽度和一个字节的高度 22 | bytes.push(img.width() as u8); 23 | bytes.push(img.height() as u8); 24 | // 依次写入原始图片像素数据,按照RGB888 25 | for (_, _, pixel) in img.pixels() { 26 | bytes.push(pixel.0[0]); 27 | bytes.push(pixel.0[1]); 28 | bytes.push(pixel.0[1]); 29 | } 30 | Attachment::new(bytes.to_vec()).attachment_type(AttachmentType::Inline) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/src/service/ping.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::{payload::PlainText, OpenApi}; 2 | 3 | pub struct PingService; 4 | 5 | #[OpenApi] 6 | impl PingService { 7 | #[oai(path = "/ping", method = "get")] 8 | async fn index(&self) -> PlainText { 9 | PlainText("pong".to_string()) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /vue-console/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? -------------------------------------------------------------------------------- /vue-console/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /vue-console/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 11 | 12 | 13 | -------------------------------------------------------------------------------- /vue-console/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-console", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.6.2", 13 | "element-plus": "^2.4.4", 14 | "vue": "^3.3.11", 15 | "vue-router": "^4.2.5", 16 | "vuex": "^4.1.0" 17 | }, 18 | "devDependencies": { 19 | "@vitejs/plugin-vue": "^4.5.2", 20 | "typescript": "^5.2.2", 21 | "vite": "^5.0.8", 22 | "vite-plugin-singlefile": "^0.13.5", 23 | "vue-tsc": "^1.8.25" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vue-console/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-console/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /vue-console/src/api/common.ts: -------------------------------------------------------------------------------- 1 | import { Axios } from 'axios' 2 | 3 | const axios = new Axios(); 4 | 5 | export async function send_message(input: any): Promise { 6 | await axios.post('/', input) 7 | } -------------------------------------------------------------------------------- /vue-console/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export { click, clicks, long_press_holding, long_press_held } from './onebutton' -------------------------------------------------------------------------------- /vue-console/src/api/onebutton.ts: -------------------------------------------------------------------------------- 1 | import { send_message } from './common' 2 | 3 | function wrap_onebutton_message(inner: any) { 4 | return { 5 | "to": "Broadcast", 6 | "body": { 7 | "OneButton": inner 8 | } 9 | } 10 | } 11 | 12 | export async function click() { 13 | await send_message(wrap_onebutton_message("Click")) 14 | } 15 | 16 | export async function clicks(n: number) { 17 | await send_message(wrap_onebutton_message({ 18 | "Clicks": n 19 | })) 20 | } 21 | 22 | export async function long_press_holding(ms: number) { 23 | await send_message(wrap_onebutton_message({ 24 | "LongPressedHolding": ms, 25 | })) 26 | } 27 | 28 | export async function long_press_held(ms: number) { 29 | await send_message(wrap_onebutton_message({ 30 | "LongPressedHeld": ms, 31 | })) 32 | } -------------------------------------------------------------------------------- /vue-console/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.css' 3 | import App from './App.vue' 4 | import router from './router' 5 | import store from './store' 6 | 7 | createApp(App) 8 | .use(router) 9 | .use(store) 10 | .mount('#app') 11 | -------------------------------------------------------------------------------- /vue-console/src/router/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; 3 | import Login from '../views/Login.vue'; 4 | 5 | const routes: Array = [ 6 | { 7 | path: '/', 8 | name: 'Login', 9 | component: Login, 10 | }, 11 | ]; 12 | 13 | const router = createRouter({ 14 | history: createWebHistory(), 15 | routes, 16 | }); 17 | 18 | export default router; -------------------------------------------------------------------------------- /vue-console/src/store/admin.ts: -------------------------------------------------------------------------------- 1 | export default {}; -------------------------------------------------------------------------------- /vue-console/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | import admin from './admin' 3 | 4 | export default createStore({ 5 | state: {}, 6 | getters: {}, 7 | mutations: {}, 8 | actions: {}, 9 | modules: { 10 | admin 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /vue-console/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | .card { 58 | padding: 2em; 59 | } 60 | 61 | #app { 62 | max-width: 1280px; 63 | margin: 0 auto; 64 | padding: 2rem; 65 | text-align: center; 66 | } 67 | 68 | @media (prefers-color-scheme: light) { 69 | :root { 70 | color: #213547; 71 | background-color: #ffffff; 72 | } 73 | a:hover { 74 | color: #747bff; 75 | } 76 | button { 77 | background-color: #f9f9f9; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /vue-console/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /vue-console/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /vue-console/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "paths": { 23 | "vuex": ["./node_modules/vuex/types"] 24 | } 25 | }, 26 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /vue-console/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vue-console/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { viteSingleFile } from 'vite-plugin-singlefile' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | vue(), 9 | viteSingleFile(), 10 | ], 11 | }) 12 | --------------------------------------------------------------------------------