Project overview

Assembly + Tests

3D-Model

Top Part

The main idea is to let the sender sit on a desk. So I thought of a slope on the top, where the display is placed. The buttons, which will signal the receiver will be mounted on the slope as well, below the display. This can be seen on figure 1.

On the side there are 2 small holes which are meant for the yellow LED, which signals "Charging"-state and a red LED to signal a low battery state as discussed. On top of them, there is a bigger rectangular hole, where the USB-C port will be mounted in. On the other side is a hole to be able to mount the antenna onto it.

3d-printed-housing-top
Figure 1: 3D model of the top part of the housing with "mouse ears" for printing stability.

Bottom Part

The bottom part will be mounted to receptables through 4 holes, which are mounted into the corners of the top part. The rest is pretty simple, there is a pocket for the battery holder and four elevation plateaus to give the bottom of the first board some space. The last could also be done through some screwable spacers, but with that I safed 4 of them. Figure 3 shows the printed bottom part with the battery holder and boards screwed onto it.

3d-model-housing-top
Figure 2: 3D model of the bottom part of the housing with "mouse ears"
for printing stability, a pocket for the battery holder and a plateau for the boards.
3d-printed-housing-top
Figure 3: 3D printed bottom part of the housing with battery holder and boards mounted onto it.

This is how the sender looks. I know the print is not that pretty, but that´s another topic.

Figure 4: Assembled sender from top, right,left, bottom

Changes in code for efficiency and stability

Before I show you the tests I want present the whole code for the sender and receiver. One reason is because I want to present the whole finished code alltogether. The other reason is because I have changed some things up to make it cleaner and optimized. I will explain in the following, what I mean by that.

Encryption

I am not an expert on encryption, but I have done a little bit of research on how to encrypt messages safely. For example, since I always send the same strings, with the previous encryption, the encrypted message would always be the same. This would lead to hackers eventually figuring the key out over time.

Instead you could use a so called nonce counter which is a simple number, that counts up with every message. With that even the same encrypted message will always differ because before encryption the nonce number gets added to the message. Another thing I changed, is the message itself. It is a waste of payload to send strings like "LED ON" or "LED OFF" to signal 2 different states. Might as well send 1 or 0, which is only one bit. Before the payload was 16 bytes but unsafe, now the payload consists of 17 bytes but safely encrypted with a tag:

[ nonce (8) ][ cipher (1) ][ tag (8) ]

The 17 bytes consist of 8 byte for the nonce counter, 1 byte ciphered message and 8 byte for the tag, which validates the message by comparing to the key. For sending and receiving, there are different keys and nonce counters. Both keys have to be known by both devices. Even better would be to use some type of crypto chip (hardware)...

EU Rules for LoRa

In EU(where I live) there are 2 simple rules for 433 MHz, because this frequency is widely used and free to use. First the sending power mustn´t exceed 10dbm, secondly the sending duration must not exceed 6s/per minute or generally max 10% duty cycle. In addition, immediately after sending ONE time the sender now waits a second for a response, whereas in the previous code it sent 18 times in a loop and then waited for a response. That makes it a lot more effizient and clean because in the previous setup it still sends even after reaching the receiver after one iteration. There are other factors to strengthen the signal and the probability of the signal being received like the spreading factor, bandwith and preamble length. This is the setup I have used:

Payload (PL)     = 17 byte
SF               = 8
Bandwidth (BW)   = 125 kHz
Coding Rate      = 4/5
Preamble         = 64 symbols
CRC              = an

1) symbol length

Tsym = 2^SF / BW
     = 256 / 125000
     = 2,048 ms

2) preamble length

Tpreamble = (64 + 4,25) · 2,048 ms
          ≈ 139,8 ms

3) payload-symbols

payloadSymbNb = 33 symbols

4) Payload-Time

Tpayload = 33 · 2,048 ms
         ≈ 67,6 ms

5) full length

ToA = 139,8 ms + 67,6 ms
    ≈ 207 ms

