Wemos 433 Mhz RF Receiver
I’ve been using RTL_433 with a SDR for a few years now with modest success. This setup will detect and identify a variety of 433Mhz sensor readings. The output is piped to a MQTT broker and from there the readings are presented graphically.
At first this was done on a Raspberry Pi running a bash script on startup, with the following one liner command.
rtl_433 -F json | mosquitto_pub -h <MQTT-Broker> -i RTL_433 -l -t RTL_433/SDR_FEED
Unpredictably, problems would arise when the process would hang and readings no longer were being received. This was addressed by placing the process under a supervisor task, details can be found in this post titled “Scheduled vs Supervised Tasks” https://www.cloudacm.com/?p=3956. Another issue discovered was that some sensors failed to be detected by the SDR. The standard antenna included with the SDR was replaced with a 433 Mhz tuned antenna, https://www.amazon.com/Zerone-433MHZ-Antenna-Signal-Amplifier/dp/B07DMXSX45. This improved coverage, but there were still some PIR sensors that were in dead spots. It was becoming clearer that the SDR had a hard limit and to get these dead spot sensors online, I would have to place some kind of bridging device closer to them.
I had seen demonstrations of 433 Mhz RF modules being used with microcontrollers in this post, https://randomnerdtutorials.com/decode-and-send-433-mhz-rf-signals-with-arduino/. However, many of the demonstrations I found online used the RF modules as a wireless link between two microcontrollers. I had a Sonoff RF bridge available and looked into using it for the purpose of receiving and processing sensors. However, that would require hardware modifications and following a rather lengthy setup process with a narrow range of supported sensors.
Along the way there were GitHub repos that caught my attention, https://github.com/sui77/rc-switch/ and https://github.com/ninjablocks/433Utils. It was this blog by Ray Wang that spelled out clearly what I was intending to do, https://rayshobby.net/reverse-engineer-wireless-temperature-humidity-rain-sensors-part-1/. This was further elaborated by Brad Hunting’s GitHub repo, https://github.com/bhunting/Acurite_00592TX_sniffer. From here out the pieces started to fall into place as I found more details on Brad’s blog, https://www.techspin.info/.
One of the key steps in getting the microcontroller to detect and decode the RF sensors was identifying the pattern. I used a logic analyzer to get the timings and sequences. From there I was able to display the sensor data with the microcontroller. This worked for my PIR motion sensors, temp/humidity sensors, door reed switch sensors, and key fob controllers.
I had some problems porting the code to work on the Wemos ESP8266. The code worked fine on the Arduino Uno, but the interrupts are handled differently on the ESP8266 microcontroller. The function called by attachInterrupt had to have ICACHE_RAM_ATTR defined. This video briefly explains some of the interrupt dependencies of the platform.
Another problem I had was with the 433 Mhz receiver hardware. It operates at 5 volts and was causing a brownout of the Wemos when the microcontroller was attempting to boot. A workaround for this was to control the power to the receiver from the Wemos using a logic level shifter. The level shifter was already in use to shift the 5 volt signal from the receiver to 3.3 volts on the Wemos. Using a level shifter as a power supply is not recommended, but the low current draw of the receiver allowed it just this time. Here is the wiring of the components.
Since this project was to detect dead spot PIR sensors, this is the code that was loaded on the Wemos. I liked that the library dependencies were minimal which made integrating functions from earlier code easier.
/* Header Title: Wemos ESP8266 433Mhz RF PIR MQTT Bridge Version: 2 Filename: Wemos-433_MQTT_PIR-Sensor_Display_ver2.ino Date: 12/10/2023 Author: Patrick Gilfeather - CloudACM https://www.cloudacm.com Base Code based on Convert RF signal into bits (PIR Sensor) Written by : Ray Wang (Rayshobby LLC) http://rayshobby.net/?p=8998 For ESP8266-Wemos, use LOLIN(WEMOS) D1, R2, and mini */ // Libraries and Declarations #include <Arduino.h> #include <ESP8266WiFi.h> #include <PubSubClient.h> WiFiClient espClient; PubSubClient MQTTclient(espClient); long laststats = 0; int programflag = 0; // NTP Libraries, Declarations, and Variables #include <NTPClient.h> #include <WiFiUdp.h> WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, "pool.ntp.org"); //Week Days String weekDays[7]={"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; //Month names String months[12]={"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; // Variables // MQTT Broker const char* mqtt_server = "#########"; // Put your MQTT Broker here // Your WiFi credentials const char* ssid = "#########"; // Put your SSID here const char* password = "#########"; // Put your PASSWORD here unsigned long lasttimeupdate = 0; // Time counter for periodic NTP time checks unsigned long lasttimereboot = 0; // Time counter for periodic reboots to avoid millis rollover ADC_MODE(ADC_VCC); // See comment block below for more details /* see - http://arduino.esp8266.com/Arduino/versions/2.0.0-rc2/doc/libraries.html ESP.getVcc() may be used to measure supply voltage. ESP needs to reconfigure the ADC at startup in order for this feature to be available. Add the following line to the top of your sketch to use getVcc: ADC_MODE(ADC_VCC); TOUT pin has to be disconnected in this mode. Note that by default ADC is configured to read from TOUT pin using analogRead(A0), and ESP.getVCC() is not available. */ // ring buffer size has to be large enough to fit // data between two successive sync signals #define RING_BUFFER_SIZE 256 #define SYNC_LENGTH 12200 #define SYNC_RANGE 300 // Was hard coded to 400 but too much crosstalk #define BIT1_HIGH 1200 #define BIT1_LOW 400 #define BIT0_HIGH 400 #define BIT0_LOW 1200 #define BIT_RANGE 100 #define DATAPIN 2 // D2 is interrupt 1 #define CONTROLPIN 4 // D4 is used to control power to the 433Mhz reciever module #define DELAYTIME 50 // 50 ms delay commonly used throughout code unsigned long timings[RING_BUFFER_SIZE]; unsigned int syncIndex1 = 0; // index of the first sync signal unsigned int syncIndex2 = 0; // index of the second sync signal bool received = false; // detect if a sync signal is present bool isSync(unsigned int idx) { // check if we've received 4 squarewaves of matching timing int i; // check if there is a long sync period prior to the 4 squarewaves unsigned long t = timings[(idx+RING_BUFFER_SIZE-i)%RING_BUFFER_SIZE]; // if(t<(SYNC_LENGTH-400) || t>(SYNC_LENGTH+400) || if(t<(SYNC_LENGTH-SYNC_RANGE) || t>(SYNC_LENGTH+SYNC_RANGE) || digitalRead(DATAPIN) != HIGH) { return false; } return true; } /* Interrupt 1 handler */ void ICACHE_RAM_ATTR handler() { static unsigned long duration = 0; static unsigned long lastTime = 0; static unsigned int ringIndex = 0; static unsigned int syncCount = 0; // ignore if we haven't processed the previous received signal if (received == true) { return; } // calculating timing since last change long time = micros(); duration = time - lastTime; lastTime = time; // store data in ring buffer ringIndex = (ringIndex + 1) % RING_BUFFER_SIZE; timings[ringIndex] = duration; // detect sync signal if (isSync(ringIndex)) { syncCount ++; // first time sync is seen, record buffer index if (syncCount == 1) { syncIndex1 = (ringIndex+1) % RING_BUFFER_SIZE; } else if (syncCount == 2) { // second time sync is seen, start bit conversion syncCount = 0; syncIndex2 = (ringIndex+1) % RING_BUFFER_SIZE; unsigned int changeCount = (syncIndex2 < syncIndex1) ? (syncIndex2+RING_BUFFER_SIZE - syncIndex1) : (syncIndex2 - syncIndex1); // changeCount must be 50 -- 24 bits x 2 + 2 for sync if (changeCount != 50) { received = false; syncIndex1 = 0; syncIndex2 = 0; } else { received = true; } } } } // MQTT Functions void callback(char* topic, byte* message, unsigned int length) { String messageTemp; for (int i = 0; i < length; i++) { messageTemp += (char)message[i]; } if (String(topic) == "Wemos-433/UpdateTime") { if(messageTemp == "CheckTime"){ timeClient.update(); } } if (String(topic) == "Wemos-433/Reboot") { if(messageTemp == "Reboot"){ ESP.restart(); } } } void reconnect() { // Loop until we're reconnected while (!MQTTclient.connected()) { // Attempt to connect if (MQTTclient.connect("Wemos-433")) { // Subscribe // Do you not subscribe to my methods? // Wemos-433/# for everything, or Wemos-433/Uptime for just the Uptime MQTTclient.subscribe("Wemos-433/#"); } else { // Wait 5 seconds before retrying delay(5000); } } } void UpdateStats() { long stats = millis(); if (stats - laststats > 5000) { laststats = stats; MQTTclient.publish("Wemos-433/Firmware", "Wemos-433_MQTT_PIR-Sensor_Display_ver2"); String StringUptime = String(millis()); MQTTclient.publish("Wemos-433/Uptime", StringUptime.c_str()); String StringHWAddress = String(WiFi.macAddress()); MQTTclient.publish("Wemos-433/HWAddress", StringHWAddress.c_str()); String StringWifiSignal = String(WiFi.RSSI()); MQTTclient.publish("Wemos-433/WifiSignal",StringWifiSignal.c_str()); String StringFreeHeapSize = String(ESP.getFreeHeap()); MQTTclient.publish("Wemos-433/FreeHeapSize",StringFreeHeapSize.c_str()); String StringHeapFragmentation = String(ESP.getHeapFragmentation()); MQTTclient.publish("Wemos-433/HeapFragmentation",StringHeapFragmentation.c_str()); String StringMaxFreeBlockSize = String(ESP.getMaxFreeBlockSize()); MQTTclient.publish("Wemos-433/MaxFreeBlockSize",StringMaxFreeBlockSize.c_str()); String StringSketchSize = String(ESP.getSketchSize()); MQTTclient.publish("Wemos-433/SketchSize",StringSketchSize.c_str()); String StringFreeSketchSpace = String(ESP.getFreeSketchSpace()); MQTTclient.publish("Wemos-433/FreeSketchSpace",StringFreeSketchSpace.c_str()); String StringCpuFreqMHz = String(ESP.getCpuFreqMHz()); MQTTclient.publish("Wemos-433/CpuFreqMHz",StringCpuFreqMHz.c_str()); String StringChipId = String(ESP.getChipId()); MQTTclient.publish("Wemos-433/ChipId",StringChipId.c_str()); String StringVcc = String(ESP.getVcc()); MQTTclient.publish("Wemos-433/Vcc",StringVcc.c_str()); //Get a Time Structure String formattedTime = timeClient.getFormattedTime(); String StringformattedTime = String(formattedTime); MQTTclient.publish("Wemos-433/Time",StringformattedTime.c_str()); //Get a Date Structure time_t epochTime = timeClient.getEpochTime(); struct tm *ptm = gmtime ((time_t *)&epochTime); int monthDay = ptm->tm_mday; int currentMonth = ptm->tm_mon+1; String currentMonthName = months[currentMonth-1]; int currentYear = ptm->tm_year+1900; //Publish complete date: String StringcurrentDate = String(currentMonth) + "/" + String(monthDay) + "/" + String(currentYear); MQTTclient.publish("Wemos-433/Date",StringcurrentDate.c_str()); //Publish Epoch: String StringEpochTime = String(timeClient.getEpochTime()); MQTTclient.publish("Wemos-433/EpochTime",StringEpochTime.c_str()); } } void setup() { delay(DELAYTIME); pinMode(CONTROLPIN, OUTPUT); digitalWrite(CONTROLPIN, LOW); delay(DELAYTIME); digitalWrite(CONTROLPIN, HIGH); delay(DELAYTIME); pinMode(DATAPIN, INPUT); attachInterrupt(digitalPinToInterrupt(DATAPIN), handler, CHANGE); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { // Just wait it out delay(250); } MQTTclient.setServer(mqtt_server, 1883); MQTTclient.setCallback(callback); timeClient.begin(); // Initialize a NTPClient to get time timeClient.setTimeOffset(-25200); // Set offset time in seconds to adjust for your timezone, for example: // GMT -7 = -25200 (see - https://time.gov/) delay(1000); timeClient.update(); } int t2b(unsigned int t0, unsigned int t1) { if (t0>(BIT1_HIGH-BIT_RANGE) && t0<(BIT1_HIGH+BIT_RANGE) && t1>(BIT1_LOW-BIT_RANGE) && t1<(BIT1_LOW+BIT_RANGE)) { return 1; } else if (t0>(BIT0_HIGH-BIT_RANGE) && t0<(BIT0_HIGH+BIT_RANGE) && t1>(BIT0_LOW-BIT_RANGE) && t1<(BIT0_LOW+BIT_RANGE)){ return 0; } return -1; // undefined } void loop() { if (!MQTTclient.connected()) { reconnect(); } MQTTclient.loop(); UpdateStats(); // Update time from NTP source every 1 day (24 * 60 * 60 * 1000 = 86400000 milli-seconds) unsigned long timeupdate = millis(); if (timeupdate - lasttimeupdate > 86400000) { lasttimeupdate = timeupdate; timeClient.update(); } // Reboot microcontroller every 30 day to avoid millis() rollover (30 * 24 * 60 * 60 * 1000 = 2592000000 milli-seconds) unsigned long timereboot = millis(); if (timereboot - lasttimereboot > 2592000000) { // Reboot command ESP.restart(); } if (received == true) { // disable interrupt to avoid new data corrupting the buffer // detachInterrupt(1); detachInterrupt(digitalPinToInterrupt(DATAPIN)); // extract Device ID unsigned int startIndex, stopIndex; unsigned long deviceid = 0; bool fail = false; startIndex = (syncIndex1 + (0*8+0)*2) % RING_BUFFER_SIZE; stopIndex = (syncIndex1 + (1*8+8)*2) % RING_BUFFER_SIZE; for(int i=startIndex; i!=stopIndex; i=(i+2)%RING_BUFFER_SIZE) { int bit = t2b(timings[i], timings[(i+1)%RING_BUFFER_SIZE]); deviceid = (deviceid<<1) + bit; if (bit < 0) fail = true; } if (fail) { MQTTclient.publish("Wemos-433/DecodeError", "Decoding error."); } else { String StringDevID = String(deviceid); MQTTclient.publish("Wemos-433/DeviceID", StringDevID.c_str()); } // delay for 50 milli seconds to avoid repetitions delay(DELAYTIME); received = false; syncIndex1 = 0; syncIndex2 = 0; // re-enable interrupt // attachInterrupt(1, handler, CHANGE); attachInterrupt(digitalPinToInterrupt(DATAPIN), handler, CHANGE); } }
The hardware build initially was in a box. The Wemos module had its mini USB port exposed so that a USB cable could provide power. It also had a cut piece of wire that is 70 cm long as the antenna. This setup was rather unsightly since the box and long wires looked out of place.
I finally settled on installing it in a Tripp Lite TLP664USBB surge protector that included USB ports, https://tripplite.eaton.com/6-outlet-surge-protector-with-4-usb-ports-6-foot-1800-joules-black~TLP664USBB. This allowed the Wemos to get its power directly from the 5 volt USB ports. The power strip is used for other purposes and none of it looks odd or attention getting. I had to loop the antenna around the inside of the power strip. This didn’t present any problems as I was careful to isolate the electronics by sealing it with silicon calk. The Wemos module was placed so that the mini USB port was still accessible, should any programming need to be done later.
This project will likely expand as time goes on, with the option to decode multiple sensors types from a single microcontroller. But for now, this will do what I need.