Compare commits

..

No commits in common. "main" and "wip" have entirely different histories.
main ... wip

9 changed files with 76 additions and 113 deletions

View File

@ -1,33 +1,5 @@
# Alert-Me Display
A simple application for an
[nRF52840](https://www.nordicsemi.com/Products/nRF52840) microcontroller using
[embassy](https://embassy.dev/).
It shows information received with BLE on a
[HD44780 LCD like](https://en.wikipedia.org/wiki/Hitachi_HD44780_LCD_controller)
screen.
This is a learning environment, not a finished product.
# Development
## Nix
If using the nix way as described in the [top-level readme](../README.md), no
further dependencies need to be installed.
## Manual Way
In addition to the [top-level readme](../README.md) dependencies, the following
has to be installed:
- [probe-rs](https://probe.rs/docs/getting-started/installation/)
## Softdevice
The softdevice blob is not distributed with this repository for licensing
reasons (even though my understanding is that it is possible).
reasons (even though it would be possible if my understanding is correct).
Download the
[S140 Softdevice](https://www.nordicsemi.com/Products/Development-software/s140/download)
@ -38,10 +10,3 @@ _Example command to flash the softdevice_
```
probe-rs download --verify --format hex --chip nRF52840_xxAA ./s140_nrf52_7.3.0_softdevice.hex
```
## Running
Verify that the microcontroller is recognized by running `probe-rs list`.
A simple `cargo run` will then connect to it and upload the code. Logging output
should be visible.

View File

@ -0,0 +1,5 @@
#[nrf_softdevice::gatt_service(uuid = "180f")]
pub struct BatteryService {
#[characteristic(uuid = "2a19", read, notify)]
pub battery_level: u8,
}

View File

@ -0,0 +1,12 @@
const MAX_STRING_LENGTH: usize = 128;
#[nrf_softdevice::gatt_service(uuid = "437dc41e-d899-40ac-9c83-188c8c4d9fe7")]
pub struct ConfigService {
#[characteristic(uuid = "e7b9ebd9-57e0-4821-8fa3-55e22cd7b705", read, write)]
#[description = "WiFi SSID"]
pub wifi_ssid: [u8; MAX_STRING_LENGTH],
#[characteristic(uuid = "1bcc70df-179e-4853-bb74-c22380f491a3", read, write)]
pub wifi_pw: [u8; MAX_STRING_LENGTH],
#[characteristic(uuid = "3c9e4967-f792-4903-a968-490271cb7eeb", read, write)]
pub mqtt_broker: [u8; MAX_STRING_LENGTH],
}

View File

@ -1,5 +1,8 @@
use crate::ble::msg_service::{MessageService, MessageServiceEvent};
use crate::events;
use crate::ble::config_service::ConfigService;
use crate::ble::config_service::ConfigServiceEvent;
use crate::ble::battery_service::BatteryService;
use crate::ble::battery_service::BatteryServiceEvent;
use defmt::info;
use nrf_softdevice::ble::gatt_server;
@ -8,18 +11,27 @@ use nrf_softdevice::ble::DisconnectedError;
#[nrf_softdevice::gatt_server]
pub struct Server {
pub config: MessageService,
pub bas: BatteryService,
pub config: ConfigService,
}
pub async fn run(conn: &Connection, server: &Server) -> DisconnectedError {
info!("gatt started");
let event_pub = events::EVENTS.publisher().unwrap();
gatt_server::run(conn, server, |e| match e {
ServerEvent::Bas(e) => match e {
BatteryServiceEvent::BatteryLevelCccdWrite { notifications } => {
info!("battery notifications: {}", notifications)
}
},
ServerEvent::Config(e) => match e {
MessageServiceEvent::MessageWrite(val) => {
info!("new message: {}", val);
event_pub.publish_immediate(events::Event::Message(val));
ConfigServiceEvent::WifiSsidWrite(val) => {
info!("new ssid: {}", val);
}
ConfigServiceEvent::WifiPwWrite(val) => {
info!("new pw: {}", val);
}
ConfigServiceEvent::MqttBrokerWrite(val) => {
info!("new broker: {}", val);
}
},
})

View File

@ -1,8 +0,0 @@
pub const MAX_STRING_LENGTH: usize = 128;
#[nrf_softdevice::gatt_service(uuid = "437dc41e-d899-40ac-9c83-188c8c4d9fe7")]
pub struct MessageService {
#[characteristic(uuid = "e7b9ebd9-57e0-4821-8fa3-55e22cd7b705", read, write)]
#[description = "Message"]
pub message: [u8; MAX_STRING_LENGTH],
}

View File

@ -1,12 +0,0 @@
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, pubsub::PubSubChannel};
use crate::ble::msg_service::MAX_STRING_LENGTH;
#[derive(Clone, Copy)]
pub enum Event {
// New message.
Message([u8; MAX_STRING_LENGTH]),
}
/// PubSub channel to listen to events and publish them as well..
pub static EVENTS: PubSubChannel<CriticalSectionRawMutex, Event, 4, 4, 4> = PubSubChannel::new();

View File

@ -7,7 +7,9 @@ const MAX_STRING_LENGTH: u32 = 128;
const CONFIG_START: u32 = 0x80000;
pub enum ConfigItem {
Message,
Ssid,
WifiPw,
MqtBroker,
}
pub struct Config<'a> {

View File

@ -3,10 +3,9 @@
//! Code mostly translated from here (an excellent project):
//! https://github.com/Pi4J/pi4j-example-components/blob/main/src/main/java/com/pi4j/catalog/components/LcdDisplay.java
use embassy_nrf::gpio::AnyPin;
use embassy_nrf::interrupt::Priority;
use embassy_nrf::interrupt::{self, InterruptExt};
use embassy_nrf::peripherals::TWISPI0;
use embassy_nrf::peripherals::{P0_03, P0_04, TWISPI0};
use embassy_nrf::twim::{self, Twim};
use embassy_nrf::{bind_interrupts, peripherals};
use embassy_time::Delay;
@ -51,6 +50,7 @@ pub enum Lines {
pub struct Lcd<'a> {
i2c: Twim<'a, TWISPI0>,
lines: [&'a str; ROWS],
address: u8,
}
@ -104,31 +104,42 @@ impl<'a> Lcd<'a> {
Ok(())
}
pub fn new(address: u8, spi0: TWISPI0, p03: AnyPin, p04: AnyPin) -> Self {
pub fn new(address: u8, spi0: TWISPI0, p03: P0_03, p04: P0_04) -> Self {
interrupt::SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0.set_priority(Priority::P2);
let config = twim::Config::default();
let i2c = Twim::new(spi0, Irqs, p03, p04, config);
Lcd { i2c, address }
Lcd {
i2c,
lines: ["", ""],
address,
}
}
pub async fn scroll_buffer(&mut self) -> Result<(), twim::Error> {
if self.lines[0].len() > LINE_LENGTH || self.lines[1].len() > LINE_LENGTH {
self.cmd(SCROLL_LEFT, 0).await?;
}
Ok(())
}
async fn write_buffer(&mut self, text: &str, line: Lines) -> Result<(), twim::Error> {
for (position, c) in text.chars().enumerate() {
self.write_character(c, position as u8, LINE_OFFSETS[line as usize])
async fn write_buffer(&mut self) -> Result<(), twim::Error> {
#[allow(clippy::needless_range_loop)]
for line in 0..ROWS {
for (position, c) in self.lines[line].chars().enumerate() {
self.write_character(c, position as u8, LINE_OFFSETS[line])
.await?;
}
}
Ok(())
}
pub async fn clear(&mut self) -> Result<(), twim::Error> {
self.cmd(CLEAR_DISPLAY, 0).await?;
self.lines = ["", ""];
Ok(())
}
@ -152,8 +163,9 @@ impl<'a> Lcd<'a> {
Ok(())
}
pub async fn write(&mut self, text: &str, line: Lines) -> Result<(), twim::Error> {
self.write_buffer(text, line).await?;
pub async fn write(&mut self, text: &'a str, line: Lines) -> Result<(), twim::Error> {
self.lines[line as usize] = text;
self.write_buffer().await?;
Ok(())
}

View File

@ -1,9 +1,9 @@
#![no_std]
#![no_main]
use ble::{gatt::Server, msg_service::MAX_STRING_LENGTH};
use ble::gatt::Server;
use defmt_rtt as _;
use embassy_nrf::{self as _, gpio::Pin};
use embassy_nrf as _;
use embassy_time::Delay;
use embedded_hal_async::delay::DelayNs;
use futures::pin_mut;
@ -14,11 +14,11 @@ use crate::lcd::Lcd;
use defmt::{error, info, unwrap};
use embassy_executor::Spawner;
pub mod events;
pub mod ble {
pub mod advertisment;
pub mod battery_service;
pub mod config_service;
pub mod gatt;
pub mod msg_service;
}
pub mod flash;
pub mod lcd;
@ -37,27 +37,9 @@ async fn ble(sd: &'static Softdevice, server: Server) {
}
#[embassy_executor::task]
async fn lcd_scroll(sd: &'static Softdevice, mut lcd: lcd::Lcd<'static>, delay: u32) {
fn until_first_zero(data: &[u8]) -> &[u8] {
match data.iter().position(|&x| x == 0) {
Some(pos) => &data[..pos],
None => data,
}
}
async fn tick(sd: &'static Softdevice, mut lcd: lcd::Lcd<'static>, delay: u32) {
let mut d = Delay {};
let mut event_sub = events::EVENTS.subscriber().unwrap();
loop {
if let Some(events::Event::Message(new_msg)) = event_sub.try_next_message_pure() {
let new_msg = until_first_zero(&new_msg);
match core::str::from_utf8(new_msg) {
Ok(msg) => lcd.write(msg, lcd::Lines::First).await.unwrap(),
Err(_) => error!("failed to parse incoming message as utf8"),
}
}
lcd.scroll_buffer()
.await
.unwrap_or_else(|e| error!("Failed to scroll lcd: {}", e));
@ -66,29 +48,22 @@ async fn lcd_scroll(sd: &'static Softdevice, mut lcd: lcd::Lcd<'static>, delay:
}
}
#[embassy_executor::task]
async fn config(sd: &'static Softdevice) {
let flash = Flash::take(sd);
pin_mut!(flash);
let config = flash::Config::new(flash);
loop {}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let peripherals = peripherals::init();
let mut lcd = Lcd::new(
0x27,
peripherals.TWISPI0,
peripherals.P0_03.degrade(),
peripherals.P0_04.degrade(),
peripherals.P0_03,
peripherals.P0_04,
);
lcd.init().await.expect("Failed to initialize lcd");
let (sd, server) = softdevice::run(spawner, DEVICE_NAME).expect("Failed to run softdevice");
let flash = Flash::take(sd);
pin_mut!(flash);
let config = flash::Config::new(flash);
unwrap!(spawner.spawn(ble(sd, server)));
unwrap!(spawner.spawn(lcd_scroll(sd, lcd, 500)));
unwrap!(spawner.spawn(config(sd)));
unwrap!(spawner.spawn(tick(sd, lcd, 500)));
}