Add support for Adafruit I2C QT Rotary Encoder which uses Seesaw and refactor existing Adafruit Seesaw Soil sensor (#24270)
- Refactored `xsns_81_seesaw_soil.ino` to utilize a base class `xsns_81_seesaw.ino` for Adafruit Seesaw devices - Add `xsns_81_seesaw_encoder.ino` for handling Adafruit I2C QT Rotary Encoder with NeoPixel and button functionalities. - Implemented option to have Adafruit I2C rotary encoder behave like a GPIO rotary encoders - Update Adafruit Seesaw library files to v1.7.9
This commit is contained in:
parent
0e19d9b367
commit
312ba73d6d
@ -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
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
*/
|
||||
|
||||
#include "Adafruit_seesaw.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
//#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,7 +309,9 @@ void Adafruit_seesaw::setGPIOInterrupts(uint32_t pins, bool enabled) {
|
||||
***********************************************************************/
|
||||
uint16_t Adafruit_seesaw::analogRead(uint8_t pin) {
|
||||
uint8_t buf[2];
|
||||
uint8_t p;
|
||||
uint8_t p = 0;
|
||||
|
||||
if (_hardwaretype == SEESAW_HW_ID_CODE_SAMD09) {
|
||||
switch (pin) {
|
||||
case ADC_INPUT_0_PIN:
|
||||
p = 0;
|
||||
@ -252,7 +327,16 @@ uint16_t Adafruit_seesaw::analogRead(uint8_t pin) {
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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];
|
||||
} while (ret == 65535);
|
||||
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,6 +508,8 @@ 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;
|
||||
|
||||
if (_hardwaretype == SEESAW_HW_ID_CODE_SAMD09) {
|
||||
switch (pin) {
|
||||
case PWM_0_PIN:
|
||||
p = 0;
|
||||
@ -423,20 +524,29 @@ void Adafruit_seesaw::analogWrite(uint8_t pin, uint16_t value, uint8_t width) {
|
||||
p = 3;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
return;
|
||||
}
|
||||
if (p > -1) {
|
||||
} 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};
|
||||
uint8_t cmd[] = {(uint8_t)p, (uint8_t)(mappedVal >> 8), (uint8_t)mappedVal};
|
||||
this->write(SEESAW_TIMER_BASE, SEESAW_TIMER_PWM, cmd, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* @brief set the PWM frequency of a PWM-enabled pin. Note that on SAMD09,
|
||||
@ -453,6 +563,8 @@ 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;
|
||||
|
||||
if (_hardwaretype == SEESAW_HW_ID_CODE_SAMD09) {
|
||||
switch (pin) {
|
||||
case PWM_0_PIN:
|
||||
p = 0;
|
||||
@ -469,6 +581,17 @@ void Adafruit_seesaw::setPWMFreq(uint8_t pin, uint16_t freq) {
|
||||
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);
|
||||
yield();
|
||||
}
|
||||
|
||||
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(",");
|
||||
Serial.print("Reading ");
|
||||
Serial.print(read_now);
|
||||
Serial.println(" bytes");
|
||||
#endif
|
||||
pos++;
|
||||
|
||||
if (!_i2c_dev->read(buf + pos, read_now)) {
|
||||
return false;
|
||||
}
|
||||
pos += read_now;
|
||||
#ifdef SEESAW_I2C_DEBUG
|
||||
Serial.println();
|
||||
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();
|
||||
}
|
||||
@ -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 <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
/*=========================================================================
|
||||
@ -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 */
|
||||
Adafruit_I2CDevice *_i2c_dev = NULL; ///< The BusIO device for I2C control
|
||||
|
||||
int8_t _flow; /*!< The flow control pin to use */
|
||||
|
||||
void write8(byte regHigh, byte regLow, byte value);
|
||||
uint8_t read8(byte regHigh, byte regLow, uint16_t delay = 125);
|
||||
uint8_t _hardwaretype = 0; /*!< what hardware type is attached! */
|
||||
uint8_t getI2CaddrEEPROMloc();
|
||||
|
||||
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();
|
||||
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
|
||||
@ -1,5 +1,5 @@
|
||||
name=Adafruit seesaw Library
|
||||
version=1.3.1
|
||||
version=1.7.9
|
||||
author=Adafruit
|
||||
maintainer=Adafruit <info@adafruit.com>
|
||||
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
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
439
tasmota/tasmota_xsns_sensor/xsns_81_seesaw.ino
Normal file
439
tasmota/tasmota_xsns_sensor/xsns_81_seesaw.ino
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
#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
|
||||
564
tasmota/tasmota_xsns_sensor/xsns_81_seesaw_encoder.ino
Normal file
564
tasmota/tasmota_xsns_sensor/xsns_81_seesaw_encoder.ino
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
#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<x> <position> - Set encoder position (e.g., SeeEncSet1 0)
|
||||
* - SeeEncColor<x> <rrggbb> - Set NeoPixel color in hex (e.g., SeeEncColor1 FF0000 for red)
|
||||
* - SeeEncColor<x> <r>,<g>,<b> - 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<x> <position>, Color<x> <rrggbb> or <r>,<g>,<b>
|
||||
// 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<SeesawEncoder*>(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<x> <position>
|
||||
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<x> <rrggbb> or <r>,<g>,<b>
|
||||
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
|
||||
@ -22,52 +22,24 @@
|
||||
#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
|
||||
#define SOIL_COMMAND_TEMP 0x04
|
||||
#define SOIL_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
|
||||
#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
|
||||
@ -76,287 +48,205 @@
|
||||
#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;
|
||||
capacitance = 0;
|
||||
#endif
|
||||
#ifdef SEESAW_SOIL_PUBLISH
|
||||
old_moist = 0;
|
||||
first_handler_call = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void seeSoilEvery50ms(void){ // i2c state machine
|
||||
static uint32_t state_time;
|
||||
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 (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;
|
||||
switch (state) {
|
||||
case STATE_COMMAND_TEMP:
|
||||
SendCommand(SOIL_COMMAND_TEMP);
|
||||
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;
|
||||
if (time_diff < SOIL_DELAY_TEMP) { return; }
|
||||
ReadTemperature();
|
||||
state = STATE_COMMAND_MOIST;
|
||||
break;
|
||||
case STATE_COMMAND_MOIST: // send moisture commands
|
||||
seeSoilCommand(COMMAND_MOIST);
|
||||
SeeSoil.state = STATE_READ_MOIST;
|
||||
|
||||
case STATE_COMMAND_MOIST:
|
||||
SendCommand(SOIL_COMMAND_MOIST);
|
||||
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;
|
||||
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();
|
||||
}
|
||||
|
||||
void seeSoilDetect(void) { // detect sensors
|
||||
uint8_t buf;
|
||||
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));
|
||||
|
||||
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;
|
||||
if (json) {
|
||||
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
|
||||
if (0 == TasmotaGlobal.tele_period) {
|
||||
DomoticzTempHumPressureSensor(temperature, moisture, -42.0f);
|
||||
}
|
||||
SeeSoilSNS[SeeSoil.count].address = addr;
|
||||
SeeSoilSNS[SeeSoil.count].temperature = NAN;
|
||||
SeeSoilSNS[SeeSoil.count].moisture = NAN;
|
||||
#endif // USE_DOMOTICZ
|
||||
#ifdef 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
|
||||
SeeSoilSNS[SeeSoil.count].capacitance = 0; // raw analog reading
|
||||
WSContentSend_PD(HTTP_SNS_ANALOG, name, 0, capacitance);
|
||||
#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
|
||||
WSContentSend_PD(HTTP_SNS_MOISTURE, name, (uint32_t) moisture);
|
||||
WSContentSend_Temp(name, temperature);
|
||||
#endif // USE_WEBSERVER
|
||||
}
|
||||
}
|
||||
|
||||
void seeSoilCommand(uint32_t command) { // issue commands to sensors
|
||||
uint8_t regLow;
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // SEESAW_SOIL_PUBLISH
|
||||
|
||||
static const char id[] PROGMEM;
|
||||
|
||||
private:
|
||||
void SendCommand(uint32_t command) {
|
||||
uint8_t regHigh = SEESAW_STATUS_BASE;
|
||||
uint32_t count = SeeSoil.count;
|
||||
uint8_t regLow;
|
||||
|
||||
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:
|
||||
case SOIL_COMMAND_TEMP:
|
||||
regLow = SEESAW_STATUS_TEMP;
|
||||
break;
|
||||
case COMMAND_MOIST:
|
||||
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 // DEBUG_SEESAW_SOIL
|
||||
#endif
|
||||
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();
|
||||
|
||||
Wire.beginTransmission(address);
|
||||
Wire.write(regHigh);
|
||||
Wire.write(regLow);
|
||||
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
|
||||
}
|
||||
AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: ADDR=%02X CMD=%02X"), address, command);
|
||||
#endif
|
||||
}
|
||||
|
||||
void seeSoilRead(uint32_t command) { // read values from sensors
|
||||
void ReadTemperature() {
|
||||
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 (4 != Wire.requestFrom(address, (uint8_t)4)) { return; }
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
buf[i] = Wire.read();
|
||||
}
|
||||
if (command == COMMAND_TEMP) {
|
||||
ret = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) |
|
||||
|
||||
int32_t 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
|
||||
}
|
||||
temperature = ConvertTemp((1.0 / (1UL << 16)) * ret);
|
||||
|
||||
#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
|
||||
}
|
||||
AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: READ TEMP ADDR=%02X RET=%X"), address, ret);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*********************************************************************************************\
|
||||
* JSON routines
|
||||
\*********************************************************************************************/
|
||||
void ReadMoisture() {
|
||||
uint8_t buf[2];
|
||||
bzero(buf, sizeof(buf));
|
||||
if (2 != Wire.requestFrom(address, (uint8_t)2)) { return; }
|
||||
|
||||
#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 < 2; i++) {
|
||||
buf[i] = Wire.read();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
#endif // SEESAW_SOIL_PUBLISH
|
||||
int32_t ret = (uint32_t)buf[0] << 8 | (uint32_t)buf[1];
|
||||
moisture = CAP_TO_MOIST(ret);
|
||||
|
||||
void seeSoilShow(bool json) {
|
||||
char sensor_name[sizeof(SeeSoil.name) + 3];
|
||||
|
||||
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) {
|
||||
#ifdef USE_DOMOTICZ
|
||||
DomoticzTempHumPressureSensor(SeeSoilSNS[i].temperature, SeeSoilSNS[i].moisture, -42.0f);
|
||||
#endif // USE_DOMOTICZ
|
||||
#ifdef USE_KNX
|
||||
KnxSensor(KNX_TEMPERATURE, SeeSoilSNS[i].temperature);
|
||||
KnxSensor(KNX_HUMIDITY, SeeSoilSNS[i].moisture);
|
||||
#endif // USE_KNX
|
||||
}
|
||||
#ifdef USE_WEBSERVER
|
||||
} else {
|
||||
#ifdef SEESAW_SOIL_RAW
|
||||
WSContentSend_PD(HTTP_SNS_ANALOG, sensor_name, 0, SeeSoilSNS[i].capacitance);
|
||||
#endif // SEESAW_SOIL_RAW
|
||||
WSContentSend_PD(HTTP_SNS_MOISTURE, sensor_name, (uint32_t) SeeSoilSNS[i].moisture);
|
||||
WSContentSend_Temp(sensor_name, SeeSoilSNS[i].temperature);
|
||||
#endif // USE_WEBSERVER
|
||||
}
|
||||
} // for each sensor connected
|
||||
capacitance = ret;
|
||||
#endif
|
||||
|
||||
#ifdef DEBUG_SEESAW_SOIL
|
||||
AddLog(LOG_LEVEL_DEBUG, PSTR("SEE: READ MOIST ADDR=%02X RET=%X"), address, ret);
|
||||
#endif
|
||||
}
|
||||
|
||||
void seeSoilJson(int no) { // common json
|
||||
char sensor_name[sizeof(SeeSoil.name) + 3];
|
||||
seeSoilName(no, sensor_name, sizeof(sensor_name));
|
||||
enum State {
|
||||
STATE_IDLE,
|
||||
STATE_COMMAND_TEMP,
|
||||
STATE_READ_TEMP,
|
||||
STATE_COMMAND_MOIST,
|
||||
STATE_READ_MOIST
|
||||
};
|
||||
|
||||
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;
|
||||
float temperature;
|
||||
float moisture;
|
||||
#ifdef SEESAW_SOIL_RAW
|
||||
uint16_t capacitance;
|
||||
#endif
|
||||
State state;
|
||||
uint32_t state_time;
|
||||
#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
|
||||
}
|
||||
}
|
||||
return result;
|
||||
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user