nesso 0.2.4

Rust SDK facade for Arduino Nesso N1 on ESP32-C6.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
#![no_std]
//! Public Rust facade crate for the Arduino Nesso N1.
//!
//! `nesso` is the crate applications should normally depend on. It owns the
//! Nesso N1 board bring-up path and exposes lower-level hardware modules for
//! advanced use.
//!
//! Short facade calls borrow the shared I2C bus only for the duration of one
//! operation, which keeps async applications cooperative without making the
//! base crate depend on Embassy.
//!
//! # Example
//!
//! ```rust,ignore
//! #![no_std]
//! #![no_main]
//!
//! use embedded_graphics::{pixelcolor::Rgb565, prelude::RgbColor};
//! use embedded_hal::delay::DelayNs;
//! use esp_hal::{clock::CpuClock, delay::Delay, main};
//! use nesso::Nesso;
//!
//! #[main]
//! fn main() -> ! {
//!     let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
//!     let peripherals = esp_hal::init(config);
//!     let mut delay = Delay::new();
//!     let mut nesso = match Nesso::new(peripherals) {
//!         Ok(nesso) => nesso,
//!         Err(_) => esp_hal::system::software_reset(),
//!     };
//!
//!     if nesso.display.clear(Rgb565::BLACK).is_err()
//!         || nesso
//!             .display
//!             .print_centered("Hello from nesso", 120, Rgb565::WHITE)
//!             .is_err()
//!     {
//!         esp_hal::system::software_reset()
//!     }
//!
//!     loop {
//!         delay.delay_ms(1000);
//!     }
//! }
//! ```

/// Audio support for the Nesso N1 passive buzzer.
pub mod audio;
/// Bluetooth Low Energy controller support for the ESP32-C6 radio.
#[cfg(feature = "ble")]
pub mod ble;
/// Board constants and board-specific setup helpers.
pub mod bsp;
/// Display support for the Nesso N1 ST7789P3 LCD.
pub mod display;
/// Environmental sensor unit support.
#[cfg(feature = "env")]
pub mod env;
/// IMU support for the Nesso N1 BMI270.
pub mod imu;
/// Button and input event helpers.
pub mod input;
/// LoRa support for the onboard SX1262 transceiver.
#[cfg(feature = "lora")]
pub mod lora;
/// Motion classification helpers built on BMI270 accelerometer samples.
pub mod motion;
/// Battery, charger, and power-management support.
pub mod power;
/// ESP radio runtime startup helpers.
#[cfg(any(feature = "wifi", feature = "ble"))]
pub mod runtime;
/// Caller-owned RGB565 sprite/framebuffer support.
pub mod sprite;
/// Settings and key/value storage primitives.
pub mod storage;
/// Touch support for the Nesso N1 FT6336U controller.
pub mod touch;
/// Small graphics and UI primitives for `embedded-graphics` draw targets.
pub mod ui;
/// Wi-Fi support for the ESP32-C6 radio.
#[cfg(feature = "wifi")]
pub mod wifi;

#[cfg(feature = "ble")]
use crate::ble::Ble;
#[cfg(any(feature = "wifi", feature = "ble"))]
use crate::bsp::RadioRuntimeResources;
#[cfg(feature = "wifi")]
use crate::bsp::WifiResources;
use crate::bsp::{
    BoardInitError, ButtonLevels, NessoBuzzer, NessoDisplay, NessoI2c, NessoN1, NessoN1Board,
};
use crate::imu::{Acceleration, Bmi270, Gyroscope};
use crate::input::{BoardButtons, ButtonTiming};
#[cfg(feature = "lora")]
use crate::lora::NessoLora;
use crate::power::{BatteryStatus, ChargeStatus, ChargingConfig, Power};
use crate::storage::{EspFlashSettingsStore, SettingsPartition};
use crate::touch::{Touch, TouchEvent, TouchState};
#[cfg(feature = "wifi")]
use crate::wifi::EspRadioWifi;
use esp_hal::delay::Delay;

