diff --git a/gem-remotes-esp32/Cargo.toml b/gem-remotes-esp32/Cargo.toml index e8f08a6..9232496 100644 --- a/gem-remotes-esp32/Cargo.toml +++ b/gem-remotes-esp32/Cargo.toml @@ -18,7 +18,7 @@ debug = true # Symbols are nice and they don't increase the size on Flash opt-level = "z" [features] -default = ["std", "embassy", "esp-idf-svc/native"] +default = ["std", "esp-idf-svc/native"] pio = ["esp-idf-svc/pio"] std = ["alloc", "esp-idf-svc/binstart", "esp-idf-svc/std"] @@ -33,6 +33,17 @@ esp-idf-svc = { version = "0.49", default-features = false } esp32-nimble = "0.7.0" esp-idf-hal = "0.44.1" esp-idf-sys = "0.35.0" +embedded-cli = "0.2.1" +embedded-io = "0.6.1" +anyhow = "1.0.86" +futures-lite = "2.3.0" +async-executor = "1.13.0" [build-dependencies] embuild = "0.32.0" + +# Cargo udeps can check for unused dependencies; but if there is a dependency we want it to ignore put it here. +[package.metadata.cargo-udeps.ignore] +#normal = [] +#development = [] +#build = [] diff --git a/gem-remotes-esp32/sdkconfig.defaults b/gem-remotes-esp32/sdkconfig.defaults index 8bb42fe..02f2b51 100644 --- a/gem-remotes-esp32/sdkconfig.defaults +++ b/gem-remotes-esp32/sdkconfig.defaults @@ -1,9 +1,13 @@ # Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000 +# USB drivers from the ESP IDF, to enable stdin. First one is for sx chips, second is for cx chips (risc) +CONFIG_ESP_CONSOLE_USB_CDC=y # for s, s3, etc. chips +# CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y # For RISC based c3 chips + # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). # This allows to use 1 ms granuality for thread sleeps (10 ms by default). -#CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_HZ=1000 # Workaround for https://github.com/espressif/esp-idf/issues/7631 #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n diff --git a/gem-remotes-esp32/src/ble_server.rs b/gem-remotes-esp32/src/ble_server.rs new file mode 100644 index 0000000..7057d6e --- /dev/null +++ b/gem-remotes-esp32/src/ble_server.rs @@ -0,0 +1,83 @@ + + +// Example for reference; not ready to use at all. + +use esp32_nimble::{enums::*, uuid128, BLEAdvertisementData, BLEDevice, NimbleProperties}; +use esp_idf_hal::delay::FreeRtos; +use esp_idf_sys as _; + +fn main() { + esp_idf_sys::link_patches(); + + // Bind the log crate to the ESP Logging facilities + esp_idf_svc::log::EspLogger::initialize_default(); + + // Take ownership of device + let ble_device = BLEDevice::take(); + + // Obtain handle for peripheral advertiser + let ble_advertiser = ble_device.get_advertising(); + + // Configure Device Security + ble_device + .security() + .set_auth(AuthReq::all()) + .set_passkey(123456) + .set_io_cap(SecurityIOCap::DisplayOnly) + .resolve_rpa(); + + // Obtain handle for server + let server = ble_device.get_server(); + + // Define server connect behaviour + server.on_connect(|server, clntdesc| { + // Print connected client data + println!("{:?}", clntdesc); + // Update connection parameters + server + .update_conn_params(clntdesc.conn_handle(), 24, 48, 0, 60) + .unwrap(); + }); + + // Define server disconnect behaviour + server.on_disconnect(|_desc, _reason| { + println!("Disconnected, back to advertising"); + }); + + // Create a service with custom UUID + let my_service = server.create_service(uuid128!("9b574847-f706-436c-bed7-fc01eb0965c1")); + + // Create a characteristic to associate with created service + let my_service_characteristic = my_service.lock().create_characteristic( + uuid128!("681285a6-247f-48c6-80ad-68c3dce18585"), + NimbleProperties::READ | NimbleProperties::READ_ENC, + ); + + // Modify characteristic value + my_service_characteristic.lock().set_value(b"Start Value"); + + // Configure Advertiser Data + ble_advertiser + .lock() + .set_data( + BLEAdvertisementData::new() + .name("ESP32 Server") + .add_service_uuid(uuid128!("9b574847-f706-436c-bed7-fc01eb0965c1")), + ) + .unwrap(); + + // Start Advertising + ble_advertiser.lock().start().unwrap(); + + // (Optional) Print dump of local GATT table + // server.ble_gatts_show_local(); + + // Init a value to pass to characteristic + let mut val = 0; + + loop { + FreeRtos::delay_ms(1000); + my_service_characteristic.lock().set_value(&[val]).notify(); + val = val.wrapping_add(1); + } +} diff --git a/gem-remotes-esp32/src/main.rs b/gem-remotes-esp32/src/main.rs index c6312c6..c97d465 100644 --- a/gem-remotes-esp32/src/main.rs +++ b/gem-remotes-esp32/src/main.rs @@ -1,81 +1,54 @@ +//use esp_idf_hal::peripherals::Peripherals; +use esp_idf_svc::timer::EspTaskTimerService; +use core::time::Duration; +use log::{info, error}; +use anyhow::Result; +use async_executor::Executor; +use futures_lite::future; -use esp32_nimble::{enums::*, uuid128, BLEAdvertisementData, BLEDevice, NimbleProperties}; -use esp_idf_hal::delay::FreeRtos; -use esp_idf_sys as _; +mod test_console; fn main() { - esp_idf_sys::link_patches(); - - // Bind the log crate to the ESP Logging facilities + // Do basic initialization + esp_idf_svc::sys::link_patches(); esp_idf_svc::log::EspLogger::initialize_default(); + info!("Logging initialized"); - // Take ownership of device - let ble_device = BLEDevice::take(); + //let peripherals = Peripherals::take().unwrap(); - // Obtain handle for peripheral advertiser - let ble_advertiser = ble_device.get_advertising(); + match future::block_on(main_loop()) { + Ok(_) => {error!("Exited main loop normally, but this should be impossible.")} + Err(e) => {error!("Exited main loop with error {}", e)} + }; - // Configure Device Security - ble_device - .security() - .set_auth(AuthReq::all()) - .set_passkey(123456) - .set_io_cap(SecurityIOCap::DisplayOnly) - .resolve_rpa(); +} - // Obtain handle for server - let server = ble_device.get_server(); - - // Define server connect behaviour - server.on_connect(|server, clntdesc| { - // Print connected client data - println!("{:?}", clntdesc); - // Update connection parameters - server - .update_conn_params(clntdesc.conn_handle(), 24, 48, 0, 60) - .unwrap(); - }); - - // Define server disconnect behaviour - server.on_disconnect(|_desc, _reason| { - println!("Disconnected, back to advertising"); - }); - - // Create a service with custom UUID - let my_service = server.create_service(uuid128!("9b574847-f706-436c-bed7-fc01eb0965c1")); - - // Create a characteristic to associate with created service - let my_service_characteristic = my_service.lock().create_characteristic( - uuid128!("681285a6-247f-48c6-80ad-68c3dce18585"), - NimbleProperties::READ | NimbleProperties::READ_ENC, - ); - - // Modify characteristic value - my_service_characteristic.lock().set_value(b"Start Value"); - - // Configure Advertiser Data - ble_advertiser - .lock() - .set_data( - BLEAdvertisementData::new() - .name("ESP32 Server") - .add_service_uuid(uuid128!("9b574847-f706-436c-bed7-fc01eb0965c1")), - ) - .unwrap(); - - // Start Advertising - ble_advertiser.lock().start().unwrap(); - - // (Optional) Print dump of local GATT table - // server.ble_gatts_show_local(); - - // Init a value to pass to characteristic - let mut val = 0; +async fn test_loop() -> Result<()> { + let timer_service = EspTaskTimerService::new()?; + let mut async_timer = timer_service.timer_async()?; loop { - FreeRtos::delay_ms(1000); - my_service_characteristic.lock().set_value(&[val]).notify(); - val = val.wrapping_add(1); + async_timer.after(Duration::from_secs(2)).await?; } } + +async fn main_loop() -> Result<()> { + info!("Entering main loop"); + + let executor = Executor::new(); + let mut tasks:Vec<_> = Vec::new(); + + //Queueu up our async tasks + tasks.push(executor.spawn(test_console::start_cli())); + tasks.push(executor.spawn(test_loop())); + + //Once we have all our tasks, await on them all to run them in parallel. + for task in tasks { + executor.run(task).await?; + } + + info!("exiting main loop"); + Ok(()) +} + \ No newline at end of file diff --git a/gem-remotes-esp32/src/test_console.rs b/gem-remotes-esp32/src/test_console.rs new file mode 100644 index 0000000..58f7668 --- /dev/null +++ b/gem-remotes-esp32/src/test_console.rs @@ -0,0 +1,195 @@ +// === Constants ============================================================== +const MAX_COMMAND_SIZE: usize = 64; // in bytes; we want to test wifi ssid which + // can be 32 bytes. +const MAX_HISTORY_SIZE: usize = 128; // in bytes +const SLEEP_TIME_MS: u64 = 10; // should be > 1/CONFIG_FREERTOS_HZ in sdkconfig + // currently this is 1ms, which is also about as + // long as it would take uart buffers to overflow + // at maximum rate. But this is intended for + // human input only. It's noticeably laggy at 50. + +// ============================================================================ + +use ::{ + anyhow::Result, + embedded_cli::{ + cli::{CliBuilder, CliHandle}, + Command, + }, + esp_idf_svc::timer::EspTaskTimerService, + std::{ + convert::Infallible, + io::{stdin, stdout, Read, Write}, + //time::Duration, // could also use core::time::Duration? + }, + core::time::Duration, + log::{info, warn}, +}; + +#[derive(Command)] +pub enum Menu<'a> { + /// Send a button press: Up + ButtonUp, + + /// Send a button press: Down + ButtonDown, + + /// Send a button press: Auto toggle on/off + ButtonAuto, + + /// Send a button press: Pair/forget + ButtonPair, + + /// Send a bluetooth characteristic: Up + BluetoothUp { + /// 0 for not pressed, 1 for pressed + data: u8, + }, + + /// Send a bluetooth characteristic: Down + BluetoothDown { + /// 0 for not pressed, 1 for pressed + data: u8, + }, + + /// Send a bluetooth characteristic: Stop + BluetoothStop { + /// 0 for not pressed, 1 for pressed + data: u8, + }, + + /// Send a bluetooth characteristic: Learn + BluetoothLearn { + /// 0 for not pressed, 1 for pressed + data: u8, + }, + + /// Send a bluetooth characteristic: Auto + BluetoothAuto { + /// 0 for not pressed, 1 for pressed + data: u8, + }, + + /// Send a bluetooth characteristic: Limits + BluetoothTopLimit { data: u8 }, + + /// Send a bluetooth characteristic: Limits + BluetoothBottomLimit { data: u8 }, + /// Send a bluetooth characteristic: Wifi SSID + BluetoothWifiSsid { ssid: &'a str }, + + /// Send a bluetooth characteristic: Wifi Password + BluetoothWifiPassword { wifipass: &'a str }, +} + +pub fn process_menu( + cli: &mut CliHandle<'_, SimpleWriter, Infallible>, + command: Menu<'_>, +) -> Result<(), Infallible> { + match command { + Menu::ButtonUp => { + cli.writer().write_str("TODO: simulate button")?; + } + Menu::ButtonDown => { + cli.writer().write_str("TODO: simulate button")?; + } + Menu::ButtonAuto => { + cli.writer().write_str("TODO: simulate button")?; + } + Menu::ButtonPair => { + cli.writer().write_str("TODO: simulate button")?; + } + Menu::BluetoothUp { data } => { + cli.writer() + .write_str("TODO: simulate bluetooth characteristic change")?; + let _ = data; + } + Menu::BluetoothDown { data } => { + cli.writer() + .write_str("TODO: simulate bluetooth characteristic change")?; + let _ = data; + } + Menu::BluetoothStop { data } => { + cli.writer() + .write_str("TODO: simulate bluetooth characteristic change")?; + let _ = data; + } + Menu::BluetoothLearn { data } => { + cli.writer() + .write_str("TODO: simulate bluetooth characteristic change")?; + let _ = data; + } + Menu::BluetoothAuto { data } => { + cli.writer() + .write_str("TODO: simulate bluetooth characteristic change")?; + let _ = data; + } + Menu::BluetoothTopLimit { data } => { + cli.writer() + .write_str("TODO: simulate bluetooth characteristic change")?; + let _ = data; + } + Menu::BluetoothBottomLimit { data } => { + cli.writer() + .write_str("TODO: simulate bluetooth characteristic change")?; + let _ = data; + } + Menu::BluetoothWifiSsid { ssid } => { + cli.writer() + .write_str("TODO: simulate bluetooth characteristic change")?; + let _ = ssid; + } + Menu::BluetoothWifiPassword { wifipass } => { + cli.writer() + .write_str("TODO: simulate bluetooth characteristic change {}")?; + let _ = wifipass; + } + } + Ok(()) +} + +// === SimpleWriter =========================================================== +pub struct SimpleWriter {} + +impl embedded_io::ErrorType for SimpleWriter { + type Error = Infallible; +} + +impl embedded_io::Write for SimpleWriter { + fn write(&mut self, buf: &[u8]) -> Result { + stdout().write(&buf).unwrap(); //TODO: handle errors? + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + let _ = stdout().flush(); //TODO currently ignoring this error + Ok(()) + } +} + +pub async fn start_cli() -> anyhow::Result<()> { + let timer_service = EspTaskTimerService::new().unwrap(); //TODO: handle this error + info!("Setting up command line listern and processor"); + let writer = SimpleWriter {}; + let mut cli = CliBuilder::default() + .writer(writer) + .command_buffer([0_u8; MAX_COMMAND_SIZE]) + .history_buffer([0_u8; MAX_HISTORY_SIZE]) + .prompt("$> ") + .build()?; + let mut reader = stdin(); + let mut buf = [0_u8; 1]; + let mut async_timer = timer_service.timer_async()?; + loop { + async_timer.after(Duration::from_millis(SLEEP_TIME_MS)).await?; + match reader.read_exact(&mut buf) { + Ok(_) => { + cli.process_byte::(buf[0], &mut Menu::processor(process_menu))?; + } + Err(e) => match e.kind() { + std::io::ErrorKind::WouldBlock => {} // This is expected if there is no input + _ => {warn!("Error waiting for input: {}", e)} + } + }; + } +}