Compare commits
No commits in common. "main" and "wip" have entirely different histories.
@ -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
|
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
|
Download the
|
||||||
[S140 Softdevice](https://www.nordicsemi.com/Products/Development-software/s140/download)
|
[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
|
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.
|
|
||||||
|
5
display/src/ble/battery_service.rs
Normal file
5
display/src/ble/battery_service.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#[nrf_softdevice::gatt_service(uuid = "180f")]
|
||||||
|
pub struct BatteryService {
|
||||||
|
#[characteristic(uuid = "2a19", read, notify)]
|
||||||
|
pub battery_level: u8,
|
||||||
|
}
|
12
display/src/ble/config_service.rs
Normal file
12
display/src/ble/config_service.rs
Normal 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],
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
use crate::ble::msg_service::{MessageService, MessageServiceEvent};
|
use crate::ble::config_service::ConfigService;
|
||||||
use crate::events;
|
use crate::ble::config_service::ConfigServiceEvent;
|
||||||
|
|
||||||
|
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;
|
||||||
@ -8,18 +11,27 @@ use nrf_softdevice::ble::DisconnectedError;
|
|||||||
|
|
||||||
#[nrf_softdevice::gatt_server]
|
#[nrf_softdevice::gatt_server]
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
pub config: MessageService,
|
pub bas: BatteryService,
|
||||||
|
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 {
|
||||||
MessageServiceEvent::MessageWrite(val) => {
|
ConfigServiceEvent::WifiSsidWrite(val) => {
|
||||||
info!("new message: {}", val);
|
info!("new ssid: {}", val);
|
||||||
event_pub.publish_immediate(events::Event::Message(val));
|
}
|
||||||
|
ConfigServiceEvent::WifiPwWrite(val) => {
|
||||||
|
info!("new pw: {}", val);
|
||||||
|
}
|
||||||
|
ConfigServiceEvent::MqttBrokerWrite(val) => {
|
||||||
|
info!("new broker: {}", val);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -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],
|
|
||||||
}
|
|
@ -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();
|
|
@ -7,7 +7,9 @@ const MAX_STRING_LENGTH: u32 = 128;
|
|||||||
const CONFIG_START: u32 = 0x80000;
|
const CONFIG_START: u32 = 0x80000;
|
||||||
|
|
||||||
pub enum ConfigItem {
|
pub enum ConfigItem {
|
||||||
Message,
|
Ssid,
|
||||||
|
WifiPw,
|
||||||
|
MqtBroker,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Config<'a> {
|
pub struct Config<'a> {
|
||||||
|
@ -3,10 +3,9 @@
|
|||||||
//! 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::TWISPI0;
|
use embassy_nrf::peripherals::{P0_03, P0_04, 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;
|
||||||
@ -51,6 +50,7 @@ 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,24 +104,34 @@ impl<'a> Lcd<'a> {
|
|||||||
Ok(())
|
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);
|
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 { i2c, address }
|
Lcd {
|
||||||
|
i2c,
|
||||||
|
lines: ["", ""],
|
||||||
|
address,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn scroll_buffer(&mut self) -> Result<(), twim::Error> {
|
pub async fn scroll_buffer(&mut self) -> Result<(), twim::Error> {
|
||||||
self.cmd(SCROLL_LEFT, 0).await?;
|
if self.lines[0].len() > LINE_LENGTH || self.lines[1].len() > LINE_LENGTH {
|
||||||
|
self.cmd(SCROLL_LEFT, 0).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write_buffer(&mut self, text: &str, line: Lines) -> Result<(), twim::Error> {
|
async fn write_buffer(&mut self) -> Result<(), twim::Error> {
|
||||||
for (position, c) in text.chars().enumerate() {
|
#[allow(clippy::needless_range_loop)]
|
||||||
self.write_character(c, position as u8, LINE_OFFSETS[line as usize])
|
for line in 0..ROWS {
|
||||||
.await?;
|
for (position, c) in self.lines[line].chars().enumerate() {
|
||||||
|
self.write_character(c, position as u8, LINE_OFFSETS[line])
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -129,6 +139,7 @@ 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(())
|
||||||
}
|
}
|
||||||
@ -152,8 +163,9 @@ impl<'a> Lcd<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn write(&mut self, text: &str, line: Lines) -> Result<(), twim::Error> {
|
pub async fn write(&mut self, text: &'a str, line: Lines) -> Result<(), twim::Error> {
|
||||||
self.write_buffer(text, line).await?;
|
self.lines[line as usize] = text;
|
||||||
|
self.write_buffer().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use ble::{gatt::Server, msg_service::MAX_STRING_LENGTH};
|
use ble::gatt::Server;
|
||||||
use defmt_rtt as _;
|
use defmt_rtt as _;
|
||||||
use embassy_nrf::{self as _, gpio::Pin};
|
use embassy_nrf as _;
|
||||||
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,27 +37,9 @@ async fn ble(sd: &'static Softdevice, server: Server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
async fn lcd_scroll(sd: &'static Softdevice, mut lcd: lcd::Lcd<'static>, delay: u32) {
|
async fn tick(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));
|
||||||
@ -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]
|
#[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.degrade(),
|
peripherals.P0_03,
|
||||||
peripherals.P0_04.degrade(),
|
peripherals.P0_04,
|
||||||
);
|
);
|
||||||
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(lcd_scroll(sd, lcd, 500)));
|
unwrap!(spawner.spawn(tick(sd, lcd, 500)));
|
||||||
unwrap!(spawner.spawn(config(sd)));
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user