/* xdrv_36_keeloq.ino - Jarolift Keeloq shutter support for Tasmota Copyright (C) 2021 he-so This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_KEELOQ /*********************************************************************************************\ * Keeloq shutter support * * Uses hardware SPI and two user configurable GPIO's (CC1101 GDO0 and CC1101 GDO2) * * Considering the implementation these two user GPIO's are fake. * Only CC1101 GDO0 is used and must always be GPIO05 dictated by the used CC1101 library. \*********************************************************************************************/ #define XDRV_36 36 #include "cc1101.h" #include #define SYNC_WORD 199 #define Lowpulse 400 #define Highpulse 800 const char kJaroliftCommands[] PROGMEM = "Keeloq|" // prefix "SendRaw|SendButton|Set"; void (* const jaroliftCommand[])(void) PROGMEM = { &CmndSendRaw, &CmdSendButton, &CmdSet}; CC1101 cc1101; struct JAROLIFT_DEVICE { int device_key_msb = 0x0; // stores cryptkey MSB int device_key_lsb = 0x0; // stores cryptkey LSB uint64_t button = 0x0; // 1000=0x8 up, 0100=0x4 stop, 0010=0x2 down, 0001=0x1 learning int disc = 0x0100; // 0x0100 for single channel remote uint32_t enc = 0x0; // stores the 32Bit encrypted code uint64_t pack = 0; // Contains data to send. int count = 0; uint32_t serial = 0x0; int8_t port_tx; int8_t port_rx; } jaroliftDevice; void CmdSet(void) { if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.payload > 0) { char *p; uint32_t i = 0; uint32_t param[4] = { 0 }; for (char *str = strtok_r(XdrvMailbox.data, ", ", &p); str && i < 4; str = strtok_r(nullptr, ", ", &p)) { param[i] = strtoul(str, nullptr, 0); i++; } for (uint32_t i = 0; i < 3; i++) { if (param[i] < 1) { param[i] = 1; } // msb, lsb, serial, counter } DEBUG_DRIVER_LOG(PSTR("KLQ: params %08x %08x %08x %08x"), param[0], param[1], param[2], param[3]); Settings->keeloq_master_msb = param[0]; Settings->keeloq_master_lsb = param[1]; Settings->keeloq_serial = param[2]; Settings->keeloq_count = param[3]; jaroliftDevice.serial = param[2]; jaroliftDevice.count = param[3]; GenerateDeviceCryptKey(); ResponseCmndDone(); } else { DEBUG_DRIVER_LOG(PSTR("KLQ: no payload")); } } else { DEBUG_DRIVER_LOG(PSTR("KLQ: no param")); } } void GenerateDeviceCryptKey() { Keeloq k(Settings->keeloq_master_msb, Settings->keeloq_master_lsb); jaroliftDevice.device_key_msb = k.decrypt(jaroliftDevice.serial | 0x60000000L); jaroliftDevice.device_key_lsb = k.decrypt(jaroliftDevice.serial | 0x20000000L); AddLog(LOG_LEVEL_DEBUG, PSTR("KLQ: generated device keys %08x %08x"), jaroliftDevice.device_key_msb, jaroliftDevice.device_key_lsb); } void CmdSendButton(void) { noInterrupts(); entertx(); if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.payload > 0) { jaroliftDevice.button = strtoul(XdrvMailbox.data, nullptr, 0); DEBUG_DRIVER_LOG(PSTR("KLQ: msb %08x, lsb %08x, serial %08x, disc %08x, button %08x"), jaroliftDevice.device_key_msb, jaroliftDevice.device_key_lsb, jaroliftDevice.serial, jaroliftDevice.disc, jaroliftDevice.button); AddLog(LOG_LEVEL_DEBUG, PSTR("KLQ: count %08x"), jaroliftDevice.count); CreateKeeloqPacket(); jaroliftDevice.count++; Settings->keeloq_count = jaroliftDevice.count; for(int repeat = 0; repeat <= 1; repeat++) { uint64_t bitsToSend = jaroliftDevice.pack; digitalWrite(jaroliftDevice.port_tx, LOW); delayMicroseconds(1150); SendSyncPreamble(13); delayMicroseconds(3500); for(int i=72; i>0; i--) { SendBit(bitsToSend & 0x0000000000000001); bitsToSend >>= 1; } DEBUG_DRIVER_LOG(PSTR("KLQ: finished sending bits at %d"), micros()); delay(16); // delay in loop context is save for wdt } } } interrupts(); enterrx(); ResponseCmndDone(); } void SendBit(byte bitToSend) { if (bitToSend==1) { digitalWrite(jaroliftDevice.port_tx, LOW); // Simple encoding of bit state 1 delayMicroseconds(Lowpulse); digitalWrite(jaroliftDevice.port_tx, HIGH); delayMicroseconds(Highpulse); } else { digitalWrite(jaroliftDevice.port_tx, LOW); // Simple encoding of bit state 0 delayMicroseconds(Highpulse); digitalWrite(jaroliftDevice.port_tx, HIGH); delayMicroseconds(Lowpulse); } } void CmndSendRaw(void) { DEBUG_DRIVER_LOG(PSTR("KLQ: cmd send called at %d"), micros()); noInterrupts(); entertx(); for(int repeat = 0; repeat <= 1; repeat++) { if (XdrvMailbox.data_len > 0) { digitalWrite(jaroliftDevice.port_tx, LOW); delayMicroseconds(1150); SendSyncPreamble(13); delayMicroseconds(3500); for(int i=XdrvMailbox.data_len-1; i>=0; i--) { SendBit(XdrvMailbox.data[i] == '1'); } DEBUG_DRIVER_LOG(PSTR("KLQ: finished sending bits at %d"), micros()); delay(16); // delay in loop context is save for wdt } interrupts(); } enterrx(); ResponseCmndDone(); } void enterrx() { unsigned char marcState = 0; cc1101.setRxState(); delay(2); unsigned long rx_time = micros(); while (((marcState = cc1101.readStatusReg(CC1101_MARCSTATE)) & 0x1F) != 0x0D ) { if (micros() - rx_time > 50000) break; // Quit when marcState does not change... } } void entertx() { unsigned char marcState = 0; cc1101.setTxState(); delay(2); unsigned long rx_time = micros(); while (((marcState = cc1101.readStatusReg(CC1101_MARCSTATE)) & 0x1F) != 0x13 && 0x14 && 0x15) { if (micros() - rx_time > 50000) break; // Quit when marcState does not change... } } void SendSyncPreamble(int l) { for (int i = 0; i < l; ++i) { digitalWrite(jaroliftDevice.port_tx, LOW); delayMicroseconds(400); digitalWrite(jaroliftDevice.port_tx, HIGH); delayMicroseconds(380); } } void CreateKeeloqPacket() { Keeloq k(jaroliftDevice.device_key_msb, jaroliftDevice.device_key_lsb); unsigned int result = (jaroliftDevice.disc << 16) | jaroliftDevice.count; jaroliftDevice.pack = (uint64_t)0; jaroliftDevice.pack |= jaroliftDevice.serial & 0xfffffffL; jaroliftDevice.pack |= (jaroliftDevice.button & 0xfL) << 28; jaroliftDevice.pack <<= 32; jaroliftDevice.enc = k.encrypt(result); jaroliftDevice.pack |= jaroliftDevice.enc; AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("KLQ: pack high %08x, low %08x"), jaroliftDevice.pack>>32, jaroliftDevice.pack); } void KeeloqInit() { jaroliftDevice.port_tx = Pin(GPIO_CC1101_GDO2); // Output port for transmission jaroliftDevice.port_rx = Pin(GPIO_CC1101_GDO0); // Input port for reception DEBUG_DRIVER_LOG(PSTR("KLQ: cc1101.init()")); delay(100); cc1101.init(); AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("KLQ: CC1101 done")); cc1101.setSyncWord(SYNC_WORD, false); cc1101.setCarrierFreq(CFREQ_433); cc1101.disableAddressCheck(); pinMode(jaroliftDevice.port_tx, OUTPUT); pinMode(jaroliftDevice.port_rx, INPUT_PULLUP); jaroliftDevice.serial = Settings->keeloq_serial; jaroliftDevice.count = Settings->keeloq_count; GenerateDeviceCryptKey(); } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv36(uint32_t function) { if (!PinUsed(GPIO_CC1101_GDO0) || !PinUsed(GPIO_CC1101_GDO2)) { return false; } bool result = false; switch (function) { case FUNC_COMMAND: AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("KLQ: calling command")); result = DecodeCommand(kJaroliftCommands, jaroliftCommand); break; case FUNC_INIT: KeeloqInit(); DEBUG_DRIVER_LOG(PSTR("KLQ: init done")); break; } return result; } #endif // USE_KEELOQ