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 + +#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 +#include +#include +#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 + +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