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:
Allen Schober 2025-12-29 11:23:50 -05:00 committed by GitHub
parent 0e19d9b367
commit 312ba73d6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1553 additions and 506 deletions

View File

@ -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

View File

@ -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();
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View 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

View 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

View File

@ -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