This means that one message with my configuration takes around 207 ms to send, where 140 ms of that time is only the preamble. The preamble, which I have made so long because the receiver wakes up only once every second, signals an incoming package. However the wake-up time of the receiver was still too short, so I configured it to stay wake for 300 ms. Otherwise the probability to receive the signal would be too low. Ideally the LoRa device would have a low power wake-on-signal mode, which would trigger the pico to wake up, but the SX1278 is not built for that.

Sender Code

#include <LoRa.h>       
#include <ChaChaPoly.h>
#include "hardware/xosc.h" 
#include "hardware/pll.h"

#include "hardware/regs/addressmap.h"
#include "hardware/structs/syscfg.h"
#include "hardware/regs/syscfg.h"   

#include "epd2in9_V2.h"
#include "epdpaint.h"



#define WAKEUP_PIN_ON 28 //GP10
#define WAKEUP_PIN_OFF 27 //GP8


// Pins for sx1278 lora module to rp2040
#define SS 17    // Chip Select Pin
#define RST 21   // Reset Pin
#define DIO0 22  // IRQ Pin



#define COLORED     0
#define UNCOLORED   1

//Has to be like this on pi pico because if 1024 used like in example
// heap errors/overflows occur
unsigned char image[296 * 128 / 8];
Paint paint(image, 0, 0);    // width should be the multiple of 8 
Epd epd;

bool currentState = false;

//EPD Pins 6,2,5,1
//25 is LED
//uint8_t myPins[] = {0, 7, 9, 11, 12, 13, 20, 21,22,  26, 27, 28};
uint8_t myPins[] = {0,3,4, 7,8, 9,10, 11, 12, 13,14,15, 20, 21, 22, 26};
uint8_t arraySize = sizeof(myPins) / sizeof(myPins[0]); // Array length


ChaChaPoly aead;



// 32-Byte secret keys for sender and receiver
uint8_t key[32] = {
  0x10,0x11,0x12,0x13, 0x20,0x21,0x22,0x23,
  0x30,0x31,0x32,0x33, 0x40,0x41,0x42,0x43,
  0x50,0x51,0x52,0x53, 0x60,0x61,0x62,0x63,
  0x70,0x71,0x72,0x73, 0x80,0x81,0x82,0x83
};

uint8_t key_rx[32] = {
    0xAA,0xBB,0xCC,0xDD,0xEE,0xFF,0x01,0x02,
    0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,
    0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,
    0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A
};


uint64_t nonceCounter = 1;

static uint8_t wake_up_pin = 0;


// Interrupt-Callback, saves the Wake-Up-Pin
void gpio_callback(uint gpio, uint32_t events) {
    wake_up_pin = gpio; 
}



// Deactivate both pll´s of rp2040
void disable_all_plls() {
  pll_deinit(pll_usb);
  pll_deinit(pll_sys);
  
}

void rosc_disable(void) {
    uint32_t tmp = rosc_hw->ctrl;
    tmp &= (~ROSC_CTRL_ENABLE_BITS);
    tmp |= (ROSC_CTRL_ENABLE_VALUE_DISABLE << ROSC_CTRL_ENABLE_LSB);

    rosc_hw->ctrl = tmp;
    // Wait for it to stabilize
    while(rosc_hw->status & ROSC_STATUS_STABLE_BITS);
}


void switchAllClocksToXOSC() {
  
  clock_configure(clk_ref, CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC, 0, 12 * MHZ, 12 * MHZ);
  //set clk_sys to get its clock from reference clock which gets its clock from xosc
  clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLK_REF, 0, 12 * MHZ, 12 * MHZ);
  
  //stop all other clocks
  clock_stop(clk_peri);
  clock_stop(clk_usb);
  clock_stop(clk_adc);
  clock_stop(clk_rtc);

  clock_stop(clk_gpout0);
  clock_stop(clk_gpout1);
  clock_stop(clk_gpout2);
  clock_stop(clk_gpout3);

}

void enter_dormant_mode(void) {

  

  //The crystal initialized
  xosc_init();
  
  switchAllClocksToXOSC();

  rosc_disable();
  
  disable_all_plls();

  uint save = scb_hw->scr;


  xosc_dormant();
 
  
  // Enable deep sleep at the proc
  scb_hw->scr = save | M0PLUS_SCR_SLEEPDEEP_BITS;
  
  // the RP2040 can go to sleep with mit WFI (Wait For Interrupt)
  __wfi();  // wait for interrupt - the processor goes into standy/sleep


  //Reconfigure the registers to the original
  scb_hw->scr = save;

}