/// SDK-level error type for facade operations.
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NessoError {
    /// Board resource initialization failed.
    Board(BoardInitError),
    /// Touch controller I2C access failed.
    Touch,
    /// BMI270 initialization or read failed.
    Imu,
    /// Power-management I2C access failed.
    Power,
    /// The flash peripheral has already been moved out of the facade.
    FlashUnavailable,
    /// Flash settings partition configuration is invalid.
    Storage,
    /// The Wi-Fi radio resources have already been moved out of the facade.
    #[cfg(feature = "wifi")]
    WifiUnavailable,
    /// The BLE controller resources have already been moved out of the facade.
    #[cfg(feature = "ble")]
    BleUnavailable,
    /// Shared radio runtime resources have already been moved out of the facade.
    #[cfg(any(feature = "wifi", feature = "ble"))]
    RadioRuntimeUnavailable,
    /// BMI270 has not been initialized with [`Nesso::init_imu`].
    ImuNotInitialized,
    /// Button expander setup or read failed.
    Input,
    /// LoRa resources have already been consumed.
    #[cfg(feature = "lora")]
    LoraUnavailable,
    /// LoRa setup failed.
    #[cfg(feature = "lora")]
    Lora,
}

/// Board-owned Nesso N1 SDK.
///
/// `display`, `audio`, and optional `lora` are exposed as fields. Touch, IMU,
/// power, and LoRa frontend control use a shared board I2C bus internally.
pub struct Nesso {
    /// ST7789P3 display driver.
    pub display: NessoDisplay,
    /// Passive buzzer driver.
    pub audio: NessoBuzzer,
    /// Onboard SX1262 LoRa driver.
    ///
    /// This field exists when the `lora` feature is enabled. It shares the
    /// board SPI bus with the display through BSP-owned synchronization.
    #[cfg(feature = "lora")]
    pub lora: NessoLora,
    i2c: NessoI2c,
    #[cfg(feature = "wifi")]
    wifi: Option<WifiResources>,
    #[cfg(feature = "ble")]
    ble: Option<crate::bsp::BleResources>,
    #[cfg(any(feature = "wifi", feature = "ble"))]
    radio_runtime: Option<RadioRuntimeResources>,
    #[cfg(any(feature = "wifi", feature = "ble"))]
    radio_runtime_started: bool,
    flash: Option<esp_hal::peripherals::FLASH<'static>>,
    imu_initialized: bool,
    previous_touch: TouchState,
}

impl Nesso {
    /// Initializes board-owned Nesso N1 resources from ESP-HAL peripherals.
    pub fn new(peripherals: esp_hal::peripherals::Peripherals) -> Result<Self, NessoError> {
        let parts = NessoN1Board::new(peripherals)
            .into_core_parts()
            .map_err(NessoError::Board)?;
        let nesso = Self {
            display: parts.display,
            audio: parts.buzzer,
            #[cfg(feature = "lora")]
            lora: parts.lora,
            i2c: parts.i2c,
            #[cfg(feature = "wifi")]
            wifi: Some(parts.wifi),
            #[cfg(feature = "ble")]
            ble: Some(parts.ble),
            #[cfg(any(feature = "wifi", feature = "ble"))]
            radio_runtime: Some(parts.radio_runtime),
            #[cfg(any(feature = "wifi", feature = "ble"))]
            radio_runtime_started: false,
            flash: Some(parts.flash),
            imu_initialized: false,
            previous_touch: TouchState::default(),
        };
        Ok(nesso)
    }

    /// Uploads BMI270 configuration and enables accelerometer/gyroscope reads.
    pub fn init_imu(&mut self) -> Result<(), NessoError> {
        Bmi270::new(&mut self.i2c, Delay::new())
            .init()
            .map_err(|_| NessoError::Imu)?;
        self.imu_initialized = true;
        Ok(())
    }

    /// Returns the current touch state from the FT6336U controller.
    pub fn touch_state(&mut self) -> Result<TouchState, NessoError> {
        Touch::new(&mut self.i2c)
            .read_state()
            .map_err(|_| NessoError::Touch)
    }

    /// Polls a touch event while preserving previous touch state in the facade.
    pub fn touch_event(&mut self) -> Result<TouchEvent, NessoError> {
        let current = self.touch_state()?;
        let event = match (self.previous_touch.primary(), current.primary()) {
            (None, Some(point)) => TouchEvent::Pressed(point),
            (Some(_), None) => TouchEvent::Released,
            (Some(previous), Some(point)) if previous != point => TouchEvent::Moved(point),
            _ => TouchEvent::Idle,
        };
        self.previous_touch = current;
        Ok(event)
    }

    /// Configures the board KEY1/KEY2 expander pins as button inputs.
    pub fn init_buttons(&mut self) -> Result<(), NessoError> {
        NessoN1::init_button_inputs(&mut self.i2c).map_err(|_| NessoError::Input)
    }

    /// Configures KEY1/KEY2 and returns a board-button event helper.
    pub fn init_button_events(&mut self) -> Result<BoardButtons, NessoError> {
        self.init_button_events_with_timing(ButtonTiming::default())
    }

