/* xdrv_47_ftc532.ino - FTC532 touch buttons support for Tasmota Copyright (C) 2021 Peter Franck and Theo Arends 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_FTC532 /*********************************************************************************************\ THE PLAN [tm]: OUTCOME: ============== ======== appear in a dropdown (D_SENSOR_FTC532 "FTC532") select pin (GPIO_FTC532) attach interrupt to pin DONE ISR updating all 8 inputs DONE de-bouncing for 50 ms DONE change report every 250 ms REPORTS EVERY 50MS Webserver display "00001001" DONE & REMOVED MQTT message DONE Rules hook DONE detach interrupt before restart POINTLESS THE PROTOCOL [tm]: ================== LEAD-IN = 3015 µs HIGH, 755 µs LOW S = 377 µs HIGH, 377 µs LOW L = 377 µs HIGH, 1130 µs LOW GROUP1 GROUP2 --------------------------- ALL OFF: SSSSLLLL SSSSLLLL 1 ON : LSSSSLLL SSSSLLLL 2 ON : SLSSLSLL SSSSLLLL 3 ON : SSLSLLSL SSSSLLLL 8 ON : SSSSLLLL SSSLLLLS 123 ON : LLLSSSSL SSSSLLLL Timing of an ALL OFF frame in clock cycles T=377µs, triggering on rising edge: IDLE-2222444422224444-IDLE ********************* * About this driver * ********************* This driver implements the reverse engineered protocol of the FTC532 touch controller. The protocol encodes the bitmap of touched keys in variable length pulses comprising a fixed length frame. These frames are then sent out continuously from the FTC532 chip. The first version of this driver was working fine on well behaved hardware. After being released to the field and installed on crappy hardware in noisy environments it developed a habit of random 'ghost' switchings at night, much to the chagrin of some users. This is almost a re-write containing more timing (and other) checks in order to detect and mitigate various incarnations of noise. If you should still experience 'ghost switching' issues a solution may be increasing FTC532_DEBOUNCE to 2 or higher. That will enable the de-bouncing code, at the expense of 'snappiness' of touch reactions. Higher values will accumulate more samples in 50 ms steps before actually firing the rules trigger. It will eat a few bytes off your RAM and Flash budget, too. Usage: ------ This driver does not actually switch anything. It is a pure "rules" driver that solely emits {"FTC532":{"KEYS":"XX"}} JSON messages to be used in a rule or by an MQTT broker. "XX" stands for the hexadecimal (big endian) representation of a bitmap of keys currently touched, where e.g. "00" means "no key touched" while "03" means "keys 1 and 2 touched simultaneously". Selecting "FTC532" on a GPIO will awake the driver. This driver can only be selected once. \*********************************************************************************************/ #define XDRV_47 47 #define FTC532_DEBOUNCE 0 // no. of cycles, < 2 disables the code #define FTC532_KEYS 4 // number of key pins on chip #define FTC532_KEYS_MAX 8 // number of key slots in protocol #define FTC532_STATE_WAITING 0x1 #define FTC532_STATE_READING 0x2 #define FTC532_STATE_COMPLETE 0x4 // Rising edge timing in microseconds #define FTC532_BIT 377 #define FTC532_NOISE (FTC532_BIT * 3 / 2) #define FTC532_SHORT (FTC532_BIT * 2) #define FTC532_LONG (FTC532_BIT * 4) #define FTC532_IDLE (FTC532_BIT * 10) #define FTC532_MAX (FTC532_BIT * 58) struct FTC532 { volatile uint32_t rxtime; // ISR timer memory volatile uint16_t tsmp = 0; // buffer for bit-coded time samples volatile uint16_t sample = 0xF0F0; // valid samples volatile uint16_t rxbit; // ISR bit counter volatile uint16_t state; // ISR state uint8_t keys = 0; // bitmap of active keys uint8_t old_keys = 0; // previously active keys bool present = false; // driver active #if FTC532_DEBOUNCE > 1 uint8_t key_cnt = 0; // used to de-bounce #endif // FTC532_DEBOUNCE > 1 #ifdef DEBUG_FTC532 volatile uint16_t e_inv = 0; // inverted key error counter volatile uint16_t e_frame = 0; // frame error counter volatile uint16_t e_noise = 0; // noise detection counter volatile bool valid = 0; // did we ever receive valid data? #endif // DEBUG_FTC532 } Ftc532; const char ftc532_json[] PROGMEM = "\"FTC532\":{\"KEYS\":\""; void IRAM_ATTR ftc532_ISR(void) { // Hardware interrupt routine, triggers on rising edge uint32_t time = micros(); uint32_t time_diff = time - Ftc532.rxtime; Ftc532.rxtime = time; if (Ftc532.state & (FTC532_STATE_WAITING | FTC532_STATE_COMPLETE)) { if (time_diff > FTC532_LONG + FTC532_SHORT) { // new frame Ftc532.rxbit = 0; if (Ftc532.state & FTC532_STATE_COMPLETE) { Ftc532.sample = Ftc532.tsmp; // copy completed frame #ifdef DEBUG_FTC532 Ftc532.valid = true; #endif // DEBUG_FTC532 } Ftc532.state = FTC532_STATE_READING; Ftc532.tsmp = 0; } else { Ftc532.state = FTC532_STATE_WAITING; } return; } // FTC532_STATE_READING starts here if (time_diff > FTC532_LONG + FTC532_BIT) { #ifdef DEBUG_FTC532 ++Ftc532.e_frame; // frame error #endif // DEBUG_FTC532 Ftc532.state = FTC532_STATE_WAITING; return; } if (time_diff > FTC532_SHORT + FTC532_BIT) { Ftc532.tsmp |= (1 << Ftc532.rxbit); // LONG } else if (time_diff < FTC532_NOISE) { // NOISE (SHORT now implicitly default) #ifdef DEBUG_FTC532 ++Ftc532.e_noise; #endif // DEBUG_FTC532 Ftc532.state = FTC532_STATE_WAITING; return; } ++Ftc532.rxbit; if (Ftc532.rxbit == FTC532_KEYS_MAX * 2) { // frame complete Ftc532.state = FTC532_STATE_COMPLETE; } } void ftc532_init(void) { // Initialize if (!PinUsed(GPIO_FTC532)) { return; } Ftc532.state = FTC532_STATE_WAITING; pinMode(Pin(GPIO_FTC532), INPUT_PULLUP); Ftc532.rxtime = micros(); attachInterrupt(Pin(GPIO_FTC532), ftc532_ISR, RISING); Ftc532.present = true; } void ftc532_update(void) { // Usually called every 50 ms if ((Ftc532.sample & 0xF0F0) == ((~Ftc532.sample & 0x0F0F) << 4) && (Ftc532.sample >> 8) == 0xF0) { Ftc532.keys = Ftc532.sample & 0xF; if (Ftc532.keys != Ftc532.old_keys) { #if FTC532_DEBOUNCE > 1 if (++Ftc532.key_cnt >= FTC532_DEBOUNCE) { #endif // FTC532_DEBOUNCE > 1 #ifdef DEBUG_FTC532 AddLog(LOG_LEVEL_DEBUG, PSTR("FTC: SAM=%04X KEY=%X OLD=%X INV=%u NOI=%u FRM=%u OK=%u TIME=%lu Pin=%u"), Ftc532.sample, Ftc532.keys, Ftc532.old_keys, Ftc532.e_inv, Ftc532.e_noise, Ftc532.e_frame, Ftc532.valid, Ftc532.rxtime, Pin(GPIO_FTC532)); #endif // DEBUG_FTC532 ftc532_publish(); Ftc532.old_keys = Ftc532.keys; #if FTC532_DEBOUNCE > 1 Ftc532.key_cnt = 0; } } else { Ftc532.key_cnt = 0; #endif // FTC532_DEBOUNCE > 1 } } #ifdef DEBUG_FTC532 else { ++Ftc532.e_inv; AddLog(LOG_LEVEL_DEBUG, PSTR("FTC: ILL SAM=%04X"), Ftc532.sample); } #endif // DEBUG_FTC532 } void ftc532_show() { ResponseAppend_P(PSTR(",%s%02X\"}"), ftc532_json, Ftc532.keys); // Hex keys need JSON quotes } void ftc532_publish(void) { Response_P(PSTR("{%s%02X\"}}"), ftc532_json, Ftc532.keys); // Hex keys need JSON quotes MqttPublishTeleSensor(); } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv47(uint32_t function) { bool result = false; if (FUNC_INIT == function) { // Initialize driver ftc532_init(); } else if (Ftc532.present) { switch (function) { // timed callback functions case FUNC_EVERY_50_MSECOND: ftc532_update(); break; // Generate JSON telemetry string case FUNC_JSON_APPEND: ftc532_show(); break; case FUNC_ACTIVE: result = true; break; } } // Return bool result return result; } #endif // USE_FTC532