nesso is a Rust-native no_std SDK for the Arduino Nesso N1, an ESP32-C6
based device with display, touch, IMU, Wi-Fi, BLE, LoRa, audio, and
battery/power-management hardware plus optional external unit support for
Nesso-compatible expansion sensors such as M5Stack Unit ENV Pro.
The public crate is nesso. The repository is
a Cargo workspace for examples and validation, but only the nesso crate is
published to crates.io.
This SDK targets only the Arduino Nesso N1. It intentionally does not provide a generic board abstraction layer, an M5Stack compatibility layer, or support for other ESP32-C6 boards.
Validated examples currently cover:
- ST7789P3 display initialization, orientation, text rendering, and partial sprite updates
- orientation-aware touch mapping and generic input event helpers
- FT6336U touch reads
- BMI270 IMU live axis reads
- KEY1/KEY2 button events
- Passive buzzer tone output with blocking and non-blocking queued playback
- BQ27220/AW32001 battery and charger status reads
- Heapless settings storage
- ESP32-C6 Wi-Fi scan/connect/disconnect lifecycle and
embassy-netinterface handoff usingesp-radio - ESP32-C6 BLE controller lifecycle with a connectable GATT peripheral example and notification-mirroring GATT surface
- SX1262 LoRa construction, safe no-TX bring-up, receive mode, and explicitly gated transmit example
- Motion/context helpers derived from BMI270 acceleration samples
- Lightweight layout, text, progress, transition, shape, and sprite helpers
- M5Stack Unit ENV Pro BME688 async-first state-machine reads over I2C/Qwiic
- Board KEY1/KEY2 button event helpers for edge, click, hold, and repeat events
- Dithered overlays, dirty-region sprite flushing, and small multi-series graph helpers for no-alloc display UIs
- Board information display
Add the public facade crate:
[dependencies]
nesso = "0.2.4"Enable Wi-Fi only for applications that use the ESP32-C6 radio:
[dependencies]
nesso = { version = "0.2.4", features = ["wifi"] }
esp-alloc = "0.10"Enable ENV Pro support only for applications that use the external unit:
[dependencies]
nesso = { version = "0.2.4", features = ["env"] }Enable BLE only for applications that use the ESP32-C6 Bluetooth controller:
[dependencies]
nesso = { version = "0.2.4", features = ["ble"] }
esp-alloc = "0.10"Enable onboard SX1262 LoRa support only for applications that use the LoRa transceiver:
[dependencies]
nesso = { version = "0.2.4", features = ["lora"] }Enable defmt formatting only for applications that use defmt logging:
[dependencies]
nesso = { version = "0.2.4", features = ["defmt"] }Enable the generic asynchronous display transfer API only when the application owns an async SPI transport:
[dependencies]
nesso = { version = "0.2.4", features = ["display-async"] }On ESP32-C6, configure SPI2 with an ESP-HAL GDMA channel and static
DmaRxBuf/DmaTxBuf descriptors, convert the resulting SpiDmaBus into async
mode, and wrap it as the display's SPI device. ESP-HAL then owns descriptor
lifetime and wakes Display::draw_sprite_async(...) when each transfer
completes. The normal Nesso::new() facade keeps its synchronous shared SPI
bus so display and LoRa remain source-compatible.
nesso::Nesso: public facade and shared board ownership.nesso::bsp: Nesso N1 board constants, GPIOs, I2C addresses, and board-specific setup helpers.nesso::display: ST7789P3 display driver withembedded-graphicsintegration.nesso::env: external environmental unit support, gated behind theenvfeature.nesso::ble: ESP32-C6 BLE controller lifecycle and HCI handoff, gated behind theblefeature.nesso::lora: onboard SX1262 LoRa transceiver support, gated behind thelorafeature.nesso::runtime: explicit ESP radio runtime startup helpers for Wi-Fi/BLE async applications.nesso::touch: FT6336U touch controller support.nesso::input: generic button, board KEY1/KEY2 event, and touch gesture state machine helpers.nesso::imu: BMI270 initialization, config upload, and sensor reads.nesso::motion: coarse motion and pose helpers built from accelerometer samples.nesso::audio: passive buzzer output with blocking and queued non-blocking tone generation.nesso::power: BQ27220 fuel gauge and AW32001 charger status support.nesso::wifi: ESP32-C6 Wi-Fi station lifecycle support and network interface handoff for application-owned network stacks, gated behind thewififeature.nesso::storage: heapless settings storage primitives andesp-storageflash-backed persistence.nesso::sprite: caller-owned and const-generic RGB565 sprite buffers, bit-masked transparency, double buffering, movement tracking, origin-aware dirty-region flushing, and overlay clears.nesso::ui: smallembedded-graphicslayout, label, progress, dither fill, graph, transition, and shape helpers.
Public modules have focused hardware or module-validation examples:
| Module | Example |
|---|---|
nesso::Nesso |
hello_world |
| composed facade usage | dashboard |
nesso::bsp |
board_info |
nesso::display |
display_test, display_orientation |
nesso::touch |
touch_test |
nesso::input |
input_test, input_events |
nesso::imu |
imu_test |
nesso::motion |
motion_test, motion_status |
nesso::audio |
audio_test, queued_audio |
nesso::power |
battery_test, power_status |
nesso::wifi |
wifi_scan, wifi_net_stack |
nesso::ble |
ble_peripheral, ble_beacon, ble_notifications, ble_notification_mirror |
nesso::lora |
lora_info, lora_receive, lora_send |
nesso::storage |
storage_test, storage_settings |
nesso::ui |
ui_test |
nesso::sprite |
dirty_regions |
nesso::env |
env_pro_test |
The facade owns the fixed Nesso N1 wiring. Applications initialize ESP-HAL once,
then hand the peripherals to Nesso::new. This example initializes the display,
enables battery charging, initializes the IMU, and renders live board state.
#![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, display::DisplayOrientation};
#[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.set_orientation(DisplayOrientation::LandscapeClockwise).is_err()
|| nesso.display.clear(Rgb565::BLACK).is_err()
|| nesso
.display
.print_centered("Nesso N1", 40, Rgb565::CYAN)
.is_err()
{
esp_hal::system::software_reset()
}
let _ = nesso.enable_battery_charging();
let _ = nesso.init_imu();
loop {
if let Ok(status) = nesso.battery_status() {
let _acceleration = nesso.acceleration();
let _ = nesso.display.clear(Rgb565::BLACK);
let _ = nesso.display.print_centered("Nesso N1", 40, Rgb565::CYAN);
let _ = nesso.display.print_centered("Battery", 78, Rgb565::WHITE);
let _ = nesso.display.print_centered("charging enabled", 112, Rgb565::GREEN);
let _ = status.percentage;
}
delay.delay_ms(1000);
}
}See examples/ for hardware-focused examples.
Wi-Fi is behind the optional wifi feature and is initialized lazily with
nesso.init_wifi(). Only applications that enable Wi-Fi need to compile
esp-radio/esp-rtos and provide an esp_alloc heap for the ESP radio stack.
Async applications that use Wi-Fi, BLE, or both can call
nesso.start_async_runtime() before creating radio controllers so task startup
ordering is explicit.
Applications that need TCP/IP create their own embassy-net stack by calling
wifi.take_interfaces() and passing interfaces.station to embassy-net.
The SDK continues to own station control through the same EspRadioWifi value.
HTTP, NTP, DNS, weather APIs, and other protocols belong in application crates.
BLE is behind the optional ble feature and is initialized lazily with
nesso.init_ble(). The SDK owns board/controller bring-up and can hand the HCI
connector to a host stack. The ble_peripheral example uses Trouble to
advertise as Nesso N1 and exposes a small custom GATT service that can be
inspected with nRF Connect. The ble_beacon example rotates passive
non-connectable advertising payloads using nesso::ble::BeaconSchedule. The
ble_notifications example accepts app|title|body writes on the Nesso mirror
characteristic and displays the latest mirrored phone/app notification.
LoRa is behind the optional lora feature. When enabled, nesso.lora is
available alongside nesso.display; the BSP shares the documented SPI bus using
embedded-hal-bus with separate chip-select pins. Nesso::new does not start
transmission or place the SX1262 in TX mode. Attach the external LoRa antenna
before using Sx1262::transmit. The lora_send example is compile-time gated
and will not transmit unless built with NESSO_LORA_ALLOW_TX=1.
ENV Pro is behind the optional env feature. For UI or async Embassy apps,
prefer the state-machine flow so the BME688 heater/conversion window does not
block display rendering:
let ready_after_us = env.start_measurement()?;
Timer::after_micros(ready_after_us.into()).await;
let sample = env.read_measurement()?;EnvPro::measure() remains available for existing blocking users. Treat
EnvError::NoNewData from read_measurement() as retryable.
For heapless display animation, use SpriteBuffer<W, H> or
MaskedSprite<W, H>. A masked draw scans one-bit opacity data into horizontal
runs, while DoubleBufferedCanvas<W, H> provides disjoint paint and transfer
buffers. DirtyRectTracker<N> consolidates old and new sprite footprints that
overlap or are within eight pixels. The const-generic buffers can be placed in
static storage when their dimensions would be too large for a task stack.
Install Rust with the target specified in rust-toolchain.toml, then run:
cargo metadata --no-deps --format-version 1
cargo fmt --check --all
cargo check --workspace --target riscv32imac-unknown-none-elf
cargo check --workspace --target riscv32imac-unknown-none-elf --all-features
cargo check -p nesso --target riscv32imac-unknown-none-elf --features defmt
cargo clippy -p nesso --target riscv32imac-unknown-none-elf -- -D warnings
cargo clippy -p nesso --target riscv32imac-unknown-none-elf --all-features -- -D warnings
(cd tests/host && cargo test)
(cd tests/host && cargo clippy --all-targets -- -D warnings)
cargo xtask lint
cargo build --workspace --target riscv32imac-unknown-none-elf
cargo build --workspace --target riscv32imac-unknown-none-elf --releaseThe host test harness exercises reusable logic that does not require hardware: notification parsing, Wi-Fi credential validation, settings serialization and checksum handling, dirty-region coalescing, touch orientation mapping, input events, touch and power I2C parsing, queued audio behavior, motion classification, power helpers, and generic graphics helpers.
Build and flash an example with espflash:
cargo build -p hello_world --target riscv32imac-unknown-none-elf --release
espflash flash --chip esp32c6 -p /dev/cu.usbmodem1101 \
target/riscv32imac-unknown-none-elf/release/hello_worldChange the serial port for your host.
Hardware and architecture notes are kept in docs/:
docs/hardware.mddocs/architecture.mddocs/m5gfx-analysis.mddocs/gap-analysis.mddocs/roadmap.mdHARDWARE_VALIDATION.md
Release maintainers should also use HARDWARE_VALIDATION.md before publishing
a new release. It is the board-run checklist for examples and peripherals that
cannot be fully validated by CI.
Releases are published from GitHub Releases.
- Merge through a pull request to
main. - Run the software validation commands from the build section.
- Run the board checklist in
HARDWARE_VALIDATION.mdon a connected Nesso N1. - Create and push the release tag.
- Publish a GitHub Release for that tag.
The release workflow validates the workspace and publishes only the public
nesso crate to crates.io.
The workflow uses crates.io trusted publishing and does not require a long-lived Cargo registry token.
Licensed under the MIT License. See the LICENSE file.