void restart_all_plls() {
  
  // Configuration of the system PLL with 125 MHz as target
  pll_init(pll_sys, 1, 1500 * MHZ, 6, 2);

  //reinitializing the USB-PLL to 48 MHz
  pll_init(pll_usb, 1, 480 * MHZ, 5, 2);

}



void reconfigureAllClocksAfterWakeUp() {  
 
  //sys will get its clock from sys pll which is configured to 125 MHz
  clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX, CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, 125 * MHZ, 125 * MHZ);
  //sys pll -> clk_peri 125 MHz
  clock_configure(clk_peri, 0, CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, 125 * MHZ, 125 * MHZ);
  //usb pll -> clk_usb 48 MHz
  clock_configure(clk_usb, 0,CLOCKS_CLK_USB_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 48 * MHZ, 48 * MHZ);
  //usb pll -> clk_adc since it also needs 48 MHz
  clock_configure(clk_adc,0, CLOCKS_CLK_ADC_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB,  48 * MHZ, 48 * MHZ);
  //xosc -> clk_rtc 46875 Hz
  clock_configure(clk_rtc, 0, CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_XOSC_CLKSRC,  12 * MHZ, 46875);
}


void wake_up_from_dormant() {
  
  //ENABLE again
  rosc_hw->ctrl = (rosc_hw->ctrl & 0xFF000FFF) | (ROSC_CTRL_ENABLE_VALUE_ENABLE << 12);
  while(rosc_hw->status & ROSC_STATUS_STABLE_BITS);

  //Activate XOSC again
  //xosc_init();

  restart_all_plls();

  reconfigureAllClocksAfterWakeUp(); 

  
}

// Configure GPIO-Interrupt for DORMANT Wake-Up
void  sleep_goto_dormant_until_pin(uint gpio_pin1, uint gpio_pin2, bool edge, bool high) {



  bool low = !high;
  bool level = !edge;

  // Configure the appropriate IRQ at IO bank 0 
  assert(gpio_pin1 < NUM_BANK0_GPIOS);
  assert(gpio_pin2 < NUM_BANK0_GPIOS);

  uint32_t event = 0; 

  if (level && low) event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_LOW_BITS; 
  if (level && high) event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_HIGH_BITS; 
  if (edge && high) event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_HIGH_BITS;
  if (edge && low) event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_LOW_BITS;

  // wake up on high for both pins
  gpio_set_dormant_irq_enabled(gpio_pin1, event, true);
  gpio_set_dormant_irq_enabled(gpio_pin2, event, true);
  
  enter_dormant_mode(); // Execution stops here until woken up 

  wake_up_from_dormant();


  // Clear the irq so we can go back to dormant mode again
  gpio_acknowledge_irq(gpio_pin1, event);
  gpio_acknowledge_irq(gpio_pin2, event);
}



uint8_t encrypted[16];
uint8_t decryptedResponse[16];
uint8_t sendTextOn[16] = "LED ON";
uint8_t sendTextOff[16] = "LED OFF";





void pullDownInitGPIO(uint8_t pins[], uint8_t arrayLength) {
  uint8_t i = 0;

  while (i < arrayLength) {
    gpio_init(pins[i]); 
    gpio_pull_down(pins[i]); // Interner Pull-down aktivieren
    i++; // Zähler erhöhen, sonst Endlosschleife!
  }
}