    /// Configures KEY1/KEY2 and returns a board-button event helper with
    /// custom timing thresholds.
    pub fn init_button_events_with_timing(
        &mut self,
        timing: ButtonTiming,
    ) -> Result<BoardButtons, NessoError> {
        self.init_buttons()?;
        Ok(BoardButtons::new(timing))
    }

    /// Returns the current KEY1/KEY2 pressed state.
    pub fn button_levels(&mut self) -> Result<ButtonLevels, NessoError> {
        NessoN1::read_button_levels(&mut self.i2c).map_err(|_| NessoError::Input)
    }

    /// Borrows a short-lived facade over shared I2C services.
    ///
    /// The helper avoids long-lived mutable aliasing while keeping repeated
    /// touch, IMU, and power polling code compact.
    pub fn with_sensors<R>(
        &mut self,
        f: impl FnOnce(&mut NessoSensors<'_>) -> Result<R, NessoError>,
    ) -> Result<R, NessoError> {
        let mut sensors = NessoSensors {
            i2c: &mut self.i2c,
            imu_initialized: self.imu_initialized,
            previous_touch: &mut self.previous_touch,
        };
        f(&mut sensors)
    }

    /// Returns raw BMI270 accelerometer data.
    pub fn acceleration(&mut self) -> Result<Acceleration, NessoError> {
        if !self.imu_initialized {
            return Err(NessoError::ImuNotInitialized);
        }
        Bmi270::new(&mut self.i2c, Delay::new())
            .acceleration()
            .map_err(|_| NessoError::Imu)
    }

    /// Returns raw BMI270 gyroscope data.
    pub fn gyroscope(&mut self) -> Result<Gyroscope, NessoError> {
        if !self.imu_initialized {
            return Err(NessoError::ImuNotInitialized);
        }
        Bmi270::new(&mut self.i2c, Delay::new())
            .gyroscope()
            .map_err(|_| NessoError::Imu)
    }

    /// Returns battery and charger status from the BQ27220/AW32001 devices.
    pub fn battery_status(&mut self) -> Result<BatteryStatus, NessoError> {
        Power::new(&mut self.i2c)
            .battery_status()
            .map_err(|_| NessoError::Power)
    }

    /// Configures the AW32001 charger with the SDK default charging profile and
    /// enables battery charging.
    pub fn enable_battery_charging(&mut self) -> Result<(), NessoError> {
        Power::new(&mut self.i2c)
            .begin_charging()
            .map_err(|_| NessoError::Power)
    }

    /// Configures and enables AW32001 battery charging.
    pub fn configure_battery_charging(&mut self, config: ChargingConfig) -> Result<(), NessoError> {
        Power::new(&mut self.i2c)
            .configure_charging(config)
            .map_err(|_| NessoError::Power)
    }

    /// Takes the ESP32-C6 radio resources and creates a Wi-Fi station driver.
    ///
    /// Wi-Fi needs the ESP allocator because `esp-radio` returns heap-backed
    /// scan results. Keeping Wi-Fi behind this method lets display, input,
    /// touch, IMU, audio, power, and storage applications avoid a global heap.
    #[cfg(feature = "wifi")]
    pub fn init_wifi(&mut self) -> Result<EspRadioWifi, NessoError> {
        let wifi = self.wifi.take().ok_or(NessoError::WifiUnavailable)?;
        if self.radio_runtime_started {
            return Ok(EspRadioWifi::new_started(wifi));
        }
        let runtime = self
            .radio_runtime
            .take()
            .ok_or(NessoError::RadioRuntimeUnavailable)?;
        Ok(EspRadioWifi::new(wifi, runtime))
    }

    /// Takes the ESP32-C6 Bluetooth resources and creates a BLE controller.
    ///
    /// The SDK owns the board lifecycle and returns an HCI connector wrapper.
    /// GATT services, phone app protocol, and notification semantics are built
    /// above this layer by a BLE host stack.
    #[cfg(feature = "ble")]
    pub fn init_ble(&mut self) -> Result<Ble, NessoError> {
        let ble = self.ble.take().ok_or(NessoError::BleUnavailable)?;
        if self.radio_runtime_started {
            return Ok(Ble::new_started(ble));
        }
        let runtime = self
            .radio_runtime
            .take()
            .ok_or(NessoError::RadioRuntimeUnavailable)?;
        Ok(Ble::new(ble, runtime))
    }

    /// Starts the shared ESP radio runtime before Wi-Fi or BLE tasks run.
    ///
    /// Call this once in async applications that use Wi-Fi, BLE, or both. After
    /// it succeeds, [`Nesso::init_wifi`] and [`Nesso::init_ble`] create
    /// controllers that reuse the already-started runtime. Simple blocking
    /// examples may skip this and let Wi-Fi or BLE start the runtime lazily.
    #[cfg(any(feature = "wifi", feature = "ble"))]
    pub fn start_async_runtime(&mut self) -> Result<(), NessoError> {
        if self.radio_runtime_started {
            return Ok(());
        }

        let runtime = self
            .radio_runtime
            .take()
            .ok_or(NessoError::RadioRuntimeUnavailable)?;
        runtime::start_radio_runtime(runtime);
        self.radio_runtime_started = true;
        Ok(())
    }

    /// Creates a flash-backed settings store at the SDK default partition.
    ///
    /// The default region is documented as [`storage::SettingsPartition::DEFAULT`].
    /// Applications with a custom partition table may prefer
    /// [`Nesso::take_flash_settings_partition`] or [`Nesso::take_flash_settings`].
    pub fn take_default_flash_settings(
        &mut self,
    ) -> Result<EspFlashSettingsStore<'static>, NessoError> {
        self.take_flash_settings_partition(SettingsPartition::DEFAULT)
    }

