Extending 433MHz Alarm Signals with LoRa

Door alarms are a common sight and have become more sophisticated over the years to increase security and reliability.Many of these new boards are either proprietary or, as with Zigbee, more expensive compared to traditional options. These old door alarms operate on 433MHz and use an amplitude-based, fixed code encoding called 1527, typically based on the EV1527 chip. Poor signal is common with these devices and the transceivers found online. If one wants to transmit these messages much further away, a forwarder is needed.

Enter LoRa

LoRa (Long RAnge) is a spread spectrum modulation technology designed for low power, long distance, and IoT communication. A basic LoRa setup using a wire antenna can easily reach a mile. Development boards are also relatively inexpensive at around $30.

The Problems

I have a shed at least 100 yards away and wanted to add a door alarm to it. The initial setup used two LoRa boards, one installed at the shed door with a reed switch, and another at the house with a buzzer connected. This shed has two double doors securing two sections. Another reed switch can be run to the other door but there is a second smaller shed on the property without power, necessitating a battery-powered wireless solution. Using more LoRa boards is overkill and more expensive than using purpose-built door sensors.

I can install the common 433MHz door switches (and a couple smoke detectors using the same 1527 encoding) and create a receiver at the house. This would cut out the LoRa boards entirely and only need a single receiver at the house. This sounded great until I implemented the system. It turns out the wireless range is was abysmal and cannot reach the house. See here

The Solution

I’ll use the LoRa transmitter in the shed as a forwarder to the house and just change the receiver’s firmware. The flow will look like this:

d o o r s e n s o r 4 3 3 M H z f o r w a r d e r 9 1 2 M H z r e c e i v e r w i t h b u z z e r

Hardware

  • Boards
  • Sensors:
    • Generic 433MHz sensor supporting 1527
    • Generic reed switch(optional)
  • Cases(3D printed):
    • Found on Thingiverse.
  • Buzzer
    • Any 3.3V buzzer large enough to hear across the house.

The RXB6 receiver The RXB6 receiver.

Wiring

The RXB6 can be powered by 3.3V and, if 5V is used, the receive pin can be connected directly to an input pin on the microcontroller as it never exceeds 3.3V. In my case, I never saw it over 1.2V. The 912MHz antenna is attached to the small ant via on the top right of the Feather board. If using the two reed(magnetic) switch pins, you can place a low value resistor inline or enable the pullup resistor which is what the code below does.

The wiring can be determined by looking at the code or at the photos below.

Forwarder Forwarder

Note: The 912MHz antenna can be pointed downward and the ground is expanded on the protoboard to accomodate all the devices. This isn’t needed if just using the RXB6

Receiver Receiver Wiring

Firmware

I used the Arduino IDE with the RCSwitch, SPI, and RadioHead libraries. They can be found in the Library Manager.

The Adafruit Feather M0 needs a board repository added: https://adafruit.github.io/arduino-board-index/package_adafruit_index.json This option can be found in the preferences menu under Additional Boards Manager URLs.

Flashing the forwarder

After wiring the forwarder, flash it with the code found at https://github.com/Daniel-McDonough/433mhz-to-lora-forwarder or below, keep it connected to the computer, and open up the serial output. Activate the 433MHz devices you intend to use and write down the codes.

Flashing the Receiver

Get the receiver code from the same Github link above and add the 1527 codes to the receiver code as shown below. Replace the current list with your codes.

For the door codes: const long triggerCodes[] = {10000001, 10000002, 00000000, 00000001, 00000002, 00000003}

Keep 10000001 and 10000002 if using the two reed switch inputs.

For the smoke detectors or other alarms: const long smokeDetectorCodes[] = {20000000, 20000002};

Flash the same as the forwarder board and then test it by activating one of the sensors.

Troubleshooting

  • Check the logs from the forwarder in the serial monitor to make sure it’s receiving, processing, and forwarding the codes.

  • Make sure the antennas are fairly straight and in the same orientation.

  • Activate the sensor from a closer distance(~2 feet).

  • Distance the boards from one another.

  • Make sure your sensors use 1527 encoding.


Failures

The type of 433MHz board matters. The MX-RM-5V has terrible reception, even with a longer antenna. There is a mod out there to make them more sensitive but it isn’t worth the effort. I wasted a lot of time trying to increase the distance only to find a comparison chart showing just how bad all the boards are except the RXB6 superheterodyne receiver. After using this board, I discovered my forwarder setup was uncecessary when I activated a door alarm accidentally at the house and the door beeper went off…. With the RXB6 and a long antenna, the house is on the edge of the signal range. The current setup is likely more reliable since the LoRa protocol is more sophisticated and the two devices are well within the maximum distance.

