From 1d04b22eb2641e9350450ac698bb7c41e4b3d85c Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Mon, 5 Jun 2023 12:12:51 -0500
Subject: [PATCH] wip

---
 components/ratgdo/__init__.py      |  16 +
 components/ratgdo/common.h         |   5 +
 components/ratgdo/ratgdo.cpp       | 498 +++++++++++++++++++++++++++++
 components/ratgdo/ratgdo.h         | 108 +++++++
 components/ratgdo/rolling_code.cpp |  97 ++++++
 components/ratgdo/rolling_code.h   |  18 ++
 components/ratgdo/secplus.c        | 490 ++++++++++++++++++++++++++++
 components/ratgdo/secplus.h        |  42 +++
 8 files changed, 1274 insertions(+)
 create mode 100644 components/ratgdo/__init__.py
 create mode 100644 components/ratgdo/common.h
 create mode 100644 components/ratgdo/ratgdo.cpp
 create mode 100644 components/ratgdo/ratgdo.h
 create mode 100644 components/ratgdo/rolling_code.cpp
 create mode 100644 components/ratgdo/rolling_code.h
 create mode 100644 components/ratgdo/secplus.c
 create mode 100644 components/ratgdo/secplus.h

diff --git a/components/ratgdo/__init__.py b/components/ratgdo/__init__.py
new file mode 100644
index 0000000..d00826b
--- /dev/null
+++ b/components/ratgdo/__init__.py
@@ -0,0 +1,16 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.core import coroutine_with_priority
+
+ratgdo_ns = cg.esphome_ns.namespace("ratgdo")
+
+CONFIG_SCHEMA = cv.All(
+    cv.Schema({}),
+)
+
+
+@coroutine_with_priority(1.0)
+async def to_code(config):
+    cg.add_library("bblanchon/ArduinoJson", "6.18.5")
+    cg.add_define("USE_JSON")
+    cg.add_global(ratgdo_ns.using)
diff --git a/components/ratgdo/common.h b/components/ratgdo/common.h
new file mode 100644
index 0000000..6c122ba
--- /dev/null
+++ b/components/ratgdo/common.h
@@ -0,0 +1,5 @@
+#include <Arduino.h>
+
+#define CODE_LENGTH 19 // the length of each command sent to the door.
+extern byte rollingCode[CODE_LENGTH];
+extern unsigned int rollingCodeCounter;
\ No newline at end of file
diff --git a/components/ratgdo/ratgdo.cpp b/components/ratgdo/ratgdo.cpp
new file mode 100644
index 0000000..95b147c
--- /dev/null
+++ b/components/ratgdo/ratgdo.cpp
@@ -0,0 +1,498 @@
+/************************************
+ * Rage
+ * Against
+ * The
+ * Garage
+ * Door
+ * Opener
+ *
+ * Copyright (C) 2022  Paul Wieland
+ *
+ * GNU GENERAL PUBLIC LICENSE
+ ************************************/
+
+#include "common.h"
+#include "ratgdo.h"
+
+#include "esphome/core/log.h"
+
+namespace esphome
+{
+  namespace ratgdo
+  {
+
+    static const char *const TAG = "ratgdo";
+
+    void RATGDOComponent::setup()
+    {
+		pinMode(TRIGGER_OPEN, INPUT_PULLUP);
+		pinMode(TRIGGER_CLOSE, INPUT_PULLUP);
+		pinMode(TRIGGER_LIGHT, INPUT_PULLUP);
+		pinMode(STATUS_DOOR, OUTPUT);
+		pinMode(STATUS_OBST, OUTPUT);
+		pinMode(INPUT_RPM1, INPUT_PULLUP); // set to pullup to add support for reed switches
+		pinMode(INPUT_RPM2, INPUT_PULLUP); // make sure pin doesn't float when using reed switch and fire interrupt by mistake
+		pinMode(INPUT_OBST, INPUT);
+
+		attachInterrupt(TRIGGER_OPEN,isrDoorOpen,CHANGE);
+		attachInterrupt(TRIGGER_CLOSE,isrDoorClose,CHANGE);
+		attachInterrupt(TRIGGER_LIGHT,isrLight,CHANGE);
+		attachInterrupt(INPUT_OBST,isrObstruction,CHANGE);
+		attachInterrupt(INPUT_RPM1,isrRPM1,RISING);
+		attachInterrupt(INPUT_RPM2,isrRPM2,RISING);
+
+		LittleFS.begin();
+
+		readCounterFromFlash();
+
+		if(useRollingCodes){
+			//if(rollingCodeCounter == 0) rollingCodeCounter = 1;
+
+			ESP_LOGD(TAG, "Syncing rolling code counter after reboot...");
+			sync(); // if rolling codes are being used (rolling code counter > 0), send reboot/sync to the opener on startup
+		}else{
+			ESP_LOGD(TAG, "Rolling codes are disabled.");
+		}
+    }
+
+	void RATGDOComponent::loop(){
+		obstructionLoop();
+		doorStateLoop();
+		dryContactLoop();
+	}
+
+
+  } // namespace ratgdo
+} // namespace esphome
+
+
+
+/*************************** DETECTING THE DOOR STATE ***************************/
+void doorStateLoop(){
+	static bool rotaryEncoderDetected = false;
+	static int lastDoorPositionCounter = 0;
+	static int lastDirectionChangeCounter = 0;
+	static int lastCounterMillis = 0;
+
+	// Handle reed switch
+	// This may need to be debounced, but so far in testing I haven't detected any bounces
+	if(!rotaryEncoderDetected){
+		if(digitalRead(INPUT_RPM1) == LOW){
+			if(doorState != "reed_closed"){
+				ESP_LOGD(TAG, "Reed switch closed");
+				doorState = "reed_closed";
+				if(isConfigFileOk){
+					bootstrapManager.publish(overallStatusTopic.c_str(), "reed_closed", true);
+					bootstrapManager.publish(doorStatusTopic.c_str(), "reed_closed", true);
+				}
+				digitalWrite(STATUS_DOOR,HIGH);
+			}
+		}else if(doorState != "reed_open"){
+			ESP_LOGD(TAG, "Reed switch open");
+			doorState = "reed_open";
+			if(isConfigFileOk){
+				bootstrapManager.publish(overallStatusTopic.c_str(), "reed_open", true);
+				bootstrapManager.publish(doorStatusTopic.c_str(), "reed_open", true);
+			}
+			digitalWrite(STATUS_DOOR,LOW);
+		}
+	}
+	// end reed switch handling
+
+	// If the previous and the current state of the RPM2 Signal are different, that means there is a rotary encoder detected and the door is moving
+	if(doorPositionCounter != lastDoorPositionCounter){
+		rotaryEncoderDetected = true; // this disables the reed switch handler
+		lastCounterMillis = millis();
+
+		ESP_LOGD(TAG, "Door Position: %d", doorPositionCounter);
+	}
+
+	// Wait 5 pulses before updating to door opening status
+	if(doorPositionCounter - lastDirectionChangeCounter > 5){
+		if(doorState != "opening"){
+			ESP_LOGD(TAG,"Door Opening...");
+			if(isConfigFileOk){
+				bootstrapManager.publish(overallStatusTopic.c_str(), "opening", true);
+				bootstrapManager.publish(doorStatusTopic.c_str(), "opening", true);
+			}
+		}
+		lastDirectionChangeCounter = doorPositionCounter;
+		doorState = "opening";
+	}
+
+	if(lastDirectionChangeCounter - doorPositionCounter > 5){
+		if(doorState != "closing"){
+			ESP_LOGD(TAG,"Door Closing...");
+			if(isConfigFileOk){
+				bootstrapManager.publish(overallStatusTopic.c_str(), "closing", true);
+				bootstrapManager.publish(doorStatusTopic.c_str(), "closing", true);
+			}
+		}
+		lastDirectionChangeCounter = doorPositionCounter;
+		doorState = "closing";
+	}
+
+	// 250 millis after the last rotary encoder pulse, the door is stopped
+	if(millis() - lastCounterMillis > 250){
+		// if the door was closing, and is now stopped, then the door is closed
+		if(doorState == "closing"){
+			doorState = "closed";
+			ESP_LOGD(TAG,"Closed");
+			if(isConfigFileOk){
+				bootstrapManager.publish(overallStatusTopic.c_str(), doorState.c_str(), true);
+				bootstrapManager.publish(doorStatusTopic.c_str(), doorState.c_str(), true);
+			}
+			digitalWrite(STATUS_DOOR,LOW);
+		}
+
+		// if the door was opening, and is now stopped, then the door is open
+		if(doorState == "opening"){
+			doorState = "open";
+			ESP_LOGD(TAG,"Open");
+			if(isConfigFileOk){
+				bootstrapManager.publish(overallStatusTopic.c_str(), doorState.c_str(), true);
+				bootstrapManager.publish(doorStatusTopic.c_str(), doorState.c_str(), true);
+			}
+			digitalWrite(STATUS_DOOR,HIGH);
+		}
+	}
+
+	lastDoorPositionCounter = doorPositionCounter;
+}
+
+/*************************** DRY CONTACT CONTROL OF LIGHT & DOOR ***************************/
+void IRAM_ATTR isrDebounce(const char *type){
+	static unsigned long lastOpenDoorTime = 0;
+	static unsigned long lastCloseDoorTime = 0;
+	static unsigned long lastToggleLightTime = 0;
+	unsigned long currentMillis = millis();
+
+	// Prevent ISR during the first 2 seconds after reboot
+	if(currentMillis < 2000) return;
+
+	if(strcmp(type, "openDoor") == 0){
+		if(digitalRead(TRIGGER_OPEN) == LOW){
+			// save the time of the falling edge
+			lastOpenDoorTime = currentMillis;
+		}else if(currentMillis - lastOpenDoorTime > 500 && currentMillis - lastOpenDoorTime < 10000){
+			// now see if the rising edge was between 500ms and 10 seconds after the falling edge
+			dryContactDoorOpen = true;
+		}
+	}
+
+	if(strcmp(type, "closeDoor") == 0){
+		if(digitalRead(TRIGGER_CLOSE) == LOW){
+			// save the time of the falling edge
+			lastCloseDoorTime = currentMillis;
+		}else if(currentMillis - lastCloseDoorTime > 500 && currentMillis - lastCloseDoorTime < 10000){
+			// now see if the rising edge was between 500ms and 10 seconds after the falling edge
+			dryContactDoorClose = true;
+		}
+	}
+
+	if(strcmp(type, "toggleLight") == 0){
+		if(digitalRead(TRIGGER_LIGHT) == LOW){
+			// save the time of the falling edge
+			lastToggleLightTime = currentMillis;
+		}else if(currentMillis - lastToggleLightTime > 500 && currentMillis - lastToggleLightTime < 10000){
+			// now see if the rising edge was between 500ms and 10 seconds after the falling edge
+			dryContactToggleLight = true;
+		}
+	}
+}
+
+void IRAM_ATTR isrDoorOpen(){
+	isrDebounce("openDoor");
+}
+
+void IRAM_ATTR isrDoorClose(){
+	isrDebounce("closeDoor");
+}
+
+void IRAM_ATTR isrLight(){
+	isrDebounce("toggleLight");
+}
+
+// Fire on RISING edge of RPM1
+void IRAM_ATTR isrRPM1(){
+	rpm1Pulsed = true;
+}
+
+// Fire on RISING edge of RPM2
+// When RPM1 HIGH on RPM2 rising edge, door closing:
+// RPM1: __|--|___
+// RPM2: ___|--|__
+
+// When RPM1 LOW on RPM2 rising edge, door opening: 
+// RPM1: ___|--|__
+// RPM2: __|--|___
+void IRAM_ATTR isrRPM2(){
+	// The encoder updates faster than the ESP wants to process, so by sampling every 5ms we get a more reliable curve
+	// The counter is behind the actual pulse counter, but it doesn't matter since we only need a reliable linear counter
+	// to determine the door direction
+	static unsigned long lastPulse = 0;
+	unsigned long currentMillis = millis();
+
+	if(currentMillis - lastPulse < 5){
+		return;
+	}
+
+	// In rare situations, the rotary encoder can be parked so that RPM2 continuously fires this ISR.
+	// This causes the door counter to change value even though the door isn't moving
+	// To solve this, check to see if RPM1 pulsed. If not, do nothing. If yes, reset the pulsed flag
+	if(rpm1Pulsed){
+		rpm1Pulsed = false;
+	}else{
+		return;
+	}
+
+	lastPulse = millis();
+
+	// If the RPM1 state is different from the RPM2 state, then the door is opening
+	if(digitalRead(INPUT_RPM1)){
+		doorPositionCounter--;
+	}else{
+		doorPositionCounter++;
+	}
+}
+
+// handle changes to the dry contact state
+void dryContactLoop(){
+	if(dryContactDoorOpen){
+		ESP_LOGD(TAG,"Dry Contact: open the door");
+		dryContactDoorOpen = false;
+		openDoor();
+	}
+
+	if(dryContactDoorClose){
+		ESP_LOGD(TAG,"Dry Contact: close the door");
+		dryContactDoorClose = false;
+		closeDoor();
+	}
+
+	if(dryContactToggleLight){
+		ESP_LOGD(TAG,"Dry Contact: toggle the light");
+		dryContactToggleLight = false;
+		toggleLight();
+	}
+}
+
+/*************************** OBSTRUCTION DETECTION ***************************/
+void IRAM_ATTR isrObstruction(){
+	if(digitalRead(INPUT_OBST)){
+		lastObstructionHigh = millis();
+	}else{
+		obstructionLowCount++;
+	}
+	
+}
+
+void obstructionLoop(){
+	long currentMillis = millis();
+	static unsigned long lastMillis = 0;
+
+	// the obstruction sensor has 3 states: clear (HIGH with LOW pulse every 7ms), obstructed (HIGH), asleep (LOW)
+	// the transitions between awake and asleep are tricky because the voltage drops slowly when falling asleep
+	// and is high without pulses when waking up
+
+	// If at least 3 low pulses are counted within 50ms, the door is awake, not obstructed and we don't have to check anything else
+
+	// Every 50ms
+	if(currentMillis - lastMillis > 50){
+		// check to see if we got between 3 and 8 low pulses on the line
+		if(obstructionLowCount >= 3 && obstructionLowCount <= 8){
+			obstructionCleared();
+
+		// if there have been no pulses the line is steady high or low			
+		}else if(obstructionLowCount == 0){
+			// if the line is high and the last high pulse was more than 70ms ago, then there is an obstruction present
+			if(digitalRead(INPUT_OBST) && currentMillis - lastObstructionHigh > 70){
+				obstructionDetected();
+			}else{
+				// asleep
+			}
+		}
+
+		lastMillis = currentMillis;
+		obstructionLowCount = 0;
+	}
+}
+
+void obstructionDetected(){
+	static unsigned long lastInterruptTime = 0;
+	unsigned long interruptTime = millis();
+	// Anything less than 100ms is a bounce and is ignored
+	if(interruptTime - lastInterruptTime > 250){
+		doorIsObstructed = true;
+		digitalWrite(STATUS_OBST,HIGH);
+
+		ESP_LOGD(TAG,"Obstruction Detected");
+
+		if(isConfigFileOk){
+			bootstrapManager.publish(overallStatusTopic.c_str(), "obstructed", true);
+			bootstrapManager.publish(obstructionStatusTopic.c_str(), "obstructed", true);
+		}
+	}
+	lastInterruptTime = interruptTime;
+}
+
+void obstructionCleared(){
+	if(doorIsObstructed){
+		doorIsObstructed = false;
+		digitalWrite(STATUS_OBST,LOW);
+
+		ESP_LOGD(TAG,"Obstruction Cleared");
+
+		if(isConfigFileOk){
+			bootstrapManager.publish(overallStatusTopic.c_str(), "clear", true);
+			bootstrapManager.publish(obstructionStatusTopic.c_str(), "clear", true);
+		}
+	}
+}
+
+void sendDoorStatus(){
+	ESP_LOGD(TAG,"Door state %s", doorState);
+
+	if(isConfigFileOk){
+		bootstrapManager.publish(overallStatusTopic.c_str(), doorState.c_str(), true);
+		bootstrapManager.publish(doorStatusTopic.c_str(), doorState.c_str(), true);
+	}
+}
+
+void sendCurrentCounter(){
+	String msg = String(rollingCodeCounter);
+	ESP_LOGD(TAG, "Current counter %d", rollingCodeCounter);
+	if(isConfigFileOk){
+		bootstrapManager.publish(rollingCodeTopic.c_str(), msg.c_str(), true);
+	}
+}
+
+/********************************** MANAGE HARDWARE BUTTON *****************************************/
+void manageHardwareButton(){
+}
+
+
+/************************* DOOR COMMUNICATION *************************/
+/*
+ * Transmit a message to the door opener over uart1
+ * The TX1 pin is controlling a transistor, so the logic is inverted
+ * A HIGH state on TX1 will pull the 12v line LOW
+ * 
+ * The opener requires a specific duration low/high pulse before it will accept a message
+ */
+void transmit(byte* payload, unsigned int length){
+	digitalWrite(OUTPUT_GDO, HIGH); // pull the line high for 1305 micros so the door opener responds to the message
+	delayMicroseconds(1305);
+	digitalWrite(OUTPUT_GDO, LOW); // bring the line low
+
+	delayMicroseconds(1260); // "LOW" pulse duration before the message start
+	swSerial.write(payload, length);
+}
+
+void sync(){
+	if(!useRollingCodes) return;
+
+	getRollingCode("reboot1");
+	transmit(rollingCode,CODE_LENGTH);
+	delay(45);
+
+	getRollingCode("reboot2");
+	transmit(rollingCode,CODE_LENGTH);
+	delay(45);
+
+	getRollingCode("reboot3");
+	transmit(rollingCode,CODE_LENGTH);
+	delay(45);
+
+	getRollingCode("reboot4");
+	transmit(rollingCode,CODE_LENGTH);
+	delay(45);
+
+	getRollingCode("reboot5");
+	transmit(rollingCode,CODE_LENGTH);
+	delay(45);
+
+	getRollingCode("reboot6");
+	transmit(rollingCode,CODE_LENGTH);
+	delay(45);
+
+	writeCounterToFlash();
+}
+
+void openDoor(){
+	if(doorState == "open" || doorState == "opening"){
+		ESP_LOGD(TAG, "The door is already %s", doorState);
+		return;
+	}
+
+	doorState = "opening"; // It takes a couple of pulses to detect opening/closing. by setting here, we can avoid bouncing from rapidly repeated commands
+
+	if(useRollingCodes){
+		getRollingCode("door1");
+		transmit(rollingCode,CODE_LENGTH);
+
+		delay(40);
+
+		getRollingCode("door2");
+		transmit(rollingCode,CODE_LENGTH);
+
+		writeCounterToFlash();
+	}else{
+		for(int i=0; i<4; i++){
+			ESP_LOGD(TAG, "sync_code[%d]", i);
+
+			transmit(SYNC_CODE[i],CODE_LENGTH);
+			delay(45);
+		}
+		ESP_LOGD(TAG, "door_code")
+		transmit(DOOR_CODE,CODE_LENGTH);
+	}
+}
+
+void closeDoor(){
+	if(doorState == "closed" || doorState == "closing"){
+		ESP_LOGD(TAG, "The door is already %s", doorState);
+		return;
+	}
+
+	doorState = "closing"; // It takes a couple of pulses to detect opening/closing. by setting here, we can avoid bouncing from rapidly repeated commands
+
+	if(useRollingCodes){
+		getRollingCode("door1");
+		transmit(rollingCode,CODE_LENGTH);
+
+		delay(40);
+
+		getRollingCode("door2");
+		transmit(rollingCode,CODE_LENGTH);
+		
+		writeCounterToFlash();
+	}else{
+		for(int i=0; i<4; i++){
+			ESP_LOGD(TAG, "sync_code[%d]", i);
+
+
+			transmit(SYNC_CODE[i],CODE_LENGTH);
+			delay(45);
+		}
+		ESP_LOGD(TAG, "door_code")
+		transmit(DOOR_CODE,CODE_LENGTH);
+	}
+}
+
+void toggleLight(){
+	if(useRollingCodes){
+		getRollingCode("light");
+		transmit(rollingCode,CODE_LENGTH);
+		writeCounterToFlash();
+	}else{
+		for(int i=0; i<4; i++){
+			ESP_LOGD(TAG, "sync_code[%d]", i);
+
+			transmit(SYNC_CODE[i],CODE_LENGTH);
+			delay(45);
+		}
+		ESP_LOGD(TAG, "light_code")
+		transmit(LIGHT_CODE,CODE_LENGTH);
+	}
+}
diff --git a/components/ratgdo/ratgdo.h b/components/ratgdo/ratgdo.h
new file mode 100644
index 0000000..a062fd9
--- /dev/null
+++ b/components/ratgdo/ratgdo.h
@@ -0,0 +1,108 @@
+/************************************
+ * Rage
+ * Against
+ * The
+ * Garage
+ * Door
+ * Opener
+ *
+ * Copyright (C) 2022  Paul Wieland
+ *
+ * GNU GENERAL PUBLIC LICENSE
+ ************************************/
+
+#ifndef _RATGDO_H
+#define _RATGDO_H
+
+
+#include "BootstrapManager.h" // Must use the https://github.com/PaulWieland/arduinoImprovBootstrapper fork, ratgdo branch
+#include "SoftwareSerial.h" // Using espsoftwareserial https://github.com/plerup/espsoftwareserial
+#include "rolling_code.h"
+#include "home_assistant.h"
+
+SoftwareSerial swSerial;
+
+/********************************** BOOTSTRAP MANAGER *****************************************/
+BootstrapManager bootstrapManager;
+
+/********************************** PIN DEFINITIONS *****************************************/
+#define OUTPUT_GDO D4 // red control terminal / GarageDoorOpener (UART1 TX) pin is D4 on D1 Mini
+#define TRIGGER_OPEN D5 // dry contact for opening door
+#define TRIGGER_CLOSE D6 // dry contact for closing door
+#define TRIGGER_LIGHT D3 // dry contact for triggering light (no discrete light commands, so toggle only)
+#define STATUS_DOOR D0 // output door status, HIGH for open, LOW for closed
+#define STATUS_OBST D8 // output for obstruction status, HIGH for obstructed, LOW for clear
+#define INPUT_RPM1 D1 // RPM1 rotary encoder input OR reed switch if not soldering to the door opener logic board
+#define INPUT_RPM2 D2 // RPM2 rotary encoder input OR not used if using reed switch
+#define INPUT_OBST D7 // black obstruction sensor terminal
+
+
+/********************************** MQTT TOPICS *****************************************/
+String doorCommandTopic = ""; // will be mqttTopicPrefix/deviceName/command
+String setCounterTopic = ""; // will be mqttTopicPrefix/deviceName/set_code_counter
+
+String doorCommand = "";      // will be [open|close|light]
+String overallStatusTopic = ""; // legacy from 1.0. Will be mqttTopicPrefix/deviceName/status
+String availabilityStatusTopic = ""; // online|offline
+String obstructionStatusTopic = ""; // obstructed|clear
+String doorStatusTopic = ""; // open|opening|closing|closed|reed_open|reed_closed
+String rollingCodeTopic = ""; // broadcast the current rolling code count for debugging purposes
+
+/********************************** GLOBAL VARS *****************************************/
+bool setupComplete = false;
+unsigned int rollingCodeCounter;
+byte rollingCode[CODE_LENGTH];
+String doorState = "unknown";        // will be [online|offline|opening|open|closing|closed|obstructed|clear|reed_open|reed_closed]
+
+unsigned int obstructionLowCount = 0;  // count obstruction low pulses
+unsigned long lastObstructionHigh = 0;  // count time between high pulses from the obst ISR
+
+bool doorIsObstructed = false;
+bool dryContactDoorOpen = false;
+bool dryContactDoorClose = false;
+bool dryContactToggleLight = false;
+int doorPositionCounter = 0;         // calculate the door's movement and position
+bool rpm1Pulsed = false;             // did rpm1 get a pulse or not - eliminates an issue when the sensor is parked on a high pulse which fires rpm2 isr
+
+/********************************** FUNCTION DECLARATION *****************************************/
+void callback(char *topic, byte *payload, unsigned int length);
+void manageDisconnections();
+void manageQueueSubscription();
+void manageHardwareButton();
+
+void transmit(byte* payload, unsigned int length);
+void sync();
+void openDoor();
+void closeDoor();
+void toggleLight();
+
+void obstructionLoop();
+void obstructionDetected();
+void obstructionCleared();
+
+void sendDoorStatus();
+
+void doorStateLoop();
+void dryContactLoop();
+
+/********************************** INTERRUPT SERVICE ROUTINES ***********************************/
+void IRAM_ATTR isrDebounce(const char *type);
+void IRAM_ATTR isrDoorOpen();
+void IRAM_ATTR isrDoorClose();
+void IRAM_ATTR isrLight();
+void IRAM_ATTR isrObstruction();
+void IRAM_ATTR isrRPM1();
+void IRAM_ATTR isrRPM2();
+
+/*** Static Codes ***/
+byte SYNC1[] = {0x55,0x01,0x00,0x61,0x12,0x49,0x2c,0x92,0x5b,0x24,0x96,0x86,0x0b,0x65,0x96,0xd9,0x8f,0x26,0x4a};
+byte SYNC2[] = {0x55,0x01,0x00,0x08,0x34,0x93,0x49,0xb4,0x92,0x4d,0x20,0x26,0x1b,0x4d,0xb4,0xdb,0xad,0x76,0x93};
+byte SYNC3[] = {0x55,0x01,0x00,0x06,0x1b,0x2c,0xbf,0x4b,0x6d,0xb6,0x4b,0x18,0x20,0x92,0x09,0x20,0xf2,0x11,0x2c};
+byte SYNC4[] = {0x55,0x01,0x00,0x95,0x29,0x36,0x91,0x29,0x36,0x9a,0x69,0x05,0x2f,0xbe,0xdf,0x6d,0x16,0xcb,0xe7};
+byte* SYNC_CODE[] = {SYNC1,SYNC2,SYNC3,SYNC4};
+
+byte DOOR_CODE[] = {0x55,0x01,0x00,0x94,0x3f,0xef,0xbc,0xfb,0x7f,0xbe,0xfc,0xa6,0x1a,0x4d,0xa6,0xda,0x8d,0x36,0xb3};
+
+byte LIGHT_CODE[] = {0x55,0x01,0x00,0x94,0x3f,0xef,0xbc,0xfb,0x7f,0xbe,0xff,0xa6,0x1a,0x4d,0xa6,0xda,0x8d,0x76,0xb1};
+
+#endif
\ No newline at end of file
diff --git a/components/ratgdo/rolling_code.cpp b/components/ratgdo/rolling_code.cpp
new file mode 100644
index 0000000..e33c087
--- /dev/null
+++ b/components/ratgdo/rolling_code.cpp
@@ -0,0 +1,97 @@
+#include "common.h"
+#include "rolling_code.h"
+#include "secplus.h"
+
+void readCounterFromFlash(){
+	//Open the file
+	File file = LittleFS.open("/rollingcode.txt", "r");
+
+	//Check if the file exists
+	if(!file){
+	Serial.println("rollingcode.txt doesn't exist. creating...");
+
+	writeCounterToFlash();
+	return;
+	}
+
+	rollingCodeCounter = file.parseInt();
+
+	//Close the file
+	file.close();
+}
+
+void writeCounterToFlash(){
+	//Open the file 
+	File file = LittleFS.open("/rollingcode.txt", "w");
+	
+	//Write to the file
+	file.print(rollingCodeCounter);
+	delay(1);
+	//Close the file
+	file.close();
+	
+	Serial.println("Write successful");
+}
+
+void getRollingCode(const char *command){
+	Serial.print("rolling code for ");
+	Serial.print(rollingCodeCounter);
+	Serial.print("|");
+	Serial.print(command);
+	Serial.print(" : ");
+
+	uint64_t id = 0x539;
+	uint64_t fixed = 0;
+	uint32_t data = 0;
+
+	if(strcmp(command,"reboot1") == 0){
+		fixed = 0x400000000;
+		data = 0x0000618b;
+	}else if(strcmp(command,"reboot2") == 0){
+		fixed = 0;
+		data = 0x01009080;
+	}else if(strcmp(command,"reboot3") == 0){
+		fixed = 0;
+		data = 0x0000b1a0;
+	}else if(strcmp(command,"reboot4") == 0){
+		fixed = 0;
+		data = 0x01009080;
+	}else if(strcmp(command,"reboot5") == 0){
+		fixed = 0x300000000;
+		data = 0x00008092;
+	}else if(strcmp(command,"reboot6") == 0){
+		fixed = 0x300000000;
+		data = 0x00008092;
+	}else if(strcmp(command,"door1") == 0){
+		fixed = 0x200000000;
+		data = 0x01018280;
+	}else if(strcmp(command,"door2") == 0){
+		fixed = 0x200000000;
+		data = 0x01009280;
+	}else if(strcmp(command,"light") == 0){
+		fixed = 0x200000000;
+		data = 0x00009281;
+	}else{
+		Serial.println("ERROR: Invalid command");
+		return;
+	}
+
+	fixed = fixed | id;
+
+	encode_wireline(rollingCodeCounter, fixed, data, rollingCode);
+
+	printRollingCode();
+
+	if(strcmp(command,"door1") != 0){ // door2 is created with same counter and should always be called after door1
+	rollingCodeCounter = (rollingCodeCounter + 1) & 0xfffffff;
+	}
+	return;
+}
+
+void printRollingCode(){
+	for(int i = 0; i < CODE_LENGTH; i++){
+	if(rollingCode[i] <= 0x0f) Serial.print("0");
+	Serial.print(rollingCode[i],HEX);
+	}
+	Serial.println("");
+}
\ No newline at end of file
diff --git a/components/ratgdo/rolling_code.h b/components/ratgdo/rolling_code.h
new file mode 100644
index 0000000..c6c8dfd
--- /dev/null
+++ b/components/ratgdo/rolling_code.h
@@ -0,0 +1,18 @@
+#ifndef _RATGDO_ROLLING_CODE_H
+#define _RATGDO_ROLLING_CODE_H
+
+#include <Arduino.h>
+#include <LittleFS.h>
+#include <ArduinoJson.h>
+#include "BootstrapManager.h"
+
+extern "C" {
+#include "secplus.h"
+}
+
+void readCounterFromFlash(); // get the rolling code counter from setup.json & return it
+void writeCounterToFlash(); // write the counter back to setup.json
+void getRollingCode(const char *command); // get the next rolling code for type [reboot1,reboot2,reboot3,reboot4,reboot5,door1,light]
+void printRollingCode();
+
+#endif
\ No newline at end of file
diff --git a/components/ratgdo/secplus.c b/components/ratgdo/secplus.c
new file mode 100644
index 0000000..1e998bb
--- /dev/null
+++ b/components/ratgdo/secplus.c
@@ -0,0 +1,490 @@
+/*
+ * Copyright 2022 Clayton Smith (argilo@gmail.com)
+ *
+ * This file is part of secplus.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include "secplus.h"
+
+int8_t encode_v1(const uint32_t rolling, uint32_t fixed, uint8_t *symbols1,
+                 uint8_t *symbols2) {
+  uint32_t rolling_reversed = 0;
+  int8_t i, half;
+  uint8_t acc;
+  uint8_t *symbols;
+
+  if (fixed >= 3486784401u) {
+    return -1;
+  }
+
+  for (i = 1; i < 32; i++) {
+    rolling_reversed |= ((rolling >> i) & 1) << (32 - i - 1);
+  }
+
+  for (half = 1; half >= 0; half--) {
+    symbols = (half == 0 ? symbols1 : symbols2);
+
+    for (i = 18; i >= 0; i -= 2) {
+      symbols[i] = rolling_reversed % 3;
+      rolling_reversed /= 3;
+      symbols[i + 1] = fixed % 3;
+      fixed /= 3;
+    }
+
+    acc = 0;
+    for (i = 0; i < 20; i += 2) {
+      acc += symbols[i];
+      acc += symbols[i + 1];
+      symbols[i + 1] = acc % 3;
+    }
+  }
+
+  return 0;
+}
+
+int8_t decode_v1(const uint8_t *symbols1, const uint8_t *symbols2,
+                 uint32_t *rolling, uint32_t *fixed) {
+  uint32_t rolling_reversed = 0;
+  uint8_t acc;
+  uint8_t digit;
+  int8_t i, half;
+  const uint8_t *symbols;
+
+  *rolling = 0;
+  *fixed = 0;
+
+  for (half = 0; half < 2; half++) {
+    symbols = (half == 0 ? symbols1 : symbols2);
+    acc = 0;
+    for (i = 0; i < 20; i += 2) {
+      digit = symbols[i];
+      rolling_reversed = (rolling_reversed * 3) + digit;
+      acc += digit;
+
+      digit = (60 + symbols[i + 1] - acc) % 3;
+      *fixed = (*fixed * 3) + digit;
+      acc += digit;
+    }
+  }
+
+  for (i = 0; i < 32; i++) {
+    *rolling |= ((rolling_reversed >> i) & 1) << (32 - i - 1);
+  }
+
+  return 0;
+}
+
+static void v2_calc_parity(const uint64_t fixed, uint32_t *data) {
+  uint32_t parity = (fixed >> 32) & 0xf;
+  int8_t offset;
+
+  *data &= 0xffff0fff;
+  for (offset = 0; offset < 32; offset += 4) {
+    parity ^= ((*data >> offset) & 0xf);
+  }
+  *data |= (parity << 12);
+}
+
+static int8_t v2_check_parity(const uint64_t fixed, const uint32_t data) {
+  uint32_t parity = (fixed >> 32) & 0xf;
+  int8_t offset;
+
+  for (offset = 0; offset < 32; offset += 4) {
+    parity ^= ((data >> offset) & 0xf);
+  }
+
+  if (parity != 0) {
+    return -1;
+  }
+
+  return 0;
+}
+
+static void encode_v2_rolling(const uint32_t rolling,
+                              uint32_t *rolling_halves) {
+  uint32_t rolling_reversed = 0;
+  int8_t i, half;
+
+  for (i = 0; i < 28; i++) {
+    rolling_reversed |= ((rolling >> i) & 1) << (28 - i - 1);
+  }
+
+  rolling_halves[0] = 0;
+  rolling_halves[1] = 0;
+
+  for (half = 0; half < 2; half++) {
+    for (i = 0; i < 8; i += 2) {
+      rolling_halves[half] |= rolling_reversed % 3 << i;
+      rolling_reversed /= 3;
+    }
+  }
+
+  for (half = 0; half < 2; half++) {
+    for (i = 10; i < 18; i += 2) {
+      rolling_halves[half] |= rolling_reversed % 3 << i;
+      rolling_reversed /= 3;
+    }
+  }
+
+  rolling_halves[0] |= (rolling_reversed % 3) << 8;
+  rolling_reversed /= 3;
+
+  rolling_halves[1] |= (rolling_reversed % 3) << 8;
+}
+
+static int8_t decode_v2_rolling(const uint32_t *rolling_halves,
+                                uint32_t *rolling) {
+  int8_t i, half;
+  uint32_t rolling_reversed;
+
+  rolling_reversed = (rolling_halves[1] >> 8) & 3;
+  rolling_reversed = (rolling_reversed * 3) + ((rolling_halves[0] >> 8) & 3);
+
+  for (half = 1; half >= 0; half--) {
+    for (i = 16; i >= 10; i -= 2) {
+      rolling_reversed =
+          (rolling_reversed * 3) + ((rolling_halves[half] >> i) & 3);
+    }
+  }
+
+  for (half = 1; half >= 0; half--) {
+    for (i = 6; i >= 0; i -= 2) {
+      rolling_reversed =
+          (rolling_reversed * 3) + ((rolling_halves[half] >> i) & 3);
+    }
+  }
+
+  if (rolling_reversed >= 0x10000000) {
+    return -1;
+  }
+
+  *rolling = 0;
+  for (i = 0; i < 28; i++) {
+    *rolling |= ((rolling_reversed >> i) & 1) << (28 - i - 1);
+  }
+
+  return 0;
+}
+
+static int8_t v2_combine_halves(const uint8_t frame_type,
+                                const uint32_t *rolling_halves,
+                                const uint32_t *fixed_halves,
+                                const uint16_t *data_halves, uint32_t *rolling,
+                                uint64_t *fixed, uint32_t *data) {
+  int8_t err = 0;
+
+  err = decode_v2_rolling(rolling_halves, rolling);
+  if (err < 0) {
+    return err;
+  }
+
+  *fixed = ((uint64_t)fixed_halves[0] << 20) | fixed_halves[1];
+
+  if (frame_type == 1) {
+    *data = ((uint32_t)data_halves[0] << 16) | data_halves[1];
+
+    err = v2_check_parity(*fixed, *data);
+    if (err < 0) {
+      return err;
+    }
+  }
+
+  return 0;
+}
+
+static const int8_t ORDER[16] = {9,  33, 6, -1, 24, 18, 36, -1,
+                                 24, 36, 6, -1, -1, -1, -1, -1};
+static const int8_t INVERT[16] = {6, 2, 1, -1, 7,  5,  3,  -1,
+                                  4, 0, 5, -1, -1, -1, -1, -1};
+
+static void v2_scramble(const uint32_t *parts, const uint8_t frame_type,
+                        uint8_t *packet_half) {
+  const int8_t order = ORDER[packet_half[0] >> 4];
+  const int8_t invert = INVERT[packet_half[0] & 0xf];
+  int8_t i;
+  uint8_t out_offset = 10;
+  int8_t end;
+  uint32_t parts_permuted[3];
+
+  end = (frame_type == 0 ? 5 : 8);
+  for (i = 1; i < end; i++) {
+    packet_half[i] = 0;
+  }
+
+  parts_permuted[0] =
+      (invert & 4) ? ~parts[(order >> 4) & 3] : parts[(order >> 4) & 3];
+  parts_permuted[1] =
+      (invert & 2) ? ~parts[(order >> 2) & 3] : parts[(order >> 2) & 3];
+  parts_permuted[2] = (invert & 1) ? ~parts[order & 3] : parts[order & 3];
+
+  end = (frame_type == 0 ? 8 : 0);
+  for (i = 18 - 1; i >= end; i--) {
+    packet_half[out_offset >> 3] |= ((parts_permuted[0] >> i) & 1)
+                                    << (7 - (out_offset % 8));
+    out_offset++;
+    packet_half[out_offset >> 3] |= ((parts_permuted[1] >> i) & 1)
+                                    << (7 - (out_offset % 8));
+    out_offset++;
+    packet_half[out_offset >> 3] |= ((parts_permuted[2] >> i) & 1)
+                                    << (7 - (out_offset % 8));
+    out_offset++;
+  }
+}
+
+static int8_t v2_unscramble(const uint8_t frame_type, const uint8_t indicator,
+                            const uint8_t *packet_half, uint32_t *parts) {
+  const int8_t order = ORDER[indicator >> 4];
+  const int8_t invert = INVERT[indicator & 0xf];
+  int8_t i;
+  uint8_t out_offset = 10;
+  const int8_t end = (frame_type == 0 ? 8 : 0);
+  uint32_t parts_permuted[3] = {0, 0, 0};
+
+  if ((order == -1) || (invert == -1)) {
+    return -1;
+  }
+
+  for (i = 18 - 1; i >= end; i--) {
+    parts_permuted[0] |=
+        (uint32_t)((packet_half[out_offset >> 3] >> (7 - (out_offset % 8))) & 1)
+        << i;
+    out_offset++;
+    parts_permuted[1] |=
+        (uint32_t)((packet_half[out_offset >> 3] >> (7 - (out_offset % 8))) & 1)
+        << i;
+    out_offset++;
+    parts_permuted[2] |=
+        (uint32_t)((packet_half[out_offset >> 3] >> (7 - (out_offset % 8))) & 1)
+        << i;
+    out_offset++;
+  }
+
+  parts[(order >> 4) & 3] =
+      (invert & 4) ? ~parts_permuted[0] : parts_permuted[0];
+  parts[(order >> 2) & 3] =
+      (invert & 2) ? ~parts_permuted[1] : parts_permuted[1];
+  parts[order & 3] = (invert & 1) ? ~parts_permuted[2] : parts_permuted[2];
+
+  return 0;
+}
+
+static void encode_v2_half_parts(const uint32_t rolling, const uint32_t fixed,
+                                 const uint16_t data, const uint8_t frame_type,
+                                 uint8_t *packet_half) {
+  uint32_t parts[3];
+
+  parts[0] = ((fixed >> 10) << 8) | (data >> 8);
+  parts[1] = ((fixed & 0x3ff) << 8) | (data & 0xff);
+  parts[2] = rolling;
+
+  packet_half[0] = (uint8_t)rolling;
+
+  v2_scramble(parts, frame_type, packet_half);
+}
+
+static int8_t decode_v2_half_parts(const uint8_t frame_type,
+                                   const uint8_t indicator,
+                                   const uint8_t *packet_half,
+                                   uint32_t *rolling, uint32_t *fixed,
+                                   uint16_t *data) {
+  int8_t err = 0;
+  int8_t i;
+  uint32_t parts[3];
+
+  err = v2_unscramble(frame_type, indicator, packet_half, parts);
+  if (err < 0) {
+    return err;
+  }
+
+  if ((frame_type == 1) && ((parts[2] & 0xff) != indicator)) {
+    return -1;
+  }
+
+  for (i = 8; i < 18; i += 2) {
+    if (((parts[2] >> i) & 3) == 3) {
+      return -1;
+    }
+  }
+
+  *rolling = (parts[2] & 0x3ff00) | indicator;
+  *fixed = ((parts[0] & 0x3ff00) << 2) | ((parts[1] & 0x3ff00) >> 8);
+  *data = ((parts[0] & 0xff) << 8) | (parts[1] & 0xff);
+
+  return 0;
+}
+
+static int8_t v2_check_limits(const uint32_t rolling, const uint64_t fixed) {
+  if ((rolling >> 28) != 0) {
+    return -1;
+  }
+
+  if ((fixed >> 40) != 0) {
+    return -1;
+  }
+
+  return 0;
+}
+
+static void encode_v2_half(const uint32_t rolling, const uint32_t fixed,
+                           const uint16_t data, const uint8_t frame_type,
+                           uint8_t *packet_half) {
+  encode_v2_half_parts(rolling, fixed, data, frame_type, packet_half);
+
+  /* shift indicator two bits to the right */
+  packet_half[1] |= (packet_half[0] & 0x3) << 6;
+  packet_half[0] >>= 2;
+
+  /* set frame type */
+  packet_half[0] |= (frame_type << 6);
+}
+
+int8_t encode_v2(const uint32_t rolling, const uint64_t fixed, uint32_t data,
+                 const uint8_t frame_type, uint8_t *packet1, uint8_t *packet2) {
+  int8_t err = 0;
+  uint32_t rolling_halves[2];
+
+  err = v2_check_limits(rolling, fixed);
+  if (err < 0) {
+    return err;
+  }
+
+  encode_v2_rolling(rolling, rolling_halves);
+  v2_calc_parity(fixed, &data);
+
+  encode_v2_half(rolling_halves[0], fixed >> 20, data >> 16, frame_type,
+                 packet1);
+  encode_v2_half(rolling_halves[1], fixed & 0xfffff, data & 0xffff, frame_type,
+                 packet2);
+
+  return 0;
+}
+
+static int8_t decode_v2_half(const uint8_t frame_type,
+                             const uint8_t *packet_half, uint32_t *rolling,
+                             uint32_t *fixed, uint16_t *data) {
+  int8_t err = 0;
+  const uint8_t indicator = (packet_half[0] << 2) | (packet_half[1] >> 6);
+
+  if ((packet_half[0] >> 6) != frame_type) {
+    return -1;
+  }
+
+  err = decode_v2_half_parts(frame_type, indicator, packet_half, rolling, fixed,
+                             data);
+  if (err < 0) {
+    return err;
+  }
+
+  return 0;
+}
+
+int8_t decode_v2(uint8_t frame_type, const uint8_t *packet1,
+                 const uint8_t *packet2, uint32_t *rolling, uint64_t *fixed,
+                 uint32_t *data) {
+  int8_t err = 0;
+  uint32_t rolling_halves[2];
+  uint32_t fixed_halves[2];
+  uint16_t data_halves[2];
+
+  err = decode_v2_half(frame_type, packet1, &rolling_halves[0],
+                       &fixed_halves[0], &data_halves[0]);
+  if (err < 0) {
+    return err;
+  }
+
+  err = decode_v2_half(frame_type, packet2, &rolling_halves[1],
+                       &fixed_halves[1], &data_halves[1]);
+  if (err < 0) {
+    return err;
+  }
+
+  err = v2_combine_halves(frame_type, rolling_halves, fixed_halves, data_halves,
+                          rolling, fixed, data);
+  if (err < 0) {
+    return err;
+  }
+
+  return 0;
+}
+
+static void encode_wireline_half(const uint32_t rolling, const uint32_t fixed,
+                                 const uint16_t data, uint8_t *packet_half) {
+  encode_v2_half_parts(rolling, fixed, data, 1, packet_half);
+}
+
+int8_t encode_wireline(const uint32_t rolling, const uint64_t fixed,
+                       uint32_t data, uint8_t *packet) {
+  int8_t err = 0;
+  uint32_t rolling_halves[2];
+
+  err = v2_check_limits(rolling, fixed);
+  if (err < 0) {
+    return err;
+  }
+
+  encode_v2_rolling(rolling, rolling_halves);
+  v2_calc_parity(fixed, &data);
+
+  packet[0] = 0x55;
+  packet[1] = 0x01;
+  packet[2] = 0x00;
+
+  encode_wireline_half(rolling_halves[0], fixed >> 20, data >> 16, &packet[3]);
+  encode_wireline_half(rolling_halves[1], fixed & 0xfffff, data & 0xffff,
+                       &packet[11]);
+
+  return 0;
+}
+
+static int8_t decode_wireline_half(const uint8_t *packet_half,
+                                   uint32_t *rolling, uint32_t *fixed,
+                                   uint16_t *data) {
+  int8_t err = 0;
+  const uint8_t indicator = packet_half[0];
+
+  if ((packet_half[1] >> 6) != 0) {
+    return -1;
+  }
+
+  err = decode_v2_half_parts(1, indicator, packet_half, rolling, fixed, data);
+  if (err < 0) {
+    return err;
+  }
+
+  return 0;
+}
+
+int8_t decode_wireline(const uint8_t *packet, uint32_t *rolling,
+                       uint64_t *fixed, uint32_t *data) {
+  int8_t err = 0;
+  uint32_t rolling_halves[2];
+  uint32_t fixed_halves[2];
+  uint16_t data_halves[2];
+
+  if ((packet[0] != 0x55) || (packet[1] != 0x01) || (packet[2] != 0x00)) {
+    return -1;
+  }
+
+  err = decode_wireline_half(&packet[3], &rolling_halves[0], &fixed_halves[0],
+                             &data_halves[0]);
+  if (err < 0) {
+    return err;
+  }
+
+  err = decode_wireline_half(&packet[11], &rolling_halves[1], &fixed_halves[1],
+                             &data_halves[1]);
+  if (err < 0) {
+    return err;
+  }
+
+  err = v2_combine_halves(1, rolling_halves, fixed_halves, data_halves, rolling,
+                          fixed, data);
+  if (err < 0) {
+    return err;
+  }
+
+  return 0;
+}
diff --git a/components/ratgdo/secplus.h b/components/ratgdo/secplus.h
new file mode 100644
index 0000000..f30b792
--- /dev/null
+++ b/components/ratgdo/secplus.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 Clayton Smith (argilo@gmail.com)
+ *
+ * This file is part of secplus.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#ifndef SECPLUS_H
+#define SECPLUS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdint.h>
+
+extern int8_t encode_v1(uint32_t rolling, uint32_t fixed, uint8_t *symbols1,
+                        uint8_t *symbols2);
+
+extern int8_t decode_v1(const uint8_t *symbols1, const uint8_t *symbols2,
+                        uint32_t *rolling, uint32_t *fixed);
+
+extern int8_t encode_v2(uint32_t rolling, uint64_t fixed, uint32_t data,
+                        uint8_t frame_type, uint8_t *packet1, uint8_t *packet2);
+
+extern int8_t decode_v2(uint8_t frame_type, const uint8_t *packet1,
+                        const uint8_t *packet2, uint32_t *rolling,
+                        uint64_t *fixed, uint32_t *data);
+
+extern int8_t encode_wireline(uint32_t rolling, uint64_t fixed, uint32_t data,
+                              uint8_t *packet);
+
+extern int8_t decode_wireline(const uint8_t *packet, uint32_t *rolling,
+                              uint64_t *fixed, uint32_t *data);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif