aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2026-03-30 16:26:33 +0000
committers-ol <s+removethis@s-ol.nu>2026-03-30 16:26:33 +0000
commit9aa80ab752c0999ba1df9e98328f09c720f8ea66 (patch)
tree12fa888425c055d0993f68282a377f4ba7f64d98 /src
downloadt144-meshcore-example-9aa80ab752c0999ba1df9e98328f09c720f8ea66.tar.gz
t144-meshcore-example-9aa80ab752c0999ba1df9e98328f09c720f8ea66.zip
initial commit
Diffstat (limited to 'src')
-rw-r--r--src/display.rs89
-rw-r--r--src/lib.rs220
-rw-r--r--src/main.rs89
-rw-r--r--src/radio.rs70
4 files changed, 468 insertions, 0 deletions
diff --git a/src/display.rs b/src/display.rs
new file mode 100644
index 0000000..5830254
--- /dev/null
+++ b/src/display.rs
@@ -0,0 +1,89 @@
+use defmt::info;
+use display_interface_spi::SpiInterface;
+use embassy_nrf::gpio::{AnyPin, Level, Output, OutputDrive};
+use embassy_nrf::{interrupt::typelevel::Binding, spim, Peri};
+use embassy_time::Timer;
+use embedded_graphics::{
+ framebuffer::{buffer_size, Framebuffer},
+ pixelcolor::{raw::LittleEndian, Gray4},
+};
+use embedded_hal_bus::spi::ExclusiveDevice;
+use ssd1322::{DisplayAsync, PixelCoord};
+
+type SPIDevice<'t> = ExclusiveDevice<spim::Spim<'t>, Output<'t>, embassy_time::Delay>;
+
+type Display<'t> = DisplayAsync<SpiInterface<SPIDevice<'t>, Output<'t>>>;
+
+pub struct Pins<'t> {
+ pub ven: Peri<'t, AnyPin>,
+ pub rst: Peri<'t, AnyPin>,
+ pub sck: Peri<'t, AnyPin>,
+ pub sdo: Peri<'t, AnyPin>,
+ pub cs: Peri<'t, AnyPin>,
+ pub dc: Peri<'t, AnyPin>,
+}
+
+pub async fn init<T: spim::Instance>(
+ spi_instance: Peri<'static, T>,
+ irq_binding: impl Binding<T::Interrupt, spim::InterruptHandler<T>> + 'static,
+ pins: Pins<'static>,
+) -> Display<'static> {
+ Output::new(pins.ven, Level::High, OutputDrive::Standard).persist();
+
+ let mut out_rst = Output::new(pins.rst, Level::Low, OutputDrive::Standard);
+ Timer::after_millis(2).await;
+ out_rst.set_high();
+ Timer::after_millis(2).await;
+
+ let out_cs = Output::new(pins.cs, Level::High, OutputDrive::Standard);
+ let out_dc = Output::new(pins.dc, Level::High, OutputDrive::Standard);
+
+ let mut spi_config = spim::Config::default();
+ spi_config.frequency = spim::Frequency::M8;
+ let spi_bus = spim::Spim::new_txonly(spi_instance, irq_binding, pins.sck, pins.sdo, spi_config);
+ let spi_dev = ExclusiveDevice::new(spi_bus, out_cs, embassy_time::Delay).unwrap();
+ let spi_iface = SpiInterface::new(spi_dev, out_dc);
+
+ let mut display = DisplayAsync::new(spi_iface, PixelCoord(256, 64), PixelCoord(0x1C * 4, 0));
+
+ info!("initializing display");
+ display
+ .init(
+ ssd1322::ConfigAsync::new(
+ ssd1322::ComScanDirection::RowZeroLast,
+ ssd1322::ComLayout::Progressive,
+ )
+ .column_remap(ssd1322::command::ColumnRemap::Reverse)
+ .clock_fosc_divset(9, 1)
+ .display_enhancements(true, true)
+ .contrast_current(0x7f)
+ .phase_lengths(5, 15)
+ .precharge_voltage(0x1f)
+ .com_deselect_voltage(0x04),
+ )
+ .await
+ .unwrap();
+
+ display
+}
+
+#[embassy_executor::task]
+pub async fn task(mut display: Display<'static>) {
+ let mut fb =
+ Framebuffer::<Gray4, _, LittleEndian, 128, 64, { buffer_size::<Gray4>(128, 64) }>::new();
+
+ info!("display task starting");
+ t114_meshcore_example::display_task(&mut fb, async |fb| {
+ display
+ .region(PixelCoord(0, 0), PixelCoord(256, 64))
+ .unwrap()
+ .draw_packed(fb.data().iter().flat_map(|n| {
+ let upper = n & 0xf0;
+ let lower = n & 0x0f;
+ [upper | (upper >> 4), lower | (lower << 4)]
+ }))
+ .await
+ .unwrap();
+ })
+ .await;
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..5bcd1ac
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,220 @@
+#![no_std]
+#![no_main]
+#![allow(async_fn_in_trait)]
+
+extern crate alloc;
+
+use arrayvec::{ArrayString, ArrayVec};
+use defmt::{info, Debug2Format};
+
+use core::{convert::Infallible, fmt::Write, str::FromStr};
+use embassy_sync::{
+ blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, pubsub::PubSubChannel,
+};
+use embassy_time::Timer;
+use embedded_graphics::{
+ draw_target::DrawTarget,
+ geometry::OriginDimensions,
+ mono_font::{ascii::FONT_4X6, MonoTextStyle},
+ pixelcolor::Gray4,
+ prelude::*,
+ primitives::{Circle, PrimitiveStyle, Rectangle},
+ text::{Alignment, Text},
+};
+use embedded_hal::digital::StatefulOutputPin;
+use lora_phy::{mod_traits::RadioKind, DelayNs, LoRa};
+use meshcore::{
+ crypto::{PrivateKey, PublicKey, SharedSecret},
+ packet::Packet,
+ packet_content::PacketContent,
+ text::ClearText,
+};
+
+pub static MESSAGE_CH: PubSubChannel<CriticalSectionRawMutex, ClearText, 3, 2, 1> =
+ PubSubChannel::new();
+
+pub async fn led_task(
+ led_pin: &Mutex<CriticalSectionRawMutex, impl StatefulOutputPin<Error = Infallible>>,
+) {
+ let mut subscriber = MESSAGE_CH.subscriber().unwrap();
+
+ loop {
+ let _ = subscriber.next_message().await;
+ info!("led: got message");
+
+ let mut led = led_pin.lock().await;
+ for _ in 0..3 {
+ led.set_low().unwrap();
+ Timer::after_millis(50).await;
+ led.set_high().unwrap();
+ Timer::after_millis(50).await;
+ }
+ }
+}
+
+fn cstr(bytes: &[u8]) -> &str {
+ let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
+ core::str::from_utf8(&bytes[..end]).unwrap_or("")
+}
+
+pub async fn display_task<D>(display: &mut D, mut finish: impl AsyncFnMut(&mut D))
+where
+ D: DrawTarget<Color = Gray4> + OriginDimensions,
+ <D as DrawTarget>::Error: core::fmt::Debug,
+{
+ let radius: u16 = 10;
+ let mut pos = Point::new(30, 30);
+ let mut vel = Point::new(-2, 1);
+
+ let pp = Point::new(radius.into(), radius.into());
+ let bounds = Rectangle::with_corners(Point::new(0, 0), -(pp - display.size()));
+
+ let mut lines = ArrayVec::<ArrayString<128>, 8>::new();
+ let mut subscriber = MESSAGE_CH.subscriber().unwrap();
+
+ loop {
+ // update ball
+ pos += vel;
+
+ let bottom_right = bounds.bottom_right().unwrap();
+
+ if pos.x < bounds.top_left.x || pos.x > bottom_right.x {
+ vel.x = -vel.x;
+ }
+ if pos.y < bounds.top_left.y || pos.y > bottom_right.y {
+ vel.y = -vel.y;
+ }
+
+ pos.x = pos.x.clamp(bounds.top_left.x, bottom_right.x);
+ pos.y = pos.y.clamp(bounds.top_left.y, bottom_right.y);
+
+ // update chat
+ if let Some(msg) = subscriber.try_next_message_pure() {
+ info!("display: got message");
+ if lines.is_full() {
+ lines.remove(0);
+ }
+
+ let mut str: ArrayString<128> = ArrayString::new();
+ write!(&mut str, "{}: {}", cstr(&msg.sender), cstr(&msg.message)).unwrap();
+ lines.push(str);
+ }
+
+ display.clear(Gray4::BLACK).unwrap();
+ Circle::new(pos, radius.into())
+ .into_styled(PrimitiveStyle::with_stroke(Gray4::new(4), 3))
+ .draw(display)
+ .unwrap();
+
+ for (i, line) in lines.iter().enumerate() {
+ Text::with_alignment(
+ line,
+ Point::new(4, 4 + 8 * (i as i32)),
+ MonoTextStyle::new(&FONT_4X6, Gray4::WHITE),
+ Alignment::Left,
+ )
+ .draw(display)
+ .unwrap();
+ }
+
+ finish(display).await;
+
+ Timer::after_millis(10).await;
+ }
+}
+
+fn hex16(hexstr: &'static str) -> [u8; 16] {
+ let mut buf = [0u8; 16];
+ hex::decode_to_slice(hexstr, &mut buf).expect("invalid hex secret");
+ buf
+}
+
+pub async fn radio_task(lora: &mut LoRa<impl RadioKind, impl DelayNs>) {
+ use lora_phy::mod_params::*;
+
+ let keystore = meshcore::no_std_identity::StaticKeystoreInput {
+ identities:
+ [PrivateKey::from_str( "5884BEECFA94A15B596C8FE51832B857EB018B37F3E85671971977F5A6139374B9C3ECD2415258A9C233D4CA9332C7BF06EB269B786EA71813DE17DFFF480177", ) .unwrap()]
+ .into_iter()
+ .collect(),
+ contacts: [].into_iter().collect(),
+ groups: [
+ SharedSecret::new_from_group_secret(&hex16("8b3387e9c5cdea6ac9e5edbaa115cd72")),
+ SharedSecret::new_from_group_secret(&hex16("4c97bfe431abe20672259d9b6969dcec")),
+ ]
+ .into_iter()
+ .collect(),
+ }
+ .compile();
+
+ let pk: PublicKey = (&keystore.identities[0].private_key).into();
+ info!("public key: {:?}", Debug2Format(&pk));
+
+ let publisher = MESSAGE_CH.publisher().unwrap();
+ let mut buffer: [u8; 256] = [0; 256];
+
+ let mdltn_params = lora
+ .create_modulation_params(
+ SpreadingFactor::_8,
+ Bandwidth::_62KHz,
+ CodingRate::_4_8,
+ 869_618_000,
+ )
+ .unwrap();
+
+ let rx_params = lora
+ .create_rx_packet_params(16, false, buffer.len() as u8, true, false, &mdltn_params)
+ .unwrap();
+
+ lora.prepare_for_rx(RxMode::Continuous, &mdltn_params, &rx_params)
+ .await
+ .unwrap();
+
+ loop {
+ info!("waiting for message");
+ let res = lora.rx(&rx_params, &mut buffer).await.unwrap();
+ let message = &buffer[0..res.0.into()];
+
+ let packet = Packet::try_from(message).unwrap();
+ info!("packet {:?}", Debug2Format(&packet));
+
+ match packet.content {
+ PacketContent::Advert(msg) => {
+ info!("got advert from {}", &msg.name);
+ }
+ PacketContent::GroupText(mut msg) => {
+ msg.decrypt(&keystore).unwrap();
+ if let Some(text) = msg.cleartext {
+ unsafe {
+ info!(
+ "decrypted group message from {}: {}",
+ alloc::str::from_utf8_unchecked(&text.sender),
+ alloc::str::from_utf8_unchecked(&text.message),
+ );
+ }
+ publisher.publish(text).await;
+ } else {
+ info!("failed to decrypt group message");
+ }
+ }
+ PacketContent::Text(mut msg) => {
+ msg.decrypt(&keystore).unwrap();
+ if let Some(text) = msg.cleartext {
+ unsafe {
+ info!(
+ "decrypted p2p message from {}: {}",
+ alloc::str::from_utf8_unchecked(&text.sender),
+ alloc::str::from_utf8_unchecked(&text.message),
+ );
+ }
+ publisher.publish(text).await;
+ } else {
+ info!("failed to decrypt p2p message");
+ }
+ }
+ content => {
+ info!("content: {:?}", Debug2Format(&content));
+ }
+ }
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..94a8c9e
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,89 @@
+#![no_std]
+#![no_main]
+
+use alloc::boxed::Box;
+use embassy_executor::Spawner;
+use embassy_nrf::gpio::{Level, Output, OutputDrive};
+use embassy_nrf::{bind_interrupts, peripherals, spim};
+use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
+use embedded_alloc::LlffHeap as Heap;
+
+use {defmt_rtt as _, panic_probe as _};
+
+extern crate alloc;
+
+#[cfg(feature = "display")]
+mod display;
+
+mod radio;
+
+#[global_allocator]
+static HEAP: Heap = Heap::empty();
+
+bind_interrupts!(struct Irqs {
+ SPIM3 => spim::InterruptHandler<peripherals::SPI3>;
+ SPI2 => spim::InterruptHandler<peripherals::SPI2>;
+});
+
+type PinMutex<'a> = Mutex<CriticalSectionRawMutex, Output<'a>>;
+
+#[embassy_executor::task]
+async fn led_task(led_pin: &'static PinMutex<'static>) {
+ t114_meshcore_example::led_task(led_pin).await;
+}
+
+#[embassy_executor::main]
+async fn main(spawner: Spawner) {
+ {
+ use core::mem::MaybeUninit;
+ const HEAP_SIZE: usize = 64 * 1024;
+ static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
+ unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
+ }
+
+ let p = embassy_nrf::init(Default::default());
+
+ {
+ // led blink task
+ let led = Output::new(p.P1_03, Level::High, OutputDrive::Standard);
+ let pin: PinMutex = Mutex::new(led);
+ let pin_ref = Box::leak(Box::new(pin));
+ spawner.spawn(led_task(pin_ref)).unwrap();
+ }
+
+ #[cfg(feature = "display")]
+ {
+ let display = display::init(
+ p.SPI3,
+ Irqs,
+ display::Pins {
+ ven: p.P0_21.into(),
+ rst: p.P0_05.into(),
+ sck: p.P1_12.into(),
+ sdo: p.P1_14.into(),
+ cs: p.P1_15.into(),
+ dc: p.P1_13.into(),
+ },
+ )
+ .await;
+ spawner.spawn(display::task(display)).unwrap();
+ }
+
+ {
+ let radio = radio::init(
+ p.SPI2,
+ Irqs,
+ radio::Pins {
+ sck: p.P0_19.into(),
+ miso: p.P0_23.into(),
+ mosi: p.P0_22.into(),
+ cs: p.P0_24.into(),
+ rst: p.P0_25.into(),
+ dio1: p.P0_20.into(),
+ busy: p.P0_17.into(),
+ },
+ )
+ .await;
+ spawner.spawn(radio::task(radio)).unwrap();
+ }
+}
diff --git a/src/radio.rs b/src/radio.rs
new file mode 100644
index 0000000..db677ee
--- /dev/null
+++ b/src/radio.rs
@@ -0,0 +1,70 @@
+use embassy_nrf::gpio::{AnyPin, Input, Level, Output, OutputDrive, Pull};
+use embassy_nrf::{interrupt::typelevel::Binding, spim, Peri};
+use embassy_time::Delay;
+use embedded_hal_bus::spi::ExclusiveDevice;
+
+use lora_phy::iv::GenericSx126xInterfaceVariant;
+use lora_phy::sx126x::{Config, Sx1262, Sx126x, TcxoCtrlVoltage};
+use lora_phy::LoRa;
+
+type SPIDevice<'t> = ExclusiveDevice<spim::Spim<'t>, Output<'t>, Delay>;
+
+type RadioImpl<'t> = LoRa<
+ Sx126x<SPIDevice<'t>, GenericSx126xInterfaceVariant<Output<'t>, Input<'t>>, Sx1262>,
+ Delay,
+>;
+
+pub struct Radio(RadioImpl<'static>);
+
+pub struct Pins<'t> {
+ pub sck: Peri<'t, AnyPin>,
+ pub miso: Peri<'t, AnyPin>,
+ pub mosi: Peri<'t, AnyPin>,
+ pub cs: Peri<'t, AnyPin>,
+ pub rst: Peri<'t, AnyPin>,
+ pub dio1: Peri<'t, AnyPin>,
+ pub busy: Peri<'t, AnyPin>,
+}
+
+pub async fn init<T: spim::Instance>(
+ spi_instance: Peri<'static, T>,
+ irq_binding: impl Binding<T::Interrupt, spim::InterruptHandler<T>> + 'static,
+ pins: Pins<'static>,
+) -> Radio {
+ let out_rst = Output::new(pins.rst, Level::High, OutputDrive::Standard);
+ let out_cs = Output::new(pins.cs, Level::High, OutputDrive::Standard);
+
+ let in_dio1 = Input::new(pins.dio1, Pull::None);
+ let in_busy = Input::new(pins.busy, Pull::None);
+
+ let mut spi_config = spim::Config::default();
+ spi_config.frequency = spim::Frequency::M8;
+ let spi_bus = spim::Spim::new(
+ spi_instance,
+ irq_binding,
+ pins.sck,
+ pins.miso,
+ pins.mosi,
+ spi_config,
+ );
+ let spi_dev = ExclusiveDevice::new(spi_bus, out_cs, Delay).unwrap();
+
+ let iv = GenericSx126xInterfaceVariant::new(out_rst, in_dio1, in_busy, None, None).unwrap();
+ let config = Config {
+ chip: Sx1262,
+ tcxo_ctrl: Some(TcxoCtrlVoltage::Ctrl1V8),
+ use_dcdc: true,
+ rx_boost: true,
+ };
+
+ let lora: RadioImpl = LoRa::new(Sx126x::new(spi_dev, iv, config), false, Delay)
+ .await
+ .unwrap();
+
+ Radio(lora)
+}
+
+#[embassy_executor::task]
+pub async fn task(mut radio: Radio) {
+ t114_meshcore_example::radio_task(&mut radio.0).await;
+}