Security Considerations

There’s an obvious security flaw in this implementation that can’t be mitigated without using a more secure method for the sensors. One can send the unique codes of the doors and trigger the door alarm repeatedly. The forwarder also blindly forwards any code it receives which may create a denial of service attack. A whitelist can be added to the forwarder but it would then require flashing both boards with the device codes. The LoRa communication can be secured and that may be something added later if more LoRa devices are added(ATV, tractor, camper trackers/alarms).

Future Projects

Connecting the Setup to HomeAssistant

Connecting the setup to HomeAssistant shouldn’t be too difficult since one can just push the codes to MQTT via WiFi. It’s possible to also track the close events since the door sensors send close codes but I’d need to determine where to place the logic. ESPHome has some support for 433MHz signals if I wanted to just capture the 1527 codes directly but that support isn’t well documented and seems specific to which receivers is supports.

Tracking Other Objects

Due to LoRa’s long distance, it’s possible to place boards in the ATVs and Tractor which can send messages when the vehicle is turned on or jostled. One can even send GPS coordinates. This can be used to great effect when integrated with HomeAssistant and configured for mobile app notifications.

Make a Repeater

This is can be done by attaching a 433MHz transmitter to the receiver board and adding functions to transmit the LoRa-received code. Useful for installed security systems that already use the same type of sensors.


The code is below but the Github repo is likely more up to date.

The forwarder code:

#include <RCSwitch.h>
#include <RH_RF95.h>
#include <SPI.h>


#define RXB6_RX_PIN   5   // RX pin for RXB6 - Adjust this pin as per your wiring
#define RF95_CS      8   // Chip select pin for RF96
#define RF95_RST     4   // Reset pin for RF96
#define RF95_INT     3
#define TRIGGER_PIN1 10  // Trigger pin 1
#define TRIGGER_PIN2 11  // Trigger pin 2

// Instantiate the RF95 (RF96) and RCSwitch objects
RH_RF95 rf95(RF95_CS, RF95_INT);
RCSwitch rcSwitch = RCSwitch();

unsigned long lastReceivedTime = 0;  // Last received time for debounce
long lastReceivedCode = -1;          // Last received code for debounce
const unsigned long debounceTime = 3000;  
bool PIN1_RESET = false;
bool PIN2_RESET = false;
    
void setup() {
    Serial.begin(9600);

    // Initialize RCSwitch for receiving 1527 encoded messages
    rcSwitch.enableReceive(digitalPinToInterrupt(RXB6_RX_PIN)); // Enable receiver on interrupt

    // Manual reset of RF96 to make sure it's initialized
    pinMode(RF95_RST, OUTPUT);
    digitalWrite(RF95_RST, HIGH);
    delay(100);
    digitalWrite(RF95_RST, LOW);
    delay(100);
    digitalWrite(RF95_RST, HIGH);
    delay(100);

    // Initialize RF95 (RF96) module
    if (!rf95.init()) {
        Serial.println("RF96 initialization failed!");
    }

    // Set the frequency for the RF96 module
    if (!rf95.setFrequency(915.0)) {
        Serial.println("Setting RF96 frequency failed!");
    } else {
        Serial.println("RF96 frequency set to 915.0 MHz");
    }

    // Initialize trigger pins
    pinMode(TRIGGER_PIN1, INPUT_PULLUP);
    pinMode(TRIGGER_PIN2, INPUT_PULLUP);
}

void loop() {
    // Check for external triggers
    checkExternalTriggers();

    // Check if RCSwitch has received a message
    if (rcSwitch.available()) {
        long receivedValue = rcSwitch.getReceivedValue();

        // Check if the code is a duplicate and if it's within the debounce time
        if (receivedValue == lastReceivedCode && millis() - lastReceivedTime < debounceTime) {
            rcSwitch.resetAvailable();
            return;
        }

        // Update the last received code and time
        lastReceivedCode = receivedValue;
        lastReceivedTime = millis();

        // Process the received code
        processReceivedCode(receivedValue);
        rcSwitch.resetAvailable();
    }
}

//Checks the two reed switches connected to the board and debounces them
//Reset logic added to stop 

void checkExternalTriggers() {

    if (digitalRead(TRIGGER_PIN1) == HIGH && !PIN1_RESET) {
        //Send code for receiver to process
        processReceivedCode(10000001);
        //Debounce
        delay(300); 
        PIN1_RESET = true;
    }
    if (digitalRead(TRIGGER_PIN1) == LOW && PIN1_RESET) {
        delay(300);
        Serial.println("1 closed");
        PIN1_RESET = false;
    }  
    if (digitalRead(TRIGGER_PIN2) == HIGH && !PIN2_RESET) {
        //Send code for receiver to process
        processReceivedCode(10000002);
        delay(300); 
        PIN2_RESET = true;
    }
    if (digitalRead(TRIGGER_PIN2) == LOW && PIN2_RESET) {
        delay(300);
        Serial.println("2 closed");
        PIN2_RESET = false;
    }

}