void showPresenceOnDisplay(){

  
  epd.Init();
  delay(10);
  epd.Reset();
 
  epd.ClearFrameMemory(0xFF);   // bit set = white, bit reset = black
  epd.DisplayFrame();

  paint.SetRotate(ROTATE_90);
  paint.SetWidth(128); //128 Width ist die höhe wenn horizontal gehalten
  paint.SetHeight(200);//296
  

  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,10,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 0, paint.GetWidth(), paint.GetHeight());
   
  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,20,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 20, paint.GetWidth(), paint.GetHeight());
  
   
  paint.Clear(COLORED);
  paint.DrawStringAt(30, 50, "ANWESEND.", &Font24, UNCOLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 48, paint.GetWidth(), paint.GetHeight());
  
   
  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,20,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 256, paint.GetWidth(), paint.GetHeight());
  
   
  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,10,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0,286, paint.GetWidth(), paint.GetHeight());


  epd.DisplayFrame();

  epd.Sleep();


  currentState = true;
}

void showAbscenceOnDisplay(){
  
  epd.Init();
  delay(10);
  epd.Reset();


  epd.ClearFrameMemory(0xFF);   // bit set = white, bit reset = black
  epd.DisplayFrame();
  
  paint.SetRotate(ROTATE_90);
  paint.SetWidth(128); //128 Width ist die höhe wenn horizontal gehalten
  paint.SetHeight(200);//296
  
  // For simplicity, the arguments are explicit numerical coordinates 
  paint.Clear(COLORED);
  
  paint.DrawStringAt(15, 40, "Bin gerade", &Font24, UNCOLORED);
  paint.DrawStringAt(15, 70, "nicht da.", &Font24, UNCOLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 0, paint.GetWidth(), paint.GetHeight());

  paint.Clear(UNCOLORED); //Hier passiert das problem
  paint.DrawFilledRectangle(0,0,20,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 210, paint.GetWidth(), paint.GetHeight()); //Offset y = 210 x=0 absolute
  
  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,15,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 240, paint.GetWidth(), paint.GetHeight());

  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,10,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 265, paint.GetWidth(), paint.GetHeight());
 
  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,5,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 285, paint.GetWidth(), paint.GetHeight());

  epd.DisplayFrame();


  epd.Sleep();

  currentState = false;
}


void setup() {


  //on board led for debugging
  pinMode(LED_BUILTIN, OUTPUT); 

  //Delay for flashing so it doesnt go dormant during flashing
  delay(5000);

  pullDownInitGPIO(myPins, arraySize); 
  

  // GPIO 10 as input for the wake up
  gpio_init(WAKEUP_PIN_ON);
  gpio_set_dir(WAKEUP_PIN_ON, GPIO_IN);
  gpio_pull_down(WAKEUP_PIN_ON); // Internal pulldown for the pin

  gpio_init(WAKEUP_PIN_OFF);
  gpio_set_dir(WAKEUP_PIN_OFF, GPIO_IN);
  gpio_pull_down(WAKEUP_PIN_OFF); // Internal pulldown for the pin


  // Interrupt für beide Pins aktivieren
  gpio_set_irq_enabled_with_callback(WAKEUP_PIN_ON, GPIO_IRQ_EDGE_RISE, true, &gpio_callback);
  gpio_set_irq_enabled_with_callback(WAKEUP_PIN_OFF, GPIO_IRQ_EDGE_RISE, true, &gpio_callback);
  
  
  // Set the LoRa-Pins
  LoRa.setPins(SS, RST, DIO0);
  
  // initialize LoRa with 433 MHz (frequency of SX1278)
  if (!LoRa.begin(433E6)) {
    while (1);
  }

  LoRa.setTxPower(10);//10dbm for 10% duty cycle (6s) - LoRa Law!
  LoRa.setSpreadingFactor(8);
  LoRa.setSignalBandwidth(125000);
  LoRa.setCodingRate4(5);
  LoRa.setPreambleLength(64);
  LoRa.enableCrc(); 


  //Serial.println("before epd init");
  if (epd.Init() != 0) return;
  epd.Sleep();
  

} 





uint8_t validateMessage(uint8_t msg[17]){
  bool success = false; 
  uint8_t decrypted;

  aead.setKey(key_rx, sizeof(key_rx));
  aead.setIV(&msg[0],8);

  // 7 Decrypt ===
  aead.decrypt(&decrypted, &msg[8], 1);

  // 8 check TAG ===
  bool valid = aead.checkTag(&msg[9], 8);

  //valid message either 1 or 0
  if (valid && (decrypted == 0 || decrypted == 1)) success = true;


  return success;
}



