LoRa Page
Introduction
I know nothing of LoRa but like putting boxes together and getting them to talk so lets do it.
Equipment
So I thought I would start with two LilyGO Model T3 v1.6.1. I think they were $30 NZD
Environment
Operating System
Been Ubuntu for a while. Mainly because it still supports team and I guess I know the most about it. So for now Ubuntu 24.04
IDE Arduino 2.3.3
Well I dislike the Arduino software with a passion. I know it is free and someone has clearly put a lot of effort into making it and a lot of people use it but it is just horrible to use in a project for me. All my problem not yours.
I did try with both the snap and the apt installs for version 1 but could not get it to work. Then second approach resulted in having to clear the /tmp directory each time I built. So went for the zip file. My preference is apt or snap as you can put the software on and take it off and (hopefully) you are back to where you started
Always a challenge run Arduino for me but the way I did it this time was download it from https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
I unzipped it to and set the permission. No idea it this is a good thing or bad so don't do it on something you care about
cd ~/bin
unzip arduino-ide_nightly-20240630_Linux_64bit.zip
cd ~/bin/arduino-ide_nightly-20240630_Linux_64bit
sudo chown root:root chrome-sandbox
sudo chmod 4755 chrome-sandbox
# Put this in your bash profile ~/.bashrc or run here
export PATH=~/bin/arduino-ide_nightly-20240630_Linux_64bit:$PATH
First Attempt
Well the first thing to do is to take something that works and demonstrate it works on you environment/equipment for me I started with https://github.com/Xinyuan-LilyGo/TTGO-LoRa-Series
git clone https://github.com/Xinyuan-LilyGo/TTGO-LoRa-Series
cd LoRa
arduino-ide LoRa.ino
Just adding the line
#define LORA_SENDER 1
Make the code the sender. I compiled the two versions and all worked.
Let's get Rusty
Now we need to get rust
cargo install espup
espup install
rustup toolchain list
# List should show
stable-x86_64-unknown-linux-gnu (default)
nightly-x86_64-unknown-linux-gnu
esp (override)
Now lest make a hello-world
cargo install cargo-generate
# Now run the command and it will prompt for name of project and esp version
# For my Lora controller is Lora T3 v1.6.1 it is ESP32 pico D4 [https://docs.nordicsemi.com/bundle/ncs-latest/page/zephyr/boards/lilygo/ttgo_lora32/doc/index.html here]
cargo generate esp-rs/esp-template
# cd into the resulting project
cd helloworld
cargo run
On 10 April 2025 this all worked with no issues. I have left the other info below in case thing don't go so well next time.
So it did not run. This is because we used espup install and we need to set up the environment. We do this running
. $HOME/export-esp.sh
# And again
cargo run
# could not execute process `espflash flash
Still not working. This is the nature of using this environment. I quite like the chase of it and the gaining of knowledge when the solution is explained. So next
cargo install espflash
# warning: build failed, waiting for other jobs to finish...
# error: failed to compile `espflash v3.1.1`, intermediate artifacts can be found at `/tmp/cargo-install20pGtu`.
If you review previous errors you will see
# error: failed to run custom build command for `libudev-sys v0.1.4`
# And googling gives
# https://stackoverflow.com/questions/73965977/error-failed-to-run-custom-build-command-for-libudev-sys-v0-1-4
sudo apt-get install -y libudev-dev
Now we have another go and finally it is flashing to the ESP32
And running the hello world example. For me getting the various elements running together gives me joy. Probably not that special for some people.
Time for some Pictures
Well we now have a the basic working. Next we need a picture. Once this is working Debugging will be quite easy or so he said
cargo add ssd1306
cargo add embedded_graphics
I run the code again after installing anything. Paranoid or what. The samples kind of worked but here is the code which work on this specific day and hour.
fn main() -> ! {
let peripherals = Peripherals::take();
let system = SystemControl::new(peripherals.SYSTEM);
let clocks = ClockControl::max(system.clock_control).freeze();
let delay = Delay::new(&clocks);
esp_println::logger::init_logger_from_env();
let io = Io::new(peripherals.GPIO, peripherals.IO_MUX);
// Create a new peripheral object with the described wiring
// and standard I2C clock speed
let i2c = I2C::new(
peripherals.I2C0,
io.pins.gpio21,
io.pins.gpio22,
100.kHz(),
&clocks,
None,
);
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display.init().unwrap();
let text_style = MonoTextStyleBuilder::new()
.font(&FONT_6X10)
.text_color(BinaryColor::On)
.build();
Text::with_baseline("Hello world!", Point::zero(), text_style, Baseline::Top)
.draw(&mut display)
.unwrap();
Text::with_baseline("Hello Rust!", Point::new(0, 16), text_style, Baseline::Top)
.draw(&mut display)
.unwrap();
display.flush().unwrap();
loop {
log::info!("Hello world!");
delay.delay(500.millis());
}
}
There is always a trick it seems to eat your weekend away. For this piece of code it was I needed to enable the embedded-hal-02 feature flag in the esp-hal
[dependencies]
embedded-graphics = "0.8.1"
esp-backtrace = { version = "0.13.0", features = [
"esp32",
"exception-handler",
"panic-handler",
"println",
] }
esp-hal = { version = "0.19.0", features = ["esp32", "embedded-hal-02"] }
esp-println = { version = "0.10.0", features = ["esp32", "log"] }
log = { version = "0.4.21" }
ssd1306 = "0.8.4"
Time to Get Connected
Let see if we can connect to the Wifi before we go for LoRa. This probably is more a case of understanding Rust and WiFi libraries so lets do it.
Configure Wifi Peripheral
We need to add and configure the wifi crate
esp-wifi = { version = "0.7.1", features = ["esp32", "wifi-default"] }
heapless = "0.8.0"
The biggest part of getting this to work was dealing with the timer. The Xtensa build uses a different timer, as your would expect, than RISC-V. Searching the Web, as ever solved this.
let timer = esp_hal::timer::PeriodicTimer::new(
esp_hal::timer::timg::TimerGroup::new(peripherals.TIMG1, &clocks, None)
.timer0
.into(),
);
let init = initialize(
EspWifiInitFor::Wifi,
timer,
Rng::new(peripherals.RNG),
peripherals.RADIO_CLK,
&clocks,
)
.unwrap();
// Configure Wifi
let wifi = peripherals.WIFI;
let mut socket_set_entries: [SocketStorage; 3] = Default::default();
let (iface, device, mut controller, sockets) =
create_network_interface(&init, wifi, WifiStaDevice, &mut socket_set_entries).unwrap();
Configure Authentication
This also was a little bit harder than expected. So set the password and not have it appear in github I used
const SSID: &str = env!("SSID");
const PASSWORD: &str = env!("PASSWORD");
And for the SSID this worked well be doing
export SSID=YourKidding
But for the password not so well because of the special characters.
export SSID=YourKidding!!!
The fix was easy, just set the values outside of VS Code. Anyway here is the code for Authentication
// Configure Authentification Configuration
let mut auth_method = AuthMethod::WPA2Personal;
if PASSWORD.is_empty() {
auth_method = AuthMethod::None;
}
let client_config = Configuration::Client(ClientConfiguration {
ssid: SSID.try_into().unwrap(),
password: PASSWORD.try_into().unwrap(),
auth_method,
..Default::default()
});
let res = controller.set_configuration(&client_config);
log::info!("Wi-Fi set_configuration returned {:?}", res);
Connect to the Controller
No drama doing this in no_std
controller.start().unwrap();
log::info!("Is wifi started: {:?}", controller.is_started());
log::info!("Start Wifi Scan");
let res: Result<(heapless::Vec<AccessPointInfo, 10>, usize), WifiError> = controller.scan_n();
if let Ok((res, _count)) = res {
for ap in res {
log::info!("{:?}", ap);
}
}
log::info!("{:?}", controller.get_capabilities());
log::info!("Wi-Fi connect: {:?}", controller.connect());
// Wait to get connected
log::info!("Wait to get connected");
loop {
let res = controller.is_connected();
match res {
Ok(connected) => {
if connected {
break;
}
}
Err(err) => {
log::info!("{:?}", err);
loop {}
}
}
}
log::info!("{:?}", controller.is_connected());
Get an IP address
Again no issue with this part
// Wait for getting an ip address
let wifi_stack = WifiStack::new(iface, device, sockets, current_millis);
log::info!("Wait to get an ip address");
loop {
wifi_stack.work();
if wifi_stack.is_iface_up() {
log::info!("got ip {:?}", wifi_stack.get_ip_info());
break;
}
}
Revisiting in 2025
This is now on my git server with examples for
- Connecting to a gateway and serving a page esp32-static-ip
- Creating an AP and serving a page esp-emb-wifi
SD Card Reader
This is the first time dabbling with a card reader so maybe my page will not be updated for a while - hang on. Back now. This was relatively easy. Just create an SPI device and use the MMC library.
Create the SPI Device
This is always a trial the first time through. The trick to this is understanding that to make a bus you need the embedded-hal-bus
embedded-hal-bus = "0.2.0"
This allows you to pass a bus to the SdCard
// Set up the SPI bus
let sclk = io.pins.gpio14;
let miso = io.pins.gpio2;
let mosi = io.pins.gpio15;
let cs = Output::new(io.pins.gpio13, Level::Low);
log::info!("Exclusive access to the SPI bus selected");
let spi_driver =
Spi::<SPI2, FullDuplexMode>::new(peripherals.SPI2, 1_000u32.kHz(), SpiMode::Mode0, &clocks)
.with_sck(sclk)
.with_mosi(mosi)
.with_miso(miso);
let time_source = FakeTimesource();
let sd_spi_device = ExclusiveDevice::new(spi_driver, cs, delay).unwrap();
read_mmc(sd_spi_device, delay, time_source).unwrap();
Reading the Card Reader
First time through for me but this too was straight forward. Place a file in the root directory formatted as fat. I fsck /dev/sdb1 to make sure all good. But it worked with no issue. Add the library to the toml
embedded-sdmmc = "0.8.0"
And use the example from the site
fn read_mmc<T>(
spi: ExclusiveDevice<Spi<SPI2, FullDuplexMode>, Output<GpioPin<13>>, Delay>,
delay: Delay,
ts: T,
) -> Result<(), Error<SdCardError>>
where
T: TimeSource,
{
let sdcard = SdCard::new(spi, delay);
// let sdcard = SdCard::new(spi, delay);
log::info!("Card size is {} bytes", sdcard.num_bytes()?);
let mut volume_mgr = VolumeManager::new(sdcard, ts);
let mut volume0 = volume_mgr.open_volume(VolumeIdx(0))?;
log::info!("Volume 0: {:?}", volume0);
let mut root_dir = volume0.open_root_dir()?;
let mut my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?;
while !my_file.is_eof() {
let mut buffer = [0u8; 32];
let num_read = my_file.read(&mut buffer)?;
for b in &buffer[0..num_read] {
log::info!("{}", *b as char);
}
}
Ok(())
}