void processReceivedCode(long receivedValue) {
    if (receivedValue > 0) {
        Serial.print("Received: ");
        Serial.println(receivedValue, DEC);

        // Prepare a buffer to send the message
        uint8_t buf[sizeof(receivedValue)];
        memcpy(buf, &receivedValue, sizeof(receivedValue));

        // Send the message via RF96
        if (rf95.send(buf, sizeof(buf))) {
            rf95.waitPacketSent();
            Serial.println("Message forwarded via RF96");
            } else {
                Serial.println("Error sending message via RF96");
            }
            } else {
               Serial.println("Received unknown code");
            }
}

The LoRa receiver code:

#include <RH_RF95.h>

#define RF95_CS      8   // Chip select pin for RF96
#define RF95_RST     4   // Reset pin for RF96
#define RF95_INT     3   // Interrupt pin for RF96
#define BUZZER_PIN   10  // Buzzer pin
#define LED_PIN      13

// Door codes
const long triggerCodes[] = {10000001, 10000002, 00000000, 00000001, 00000002, 00000003}; // Add your standard codes here
const int numTriggerCodes = sizeof(triggerCodes) / sizeof(triggerCodes[0]);

// Smoke detector codes
const long smokeDetectorCodes[] = {20000000, 20000002}; // Replace with actual smoke detector codes
const int numSmokeDetectorCodes = sizeof(smokeDetectorCodes) / sizeof(smokeDetectorCodes[0]);

RH_RF95 rf95(RF95_CS, RF95_INT);

void setup() {
    pinMode(RF95_RST, OUTPUT);
    digitalWrite(RF95_RST, HIGH);
    pinMode(BUZZER_PIN, OUTPUT);
    pinMode(LED_PIN, OUTPUT); 

    if (!rf95.init()) {
        // If init fails, blink LED rapidly
        while (1) {
            digitalWrite(LED_PIN, HIGH);
            delay(100);
            digitalWrite(LED_PIN, LOW);
            delay(100);
        }
    }
    if (!rf95.setFrequency(915.0)) {
        // If frequency set fails, blink LED slowly
        while (1) {
            digitalWrite(LED_PIN, HIGH);
            delay(1000);
            digitalWrite(LED_PIN, LOW);
            delay(1000);
        }
    }
    // If setup is successful, turn on LED for 2 seconds
    digitalWrite(LED_PIN, HIGH);
    delay(2000);
    digitalWrite(LED_PIN, LOW);
}

void loop() {
    if (rf95.available()) {
        uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];
        uint8_t len = sizeof(buf);

        if (rf95.recv(buf, &len)) {
            if (len == sizeof(long)) {
                long receivedCode;
                memcpy(&receivedCode, buf, len);

                if (processReceivedCode(receivedCode)) {
                    // Turn on LED to indicate code processed
                    digitalWrite(LED_PIN, HIGH);
                    delay(500);
                    digitalWrite(LED_PIN, LOW);
                }
            }
        }
    }
}
//Determine which function to call based on door or smoke detector
bool processReceivedCode(long code) {
    for (int i = 0; i < numSmokeDetectorCodes; i++) {
        if (code == smokeDetectorCodes[i]) {
            smokeDetectorAlert();
            return true;
        }
    }

    for (int i = 0; i < numTriggerCodes; i++) {
        if (code == triggerCodes[i]) {
            beep(3);
            return true;
        }
    }
    return false;
}

// Change the timings and count to your liking
void beep(int numOfBeeps) {
    for (int i = 0; i < numOfBeeps; i++) {
        digitalWrite(BUZZER_PIN, HIGH);
        delay(300);
        digitalWrite(BUZZER_PIN, LOW);
        if (i < numOfBeeps - 1) {
            delay(300);
        }
    }
}

void smokeDetectorAlert() {
    for (int i = 0; i < 10; i++) {
        digitalWrite(BUZZER_PIN, HIGH);
        delay(1000);
        digitalWrite(BUZZER_PIN, LOW);
        delay(100);
        
        for (int i = 0; i < 3; i++) {
            digitalWrite(BUZZER_PIN, HIGH);
            delay(300);
            digitalWrite(BUZZER_PIN, LOW);
            delay(100);
        }
    }
}
updatedupdated2024-02-072024-02-07