bool sendEncryptedPacket(bool state) {

  uint8_t data[17] = {0};
  uint8_t receivedData[17] = {0};

  // 1 byte message
  uint8_t plain[1];
  plain[0] = state ? 1 : 0;

  // 8 byte nonce
  memcpy(&data[0], &nonceCounter, 8);
  nonceCounter++;

  //encryption key
  aead.setKey(key, sizeof(key));
  // AEAD initialize with nonce
  aead.setIV(&data[0], 8);

  // Cipher in data[8]
  aead.encrypt(&data[8], plain, 1);

  // Tag in data[9]
  aead.computeTag(&data[9], 8);

  
  uint8_t i = 0;
  bool acknowledged = false;
  unsigned long startTime;
  
  while(i<3 && !acknowledged){

    LoRa.beginPacket();
    LoRa.write(data, sizeof(data));  // 17 Bytes
    LoRa.endPacket();


    // Wait for ACK from receiver for 1000ms
    LoRa.receive(); // switch to receiver

    
    startTime = millis();
    
    while (millis() - startTime < 1000 && !acknowledged) {    
    
      delay(20); // short pause between checks

      int packetSize = LoRa.parsePacket();
      if (packetSize == 17) {
        while (LoRa.available()) {

          int n = LoRa.readBytes(receivedData, 17);
          if (n == 17) acknowledged = validateMessage(receivedData);
        }
      }
      
      
    }

    i++;
    if(!acknowledged)delay(300);
  }
  
  return acknowledged;
}


void loop() {

  bool wasAcknowledged = false;
  
  
  LoRa.sleep();     

  uart_default_tx_wait_blocking();
  delay(10);


  sleep_goto_dormant_until_pin(WAKEUP_PIN_ON, WAKEUP_PIN_OFF, true, true);


  if (wake_up_pin == WAKEUP_PIN_ON && !currentState) {
    digitalWrite(LED_BUILTIN, HIGH);
    wasAcknowledged = sendEncryptedPacket(true);
    digitalWrite(LED_BUILTIN, LOW);
    
    if(wasAcknowledged) showPresenceOnDisplay();
  }
  else if (wake_up_pin == WAKEUP_PIN_OFF && currentState) {
    digitalWrite(LED_BUILTIN, HIGH);
    wasAcknowledged = sendEncryptedPacket(false);
    digitalWrite(LED_BUILTIN, LOW);
    
    if(wasAcknowledged) showAbscenceOnDisplay();
  }

  wake_up_pin = 0; 


}

  

Receiver code

#include <LoRa.h>       
#include <ChaChaPoly.h>
#include "hardware/xosc.h"
#include "hardware/pll.h"
#include "hardware/rtc.h"




// Pin-Configuration for LoRa
#define SS    17   // Chip Select (GPIO17)
#define RST   21  // Reset (GPIO14)
#define DIO0  22  // Interrupt/IRQ (GPIO15)


#define COLORED     0
#define UNCOLORED   1

#include "epd2in9_V2.h"
#include "epdpaint.h"

ChaChaPoly aead;




uint64_t sendingNonceCounter = 1;

// Key for Sender -> Receiver
uint8_t key[32] = {
  0x10,0x11,0x12,0x13, 0x20,0x21,0x22,0x23,
  0x30,0x31,0x32,0x33, 0x40,0x41,0x42,0x43,
  0x50,0x51,0x52,0x53, 0x60,0x61,0x62,0x63,
  0x70,0x71,0x72,0x73, 0x80,0x81,0x82,0x83
};

// Key for Receiver -> Sender
uint8_t key_rx[32] = {
    0xAA,0xBB,0xCC,0xDD,0xEE,0xFF,0x01,0x02,
    0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,
    0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,
    0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A
};


//Has to be like this on pi pico because if 1024 used like in example
// heap errors/overflows occur
unsigned char image[296 * 128 / 8];
Paint paint(image, 0, 0);    // width should be the multiple of 8 
Epd epd;




