Compare commits

..

3 Commits
wip ... main

Author SHA1 Message Date
df605bfd77
prepare simple example 2024-11-16 16:55:15 +01:00
9419afb49e
typo in display readme 2024-10-02 15:25:49 +02:00
66e93d931a
initial display readme 2024-10-02 13:26:05 +02:00
9 changed files with 113 additions and 76 deletions

View File

@ -1,5 +1,33 @@
# 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 The softdevice blob is not distributed with this repository for licensing
reasons (even though it would be possible if my understanding is correct). reasons (even though my understanding is that it is possible).
Download the Download the
[S140 Softdevice](https://www.nordicsemi.com/Products/Development-software/s140/download) [S140 Softdevice](https://www.nordicsemi.com/Products/Development-software/s140/download)
@ -10,3 +38,10 @@ _Example command to flash the softdevice_
``` ```
probe-rs download --verify --format hex --chip nRF52840_xxAA ./s140_nrf52_7.3.0_softdevice.hex 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

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

View File

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

View File

@ -0,0 +1,8 @@
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],
}

12
display/src/events.rs Normal file
View File

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

View File

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

View File

@ -1,9 +1,9 @@
#![no_std] #![no_std]
#![no_main] #![no_main]
use ble::gatt::Server; use ble::{gatt::Server, msg_service::MAX_STRING_LENGTH};
use defmt_rtt as _; use defmt_rtt as _;
use embassy_nrf as _; use embassy_nrf::{self as _, gpio::Pin};
use embassy_time::Delay; use embassy_time::Delay;
use embedded_hal_async::delay::DelayNs; use embedded_hal_async::delay::DelayNs;
use futures::pin_mut; use futures::pin_mut;
@ -14,11 +14,11 @@ use crate::lcd::Lcd;
use defmt::{error, info, unwrap}; use defmt::{error, info, unwrap};
use embassy_executor::Spawner; use embassy_executor::Spawner;
pub mod events;
pub mod ble { pub mod ble {
pub mod advertisment; pub mod advertisment;
pub mod battery_service;
pub mod config_service;
pub mod gatt; pub mod gatt;
pub mod msg_service;
} }
pub mod flash; pub mod flash;
pub mod lcd; pub mod lcd;
@ -37,9 +37,27 @@ async fn ble(sd: &'static Softdevice, server: Server) {
} }
#[embassy_executor::task] #[embassy_executor::task]
async fn tick(sd: &'static Softdevice, mut lcd: lcd::Lcd<'static>, delay: u32) { 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,
}
}
let mut d = Delay {}; let mut d = Delay {};
let mut event_sub = events::EVENTS.subscriber().unwrap();
loop { 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() lcd.scroll_buffer()
.await .await
.unwrap_or_else(|e| error!("Failed to scroll lcd: {}", e)); .unwrap_or_else(|e| error!("Failed to scroll lcd: {}", e));
@ -48,22 +66,29 @@ async fn tick(sd: &'static Softdevice, mut lcd: lcd::Lcd<'static>, delay: u32) {
} }
} }
#[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] #[embassy_executor::main]
async fn main(spawner: Spawner) { async fn main(spawner: Spawner) {
let peripherals = peripherals::init(); let peripherals = peripherals::init();
let mut lcd = Lcd::new( let mut lcd = Lcd::new(
0x27, 0x27,
peripherals.TWISPI0, peripherals.TWISPI0,
peripherals.P0_03, peripherals.P0_03.degrade(),
peripherals.P0_04, peripherals.P0_04.degrade(),
); );
lcd.init().await.expect("Failed to initialize lcd"); lcd.init().await.expect("Failed to initialize lcd");
let (sd, server) = softdevice::run(spawner, DEVICE_NAME).expect("Failed to run softdevice"); 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(ble(sd, server)));
unwrap!(spawner.spawn(tick(sd, lcd, 500))); unwrap!(spawner.spawn(lcd_scroll(sd, lcd, 500)));
unwrap!(spawner.spawn(config(sd)));
} }