diff --git a/I2CDEVICES.md b/I2CDEVICES.md index e8aee48d9..8580080ee 100644 --- a/I2CDEVICES.md +++ b/I2CDEVICES.md @@ -93,7 +93,8 @@ Index | Define | Driver | Device | Address(es) | Bus2 | Descrip 55 | USE_EZODO | xsns_78 | EZODO | 0x61 - 0x70 | | Disolved Oxygen sensor 55 | USE_EZORGB | xsns_78 | EZORGB | 0x61 - 0x70 | | Color sensor 55 | USE_EZOPMP | xsns_78 | EZOPMP | 0x61 - 0x70 | | Peristaltic Pump - 56 | USE_SEESAW_SOIL | xsns_81 | SEESOIL | 0x36 - 0x39 | | Adafruit seesaw soil moisture sensor + 56 | USE_SEESAW_SOIL | xsns_81 | SEESOIL | 0x36 - 0x39 | | Adafruit Seesaw soil moisture & temp sensor + 56 | USE_SEESAW_ENCODER | xsns_81 | SEEENC | 0x36 - 0x39 | | Adafruit Seesaw rotary encoder 57 | USE_TOF10120 | xsns_84 | TOF10120 | 0x52 | | Time-of-flight (ToF) distance sensor 58 | USE_MPU_ACCEL | xsns_85 | MPU_ACCEL| 0x68 | Yes | MPU6886/MPU9250 6-axis MotionTracking sensor from M5Stack 59 | USE_BM8563 | xdrv_56 | BM8563 | 0x51 | Yes | BM8563 RTC from M5Stack diff --git a/lib/lib_i2c/Adafruit_seesaw_soilsensor_1.3.1/Adafruit_seesaw.cpp b/lib/lib_i2c/Adafruit_Seesaw-1.7.9/Adafruit_seesaw.cpp similarity index 78% rename from lib/lib_i2c/Adafruit_seesaw_soilsensor_1.3.1/Adafruit_seesaw.cpp rename to lib/lib_i2c/Adafruit_Seesaw-1.7.9/Adafruit_seesaw.cpp index bbf49c265..5a6f37f47 100644 --- a/lib/lib_i2c/Adafruit_seesaw_soilsensor_1.3.1/Adafruit_seesaw.cpp +++ b/lib/lib_i2c/Adafruit_Seesaw-1.7.9/Adafruit_seesaw.cpp @@ -27,6 +27,7 @@ */ #include "Adafruit_seesaw.h" +#include //#define SEESAW_I2C_DEBUG @@ -59,24 +60,75 @@ Adafruit_seesaw::Adafruit_seesaw(TwoWire *i2c_bus) { * @return true if we could connect to the seesaw, false otherwise ****************************************************************************************/ bool Adafruit_seesaw::begin(uint8_t addr, int8_t flow, bool reset) { - _i2caddr = addr; _flow = flow; if (_flow != -1) ::pinMode(_flow, INPUT); - _i2c_init(); - - if (reset) { - SWReset(); - delay(500); + if (_i2c_dev) { + delete _i2c_dev; } - uint8_t c = this->read8(SEESAW_STATUS_BASE, SEESAW_STATUS_HW_ID); - if (c != SEESAW_HW_ID_CODE) { + _i2c_dev = new Adafruit_I2CDevice(addr, _i2cbus); + + bool found = false; + for (int retries = 0; retries < 10; retries++) { + if (_i2c_dev->begin()) { + found = true; + break; + } + delay(10); + } + + if (!found) { return false; } - return true; + +#ifdef SEESAW_I2C_DEBUG + Serial.println("Begun"); +#endif + + if (reset) { + found = false; + SWReset(); + for (int retries = 0; retries < 10; retries++) { + if (_i2c_dev->detected()) { + found = true; + break; + } + delay(10); + } + } + + if (!found) { + return false; + } + +#ifdef SEESAW_I2C_DEBUG + Serial.println("Reset"); +#endif + + found = false; + for (int retries = 0; !found && retries < 10; retries++) { + uint8_t c = 0; + + this->read(SEESAW_STATUS_BASE, SEESAW_STATUS_HW_ID, &c, 1); + if ((c == SEESAW_HW_ID_CODE_SAMD09) || (c == SEESAW_HW_ID_CODE_TINY817) || + (c == SEESAW_HW_ID_CODE_TINY807) || (c == SEESAW_HW_ID_CODE_TINY816) || + (c == SEESAW_HW_ID_CODE_TINY806) || (c == SEESAW_HW_ID_CODE_TINY1616) || + (c == SEESAW_HW_ID_CODE_TINY1617)) { + found = true; + _hardwaretype = c; + } + + delay(10); + } + +#ifdef SEESAW_I2C_DEBUG + Serial.println("Done!"); +#endif + + return found; } /*! @@ -85,10 +137,11 @@ bool Adafruit_seesaw::begin(uint8_t addr, int8_t flow, bool reset) { *their default values. * This is called automatically from *Adafruit_seesaw.begin() + * @returns True on I2C write success, false otherwise ********************************************************************/ -void Adafruit_seesaw::SWReset() { - this->write8(SEESAW_STATUS_BASE, SEESAW_STATUS_SWRST, 0xFF); +bool Adafruit_seesaw::SWReset() { + return this->write8(SEESAW_STATUS_BASE, SEESAW_STATUS_SWRST, 0xFF); } /*! @@ -120,6 +173,26 @@ uint32_t Adafruit_seesaw::getVersion() { return ret; } +/*! + ********************************************************************* + * @brief Returns the version of the seesaw + * @param pid Pointer to uint16_t for product code result. + * @param year Pointer to uint8_t for date code year result. + * @param mon Pointer to uint8_t for date code month result. + * @param day Pointer to uint8_t for date code day result. + * @return Always returns true. + ********************************************************************/ +bool Adafruit_seesaw::getProdDatecode(uint16_t *pid, uint8_t *year, + uint8_t *mon, uint8_t *day) { + uint32_t vers = getVersion(); + *pid = vers >> 16; + + *year = vers & 0x3F; + *mon = (vers >> 7) & 0xF; + *day = (vers >> 11) & 0x1F; + return true; +} + /*! ************************************************************************** * @brief Set the mode of a GPIO pin. @@ -236,23 +309,34 @@ void Adafruit_seesaw::setGPIOInterrupts(uint32_t pins, bool enabled) { ***********************************************************************/ uint16_t Adafruit_seesaw::analogRead(uint8_t pin) { uint8_t buf[2]; - uint8_t p; - switch (pin) { - case ADC_INPUT_0_PIN: - p = 0; - break; - case ADC_INPUT_1_PIN: - p = 1; - break; - case ADC_INPUT_2_PIN: - p = 2; - break; - case ADC_INPUT_3_PIN: - p = 3; - break; - default: + uint8_t p = 0; + + if (_hardwaretype == SEESAW_HW_ID_CODE_SAMD09) { + switch (pin) { + case ADC_INPUT_0_PIN: + p = 0; + break; + case ADC_INPUT_1_PIN: + p = 1; + break; + case ADC_INPUT_2_PIN: + p = 2; + break; + case ADC_INPUT_3_PIN: + p = 3; + break; + default: + return 0; + } + } else if ((_hardwaretype == SEESAW_HW_ID_CODE_TINY807) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY817) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY816) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY806) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY1616) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY1617)) { + p = pin; + } else { return 0; - break; } this->read(SEESAW_ADC_BASE, SEESAW_ADC_CHANNEL_OFFSET + p, buf, 2, 500); @@ -273,12 +357,14 @@ uint16_t Adafruit_seesaw::touchRead(uint8_t pin) { uint8_t buf[2]; uint8_t p = pin; uint16_t ret = 65535; - do { - delay(1); - this->read(SEESAW_TOUCH_BASE, SEESAW_TOUCH_CHANNEL_OFFSET + p, buf, 2, - 1000); - ret = ((uint16_t)buf[0] << 8) | buf[1]; - } while (ret == 65535); + + for (uint8_t retry = 0; retry < 5; retry++) { + if (this->read(SEESAW_TOUCH_BASE, SEESAW_TOUCH_CHANNEL_OFFSET + p, buf, 2, + 3000 + retry * 1000)) { + ret = ((uint16_t)buf[0] << 8) | buf[1]; + break; + } + } return ret; } @@ -396,6 +482,19 @@ void Adafruit_seesaw::digitalWriteBulk(uint32_t pinsa, uint32_t pinsb, this->write(SEESAW_GPIO_BASE, SEESAW_GPIO_BULK_CLR, cmd, 8); } +/*! + ***************************************************************************************** + * @brief write the entire GPIO port at once. + * + * @param port_values The up-to-32 values to write to the pins, doesn't + *set direction used for bulk writing quickly all valid pins + ****************************************************************************************/ +void Adafruit_seesaw::digitalWriteBulk(uint32_t port_values) { + uint8_t cmd[] = {(uint8_t)(port_values >> 24), (uint8_t)(port_values >> 16), + (uint8_t)(port_values >> 8), (uint8_t)port_values}; + this->write(SEESAW_GPIO_BASE, SEESAW_GPIO_BULK, cmd, 4); +} + /*! ***************************************************************************************** * @brief write a PWM value to a PWM-enabled pin @@ -409,32 +508,43 @@ void Adafruit_seesaw::digitalWriteBulk(uint32_t pinsa, uint32_t pinsb, ****************************************************************************************/ void Adafruit_seesaw::analogWrite(uint8_t pin, uint16_t value, uint8_t width) { int8_t p = -1; - switch (pin) { - case PWM_0_PIN: - p = 0; - break; - case PWM_1_PIN: - p = 1; - break; - case PWM_2_PIN: - p = 2; - break; - case PWM_3_PIN: - p = 3; - break; - default: - break; - } - if (p > -1) { - if (width == 16) { - uint8_t cmd[] = {(uint8_t)p, (uint8_t)(value >> 8), (uint8_t)value}; - this->write(SEESAW_TIMER_BASE, SEESAW_TIMER_PWM, cmd, 3); - } else { - uint16_t mappedVal = map(value, 0, 255, 0, 65535); - uint8_t cmd[] = {(uint8_t)p, (uint8_t)(mappedVal >> 8), - (uint8_t)mappedVal}; - this->write(SEESAW_TIMER_BASE, SEESAW_TIMER_PWM, cmd, 3); + + if (_hardwaretype == SEESAW_HW_ID_CODE_SAMD09) { + switch (pin) { + case PWM_0_PIN: + p = 0; + break; + case PWM_1_PIN: + p = 1; + break; + case PWM_2_PIN: + p = 2; + break; + case PWM_3_PIN: + p = 3; + break; + default: + return; } + } else if ((_hardwaretype == SEESAW_HW_ID_CODE_SAMD09) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY817) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY807) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY816) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY806) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY1616) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY1617)) { + p = pin; + } else { + return; + } + + if (width == 16) { + uint8_t cmd[] = {(uint8_t)p, (uint8_t)(value >> 8), (uint8_t)value}; + this->write(SEESAW_TIMER_BASE, SEESAW_TIMER_PWM, cmd, 3); + } else { + uint16_t mappedVal = map(value, 0, 255, 0, 65535); + uint8_t cmd[] = {(uint8_t)p, (uint8_t)(mappedVal >> 8), (uint8_t)mappedVal}; + this->write(SEESAW_TIMER_BASE, SEESAW_TIMER_PWM, cmd, 3); } } @@ -453,22 +563,35 @@ void Adafruit_seesaw::analogWrite(uint8_t pin, uint16_t value, uint8_t width) { ******************************************************************************/ void Adafruit_seesaw::setPWMFreq(uint8_t pin, uint16_t freq) { int8_t p = -1; - switch (pin) { - case PWM_0_PIN: - p = 0; - break; - case PWM_1_PIN: - p = 1; - break; - case PWM_2_PIN: - p = 2; - break; - case PWM_3_PIN: - p = 3; - break; - default: - break; + + if (_hardwaretype == SEESAW_HW_ID_CODE_SAMD09) { + switch (pin) { + case PWM_0_PIN: + p = 0; + break; + case PWM_1_PIN: + p = 1; + break; + case PWM_2_PIN: + p = 2; + break; + case PWM_3_PIN: + p = 3; + break; + default: + break; + } + } else if ((_hardwaretype == SEESAW_HW_ID_CODE_TINY817) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY807) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY816) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY806) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY1616) || + (_hardwaretype == SEESAW_HW_ID_CODE_TINY1617)) { + p = pin; + } else { + return; } + if (p > -1) { uint8_t cmd[] = {(uint8_t)p, (uint8_t)(freq >> 8), (uint8_t)freq}; this->write(SEESAW_TIMER_BASE, SEESAW_TIMER_FREQ, cmd, 3); @@ -516,6 +639,33 @@ char Adafruit_seesaw::readSercomData(uint8_t sercom) { return this->read8(SEESAW_SERCOM0_BASE + sercom, SEESAW_SERCOM_DATA); } +/*! + ***************************************************************************************** + * @brief Return the EEPROM address used to store I2C address + * + * @return the EEPROM address location + ****************************************************************************************/ +uint8_t Adafruit_seesaw::getI2CaddrEEPROMloc() { + // All SAMDs use fixed location -> 0x3F + // ATtinys place at end of EEPROM, so can vary: + // 8xx have 128B of EEPROM -> 0x7F + // 16xx have 256B of EERPOM -> 0xFF + switch (_hardwaretype) { + case SEESAW_HW_ID_CODE_SAMD09: + return 0x3F; + case SEESAW_HW_ID_CODE_TINY817: + case SEESAW_HW_ID_CODE_TINY807: + case SEESAW_HW_ID_CODE_TINY816: + case SEESAW_HW_ID_CODE_TINY806: + return 0x7F; + case SEESAW_HW_ID_CODE_TINY1616: + case SEESAW_HW_ID_CODE_TINY1617: + return 0xFF; + default: + return 0x00; + } +} + /*! ***************************************************************************************** * @brief Set the seesaw I2C address. This will automatically call @@ -525,7 +675,7 @@ char Adafruit_seesaw::readSercomData(uint8_t sercom) { *I2C address. ****************************************************************************************/ void Adafruit_seesaw::setI2CAddr(uint8_t addr) { - this->EEPROMWrite8(SEESAW_EEPROM_I2C_ADDR, addr); + this->EEPROMWrite8(getI2CaddrEEPROMloc(), addr); delay(250); this->begin(addr); // restart w/ the new addr } @@ -538,7 +688,7 @@ void Adafruit_seesaw::setI2CAddr(uint8_t addr) { *already know because you just read data from it. ****************************************************************************************/ uint8_t Adafruit_seesaw::getI2CAddr() { - return this->read8(SEESAW_EEPROM_BASE, SEESAW_EEPROM_I2C_ADDR); + return this->EEPROMRead8(getI2CaddrEEPROMloc()); } /*! @@ -644,8 +794,9 @@ uint8_t Adafruit_seesaw::getKeypadCount() { * * @param buf pointer to where the keyEvents should be stored * @param count the number of events to read + * @returns True on I2C read success ****************************************************************************************/ -void Adafruit_seesaw::readKeypad(keyEventRaw *buf, uint8_t count) { +bool Adafruit_seesaw::readKeypad(keyEventRaw *buf, uint8_t count) { return this->read(SEESAW_KEYPAD_BASE, SEESAW_KEYPAD_FIFO, (uint8_t *)buf, count, 1000); } @@ -667,11 +818,12 @@ float Adafruit_seesaw::getTemp() { /** ***************************************************************************************** * @brief Read the current position of the encoder + * @param encoder Which encoder to use, defaults to 0 * @return The encoder position as a 32 bit signed integer. ****************************************************************************************/ -int32_t Adafruit_seesaw::getEncoderPosition() { +int32_t Adafruit_seesaw::getEncoderPosition(uint8_t encoder) { uint8_t buf[4]; - this->read(SEESAW_ENCODER_BASE, SEESAW_ENCODER_POSITION, buf, 4); + this->read(SEESAW_ENCODER_BASE, SEESAW_ENCODER_POSITION + encoder, buf, 4); int32_t ret = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | (uint32_t)buf[3]; @@ -681,22 +833,24 @@ int32_t Adafruit_seesaw::getEncoderPosition() { /** ***************************************************************************************** * @brief Set the current position of the encoder + * @param encoder Which encoder to use, defaults to 0 * @param pos the position to set the encoder to. ****************************************************************************************/ -void Adafruit_seesaw::setEncoderPosition(int32_t pos) { +void Adafruit_seesaw::setEncoderPosition(int32_t pos, uint8_t encoder) { uint8_t buf[] = {(uint8_t)(pos >> 24), (uint8_t)(pos >> 16), (uint8_t)(pos >> 8), (uint8_t)(pos & 0xFF)}; - this->write(SEESAW_ENCODER_BASE, SEESAW_ENCODER_POSITION, buf, 4); + this->write(SEESAW_ENCODER_BASE, SEESAW_ENCODER_POSITION + encoder, buf, 4); } /** ***************************************************************************************** * @brief Read the change in encoder position since it was last read. + * @param encoder Which encoder to use, defaults to 0 * @return The encoder change as a 32 bit signed integer. ****************************************************************************************/ -int32_t Adafruit_seesaw::getEncoderDelta() { +int32_t Adafruit_seesaw::getEncoderDelta(uint8_t encoder) { uint8_t buf[4]; - this->read(SEESAW_ENCODER_BASE, SEESAW_ENCODER_DELTA, buf, 4); + this->read(SEESAW_ENCODER_BASE, SEESAW_ENCODER_DELTA + encoder, buf, 4); int32_t ret = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | (uint32_t)buf[3]; @@ -706,18 +860,24 @@ int32_t Adafruit_seesaw::getEncoderDelta() { /** ***************************************************************************************** * @brief Enable the interrupt to fire when the encoder changes position. + * @param encoder Which encoder to use, defaults to 0 + * @returns True on I2C write success ****************************************************************************************/ -void Adafruit_seesaw::enableEncoderInterrupt() { - this->write8(SEESAW_ENCODER_BASE, SEESAW_ENCODER_INTENSET, 0x01); +bool Adafruit_seesaw::enableEncoderInterrupt(uint8_t encoder) { + return this->write8(SEESAW_ENCODER_BASE, SEESAW_ENCODER_INTENSET + encoder, + 0x01); } /** ***************************************************************************************** * @brief Disable the interrupt from firing when the encoder changes *position. + * @param encoder Which encoder to use, defaults to 0 + * @returns True on I2C write success ****************************************************************************************/ -void Adafruit_seesaw::disableEncoderInterrupt() { - this->write8(SEESAW_ENCODER_BASE, SEESAW_ENCODER_INTENCLR, 0x01); +bool Adafruit_seesaw::disableEncoderInterrupt(uint8_t encoder) { + return this->write8(SEESAW_ENCODER_BASE, SEESAW_ENCODER_INTENCLR + encoder, + 0x01); } /** @@ -728,9 +888,10 @@ void Adafruit_seesaw::disableEncoderInterrupt() { * @param regLow the function address register (ex. *SEESAW_NEOPIXEL_PIN) * @param value the value between 0 and 255 to write + * @returns True on I2C write success ****************************************************************************************/ -void Adafruit_seesaw::write8(byte regHigh, byte regLow, byte value) { - this->write(regHigh, regLow, &value, 1); +bool Adafruit_seesaw::write8(byte regHigh, byte regLow, byte value) { + return this->write(regHigh, regLow, &value, 1); } /** @@ -753,17 +914,6 @@ uint8_t Adafruit_seesaw::read8(byte regHigh, byte regLow, uint16_t delay) { return ret; } -/** - ***************************************************************************************** - * @brief Initialize I2C. On arduino this just calls i2c->begin() - ****************************************************************************************/ -void Adafruit_seesaw::_i2c_init() { -#ifdef SEESAW_I2C_DEBUG - Serial.println("I2C Begin"); -#endif - _i2cbus->begin(); -} - /** ***************************************************************************************** * @brief Read a specified number of bytes into a buffer from the seesaw. @@ -776,49 +926,54 @@ void Adafruit_seesaw::_i2c_init() { * @param delay an optional delay in between setting the read *register and reading out the data. This is required for some seesaw functions *(ex. reading ADC data) + * @returns True on I2C read success ****************************************************************************************/ -void Adafruit_seesaw::read(uint8_t regHigh, uint8_t regLow, uint8_t *buf, +bool Adafruit_seesaw::read(uint8_t regHigh, uint8_t regLow, uint8_t *buf, uint8_t num, uint16_t delay) { uint8_t pos = 0; + uint8_t prefix[2]; + prefix[0] = (uint8_t)regHigh; + prefix[1] = (uint8_t)regLow; // on arduino we need to read in 32 byte chunks while (pos < num) { uint8_t read_now = min(32, num - pos); - _i2cbus->beginTransmission((uint8_t)_i2caddr); - _i2cbus->write((uint8_t)regHigh); - _i2cbus->write((uint8_t)regLow); -#ifdef SEESAW_I2C_DEBUG - Serial.print("I2C read $"); - Serial.print((uint16_t)regHigh << 8 | regLow, HEX); - Serial.print(" : "); -#endif - if (_flow != -1) + if (_flow != -1) { while (!::digitalRead(_flow)) - ; - _i2cbus->endTransmission(); + yield(); + } + + if (!_i2c_dev->write(prefix, 2)) { + return false; + } // TODO: tune this delayMicroseconds(delay); - if (_flow != -1) + if (_flow != -1) { while (!::digitalRead(_flow)) - ; - _i2cbus->requestFrom((uint8_t)_i2caddr, read_now); - - for (int i = 0; i < read_now; i++) { - buf[pos] = _i2cbus->read(); -#ifdef SEESAW_I2C_DEBUG - Serial.print("0x"); - Serial.print(buf[pos], HEX); - Serial.print(","); -#endif - pos++; + yield(); } + #ifdef SEESAW_I2C_DEBUG - Serial.println(); + Serial.print("Reading "); + Serial.print(read_now); + Serial.println(" bytes"); +#endif + + if (!_i2c_dev->read(buf + pos, read_now)) { + return false; + } + pos += read_now; +#ifdef SEESAW_I2C_DEBUG + Serial.print("pos: "); + Serial.print(pos); + Serial.print(" num:"); + Serial.println(num); #endif } + return true; } /*! @@ -830,29 +985,23 @@ void Adafruit_seesaw::read(uint8_t regHigh, uint8_t regLow, uint8_t *buf, * @param regLow the function address register (ex. SEESAW_GPIO_BULK_SET) * @param buf the buffer the the bytes from * @param num the number of bytes to write. + * @returns True on I2C write success ****************************************************************************************/ -void Adafruit_seesaw::write(uint8_t regHigh, uint8_t regLow, uint8_t *buf, - uint8_t num) { - _i2cbus->beginTransmission((uint8_t)_i2caddr); - _i2cbus->write((uint8_t)regHigh); - _i2cbus->write((uint8_t)regLow); - _i2cbus->write((uint8_t *)buf, num); -#ifdef SEESAW_I2C_DEBUG - Serial.print("I2C write $"); - Serial.print((uint16_t)regHigh << 8 | regLow, HEX); - Serial.print(" : "); - for (int i = 0; i < num; i++) { - Serial.print("0x"); - Serial.print(buf[i], HEX); - Serial.print(","); - } - Serial.println(); -#endif +bool Adafruit_seesaw::write(uint8_t regHigh, uint8_t regLow, + uint8_t *buf = NULL, uint8_t num = 0) { + uint8_t prefix[2]; + prefix[0] = (uint8_t)regHigh; + prefix[1] = (uint8_t)regLow; if (_flow != -1) while (!::digitalRead(_flow)) - ; - _i2cbus->endTransmission(); + yield(); + + if (!_i2c_dev->write(buf, num, true, prefix, 2)) { + return false; + } + + return true; } /*! @@ -896,22 +1045,3 @@ size_t Adafruit_seesaw::write(const char *str) { this->write(SEESAW_SERCOM0_BASE, SEESAW_SERCOM_DATA, buf, len); return len; } - -/*! - ********************************************************************** - * @brief Write only the module base address register and the function - *address register. - * - * @param regHigh the module address register (ex. SEESAW_STATUS_BASE) - * @param regLow the function address register (ex. - *SEESAW_STATUS_SWRST) - **********************************************************************/ -void Adafruit_seesaw::writeEmpty(uint8_t regHigh, uint8_t regLow) { - _i2cbus->beginTransmission((uint8_t)_i2caddr); - _i2cbus->write((uint8_t)regHigh); - _i2cbus->write((uint8_t)regLow); - if (_flow != -1) - while (!::digitalRead(_flow)) - ; - _i2cbus->endTransmission(); -} diff --git a/lib/lib_i2c/Adafruit_seesaw_soilsensor_1.3.1/Adafruit_seesaw.h b/lib/lib_i2c/Adafruit_Seesaw-1.7.9/Adafruit_seesaw.h similarity index 73% rename from lib/lib_i2c/Adafruit_seesaw_soilsensor_1.3.1/Adafruit_seesaw.h rename to lib/lib_i2c/Adafruit_Seesaw-1.7.9/Adafruit_seesaw.h index 8f1f75400..a18fee8f1 100644 --- a/lib/lib_i2c/Adafruit_seesaw_soilsensor_1.3.1/Adafruit_seesaw.h +++ b/lib/lib_i2c/Adafruit_Seesaw-1.7.9/Adafruit_seesaw.h @@ -21,12 +21,8 @@ #ifndef LIB_SEESAW_H #define LIB_SEESAW_H -#if (ARDUINO >= 100) -#include "Arduino.h" -#else -#include "WProgram.h" -#endif - +#include "Adafruit_I2CDevice.h" +#include #include /*========================================================================= @@ -57,9 +53,10 @@ enum { SEESAW_TOUCH_BASE = 0x0F, SEESAW_KEYPAD_BASE = 0x10, SEESAW_ENCODER_BASE = 0x11, + SEESAW_SPECTRUM_BASE = 0x12, }; -/** GPIO module function addres registers +/** GPIO module function address registers */ enum { SEESAW_GPIO_DIRSET_BULK = 0x02, @@ -75,7 +72,7 @@ enum { SEESAW_GPIO_PULLENCLR = 0x0C, }; -/** status module function addres registers +/** status module function address registers */ enum { SEESAW_STATUS_HW_ID = 0x01, @@ -85,7 +82,7 @@ enum { SEESAW_STATUS_SWRST = 0x7F, }; -/** timer module function addres registers +/** timer module function address registers */ enum { SEESAW_TIMER_STATUS = 0x00, @@ -93,7 +90,7 @@ enum { SEESAW_TIMER_FREQ = 0x02, }; -/** ADC module function addres registers +/** ADC module function address registers */ enum { SEESAW_ADC_STATUS = 0x00, @@ -104,7 +101,7 @@ enum { SEESAW_ADC_CHANNEL_OFFSET = 0x07, }; -/** Sercom module function addres registers +/** Sercom module function address registers */ enum { SEESAW_SERCOM_STATUS = 0x00, @@ -114,7 +111,7 @@ enum { SEESAW_SERCOM_DATA = 0x05, }; -/** neopixel module function addres registers +/** neopixel module function address registers */ enum { SEESAW_NEOPIXEL_STATUS = 0x00, @@ -125,13 +122,13 @@ enum { SEESAW_NEOPIXEL_SHOW = 0x05, }; -/** touch module function addres registers +/** touch module function address registers */ enum { SEESAW_TOUCH_CHANNEL_OFFSET = 0x10, }; -/** keypad module function addres registers +/** keypad module function address registers */ enum { SEESAW_KEYPAD_STATUS = 0x00, @@ -155,10 +152,24 @@ enum { */ enum { SEESAW_ENCODER_STATUS = 0x00, - SEESAW_ENCODER_INTENSET = 0x02, - SEESAW_ENCODER_INTENCLR = 0x03, - SEESAW_ENCODER_POSITION = 0x04, - SEESAW_ENCODER_DELTA = 0x05, + SEESAW_ENCODER_INTENSET = 0x10, + SEESAW_ENCODER_INTENCLR = 0x20, + SEESAW_ENCODER_POSITION = 0x30, + SEESAW_ENCODER_DELTA = 0x40, +}; + +/** Audio spectrum module function address registers + */ +enum { + SEESAW_SPECTRUM_RESULTS_LOWER = 0x00, // Audio spectrum bins 0-31 + SEESAW_SPECTRUM_RESULTS_UPPER = 0x01, // Audio spectrum bins 32-63 + // If some future device supports a larger spectrum, can add additional + // "bins" working upward from here. Configurable setting registers then + // work downward from the top to avoid collision between spectrum bins + // and configurables. + SEESAW_SPECTRUM_CHANNEL = 0xFD, + SEESAW_SPECTRUM_RATE = 0xFE, + SEESAW_SPECTRUM_STATUS = 0xFF, }; #define ADC_INPUT_0_PIN 2 ///< default ADC input pin @@ -178,11 +189,15 @@ enum { #endif /*=========================================================================*/ - -#define SEESAW_HW_ID_CODE 0x55 ///< seesaw HW ID code -#define SEESAW_EEPROM_I2C_ADDR \ - 0x3F ///< EEPROM address of i2c address to start up with (for devices that - ///< support this feature) +// clang-format off +#define SEESAW_HW_ID_CODE_SAMD09 0x55 ///< seesaw HW ID code for SAMD09 +#define SEESAW_HW_ID_CODE_TINY806 0x84 ///< seesaw HW ID code for ATtiny806 +#define SEESAW_HW_ID_CODE_TINY807 0x85 ///< seesaw HW ID code for ATtiny807 +#define SEESAW_HW_ID_CODE_TINY816 0x86 ///< seesaw HW ID code for ATtiny816 +#define SEESAW_HW_ID_CODE_TINY817 0x87 ///< seesaw HW ID code for ATtiny817 +#define SEESAW_HW_ID_CODE_TINY1616 0x88 ///< seesaw HW ID code for ATtiny1616 +#define SEESAW_HW_ID_CODE_TINY1617 0x89 ///< seesaw HW ID code for ATtiny1617 +// clang-format on /** raw key event stucture for keypad module */ union keyEventRaw { @@ -227,13 +242,17 @@ public: bool reset = true); uint32_t getOptions(); uint32_t getVersion(); - void SWReset(); + bool getProdDatecode(uint16_t *pid, uint8_t *year, uint8_t *mon, + uint8_t *day); + + bool SWReset(); void pinMode(uint8_t pin, uint8_t mode); void pinModeBulk(uint32_t pins, uint8_t mode); void pinModeBulk(uint32_t pinsa, uint32_t pinsb, uint8_t mode); virtual void analogWrite(uint8_t pin, uint16_t value, uint8_t width = 8); void digitalWrite(uint8_t pin, uint8_t value); + void digitalWriteBulk(uint32_t port_values); void digitalWriteBulk(uint32_t pins, uint8_t value); void digitalWriteBulk(uint32_t pinsa, uint32_t pinsb, uint8_t value); @@ -267,32 +286,34 @@ public: void enableKeypadInterrupt(); void disableKeypadInterrupt(); uint8_t getKeypadCount(); - void readKeypad(keyEventRaw *buf, uint8_t count); + bool readKeypad(keyEventRaw *buf, uint8_t count); float getTemp(); - int32_t getEncoderPosition(); - int32_t getEncoderDelta(); - void enableEncoderInterrupt(); - void disableEncoderInterrupt(); - void setEncoderPosition(int32_t pos); + int32_t getEncoderPosition(uint8_t encoder = 0); + int32_t getEncoderDelta(uint8_t encoder = 0); + bool enableEncoderInterrupt(uint8_t encoder = 0); + bool disableEncoderInterrupt(uint8_t encoder = 0); + void setEncoderPosition(int32_t pos, uint8_t encoder = 0); virtual size_t write(uint8_t); virtual size_t write(const char *str); protected: - uint8_t _i2caddr; /*!< The I2C address used to communicate with the seesaw */ TwoWire *_i2cbus; /*!< The I2C Bus used to communicate with the seesaw */ - int8_t _flow; /*!< The flow control pin to use */ + Adafruit_I2CDevice *_i2c_dev = NULL; ///< The BusIO device for I2C control - void write8(byte regHigh, byte regLow, byte value); - uint8_t read8(byte regHigh, byte regLow, uint16_t delay = 125); + int8_t _flow; /*!< The flow control pin to use */ - void read(uint8_t regHigh, uint8_t regLow, uint8_t *buf, uint8_t num, - uint16_t delay = 125); - void write(uint8_t regHigh, uint8_t regLow, uint8_t *buf, uint8_t num); - void writeEmpty(uint8_t regHigh, uint8_t regLow); - void _i2c_init(); + uint8_t _hardwaretype = 0; /*!< what hardware type is attached! */ + uint8_t getI2CaddrEEPROMloc(); + + bool write8(byte regHigh, byte regLow, byte value); + uint8_t read8(byte regHigh, byte regLow, uint16_t delay = 250); + + bool read(uint8_t regHigh, uint8_t regLow, uint8_t *buf, uint8_t num, + uint16_t delay = 250); + bool write(uint8_t regHigh, uint8_t regLow, uint8_t *buf, uint8_t num); /*========================================================================= REGISTER BITFIELDS diff --git a/lib/lib_i2c/Adafruit_seesaw_soilsensor_1.3.1/README.md b/lib/lib_i2c/Adafruit_Seesaw-1.7.9/README.md similarity index 100% rename from lib/lib_i2c/Adafruit_seesaw_soilsensor_1.3.1/README.md rename to lib/lib_i2c/Adafruit_Seesaw-1.7.9/README.md diff --git a/lib/lib_i2c/Adafruit_seesaw_soilsensor_1.3.1/library.properties b/lib/lib_i2c/Adafruit_Seesaw-1.7.9/library.properties similarity index 79% rename from lib/lib_i2c/Adafruit_seesaw_soilsensor_1.3.1/library.properties rename to lib/lib_i2c/Adafruit_Seesaw-1.7.9/library.properties index eca5e1ba1..e00411f53 100644 --- a/lib/lib_i2c/Adafruit_seesaw_soilsensor_1.3.1/library.properties +++ b/lib/lib_i2c/Adafruit_Seesaw-1.7.9/library.properties @@ -1,5 +1,5 @@ name=Adafruit seesaw Library -version=1.3.1 +version=1.7.9 author=Adafruit maintainer=Adafruit sentence=This is a library for the Adafruit seesaw helper IC. @@ -7,4 +7,4 @@ paragraph=This is a library for the Adafruit seesaw helper IC. category=Other url=https://github.com/adafruit/Adafruit_Seesaw architectures=* -depends=Adafruit ST7735 and ST7789 Library +depends=Adafruit BusIO, Adafruit ST7735 and ST7789 Library diff --git a/tasmota/include/tasmota_configurations.h b/tasmota/include/tasmota_configurations.h index 2a54c1609..ac9932a84 100644 --- a/tasmota/include/tasmota_configurations.h +++ b/tasmota/include/tasmota_configurations.h @@ -158,7 +158,8 @@ //#define USE_EZODO // [I2cDriver55] Enable support for EZO's DO sensor (+0k3 code) - Shared EZO code required for any EZO device (+1k2 code) //#define USE_EZORGB // [I2cDriver55] Enable support for EZO's RGB sensor (+0k5 code) - Shared EZO code required for any EZO device (+1k2 code) //#define USE_EZOPMP // [I2cDriver55] Enable support for EZO's PMP sensor (+0k3 code) - Shared EZO code required for any EZO device (+1k2 code) -//#define USE_SEESAW_SOIL // [I2cDriver56] Enable Capacitice Soil Moisture & Temperature Sensor (I2C addresses 0x36 - 0x39) (+1k3 code) +//#define USE_SEESAW_SOIL // [I2cDriver56] Enable Adafruit Soil Moisture & Temp Sensor (I2C addresses 0x36 - 0x39) (+1k code) - Shared Seesaw code required (+1k code) +//#define USE_SEESAW_ENCODER // [I2cDriver56] Enable Adafruit Rotary Encoder (I2C addresses 0x36 - 0x39) (+2k code) - Shared Seesaw code required (+1k code) //#define USE_MPU_ACCEL // [I2cDriver58] Enable MPU6886/MPU9250 - found in M5Stack - support both I2C buses on ESP32 (I2C address 0x68) (+2k code) //#define USE_AM2320 // [I2cDriver60] Enable AM2320 temperature and humidity Sensor (I2C address 0x5C) (+1k code) //#define USE_T67XX // [I2cDriver61] Enable Telaire T67XX CO2 sensor (I2C address 0x15) (+1k3 code) diff --git a/tasmota/include/tasmota_configurations_ESP32.h b/tasmota/include/tasmota_configurations_ESP32.h index bc6c581f2..f54232240 100644 --- a/tasmota/include/tasmota_configurations_ESP32.h +++ b/tasmota/include/tasmota_configurations_ESP32.h @@ -438,7 +438,8 @@ //#define USE_EZODO // [I2cDriver55] Enable support for EZO's DO sensor (+0k3 code) - Shared EZO code required for any EZO device (+1k2 code) //#define USE_EZORGB // [I2cDriver55] Enable support for EZO's RGB sensor (+0k5 code) - Shared EZO code required for any EZO device (+1k2 code) //#define USE_EZOPMP // [I2cDriver55] Enable support for EZO's PMP sensor (+0k3 code) - Shared EZO code required for any EZO device (+1k2 code) -//#define USE_SEESAW_SOIL // [I2cDriver56] Enable Capacitice Soil Moisture & Temperature Sensor (I2C addresses 0x36 - 0x39) (+1k3 code) +//#define USE_SEESAW_SOIL // [I2cDriver56] Enable Adafruit Soil Moisture & Temp Sensor (I2C addresses 0x36 - 0x39) (+1k code) - Shared Seesaw code required (+1k code) +//#define USE_SEESAW_ENCODER // [I2cDriver56] Enable Adafruit Rotary Encoder (I2C addresses 0x36 - 0x39) (+2k code) - Shared Seesaw code required (+1k code) //#define USE_MPU_ACCEL // [I2cDriver58] Enable MPU6886/MPU9250 - found in M5Stack - support both I2C buses on ESP32 (I2C address 0x68) (+2k code) //#define USE_AM2320 // [I2cDriver60] Enable AM2320 temperature and humidity Sensor (I2C address 0x5C) (+1k code) //#define USE_T67XX // [I2cDriver61] Enable Telaire T67XX CO2 sensor (I2C address 0x15) (+1k3 code) diff --git a/tasmota/tasmota_xsns_sensor/xsns_81_seesaw.ino b/tasmota/tasmota_xsns_sensor/xsns_81_seesaw.ino new file mode 100644 index 000000000..f45d10902 --- /dev/null +++ b/tasmota/tasmota_xsns_sensor/xsns_81_seesaw.ino @@ -0,0 +1,439 @@ +/* + xsns_81_seesaw - Adafruit Seesaw family base class + + Copyright (C) 2021 Wayne Ross, Theo Arends, Peter Franck, Allen Schober + + 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_I2C + +#if defined(USE_SEESAW_SOIL) || defined(USE_SEESAW_ENCODER) + #define USE_SEESAW +#endif + +#if defined(USE_SEESAW) + +/*********************************************************************************************\ + * Seesaw - Base class for Adafruit seesaw devices + * + * This driver provides a unified interface for multiple seesaw device types: + * - STEMMA Soil Sensor - I2C Capacitive Moisture Sensor (USE_SEESAW_SOIL) + * - I2C QT Rotary Encoder (USE_SEESAW_ENCODER) + * + * Address ranges: + * - Soil sensors: 0x36 - 0x39 + * - Encoders: 0x36 - 0x3D + * + * The driver detects which type of module is at each address by reading the + * hardware ID, version, and options register. +\*********************************************************************************************/ + +#define XSNS_81 81 +#define XI2C_56 56 // See I2CDEVICES.md + +#include "Adafruit_seesaw.h" // we only use definitions, no code + +#define SEESAW_ADDR_MIN 0x36 // First seesaw address +#define SEESAW_ADDR_MAX 0x39 // Last seesaw address (limited to 4 devices) +#define SEESAW_MAX_SENSORS 4 // Maximum supported devices + +// I2C delays +#define SEESAW_DELAY_DETECT 10 // ms delay before reading ID +#define SEESAW_DELAY_RESET 100 // ms delay after slave reset + +// Supported module types +enum SeesawDeviceType { + SEESAW_TYPE_UNKNOWN = 0, + SEESAW_TYPE_SOIL, + SEESAW_TYPE_ENCODER +}; + +// Base struct for all seesaw devices +struct SeesawDevice { + SeesawDevice(uint8_t addr) : address(addr), type(SEESAW_TYPE_UNKNOWN), valid(false), device_index(0) {} + + virtual ~SeesawDevice() {} + + virtual void Init() = 0; + virtual void Read() = 0; + virtual void Show(bool json, const char *name) = 0; + virtual void Handler() {} // Optional handler for devices that need periodic processing + virtual bool HandleCommand(const char* cmd, uint32_t len) { return false; } // Optional command handler + + bool IsValid() const { return valid; } + uint8_t GetAddress() const { return address; } + SeesawDeviceType GetType() const { return type; } + uint8_t GetDeviceIndex() const { return device_index; } + void SetDeviceIndex(uint8_t idx) { device_index = idx; } + + static const char id[] PROGMEM; + +protected: + uint8_t address; + SeesawDeviceType type; + bool valid; + uint8_t device_index; // Index in manager's device array (0-based) + char device_name[16]; // Stores formatted name from Show() for Handler/debug use +}; + +const char SeesawDevice::id[] PROGMEM = ""; + +// Device type names for identification +#ifdef USE_SEESAW_SOIL +const char SEESAW_SOIL_ID[] PROGMEM = "SOIL"; +#endif +#ifdef USE_SEESAW_ENCODER +const char SEESAW_ENCODER_ID[] PROGMEM = "ENCODER"; +#endif + +// Common seesaw I2C helper functions +namespace Seesaw { + bool Write8(uint8_t addr, uint8_t regHigh, uint8_t regLow, uint8_t value) { + Wire.beginTransmission(addr); + Wire.write(regHigh); + Wire.write(regLow); + Wire.write(value); + return (Wire.endTransmission() == 0); + } + + bool Write(uint8_t addr, uint8_t regHigh, uint8_t regLow, const uint8_t *buf, uint8_t num) { + Wire.beginTransmission(addr); + Wire.write(regHigh); + Wire.write(regLow); + for (uint8_t i = 0; i < num; i++) { + Wire.write(buf[i]); + } + return (Wire.endTransmission() == 0); + } + + bool Read(uint8_t addr, uint8_t regHigh, uint8_t regLow, uint8_t *buf, uint8_t num) { + Wire.beginTransmission(addr); + Wire.write(regHigh); + Wire.write(regLow); + if (Wire.endTransmission() != 0) { return false; } + + delay(1); // Small delay for register read + + if (num != Wire.requestFrom(addr, num)) { return false; } + for (uint8_t i = 0; i < num; i++) { + buf[i] = Wire.read(); + } + return true; + } +} + +// Manager class to handle all seesaw devices +struct SeesawManager { + void Init() { + // Send reset to all potential addresses + for (uint8_t addr = SEESAW_ADDR_MIN; addr <= SEESAW_ADDR_MAX; addr++) { + if (!I2cSetDevice(addr)) { continue; } + Wire.beginTransmission(addr); + Wire.write(SEESAW_STATUS_BASE); + Wire.write(SEESAW_STATUS_SWRST); + Wire.write(0xFF); + Wire.endTransmission(); + } + + state = STATE_RESET; + state_time = millis(); + } + + void Every50ms() { + uint32_t time_diff = millis() - state_time; + + switch (state) { + case STATE_RESET: + state = STATE_INIT; + break; + + case STATE_INIT: + if (time_diff < SEESAW_DELAY_RESET) { return; } + // Send hardware ID read command to all potential addresses + for (uint8_t addr = SEESAW_ADDR_MIN; addr <= SEESAW_ADDR_MAX; addr++) { + if (!I2cSetDevice(addr)) { continue; } + Wire.beginTransmission(addr); + Wire.write(SEESAW_STATUS_BASE); + Wire.write(SEESAW_STATUS_HW_ID); + Wire.endTransmission(); + } + state = STATE_DETECT; + break; + + case STATE_DETECT: + if (time_diff < SEESAW_DELAY_DETECT) { return; } + Detect(); + state = STATE_READ; + break; + + case STATE_READ: + // Read all sensors + for (uint8_t i = 0; i < count; i++) { + if (devices[i]) { + devices[i]->Read(); + devices[i]->Handler(); + } + } + break; + } + state_time = millis(); + } + + void Show(bool json) { + for (uint8_t i = 0; i < count; i++) { + if (devices[i] && devices[i]->IsValid()) { + char name[12]; + GetDeviceName(i, name, sizeof(name)); + devices[i]->Show(json, name); + } + } + } + + bool HandleCommand(uint32_t index, const char* cmd, uint32_t len) { + if (index == 0 || index > count) { return false; } + return devices[index - 1]->HandleCommand(cmd, len); + } + + uint8_t GetCount() const { return count; } + + uint8_t GetTypeCount(SeesawDeviceType type) const { + uint8_t type_count = 0; + for (uint8_t i = 0; i < count; i++) { + if (devices[i] && devices[i]->GetType() == type) { + type_count++; + } + } + return type_count; + } + + SeesawDevice* GetDevice(uint8_t index) { + if (index >= count) { return nullptr; } + return devices[index]; + } + +private: + void Detect() { + count = 0; + + for (uint8_t addr = SEESAW_ADDR_MIN; addr <= SEESAW_ADDR_MAX && count < SEESAW_MAX_SENSORS; addr++) { + if (!I2cSetDevice(addr)) { continue; } + + // Check for valid hardware ID + if (1 != Wire.requestFrom(addr, (uint8_t)1)) { + AddLog(LOG_LEVEL_INFO, PSTR("SEE: No response at ADDR=0x%02X, skipping device."), addr); + continue; + } + uint8_t hw_id = Wire.read(); + bool valid_hw_id = (hw_id == SEESAW_HW_ID_CODE_SAMD09 || // Soil sensor, encoder + hw_id == SEESAW_HW_ID_CODE_TINY806 || + hw_id == SEESAW_HW_ID_CODE_TINY807 || + hw_id == SEESAW_HW_ID_CODE_TINY816 || + hw_id == SEESAW_HW_ID_CODE_TINY817 || + hw_id == SEESAW_HW_ID_CODE_TINY1616 || + hw_id == SEESAW_HW_ID_CODE_TINY1617); + if (!valid_hw_id) { + AddLog(LOG_LEVEL_INFO, PSTR("SEE: Unknown HW ID 0x%02X at ADDR=0x%02X, skipping device."), hw_id, addr); + continue; + } + + uint8_t version_buf[4]; + if (!Seesaw::Read(addr, SEESAW_STATUS_BASE, SEESAW_STATUS_VERSION, version_buf, 4)) { + AddLog(LOG_LEVEL_INFO, PSTR("SEE: Failed to read VERSION at ADDR=0x%02X, skipping device."), addr); + continue; + } + uint32_t version = ((uint32_t)version_buf[0] << 24) | ((uint32_t)version_buf[1] << 16) | + ((uint32_t)version_buf[2] << 8) | (uint32_t)version_buf[3]; + AddLog(LOG_LEVEL_INFO, PSTR("SEE: Seesaw module at ADDR=0x%02X with firmware 0x%08X"), addr, version); + + // Determine device type by reading module options register + // The SEESAW_STATUS_OPTIONS register returns a 32-bit bitmask where each bit + // corresponds to a module base address (e.g., bit 0x11 = SEESAW_ENCODER_BASE) + SeesawDevice* device = nullptr; + SeesawDeviceType detected_type = SEESAW_TYPE_UNKNOWN; + + uint8_t options_buf[4]; + if (!Seesaw::Read(addr, SEESAW_STATUS_BASE, SEESAW_STATUS_OPTIONS, options_buf, 4)) { + AddLog(LOG_LEVEL_INFO, PSTR("SEE: Failed to read OPTIONS register at ADDR=0x%02X, skipping device."), addr); + continue; + } + uint32_t options = ((uint32_t)options_buf[0] << 24) | ((uint32_t)options_buf[1] << 16) | + ((uint32_t)options_buf[2] << 8) | (uint32_t)options_buf[3]; + +#ifdef USE_SEESAW_ENCODER + // Check for encoder module (bit 0x11 = SEESAW_ENCODER_BASE) + if (!device && (options & (1UL << SEESAW_ENCODER_BASE))) { + device = CreateEncoderDevice(addr); + detected_type = SEESAW_TYPE_ENCODER; + AddLog(LOG_LEVEL_INFO, PSTR("SEE: Detected Seesaw encoder at 0x%02X"), addr); + } +#endif + +#ifdef USE_SEESAW_SOIL + // Check for capacitive module (bit 0x0F = SEESAW_TOUCH_BASE) + if (!device && (options & (1UL << SEESAW_TOUCH_BASE))) { + device = CreateSoilDevice(addr); + detected_type = SEESAW_TYPE_SOIL; + AddLog(LOG_LEVEL_INFO, PSTR("SEE: Detected Seesaw soil sensor at 0x%02X"), addr); + } +#endif + + if(!device) { + AddLog(LOG_LEVEL_INFO, PSTR("SEE: No known modules found at ADDR=0x%02X with OPTIONS=0x%08X, skipping device."), addr, options); + continue; + } else { + devices[count] = device; + + // Set the type-specific index based on the number of same-type devices detected so far + uint8_t type_index = GetTypeCount(detected_type); + device->SetDeviceIndex(type_index); + device->Init(); + + char name[12]; + GetDeviceName(count, name, sizeof(name)); + I2cSetActiveFound(addr, name); + + count++; + } + } + } + + void GetDeviceName(uint8_t index, char* name, size_t len) { + if (index >= count || !devices[index]) { + snprintf_P(name, len, PSTR("Seesaw")); + return; + } + + const char* type_prefix = "Seesaw"; + bool use_address = false; + SeesawDeviceType device_type = devices[index]->GetType(); + + switch (device_type) { +#ifdef USE_SEESAW_SOIL + case SEESAW_TYPE_SOIL: + type_prefix = "SeeSoil"; +#ifdef SEESAW_SOIL_PERSISTENT_NAMING + use_address = true; +#endif + break; +#endif +#ifdef USE_SEESAW_ENCODER + case SEESAW_TYPE_ENCODER: + type_prefix = "SeeEnc"; +#ifdef SEESAW_ENCODER_PERSISTENT_NAMING + use_address = true; +#endif + break; +#endif + default: + break; + } + + if (use_address) { + // Address-based naming: always include address, even for single device (e.g. "SeeSoil-36", "SeeEnc-38") + snprintf_P(name, len, PSTR("%s%c%02X"), type_prefix, IndexSeparator(), devices[index]->GetAddress()); + } else { + // Index-based naming: only add index if multiple devices of same type + uint8_t type_count = GetTypeCount(device_type); + + if (type_count > 1) { + // Multiple devices: "SeeSoil-1", "SeeEnc-1" using a type-specific device_index + snprintf_P(name, len, PSTR("%s%c%u"), type_prefix, IndexSeparator(), devices[index]->GetDeviceIndex() + 1); + } else { + // Single device of this type: just "SeeSoil" or "SeeEnc" + strlcpy(name, type_prefix, len); + } + } + } + + SeesawDevice* CreateSoilDevice(uint8_t addr); + SeesawDevice* CreateEncoderDevice(uint8_t addr); + + enum State { + STATE_RESET, + STATE_INIT, + STATE_DETECT, + STATE_READ + }; + + State state = STATE_RESET; + uint32_t state_time = 0; + uint8_t count = 0; + SeesawDevice* devices[SEESAW_MAX_SENSORS] = {nullptr}; +} SeesawMgr; + +/*********************************************************************************************\ + * Stub implementations for factory functions when device types are not compiled in +\*********************************************************************************************/ +#ifndef USE_SEESAW_SOIL +SeesawDevice* SeesawManager::CreateSoilDevice(uint8_t addr) { + return nullptr; +} +#endif + +#ifndef USE_SEESAW_ENCODER +SeesawDevice* SeesawManager::CreateEncoderDevice(uint8_t addr) { + return nullptr; +} +#endif + +/*********************************************************************************************\ + * Forward declarations for Encoder commands (defined in xsns_81_seesaw_encoder.ino) +\*********************************************************************************************/ +#ifdef USE_SEESAW_ENCODER +extern const char kSeeEncCommands[]; +extern void (* const SeeEncCommand[])(void); +#endif + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xsns81(uint32_t function) +{ + if (!I2cEnabled(XI2C_56)) { return false; } + bool result = false; + + switch (function) { + case FUNC_INIT: + SeesawMgr.Init(); + break; + + case FUNC_EVERY_50_MSECOND: + SeesawMgr.Every50ms(); + break; + + case FUNC_JSON_APPEND: + SeesawMgr.Show(true); + break; + +#ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: + SeesawMgr.Show(false); + break; +#endif + +#ifdef USE_SEESAW_ENCODER + case FUNC_COMMAND: + result = DecodeCommand(kSeeEncCommands, SeeEncCommand); + break; +#endif + } + + return result; +} + +#endif // USE_SEESAW +#endif // USE_I2C diff --git a/tasmota/tasmota_xsns_sensor/xsns_81_seesaw_encoder.ino b/tasmota/tasmota_xsns_sensor/xsns_81_seesaw_encoder.ino new file mode 100644 index 000000000..a4a8d66f4 --- /dev/null +++ b/tasmota/tasmota_xsns_sensor/xsns_81_seesaw_encoder.ino @@ -0,0 +1,564 @@ +/* + xsns_81_seesaw_encoder - Adafruit I2C QT Rotary Encoder support for Tasmota + + Copyright (C) 2025 Allen Schober + + 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_I2C +#ifdef USE_SEESAW_ENCODER + +/*********************************************************************************************\ + * SEESAW_ENCODER - Adafruit I2C QT Rotary Encoder with NeoPixel and Button + * + * I2C Address: 0x36, 0x37, 0x38, 0x39 (though Adafruit hw is configurable from 0x36 to 0x3D) + * + * The Adafruit I2C QT Rotary Encoder features: + * - Quadrature rotary encoder with detents + * - Push button (seesaw pin 24) + * - RGB NeoPixel LED (seesaw pin 6) + * - Controlled via seesaw firmware over I2C + * + * Implementation Note: + * Add #define SEESAW_ENCODER_LIKE_ROTARY to have driver follow the same patterns + * as tasmota_support/support_rotary.ino (ROTARY_V1) and to have consistent behavior + * across GPIO rotary encoders and this I2C rotary encoder. See support_rotary.ino + * for rotary encoder operation. + * + * Device Numbering: + * - Encoders are numbered based on detection order by default (SeeEnc-1, SeeEnc-2, etc.) + * - Detection scans addresses 0x36-0x39 in order + * - A single encoder at any address will always be named SeeEnc (no number) + * - #define SEESAW_ENCODER_PERSISTENT_NAMING to use I2C address-based naming + * (e.g., SeeEnc-36 instead of SeeEnc-1) for consistent naming across restarts + * + * Commands: + * - SeeEncSet - Set encoder position (e.g., SeeEncSet1 0) + * - SeeEncColor - Set NeoPixel color in hex (e.g., SeeEncColor1 FF0000 for red) + * - SeeEncColor ,, - Set NeoPixel color as RGB values (e.g., SeeEncColor1 255,0,0) + * + * Light Control (when USE_LIGHT enabled): + * - First two encoders detected control lights similar to GPIO rotary encoders + * - SeeEnc1 (first encoder detected): + * * Button released: Dimmer control (RGB or all channels) + * * Button pressed: Color (RGB) or Color Temperature control + * - SeeEnc2 (second encoder detected): + * * Button released: Dimmer CW control + * * Button pressed: Color Temperature control + * - Configuration via existing SetOptions: + * * SetOption43 (steps): Change Rotary Max Steps (default 10) + * * SetOption98 (0/1): Direct light control (0, default) or rules mode (1) + * * SetOption113 (0/1): Power on with low dimmer when rotated while off + * + * Button Behavior: + * - Click alone (no rotation): Toggle relay (1st encoder -> relay 1, 2nd encoder -> relay 2) + * - Press during rotation: Modify rotation behavior (color/CT instead of dimmer). Clicks + * during rotation are ignored to prevent unwanted toggles. +\*********************************************************************************************/ + +// Have Seesaw I2C Encoder behave like a GPIO Rotary Encoder for light control and button handling +// #define SEESAW_ENCODER_LIKE_ROTARY + +// Encoder Pin configuration +#define SEESAW_ENCODER_BUTTON_PIN 24 +#define SEESAW_ENCODER_NEOPIXEL_PIN 6 + +#define SEESAW_ENCODER_TIMEOUT 2 // 2 * Handler() call which is usually 2 * 0.05 seconds + +struct SeesawEncoder : public SeesawDevice { + SeesawEncoder(uint8_t addr) : SeesawDevice(addr), + position(0), previous_position(0), delta(0), + button(0), button_previous(0), pixel_color(0), + timeout(0), rel_position(0), changed(false), + last_change_time(0), rotation_occurred(false) { + type = SEESAW_TYPE_ENCODER; + abs_position[0] = 0; + abs_position[1] = 0; + } + + virtual void Init() override { + // Enable encoder interrupt + Seesaw::Write8(address, SEESAW_ENCODER_BASE, SEESAW_ENCODER_INTENSET, 0x01); + + // Set initial encoder position to 0 + SetEncoderPosition(0); + + // Configure button pin (24) as input with pullup + uint32_t pin_mask = (uint32_t)1 << SEESAW_ENCODER_BUTTON_PIN; + // Convert 32-bit pin mask to 4-byte buffer (big-endian) + uint8_t pin_buf[4] = { + (uint8_t)(pin_mask >> 24), + (uint8_t)(pin_mask >> 16), + (uint8_t)(pin_mask >> 8), + (uint8_t)(pin_mask & 0xFF) + }; + // Set as input (clear direction bit) + Seesaw::Write(address, SEESAW_GPIO_BASE, SEESAW_GPIO_DIRCLR_BULK, pin_buf, 4); + // Enable pullup + Seesaw::Write(address, SEESAW_GPIO_BASE, SEESAW_GPIO_PULLENSET, pin_buf, 4); + // Set pin high (for pullup) + Seesaw::Write(address, SEESAW_GPIO_BASE, SEESAW_GPIO_BULK_SET, pin_buf, 4); + + // Check if NeoPixel module is available (bit 14 = 0x4000) + // if (options & (1UL << SEESAW_NEOPIXEL_BASE)) { + // AddLog(LOG_LEVEL_INFO, PSTR("SEE: NeoPixel module IS available")); + // } else { + // AddLog(LOG_LEVEL_ERROR, PSTR("SEE: NeoPixel module NOT available in firmware!")); + // } + // } + + // NeoPixel Init Step 1: updateType - Set speed to 800KHz (NEO_KHZ800 = 0x0000, so is800KHz = true = 1) + bool speed_ok = Seesaw::Write8(address, SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_SPEED, 1); + + // NeoPixel Init Step 2: updateLength - Set buffer length to numBytes + uint16_t num_bytes = 1 * 3; // 1 pixel * 3 bytes = 3 bytes + uint8_t len_buf[2] = { + (uint8_t)(num_bytes >> 8), // high byte + (uint8_t)(num_bytes & 0xFF) // low byte + }; + bool len_ok = Seesaw::Write(address, SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF_LENGTH, len_buf, 2); + + // NeoPixel Init Step 3: setPin - Set the NeoPixel output pin to 6 + bool pin_ok = Seesaw::Write8(address, SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_PIN, SEESAW_ENCODER_NEOPIXEL_PIN); + + // NeoPixel Init Step 4: Initialize NeoPixel to off + SetPixelColor(0x000000); + + // Read initial encoder state + position = GetEncoderPosition(); + previous_position = position; + button = GetButton() ? 1 : 0; + button_previous = button; + last_change_time = millis(); + + valid = true; + +#ifdef DEBUG_SEESAW_ENCODER + AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: Init Encoder ADDR=%02X POS=%d BTN=%d PXL_SPD_OK=%d PXL_LEN_OK=%d PXL_PIN_OK=%d"), + address, position, button, speed_ok, len_ok, pin_ok); +#endif + +#if defined(SEESAW_ENCODER_LIKE_ROTARY) && defined(USE_LIGHT) + // Initialize rotary settings if needed + InitRotarySettings(); +#endif // SEESAW_ENCODER_LIKE_ROTARY && USE_LIGHT + } + + virtual void Read() override { + // Read encoder delta (change since last read) + int32_t new_delta = GetEncoderDelta(); + + // Read encoder position + int32_t new_position = GetEncoderPosition(); + + // Update delta + delta = new_delta; + + // Update position + previous_position = position; + position = new_position; + + // Read button state + button_previous = button; + button = GetButton() ? 1 : 0; + + // Update timestamp if changed + if (delta != 0 || button != button_previous) { + last_change_time = millis(); + } + +#ifdef DEBUG_SEESAW_ENCODER + if (delta != 0 || button != button_previous) { + AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: READ ADDR=%02X POS=%d DELTA=%d BTN=%d"), + address, position, delta, button); + } +#endif + } + + virtual void Handler() override { + // Handler logic mirrors support_rotary.ino RotaryHandler() (lines 195-278) + // to provide consistent behavior between GPIO and I2C rotary encoders + + if (timeout) { + timeout--; + if (!timeout) { +#ifdef USE_LIGHT + if (!Settings->flag4.rotary_uses_rules) { // SetOption98 - Use rules instead of light control + ResponseLightState(0); + MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, PSTR(D_CMND_STATE)); + } +#endif // USE_LIGHT + } + } + + // Reset changed flag when button released + if (button_previous && !button) { + if (changed) { + changed = false; + } + } + + // Check for rotation or button change + if (delta == 0 && button == button_previous) { return; } + + timeout = SEESAW_ENCODER_TIMEOUT; // Prevent fast direction changes within 100ms + + int32_t current_delta = delta; + delta = 0; // Clear delta after reading + + // Postpone flash writes during rapid rotation + // Mirrors support_rotary.ino line 218-220 + if (Settings->save_data && (TasmotaGlobal.save_data_counter < 2)) { + TasmotaGlobal.save_data_counter = 3; + } + + bool button_pressed = button; // Button is pressed: set color temperature + if (button_pressed) { changed = true; } + + abs_position[button_pressed] += current_delta; + if (abs_position[button_pressed] < 0) { + abs_position[button_pressed] = 0; + } + if (abs_position[button_pressed] > Settings->param[P_ROTARY_MAX_STEP]) { + abs_position[button_pressed] = Settings->param[P_ROTARY_MAX_STEP]; + } + + rel_position += current_delta; + if (rel_position > Settings->param[P_ROTARY_MAX_STEP]) { + rel_position = Settings->param[P_ROTARY_MAX_STEP]; + } + if (rel_position < -(Settings->param[P_ROTARY_MAX_STEP])) { + rel_position = -(Settings->param[P_ROTARY_MAX_STEP]); + } + +#ifdef DEBUG_SEESAW_ENCODER + AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: %s btn=%d delta=%d abs_position[0]=%d abs_position[1]=%d, rel_position=%d"), + device_name, button_pressed, current_delta, + abs_position[0], abs_position[1], rel_position); +#endif + +#ifdef SEESAW_ENCODER_LIKE_ROTARY + // Button click handling - toggle relay on a click without rotation + // On button click - track if rotation occurs + if (current_delta != 0 && button) { + rotation_occurred = true; + } + // On button release - check if it was just a click (no rotation) + if (button_previous && !button) { + if (!changed && !rotation_occurred) { + // Button released without being used for rotation - toggle relay + uint8_t relay_index = device_index + 1; // device_index is 0-based + ExecuteCommandPower(relay_index, POWER_TOGGLE, SRC_BUTTON); + // Early return to prevent light control code from interfering with toggle + return; + } + // Reset rotation flag for next button press + rotation_occurred = false; + } + +#ifdef USE_LIGHT + // Light control (only first 2 encoders detected) + // Mirrors support_rotary.ino lines 227-254 (inline logic matching GPIO rotary behavior) + if (device_index < 2 && !Settings->flag4.rotary_uses_rules) { // SetOption98 - Use rules instead of light control + // Check if second encoder exists + // Matches support_rotary.ino line 228: bool second_rotary = (Encoder[1].pinb >= 0); + bool second_encoder = (SeesawMgr.GetTypeCount(SEESAW_TYPE_ENCODER) > 1); + + if (device_index == 0) { // First encoder (lines 229-247 in support_rotary.ino) + if (button_pressed) { + // Color or CT control + if (second_encoder) { + // With second encoder: control color only + LightColorOffset(current_delta * Rotary.color_increment); + } else { + // Without second encoder: try CT, fallback to color + if (!LightColorTempOffset(current_delta * Rotary.ct_increment)) { + LightColorOffset(current_delta * Rotary.color_increment); + } + } + } else { + // Dimmer RGBCW or RGB only if second rotary + uint32_t dimmer_index = second_encoder ? 1 : 0; + if (!Settings->flag4.rotary_poweron_dimlow || TasmotaGlobal.power) { // SetOption113 + LightDimmerOffset(dimmer_index, current_delta * Rotary.dimmer_increment); + } else { + if (current_delta > 0) { // Only power on if rotary increase + LightDimmerOffset(dimmer_index, -LightGetDimmer(dimmer_index) + ROTARY_START_DIM); + } + } + } + } else { // Second encoder (lines 248-254 in support_rotary.ino) + if (button_pressed) { + // Color Temperature + LightColorTempOffset(current_delta * Rotary.ct_increment); + } else { + // Dimmer CW + LightDimmerOffset(2, current_delta * Rotary.dimmer_increment); + } + } + return; // Skip rules processing for light control mode + } +#endif // USE_LIGHT +#endif // SEESAW_ENCODER_LIKE_ROTARY + + // Trigger rules (when not in direct light control mode) + // Mirrors support_rotary.ino lines 257-273 + Response_P(PSTR("{\"%s\":{\"Pos1\":%d,\"Pos2\":%d,\"Button\":%d}}"), + device_name, + abs_position[0], + abs_position[1], + button); + XdrvRulesProcess(0); + } + + virtual void Show(bool json, const char *name) override { + // Store name for use in Handler() and debug logging + strlcpy(device_name, name, sizeof(device_name)); + + if (json) { + ResponseAppend_P(PSTR(",\"%s\":{\"Pos1\":%d,\"Pos2\":%d,\"Button\":%d,\"Color\":\"%06X\"}"), + name, abs_position[0], abs_position[1], + button, pixel_color); +#ifdef USE_WEBSERVER + } else { + WSContentSend_PD(PSTR("{s}%s Pos1{m}%d{e}"), name, abs_position[0]); + WSContentSend_PD(PSTR("{s}%s Pos2{m}%d{e}"), name, abs_position[1]); + WSContentSend_PD(PSTR("{s}%s Button{m}%d{e}"), name, button); + WSContentSend_PD(PSTR("{s}%s Color{m}#%06X{e}"), name, pixel_color); +#endif // USE_WEBSERVER + } + } + + virtual bool HandleCommand(const char* cmd, uint32_t len) override { + // Commands: Set , Color or ,, + // This is called from the manager with cmd already pointing to the command + return false; // Commands handled via Tasmota command interface + } + + bool SetEncoderPosition(int32_t pos) { + uint8_t buf[4] = { + (uint8_t)(pos >> 24), + (uint8_t)(pos >> 16), + (uint8_t)(pos >> 8), + (uint8_t)(pos & 0xFF) + }; + bool success = Seesaw::Write(address, SEESAW_ENCODER_BASE, SEESAW_ENCODER_POSITION, buf, 4); + + if (success) { + position = pos; + previous_position = pos; + } + +#ifdef DEBUG_SEESAW_ENCODER + AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: WRITE ADDR=%02X val=%d success=%d"), + address, pos, success); +#endif + + return success; + } + + bool SetPixelColor(uint32_t color) { + // Set NeoPixel buffer: pixel index (2 bytes) + GRB color data (3 bytes) + uint8_t buf[5] = { + 0, // index high byte + 0, // index low byte + (uint8_t)(color >> 8), // G + (uint8_t)(color >> 16), // R + (uint8_t)(color & 0xFF) // B + }; + + if (!Seesaw::Write(address, SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF, buf, 5)) { + return false; + } + + // Show the pixel + bool success = Seesaw::Write(address, SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_SHOW, nullptr, 0); + if (success) { + pixel_color = color; + } + +#ifdef DEBUG_SEESAW_ENCODER + AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: COLOR ADDR=%02X color=%06X success=%d"), + address, color, success); +#endif + + return success; + } + + static const char id[] PROGMEM; + +private: + int32_t GetEncoderPosition() { + uint8_t buf[4]; + if (!Seesaw::Read(address, SEESAW_ENCODER_BASE, SEESAW_ENCODER_POSITION, buf, 4)) { + return 0; + } + return ((int32_t)buf[0] << 24) | ((int32_t)buf[1] << 16) | ((int32_t)buf[2] << 8) | (int32_t)buf[3]; + } + + int32_t GetEncoderDelta() { + uint8_t buf[4]; + if (!Seesaw::Read(address, SEESAW_ENCODER_BASE, SEESAW_ENCODER_DELTA, buf, 4)) { + return 0; + } + return ((int32_t)buf[0] << 24) | ((int32_t)buf[1] << 16) | ((int32_t)buf[2] << 8) | (int32_t)buf[3]; + } + + bool GetButton() { + uint8_t buf[4]; + if (!Seesaw::Read(address, SEESAW_GPIO_BASE, SEESAW_GPIO_BULK, buf, 4)) { + return false; + } + uint32_t gpio_value = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | + ((uint32_t)buf[2] << 8) | (uint32_t)buf[3]; + // Button is on pin 24, active low with pullup + return !(gpio_value & ((uint32_t)1 << SEESAW_ENCODER_BUTTON_PIN)); + } + +#if defined(SEESAW_ENCODER_LIKE_ROTARY) && defined(USE_LIGHT) + void InitRotarySettings() { + #ifdef ROTARY_V1 + if (Rotary.present) { return; } // GPIO rotaries already initialized their settings + #endif + + // No GPIO Rotary present, initialize for Seesaw Encoders + RotaryInitMaxSteps(); + } +#endif // SEESAW_ENCODER_LIKE_ROTARY && USE_LIGHT + + int32_t position; + int32_t previous_position; + int32_t delta; + uint8_t button; + uint8_t button_previous; + uint32_t pixel_color; + uint8_t timeout; + int8_t abs_position[2]; + int8_t rel_position; + bool changed; + uint32_t last_change_time; + bool rotation_occurred; +}; + +const char SeesawEncoder::id[] PROGMEM = "ENCODER"; + +// Factory function implementation +SeesawDevice* SeesawManager::CreateEncoderDevice(uint8_t addr) { + return new SeesawEncoder(addr); +} + +// Helper function to find encoder by command index +// Returns nullptr if index is out of range +// Sets total_count to the total number of encoders found +SeesawEncoder* GetEncoderByIndex(uint8_t cmd_index, uint8_t* total_count = nullptr) { + uint8_t encoder_count = 0; + + // Count encoder devices + for (uint8_t i = 0; i < SeesawMgr.GetCount(); i++) { + SeesawDevice* dev = SeesawMgr.GetDevice(i); + if (dev && dev->GetType() == SEESAW_TYPE_ENCODER) { + encoder_count++; + } + } + + if (total_count) { + *total_count = encoder_count; + } + + if (cmd_index < 1 || cmd_index > encoder_count) { + return nullptr; + } + + // Find the Nth encoder device + uint8_t encoder_index = 0; + for (uint8_t i = 0; i < SeesawMgr.GetCount(); i++) { + SeesawDevice* dev = SeesawMgr.GetDevice(i); + if (dev && dev->GetType() == SEESAW_TYPE_ENCODER) { + encoder_index++; + if (encoder_index == cmd_index) { + return static_cast(dev); // Safe: dev validated above + } + } + } + + return nullptr; +} + +// Command handlers +#define D_PRFX_SEEENC "SeeEnc" +#define D_CMND_SEEENC_SET "Set" +#define D_CMND_SEEENC_COLOR "Color" + +const char kSeeEncCommands[] PROGMEM = D_PRFX_SEEENC "|" + D_CMND_SEEENC_SET "|" D_CMND_SEEENC_COLOR; + +void CmndSeeEncSet(void) { + // Command format: SeeEncSet + uint8_t encoder_count = 0; + SeesawEncoder* encoder = GetEncoderByIndex(XdrvMailbox.index, &encoder_count); + + if (!encoder) { + ResponseCmndIdxNumber(encoder_count); + return; + } + + if (encoder->SetEncoderPosition(XdrvMailbox.payload)) { + ResponseCmndNumber(XdrvMailbox.payload); + } else { + ResponseCmndFailed(); + } +} + +void CmndSeeEncColor(void) { + // Command format: SeeEncColor or ,, + uint8_t encoder_count = 0; + SeesawEncoder* encoder = GetEncoderByIndex(XdrvMailbox.index, &encoder_count); + + if (!encoder) { + ResponseCmndIdxNumber(encoder_count); + return; + } + + uint32_t color = 0; + + // Check if input is hex format (RRGGBB) or comma-separated (R,G,B) + if (strchr(XdrvMailbox.data, ',')) { + // Parse R,G,B format + uint8_t r, g, b; + if (sscanf(XdrvMailbox.data, "%hhu,%hhu,%hhu", &r, &g, &b) == 3) { + color = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b; + } else { + ResponseCmndError(); + return; + } + } else { + // Parse hex format + color = strtoul(XdrvMailbox.data, nullptr, 16); + } + + if (encoder->SetPixelColor(color)) { + ResponseCmndIdxChar(XdrvMailbox.data); + } else { + ResponseCmndFailed(); + } +} + +void (* const SeeEncCommand[])(void) PROGMEM = { + &CmndSeeEncSet, + &CmndSeeEncColor }; + +#endif // USE_SEESAW_ENCODER +#endif // USE_I2C diff --git a/tasmota/tasmota_xsns_sensor/xsns_81_seesaw_soil.ino b/tasmota/tasmota_xsns_sensor/xsns_81_seesaw_soil.ino index 429c43856..c3139c665 100644 --- a/tasmota/tasmota_xsns_sensor/xsns_81_seesaw_soil.ino +++ b/tasmota/tasmota_xsns_sensor/xsns_81_seesaw_soil.ino @@ -22,341 +22,231 @@ #ifdef USE_SEESAW_SOIL /*********************************************************************************************\ - * SEESAW_SOIL - Capacitice Soil Moisture & Temperature Sensor + * SEESAW_SOIL - Capacitive Soil Moisture & Temperature Sensor * * I2C Address: 0x36, 0x37, 0x38, 0x39 * - * This version of the driver replaces all delay loops by a state machine. So the number - * of instruction cycles consumed has been reduced dramatically. The sensors are reset, - * detected, commanded and read all at once. So the reading times won't increase with the - * number of sensors attached. The detection of sensors does not happen in FUNC_INIT any - * more. All i2c handling happens in the 50ms state machine. - * The memory footprint has suffered a little bit from this redesign, naturally. - * - * Memory footprint: 1444 bytes flash / 68 bytes RAM - * * NOTE: #define SEESAW_SOIL_PUBLISH enables immediate MQTT on soil moisture change * otherwise the moisture value will only be emitted every TelePeriod * #define SEESAW_SOIL_RAW enables displaying analog capacitance input in the * web page for calibration purposes - * #define SEESAW_SOIL_PERSISTENT_NAMING to get sensor names indexed by i2c address + * #define SEESAW_SOIL_PERSISTENT_NAMING to get sensor names indexed by I2C address + * (e.g., SeeSoil-36 instead of SeeSoil-1) for consistent naming across restarts \*********************************************************************************************/ -#define XSNS_81 81 -#define XI2C_56 56 // See I2CDEVICES.md - -#include "Adafruit_seesaw.h" // we only use definitions, no code - -#define SEESAW_SOIL_MAX_SENSORS 4 -#define SEESAW_SOIL_START_ADDRESS 0x36 - // I2C state machine -#define STATE_IDLE 0x00 -#define STATE_RESET 0x01 -#define STATE_INIT 0x02 -#define STATE_DETECT 0x04 -#define STATE_COMMAND_TEMP 0x08 -#define STATE_READ_TEMP 0x10 -#define STATE_COMMAND_MOIST 0x20 -#define STATE_READ_MOIST 0x40 - // I2C commands -#define COMMAND_RESET 0x01 -#define COMMAND_ID 0x02 -#define COMMAND_TEMP 0x04 -#define COMMAND_MOIST 0x08 - // I2C delays -#define DELAY_DETECT 1 // ms delay before reading ID -#define DELAY_TEMP 1 // ms delay between command and reading -#define DELAY_MOIST 5 // ms delay between command and reading -#define DELAY_RESET 500 // ms delay after slave reset +// I2C commands +#define SOIL_COMMAND_TEMP 0x04 +#define SOIL_COMMAND_MOIST 0x08 +// I2C delays +#define SOIL_DELAY_TEMP 1 // ms delay between command and reading +#define SOIL_DELAY_MOIST 5 // ms delay between command and reading // Convert capacitance into a moisture. // From observation, a free air reading is at 320, immersed in tap water, reading is 1014 // So let's make a scale that converts those (apparent) facts into a percentage -#define MAX_CAPACITANCE 1020.0f // subject to calibration -#define MIN_CAPACITANCE 320 // subject to calibration +#define MAX_CAPACITANCE 1020.0f // subject to calibration +#define MIN_CAPACITANCE 320 // subject to calibration #define CAP_TO_MOIST(c) ((max((int)(c),MIN_CAPACITANCE)-MIN_CAPACITANCE)/(MAX_CAPACITANCE-MIN_CAPACITANCE)*100) -struct SEESAW_SOIL { - const char name[8] = "SeeSoil"; // spaces not allowed for Homeassistant integration/mqtt topics - uint8_t count = 0; // global sensor count (0xFF = not initialized) - uint8_t state = STATE_IDLE; // current state - bool present = false; // driver active -} SeeSoil; - -struct SEESAW_SOIL_SNS { - uint8_t address; // i2c address - float moisture; - float temperature; +struct SeesawSoil : public SeesawDevice { + SeesawSoil(uint8_t addr) : SeesawDevice(addr), temperature(NAN), moisture(NAN), state(STATE_IDLE) { + type = SEESAW_TYPE_SOIL; #ifdef SEESAW_SOIL_RAW - uint16_t capacitance; // raw analog reading -#endif // SEESAW_SOIL_RAW -} SeeSoilSNS[SEESAW_SOIL_MAX_SENSORS]; - -/*********************************************************************************************\ - * i2c routines -\*********************************************************************************************/ - -void seeSoilInit(void) { - for (int i = 0; i < SEESAW_SOIL_MAX_SENSORS; i++) { - int addr = SEESAW_SOIL_START_ADDRESS + i; - if ( ! I2cSetDevice(addr) ) { continue; } - seeSoilCommand(COMMAND_RESET); - } - SeeSoil.state = STATE_RESET; - SeeSoil.present = true; -} - -void seeSoilEvery50ms(void){ // i2c state machine - static uint32_t state_time; - - uint32_t time_diff = millis() - state_time; - - switch (SeeSoil.state) { - case STATE_RESET: // reset was just issued - SeeSoil.state = STATE_INIT; - break; - case STATE_INIT: // wait for sensors to settle - if (time_diff < DELAY_RESET) { return; } - seeSoilCommand(COMMAND_ID); // send hardware id commands - SeeSoil.state = STATE_DETECT; - break; - case STATE_DETECT: // detect sensors - if (time_diff < DELAY_DETECT) { return; } - seeSoilDetect(); - SeeSoil.state=STATE_COMMAND_TEMP; - break; - case STATE_COMMAND_TEMP: // send temperature commands - seeSoilCommand(COMMAND_TEMP); - SeeSoil.state = STATE_READ_TEMP; - break; - case STATE_READ_TEMP: - if (time_diff < DELAY_TEMP) { return; } - seeSoilRead(COMMAND_TEMP); // read temperature values - SeeSoil.state = STATE_COMMAND_MOIST; - break; - case STATE_COMMAND_MOIST: // send moisture commands - seeSoilCommand(COMMAND_MOIST); - SeeSoil.state = STATE_READ_MOIST; - break; - case STATE_READ_MOIST: - if (time_diff < DELAY_MOIST) { return; } - seeSoilRead(COMMAND_MOIST); // read moisture values - SeeSoil.state = STATE_COMMAND_TEMP; - break; - } - state_time = millis(); -} - -void seeSoilDetect(void) { // detect sensors - uint8_t buf; - - SeeSoil.count = 0; - SeeSoil.present = false; - for (int i = 0; i < SEESAW_SOIL_MAX_SENSORS; i++) { - uint32_t addr = SEESAW_SOIL_START_ADDRESS + i; - if ( ! I2cSetDevice(addr)) { continue; } - if (1 != Wire.requestFrom((uint8_t) addr, (uint8_t) 1)) { continue; } - buf = (uint8_t) Wire.read(); - if (buf != SEESAW_HW_ID_CODE) { // check hardware id -#ifdef DEBUG_SEESAW_SOIL - AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: HWID mismatch ADDR=%X, ID=%X"), addr, buf); -#endif // DEBUG_SEESAW_SOIL - continue; - } - SeeSoilSNS[SeeSoil.count].address = addr; - SeeSoilSNS[SeeSoil.count].temperature = NAN; - SeeSoilSNS[SeeSoil.count].moisture = NAN; -#ifdef SEESAW_SOIL_RAW - SeeSoilSNS[SeeSoil.count].capacitance = 0; // raw analog reading -#endif // SEESAW_SOIL_RAW - I2cSetActiveFound(SeeSoilSNS[SeeSoil.count].address, SeeSoil.name); - SeeSoil.count++; - SeeSoil.present = true; -#ifdef DEBUG_SEESAW_SOIL - AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: FOUND sensor %u at %02X"), i, addr); -#endif // DEBUG_SEESAW_SOIL - } -} - -void seeSoilCommand(uint32_t command) { // issue commands to sensors - uint8_t regLow; - uint8_t regHigh = SEESAW_STATUS_BASE; - uint32_t count = SeeSoil.count; - - switch (command) { - case COMMAND_RESET: - count = SEESAW_SOIL_MAX_SENSORS; - regLow = SEESAW_STATUS_SWRST; - break; - case COMMAND_ID: - count = SEESAW_SOIL_MAX_SENSORS; - regLow = SEESAW_STATUS_HW_ID; - break; - case COMMAND_TEMP: - regLow = SEESAW_STATUS_TEMP; - break; - case COMMAND_MOIST: - regHigh = SEESAW_TOUCH_BASE; - regLow = SEESAW_TOUCH_CHANNEL_OFFSET; - break; - default: -#ifdef DEBUG_SEESAW_SOIL - AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: ILL CMD:%02X"), command); -#endif // DEBUG_SEESAW_SOIL - return; - } - for (int i = 0; i < count; i++) { - uint32_t addr = (command & (COMMAND_RESET|COMMAND_ID)) ? SEESAW_SOIL_START_ADDRESS + i : SeeSoilSNS[i].address; - Wire.beginTransmission((uint8_t) addr); - Wire.write((uint8_t) regHigh); - Wire.write((uint8_t) regLow); - uint32_t err = Wire.endTransmission(); -#ifdef DEBUG_SEESAW_SOIL - AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: SNS=%u ADDR=%02X CMD=%02X ERR=%u"), i, addr, command, err); -#endif // DEBUG_SEESAW_SOIL - } -} - -void seeSoilRead(uint32_t command) { // read values from sensors - uint8_t buf[4]; - uint32_t num; - int32_t ret; - - num = (command == COMMAND_TEMP) ? 4 : 2; // response size in bytes - - for (int i = 0; i < SeeSoil.count; i++) { // for all sensors - if (num != Wire.requestFrom((uint8_t) SeeSoilSNS[i].address, (uint8_t) num)) { continue; } - bzero(buf, sizeof(buf)); - for (int b = 0; b < num; b++) { - buf[b] = (uint8_t) Wire.read(); - } - if (command == COMMAND_TEMP) { - ret = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | - ((uint32_t)buf[2] << 8) | (uint32_t)buf[3]; - SeeSoilSNS[i].temperature = ConvertTemp((1.0 / (1UL << 16)) * ret); - } else { // COMMAND_MOIST - ret = (uint32_t)buf[0] << 8 | (uint32_t)buf[1]; - SeeSoilSNS[i].moisture = CAP_TO_MOIST(ret); -#ifdef SEESAW_SOIL_RAW - SeeSoilSNS[i].capacitance = ret; -#endif // SEESAW_SOIL_RAW - } -#ifdef DEBUG_SEESAW_SOIL - AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: READ #%u ADDR=%02X NUM=%u RET=%X"), i, SeeSoilSNS[i].address, num, ret); -#endif // DEBUG_SEESAW_SOIL - } -} - -/*********************************************************************************************\ - * JSON routines -\*********************************************************************************************/ - + capacitance = 0; +#endif #ifdef SEESAW_SOIL_PUBLISH -void seeSoilEverySecond(void) { // update sensor values and publish if changed - static uint16_t old_moist[SEESAW_SOIL_MAX_SENSORS]; - static bool firstcall = true; - - for (int i = 0; i < SeeSoil.count; i++) { - if (firstcall) { firstcall = false; } - else { - if ((uint32_t) SeeSoilSNS[i].moisture != old_moist[i]) { - Response_P(PSTR("{")); // send values to MQTT & rules - seeSoilJson(i); - ResponseJsonEnd(); - MqttPublishTeleSensor(); - } - } - old_moist[i] = (uint32_t) SeeSoilSNS[i].moisture; + old_moist = 0; + first_handler_call = true; +#endif } -} -#endif // SEESAW_SOIL_PUBLISH -void seeSoilShow(bool json) { - char sensor_name[sizeof(SeeSoil.name) + 3]; + virtual void Init() override { + // Device already reset by manager + state = STATE_COMMAND_TEMP; + state_time = millis(); + valid = true; + } + + virtual void Read() override { + uint32_t time_diff = millis() - state_time; + + switch (state) { + case STATE_COMMAND_TEMP: + SendCommand(SOIL_COMMAND_TEMP); + state = STATE_READ_TEMP; + break; + + case STATE_READ_TEMP: + if (time_diff < SOIL_DELAY_TEMP) { return; } + ReadTemperature(); + state = STATE_COMMAND_MOIST; + break; + + case STATE_COMMAND_MOIST: + SendCommand(SOIL_COMMAND_MOIST); + state = STATE_READ_MOIST; + break; + + case STATE_READ_MOIST: + if (time_diff < SOIL_DELAY_MOIST) { return; } + ReadMoisture(); + state = STATE_COMMAND_TEMP; + break; + + case STATE_IDLE: + default: + state = STATE_COMMAND_TEMP; + break; + } + state_time = millis(); + } + + virtual void Show(bool json, const char *name) override { + // Store name for use in Handler() and debug logging + strlcpy(device_name, name, sizeof(device_name)); - for (uint32_t i = 0; i < SeeSoil.count; i++) { - seeSoilName(i, sensor_name, sizeof(sensor_name)); if (json) { - ResponseAppend_P(PSTR(",")); // compose tele json - seeSoilJson(i); - if (0 == TasmotaGlobal.tele_period) { + ResponseAppend_P(PSTR(",\"%s\":{\"" D_JSON_ID "\":\"%02X\",\"" D_JSON_TEMPERATURE "\":%*_f,\"" D_JSON_MOISTURE "\":%u}"), + name, address, + Settings->flag2.temperature_resolution, &temperature, + (uint32_t) moisture); #ifdef USE_DOMOTICZ - DomoticzTempHumPressureSensor(SeeSoilSNS[i].temperature, SeeSoilSNS[i].moisture, -42.0f); + if (0 == TasmotaGlobal.tele_period) { + DomoticzTempHumPressureSensor(temperature, moisture, -42.0f); + } #endif // USE_DOMOTICZ #ifdef USE_KNX - KnxSensor(KNX_TEMPERATURE, SeeSoilSNS[i].temperature); - KnxSensor(KNX_HUMIDITY, SeeSoilSNS[i].moisture); -#endif // USE_KNX + if (0 == TasmotaGlobal.tele_period) { + KnxSensor(KNX_TEMPERATURE, temperature); + KnxSensor(KNX_HUMIDITY, moisture); } +#endif // USE_KNX #ifdef USE_WEBSERVER } else { #ifdef SEESAW_SOIL_RAW - WSContentSend_PD(HTTP_SNS_ANALOG, sensor_name, 0, SeeSoilSNS[i].capacitance); + WSContentSend_PD(HTTP_SNS_ANALOG, name, 0, capacitance); #endif // SEESAW_SOIL_RAW - WSContentSend_PD(HTTP_SNS_MOISTURE, sensor_name, (uint32_t) SeeSoilSNS[i].moisture); - WSContentSend_Temp(sensor_name, SeeSoilSNS[i].temperature); + WSContentSend_PD(HTTP_SNS_MOISTURE, name, (uint32_t) moisture); + WSContentSend_Temp(name, temperature); #endif // USE_WEBSERVER } - } // for each sensor connected -} - -void seeSoilJson(int no) { // common json - char sensor_name[sizeof(SeeSoil.name) + 3]; - seeSoilName(no, sensor_name, sizeof(sensor_name)); - - ResponseAppend_P(PSTR ("\"%s\":{\"" D_JSON_ID "\":\"%02X\",\"" D_JSON_TEMPERATURE "\":%*_f,\"" D_JSON_MOISTURE "\":%u}"), - sensor_name, SeeSoilSNS[no].address, - Settings->flag2.temperature_resolution, &SeeSoilSNS[no].temperature, - (uint32_t) SeeSoilSNS[no].moisture); -} - -void seeSoilName(int no, char *name, int len) // generates a sensor name -{ -#ifdef SEESAW_SOIL_PERSISTENT_NAMING - snprintf_P(name, len, PSTR("%s%c%02X"), SeeSoil.name, IndexSeparator(), SeeSoilSNS[no].address); -#else - if (SeeSoil.count > 1) { - snprintf_P(name, len, PSTR("%s%c%u"), SeeSoil.name, IndexSeparator(), no + 1); } - else { - strlcpy(name, SeeSoil.name, len); - } -#endif // SEESAW_SOIL_PERSISTENT_NAMING -} -/*********************************************************************************************\ - * Interface -\*********************************************************************************************/ - -bool Xsns81(uint32_t function) -{ - if (!I2cEnabled(XI2C_56)) { return false; } - bool result = false; - - if (FUNC_INIT == function) { - seeSoilInit(); - } - else if (SeeSoil.present){ - switch (function) { - case FUNC_EVERY_50_MSECOND: - seeSoilEvery50ms(); - break; - #ifdef SEESAW_SOIL_PUBLISH - case FUNC_EVERY_SECOND: - seeSoilEverySecond(); - break; - #endif // SEESAW_SOIL_PUBLISH - case FUNC_JSON_APPEND: - seeSoilShow(1); - break; - #ifdef USE_WEBSERVER - case FUNC_WEB_SENSOR: - seeSoilShow(0); - break; - #endif // USE_WEBSERVER +#ifdef SEESAW_SOIL_PUBLISH + virtual void Handler() override { + // Publish immediately on moisture change + if (first_handler_call) { + first_handler_call = false; + old_moist = (uint32_t) moisture; + } else { + if ((uint32_t) moisture != old_moist) { + Response_P(PSTR("{")); + Show(true, device_name); + ResponseJsonEnd(); + MqttPublishTeleSensor(); + old_moist = (uint32_t) moisture; + } } } - return result; +#endif // SEESAW_SOIL_PUBLISH + + static const char id[] PROGMEM; + +private: + void SendCommand(uint32_t command) { + uint8_t regHigh = SEESAW_STATUS_BASE; + uint8_t regLow; + + switch (command) { + case SOIL_COMMAND_TEMP: + regLow = SEESAW_STATUS_TEMP; + break; + case SOIL_COMMAND_MOIST: + regHigh = SEESAW_TOUCH_BASE; + regLow = SEESAW_TOUCH_CHANNEL_OFFSET; + break; + default: +#ifdef DEBUG_SEESAW_SOIL + AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: ILL CMD:%02X"), command); +#endif + return; + } + + Wire.beginTransmission(address); + Wire.write(regHigh); + Wire.write(regLow); + Wire.endTransmission(); + +#ifdef DEBUG_SEESAW_SOIL + AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: ADDR=%02X CMD=%02X"), address, command); +#endif + } + + void ReadTemperature() { + uint8_t buf[4]; + bzero(buf, sizeof(buf)); + if (4 != Wire.requestFrom(address, (uint8_t)4)) { return; } + + for (int i = 0; i < 4; i++) { + buf[i] = Wire.read(); + } + + int32_t ret = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | + ((uint32_t)buf[2] << 8) | (uint32_t)buf[3]; + temperature = ConvertTemp((1.0 / (1UL << 16)) * ret); + +#ifdef DEBUG_SEESAW_SOIL + AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: READ TEMP ADDR=%02X RET=%X"), address, ret); +#endif + } + + void ReadMoisture() { + uint8_t buf[2]; + bzero(buf, sizeof(buf)); + if (2 != Wire.requestFrom(address, (uint8_t)2)) { return; } + + for (int i = 0; i < 2; i++) { + buf[i] = Wire.read(); + } + + int32_t ret = (uint32_t)buf[0] << 8 | (uint32_t)buf[1]; + moisture = CAP_TO_MOIST(ret); + +#ifdef SEESAW_SOIL_RAW + capacitance = ret; +#endif + +#ifdef DEBUG_SEESAW_SOIL + AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: READ MOIST ADDR=%02X RET=%X"), address, ret); +#endif + } + + enum State { + STATE_IDLE, + STATE_COMMAND_TEMP, + STATE_READ_TEMP, + STATE_COMMAND_MOIST, + STATE_READ_MOIST + }; + + float temperature; + float moisture; +#ifdef SEESAW_SOIL_RAW + uint16_t capacitance; +#endif + State state; + uint32_t state_time; +#ifdef SEESAW_SOIL_PUBLISH + uint16_t old_moist; + bool first_handler_call; +#endif +}; + +const char SeesawSoil::id[] PROGMEM = "SOIL"; + +// Factory function implementation +SeesawDevice* SeesawManager::CreateSoilDevice(uint8_t addr) { + return new SeesawSoil(addr); } #endif // USE_SEESAW_SOIL