//wrong time but doesn´t matter, we  just need a reference
datetime_t t = {
      .year  = 2025,
      .month = 7,
      .day   = 2,
      .dotw  = 3,  // 0 = sunday, 1 = monday, ..., 6 = saturday
      .hour  = 0,
      .min   = 0,
      .sec   = 0
  };


// Months with their maximum days (non-leap year)
const int days_in_month[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

 


// Deactivating the system and USB PLL
void disable_all_plls() {
  // Deactivates the USB PLL (clk_usb)
  pll_deinit(pll_usb);
  // Deactivates the system PLL (clk_sys)
  pll_deinit(pll_sys);
  
}

void rosc_disable(void) {
    uint32_t tmp = rosc_hw->ctrl;
    tmp &= (~ROSC_CTRL_ENABLE_BITS);
    tmp |= (ROSC_CTRL_ENABLE_VALUE_DISABLE << ROSC_CTRL_ENABLE_LSB);

    rosc_hw->ctrl = tmp;
    while(rosc_hw->status & ROSC_STATUS_STABLE_BITS);
}




void showPresenceOnDisplay(){

  epd.Init();
  epd.Reset();
  delay(10);
  
  epd.ClearFrameMemory(0xFF);   // bit set = white, bit reset = black
  epd.DisplayFrame();

  paint.SetRotate(ROTATE_90);
  paint.SetWidth(128); //128 Width ist die höhe wenn horizontal gehalten
  paint.SetHeight(200);//296
  

  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,10,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 0, paint.GetWidth(), paint.GetHeight());
   
  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,20,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 20, paint.GetWidth(), paint.GetHeight());
  
   
  paint.Clear(COLORED);
  paint.DrawStringAt(30, 50, "ANWESEND.", &Font24, UNCOLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 48, paint.GetWidth(), paint.GetHeight());
  
   
  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,20,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 256, paint.GetWidth(), paint.GetHeight());
  
   
  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,10,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0,286, paint.GetWidth(), paint.GetHeight());


  epd.DisplayFrame();


  epd.Sleep();
}

void showAbscenceOnDisplay(){

  epd.Init();
  epd.Reset();
  delay(10);

  epd.ClearFrameMemory(0xFF);   // bit set = white, bit reset = black
  epd.DisplayFrame();
  
  paint.SetRotate(ROTATE_90);
  paint.SetWidth(128); //128 Width ist die höhe wenn horizontal gehalten
  paint.SetHeight(200);//296
  
  // For simplicity, the arguments are explicit numerical coordinates 
  paint.Clear(COLORED);
  
  paint.DrawStringAt(15, 40, "Bin gerade", &Font24, UNCOLORED);
  paint.DrawStringAt(15, 70, "nicht da.", &Font24, UNCOLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 0, paint.GetWidth(), paint.GetHeight());

  paint.Clear(UNCOLORED); //Hier passiert das problem
  paint.DrawFilledRectangle(0,0,20,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 210, paint.GetWidth(), paint.GetHeight()); //Offset y = 210 x=0 absolute
  
  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,15,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 240, paint.GetWidth(), paint.GetHeight());

  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,10,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 265, paint.GetWidth(), paint.GetHeight());
 
  paint.Clear(UNCOLORED);
  paint.DrawFilledRectangle(0,0,5,128,COLORED);
  epd.SetFrameMemory(paint.GetImage(), 0, 285, paint.GetWidth(), paint.GetHeight());

  epd.DisplayFrame();


  epd.Sleep();

}



void restart_all_plls() {
  
  pll_init(pll_sys, 1, 1500 * MHZ, 6, 2);
  pll_init(pll_usb, 1, 480 * MHZ, 5, 2);

}


void switchAllClocksToXOSC() {

  clock_configure(clk_ref, CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC, 0, 12 * MHZ, 12 * MHZ);
  //set sys clock to get its clock from reference clock which initially gets its clock from xosc
  clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLK_REF, 0, 12 * MHZ, 12 * MHZ);
  //stop all other important clocks
  //xosc -> clk_rtc 46875 Hz
  clock_configure(clk_rtc, 0, CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_XOSC_CLKSRC,  12 * MHZ, 46875);


  clock_stop(clk_peri);
  clock_stop(clk_usb);
  clock_stop(clk_adc);

}