    /// Creates a flash-backed settings store from a documented flash partition.
    pub fn take_flash_settings_partition(
        &mut self,
        partition: SettingsPartition,
    ) -> Result<EspFlashSettingsStore<'static>, NessoError> {
        let flash = self.flash.take().ok_or(NessoError::FlashUnavailable)?;
        EspFlashSettingsStore::from_partition(flash, partition).map_err(|_| NessoError::Storage)
    }

    /// Creates a flash-backed settings store at an application-selected offset.
    ///
    /// Prefer [`Nesso::take_default_flash_settings`] unless the application has
    /// its own partition table or flash allocation policy.
    pub fn take_flash_settings(
        &mut self,
        offset: u32,
    ) -> Result<EspFlashSettingsStore<'static>, NessoError> {
        let flash = self.flash.take().ok_or(NessoError::FlashUnavailable)?;
        Ok(EspFlashSettingsStore::from_flash(flash, offset))
    }
}

/// Borrow-scoped shared I2C sensor facade.
pub struct NessoSensors<'a> {
    i2c: &'a mut NessoI2c,
    imu_initialized: bool,
    previous_touch: &'a mut TouchState,
}

impl NessoSensors<'_> {
    /// Returns the current touch state.
    pub fn touch_state(&mut self) -> Result<TouchState, NessoError> {
        Touch::new(&mut *self.i2c)
            .read_state()
            .map_err(|_| NessoError::Touch)
    }

    /// Polls a touch event while preserving previous touch state.
    pub fn touch_event(&mut self) -> Result<TouchEvent, NessoError> {
        let current = self.touch_state()?;
        let event = match (self.previous_touch.primary(), current.primary()) {
            (None, Some(point)) => TouchEvent::Pressed(point),
            (Some(_), None) => TouchEvent::Released,
            (Some(previous), Some(point)) if previous != point => TouchEvent::Moved(point),
            _ => TouchEvent::Idle,
        };
        *self.previous_touch = current;
        Ok(event)
    }

    /// Returns raw BMI270 accelerometer data.
    pub fn acceleration(&mut self) -> Result<Acceleration, NessoError> {
        if !self.imu_initialized {
            return Err(NessoError::ImuNotInitialized);
        }
        Bmi270::new(&mut *self.i2c, Delay::new())
            .acceleration()
            .map_err(|_| NessoError::Imu)
    }

    /// Returns raw BMI270 gyroscope data.
    pub fn gyroscope(&mut self) -> Result<Gyroscope, NessoError> {
        if !self.imu_initialized {
            return Err(NessoError::ImuNotInitialized);
        }
        Bmi270::new(&mut *self.i2c, Delay::new())
            .gyroscope()
            .map_err(|_| NessoError::Imu)
    }

    /// Returns battery and charger status.
    pub fn battery_status(&mut self) -> Result<BatteryStatus, NessoError> {
        Power::new(&mut *self.i2c)
            .battery_status()
            .map_err(|_| NessoError::Power)
    }

    /// Returns charger status only.
    pub fn charge_status(&mut self) -> Result<ChargeStatus, NessoError> {
        Power::new(&mut *self.i2c)
            .charge_status()
            .map_err(|_| NessoError::Power)
    }
}