void reconfigureAllClocksAfterWakeUp() {  
 
  //sys will get its clock from sys pll which is configured to 125 MHz
  clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX, CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS, 125 * MHZ, 125 * MHZ);
  //sys pll -> clk_peri 125 MHz
  clock_configure(clk_peri, 0, CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, 125 * MHZ, 125 * MHZ);
  //usb pll -> clk_usb 48 MHz
  clock_configure(clk_usb, 0,CLOCKS_CLK_USB_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 48 * MHZ, 48 * MHZ);
  //usb pll -> clk_adc since it also needs 48 MHz
  clock_configure(clk_adc,0, CLOCKS_CLK_ADC_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB,  48 * MHZ, 48 * MHZ);


  
}


void wake_up_from_dormant() {
  
  //I enable it just to make sure. Normally it shouldnt be needed, but then 
  rosc_hw->ctrl = (rosc_hw->ctrl & 0xFF000FFF) | (ROSC_CTRL_ENABLE_VALUE_ENABLE << 12);
  while(rosc_hw->status & ROSC_STATUS_STABLE_BITS);

  //XOSC was active the whole time

  restart_all_plls();

  reconfigureAllClocksAfterWakeUp();  
}


// checks if leap year
bool is_leap_year(int year) {
    return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}


// Function for increasing the current time by X seconds and setting an alarm
void set_alarm_in_seconds(int seconds) {

    
    // Get current time from the RTC
     uint8_t timeout = 10;
    while (!rtc_get_datetime(&t)) {
      delay(100);
      timeout--;
      
      if(timeout <= 0){
        while(true){
          /* setLEDs(false, true, true);
          delay(4000);
          setLEDs(false, false, true); */
        }
      }
    }


    
    // Add seconds to the current time
    t.sec += seconds;

    // Adjust seconds, minutes and hours
    while (t.sec >= 60) {
        t.sec -= 60;
        t.min++;
    }
    while (t.min >= 60) {
        t.min -= 60;
        t.hour++;
    }
    while (t.hour >= 24) {
        t.hour -= 24;
        t.day++;
        
        t.dotw++;
        if(t.dotw >= 7) t.dotw -= 7;
    }

    // Check days, months and years
    while (true) {
        int days_in_current_month = days_in_month[t.month - 1];

        // Consider February in leap years
        if (t.month == 2 && is_leap_year(t.year)) {
            days_in_current_month = 29;
        }

        if (t.day <= days_in_current_month) {
            break; // Gültiger Tag, Schleife beenden
        }

        // Increase month, adjust day
        t.day -= days_in_current_month;
        t.month++;

        // If month > 12, then start new year
        if (t.month > 12) {
            t.month = 1;
            t.year++;
        }
    }

    // Set RTC alarm
    rtc_set_alarm(&t, &wake_up_from_dormant);
}


void enter_dormant_mode_with_timer(uint time_in_seconds) {

  

  if(!rtc_running()) {
    rtc_init(); 
    delay(10);
  }

  switchAllClocksToXOSC();

  rosc_disable();


  disable_all_plls();

  
  uint en0_orig = clocks_hw->sleep_en0;
  uint en1_orig = clocks_hw->sleep_en1;

  uint save = scb_hw->scr;

  // Turn off all clocks when in sleep mode except for RTC 
  clocks_hw->sleep_en0 = CLOCKS_SLEEP_EN0_CLK_RTC_RTC_BITS; 
  clocks_hw->sleep_en1 = 0x0;

  set_alarm_in_seconds(time_in_seconds);
  
  // Enable deep sleep at the proc
  scb_hw->scr = save | M0PLUS_SCR_SLEEPDEEP_BITS;


  // the RP2040 can go to sleep with mit WFI (Wait For Interrupt)
  __wfi();  // wait for interrupt - the processor goes into standy/sleep

  //Reconfigure the registers to the original
  scb_hw->scr = save;
  clocks_hw->sleep_en0 = en0_orig;
  clocks_hw->sleep_en1 = en1_orig;

}




void incrementNonce(uint8_t nonce[8]) {
  for (int i = 0; i < 8; i++) {
    nonce[i]++;
    if (nonce[i] != 0) break;
  }
}



void sendResponse(uint8_t state) {
  uint8_t data[17] = {0};


  // 8 Byte Nonce aus Counter
  memcpy(&data[0], &sendingNonceCounter, 8);
  sendingNonceCounter++;

  aead.setKey(key_rx, sizeof(key_rx));
  // AEAD mit diesem Nonce initialisieren
  aead.setIV(&data[0], 8);

  // Cipher direkt in data[8]
  aead.encrypt(&data[8], &state, 1);

  // Tag direkt in data[9]
  aead.computeTag(&data[9], 8);

  //17 bytes are sent in 109ms
  // Three times
  for (int i = 0; i < 3; i++) {
    delay(50);
    LoRa.beginPacket();
    LoRa.write(data, sizeof(data));  // 17 Bytes
    LoRa.endPacket();
    
  }

  

}

uint8_t validateMessage(uint8_t msg[17]){
  uint8_t state = 2; //2 is error
  uint8_t decrypted;

  aead.setKey(key, sizeof(key));
  aead.setIV(&msg[0],8);

  // 7 Decrypt ===
  aead.decrypt(&decrypted, &msg[8], 1);

  // 8 TAG prüfen ===
  bool valid = aead.checkTag(&msg[9], 8);


  if (valid && (decrypted == 0 || decrypted == 1)) {
    if (decrypted)state = 1;
    else state = 0; 
  }
  return state;
}


void setup() {

  //on board led for debugging
  pinMode(LED_BUILTIN, OUTPUT); 
  // The Crystal OSC gets activated
  xosc_init();

  rtc_init();
  
  //set rtc time once
  rtc_set_datetime(&t);
  
  delay(5000);
  
  // initialize LoRa-Module
  LoRa.setPins(SS, RST, DIO0); 

  if (!LoRa.begin(433E6)) {
    while (1);
  }
  LoRa.setTxPower(10);
  LoRa.setSpreadingFactor(8);      // SF7
  LoRa.setSignalBandwidth(125E3);  // 125 kHz
  LoRa.setCodingRate4(5);          // 4/5
  LoRa.setPreambleLength(64);
  LoRa.enableCrc();

  if (epd.Init() != 0) return;
  epd.Sleep();



}


void loop() {
  bool receivedMessageFlag = false;
  unsigned long start;
  uint8_t receivedState=2;
  
  uart_default_tx_wait_blocking();
  enter_dormant_mode_with_timer(1);   // 1 second sleep

  start = millis();

  while (millis() - start < 300 && !receivedMessageFlag) {    // 300ms Waiting window
    
    int packetSize = LoRa.parsePacket();
    if (packetSize == 17) {
      
      uint8_t receivedData[17];
    
      int n = LoRa.readBytes(receivedData, 17);
      if (n == 17) {
          receivedState = validateMessage(receivedData);
          receivedMessageFlag = true;
      }
    }
  }

  if(receivedMessageFlag && (receivedState == 0 || receivedState == 1)){
    delay(20); // wait for sender to go into receive mode
    digitalWrite(LED_BUILTIN, HIGH);
    
    sendResponse(receivedState);
    digitalWrite(LED_BUILTIN, LOW);

    if(receivedState == 1) showPresenceOnDisplay();
    else showAbscenceOnDisplay();
  }  
  
}
    

Test

For checking the functionality and range of the devices, I was not able to make a video. However I measured a range of around 100 metres from my room to a curved corner on the road. While the receiver was in my apartment in the second floor, I took the sender with me. In between those 100 metres were a thick wall of the house, 4 metres of height difference, three big trees and some bushes. If you remember, my initial idea was to have them connect over around 50 metres through some walls and also around 4 metres height difference. That´s why I have tried to have a pretty similar situation to simulate the company floors (where I quit a while ago, so I can´t test it).

To sum it up, I think the range is quite good, for my case. I don´t know how the range would be on a free field, but that is something I will maybe do later on.

Leave a comment

But be careful and mindful. Only I can delete the messages!