Skip to content

Instantly share code, notes, and snippets.

@glmnet
Last active October 23, 2024 14:54
Show Gist options
  • Save glmnet/49ca3d6a9742fc3649f4fbdeaa4cdf5d to your computer and use it in GitHub Desktop.
Save glmnet/49ca3d6a9742fc3649f4fbdeaa4cdf5d to your computer and use it in GitHub Desktop.
// Must disable logging if using logging in main.cpp or in other custom components for the
// __c causes a section type conflict with __c thingy
// you can enable logging and use it if you enable this in logger:
/*
logger:
level: DEBUG
esp8266_store_log_strings_in_flash: False
*/
//#define APE_LOGGING
// take advantage of LOG_ defines to decide which code to include
#ifdef LOG_BINARY_OUTPUT
#define APE_BINARY_OUTPUT
#endif
#ifdef LOG_BINARY_SENSOR
#define APE_BINARY_SENSOR
#endif
#ifdef LOG_SENSOR
#define APE_SENSOR
#endif
static const char *TAGape = "ape";
#define APE_CMD_DIGITAL_READ 0
#define APE_CMD_WRITE_ANALOG 2
#define APE_CMD_WRITE_DIGITAL_HIGH 3
#define APE_CMD_WRITE_DIGITAL_LOW 4
#define APE_CMD_SETUP_PIN_OUTPUT 5
#define APE_CMD_SETUP_PIN_INPUT_PULLUP 6
#define APE_CMD_SETUP_PIN_INPUT 7
// 8 analog registers.. A0 to A7
// A4 and A5 not supported due to I2C
#define CMD_ANALOG_READ_A0 0b1000 // 0x8
// ....
#define CMD_ANALOG_READ_A7 0b1111 // 0xF
#define CMD_SETUP_ANALOG_INTERNAL 0x10
#define CMD_SETUP_ANALOG_DEFAULT 0x11
#define get_ape(constructor) static_cast<ArduinoPortExpander *>(constructor.get_component(0))
#define ape_binary_output(ape, pin) get_ape(ape)->get_binary_output(pin)
#define ape_binary_sensor(ape, pin) get_ape(ape)->get_binary_sensor(pin)
#define ape_analog_input(ape, pin) get_ape(ape)->get_analog_input(pin)
class ArduinoPortExpander;
using namespace esphome;
#ifdef APE_BINARY_OUTPUT
class ApeBinaryOutput : public output::BinaryOutput
{
public:
ApeBinaryOutput(ArduinoPortExpander *parent, uint8_t pin)
{
this->parent_ = parent;
this->pin_ = pin;
}
void write_state(bool state) override;
uint8_t get_pin() { return this->pin_; }
protected:
ArduinoPortExpander *parent_;
uint8_t pin_;
// Pins are setup as output after the state is written, Arduino has no open drain outputs, after setting an output it will either sink or source thus activating outputs writen to false during a flick.
bool setup_{true};
bool state_{false};
friend class ArduinoPortExpander;
};
#endif
#ifdef APE_BINARY_SENSOR
class ApeBinarySensor : public binary_sensor::BinarySensor
{
public:
ApeBinarySensor(ArduinoPortExpander *parent, uint8_t pin)
{
this->pin_ = pin;
}
uint8_t get_pin() { return this->pin_; }
protected:
uint8_t pin_;
};
#endif
#ifdef APE_SENSOR
class ApeAnalogInput : public sensor::Sensor
{
public:
ApeAnalogInput(ArduinoPortExpander *parent, uint8_t pin)
{
this->pin_ = pin;
}
uint8_t get_pin() { return this->pin_; }
protected:
uint8_t pin_;
};
#endif
class ArduinoPortExpander : public Component, public I2CDevice
{
public:
ArduinoPortExpander(I2CBus *bus, uint8_t address, bool vref_default = false)
{
set_i2c_address(address);
set_i2c_bus(bus);
this->vref_default_ = vref_default;
}
void setup() override
{
#ifdef APE_LOGGING
ESP_LOGCONFIG(TAGape, "Setting up ArduinoPortExpander at %#02x ...", address_);
#endif
/* We cannot setup as usual as arduino boots later than esp8266
Poll i2c bus for our Arduino for a n seconds instead of failing fast,
also this is important as pin setup (INPUT_PULLUP, OUTPUT it's done once)
*/
this->configure_timeout_ = millis() + 5000;
}
void loop() override
{
if (millis() < this->configure_timeout_)
{
bool try_configure = millis() % 100 > 50;
if (try_configure == this->configure_)
return;
this->configure_ = try_configure;
if (ERROR_OK == this->read_register(APE_CMD_DIGITAL_READ, const_cast<uint8_t *>(this->read_buffer_), 3))
{
#ifdef APE_LOGGING
ESP_LOGCONFIG(TAGape, "ArduinoPortExpander found at %#02x", address_);
#endif
delay(10);
if (this->vref_default_)
{
this->write_register(CMD_SETUP_ANALOG_DEFAULT, nullptr, 0); // 0: unused
}
// Config success
this->configure_timeout_ = 0;
this->status_clear_error();
#ifdef APE_BINARY_SENSOR
for (ApeBinarySensor *pin : this->input_pins_)
{
App.feed_wdt();
uint8_t pinNo = pin->get_pin();
#ifdef APE_LOGGING
ESP_LOGCONFIG(TAGape, "Setup input pin %d", pinNo);
#endif
this->write_register(APE_CMD_SETUP_PIN_INPUT_PULLUP, &pinNo, 1);
delay(20);
}
#endif
#ifdef APE_BINARY_OUTPUT
for (ApeBinaryOutput *output : this->output_pins_)
{
if (!output->setup_)
{ // this output has a valid value already
this->write_state(output->pin_, output->state_, true);
App.feed_wdt();
delay(20);
}
}
#endif
#ifdef APE_SENSOR
for (ApeAnalogInput *sensor : this->analog_pins_)
{
App.feed_wdt();
uint8_t pinNo = sensor->get_pin();
#ifdef APE_LOGGING
ESP_LOGCONFIG(TAGape, "Setup analog input pin %d", pinNo);
#endif
this->write_register(APE_CMD_SETUP_PIN_INPUT, &pinNo, 1);
delay(20);
}
#endif
return;
}
// Still not answering
return;
}
if (this->configure_timeout_ != 0 && millis() > this->configure_timeout_)
{
#ifdef APE_LOGGING
ESP_LOGE(TAGape, "ArduinoPortExpander NOT found at %#02x", address_);
#endif
this->mark_failed();
return;
}
#ifdef APE_BINARY_SENSOR
if (ERROR_OK != this->read_register(APE_CMD_DIGITAL_READ, const_cast<uint8_t *>(this->read_buffer_), 3))
{
#ifdef APE_LOGGING
ESP_LOGE(TAGape, "Error reading. Reconfiguring pending.");
#endif
this->status_set_error();
this->configure_timeout_ = millis() + 5000;
return;
}
for (ApeBinarySensor *pin : this->input_pins_)
{
uint8_t pinNo = pin->get_pin();
uint8_t bit = pinNo % 8;
uint8_t value = pinNo < 8 ? this->read_buffer_[0] : pinNo < 16 ? this->read_buffer_[1] : this->read_buffer_[2];
bool ret = value & (1 << bit);
if (this->initial_state_)
pin->publish_initial_state(ret);
else
pin->publish_state(ret);
}
#endif
#ifdef APE_SENSOR
for (ApeAnalogInput *pin : this->analog_pins_)
{
uint8_t pinNo = pin->get_pin();
pin->publish_state(analogRead(pinNo));
}
#endif
this->initial_state_ = false;
}
#ifdef APE_SENSOR
uint16_t analogRead(uint8_t pin)
{
bool ok = (ERROR_OK == this->read_register((uint8_t)(CMD_ANALOG_READ_A0 + pin), const_cast<uint8_t *>(this->read_buffer_), 2));
#ifdef APE_LOGGING
ESP_LOGVV(TAGape, "analog read pin: %d ok: %d byte0: %d byte1: %d", pin, ok, this->read_buffer_[0], this->read_buffer_[1]);
#endif
uint16_t value = this->read_buffer_[0] | ((uint16_t)this->read_buffer_[1] << 8);
return value;
}
#endif
#ifdef APE_BINARY_OUTPUT
output::BinaryOutput *get_binary_output(uint8_t pin)
{
ApeBinaryOutput *output = new ApeBinaryOutput(this, pin);
output_pins_.push_back(output);
return output;
}
#endif
#ifdef APE_BINARY_SENSOR
binary_sensor::BinarySensor *get_binary_sensor(uint8_t pin)
{
ApeBinarySensor *binarySensor = new ApeBinarySensor(this, pin);
input_pins_.push_back(binarySensor);
return binarySensor;
}
#endif
#ifdef APE_SENSOR
sensor::Sensor *get_analog_input(uint8_t pin)
{
ApeAnalogInput *input = new ApeAnalogInput(this, pin);
analog_pins_.push_back(input);
return input;
}
#endif
void write_state(uint8_t pin, bool state, bool setup = false)
{
if (this->configure_timeout_ != 0)
return;
#ifdef APE_LOGGING
ESP_LOGD(TAGape, "Writing %d to pin %d", state, pin);
#endif
this->write_register(state ? APE_CMD_WRITE_DIGITAL_HIGH : APE_CMD_WRITE_DIGITAL_LOW, &pin, 1);
if (setup)
{
App.feed_wdt();
delay(20);
#ifdef APE_LOGGING
ESP_LOGI(TAGape, "Setup output pin %d", pin);
#endif
this->write_register(APE_CMD_SETUP_PIN_OUTPUT, &pin, 1);
}
}
protected:
bool configure_{true};
bool initial_state_{true};
uint8_t read_buffer_[3]{0, 0, 0};
unsigned long configure_timeout_{5000};
bool vref_default_{false};
#ifdef APE_BINARY_OUTPUT
std::vector<ApeBinaryOutput *> output_pins_;
#endif
#ifdef APE_BINARY_SENSOR
std::vector<ApeBinarySensor *> input_pins_;
#endif
#ifdef APE_SENSOR
std::vector<ApeAnalogInput *> analog_pins_;
#endif
};
#ifdef APE_BINARY_OUTPUT
void ApeBinaryOutput::write_state(bool state)
{
this->state_ = state;
this->parent_->write_state(this->pin_, state, this->setup_);
this->setup_ = false;
}
#endif
/*
Ports:
0 0 .. 13 13
A0: 14, A1: 15, A2: 16, A3: 17: A4: 18: A5: 19: A6: 20, A7: 21
port bits: 5 ... 0..32
0: 0: 00000
1: 1: 00001
A7: 21: 10101
*/
#include <Arduino.h>
#include <Wire.h>
//#define DEBUG // remove debug so pin 0 and 1 can be used for IO
#define I2C_ADDRESS 8
void onRequest();
void onReceive(int);
void setup()
{
#ifdef DEBUG
Serial.begin(115200);
Serial.println(F("Init "));
#endif
analogReference(INTERNAL);
Wire.begin(I2C_ADDRESS);
Wire.onRequest(onRequest);
Wire.onReceive(onReceive);
#ifdef DEBUG
Serial.println(F("Wire ok"));
#endif
}
void loop()
{
//int temp = analogRead(A1);
//Serial.println(temp);
}
volatile byte buffer[3];
volatile byte len = 1;
#define DIGITAL_READ(b, pin, mask) \
if (digitalRead(pin)) \
buffer[b] |= mask;
void readDigital()
{
len = 3;
buffer[0] = 0;
DIGITAL_READ(0, 0, 1);
DIGITAL_READ(0, 1, 2);
DIGITAL_READ(0, 2, 4);
DIGITAL_READ(0, 3, 8);
DIGITAL_READ(0, 4, 16);
DIGITAL_READ(0, 5, 32);
DIGITAL_READ(0, 6, 64);
DIGITAL_READ(0, 7, 128);
buffer[1] = 0;
DIGITAL_READ(1, 8, 1);
DIGITAL_READ(1, 9, 2);
DIGITAL_READ(1, 10, 4);
DIGITAL_READ(1, 11, 8);
DIGITAL_READ(1, 12, 16);
DIGITAL_READ(1, 13, 32);
DIGITAL_READ(1, A0, 64);
DIGITAL_READ(1, A1, 128);
buffer[2] = 0;
DIGITAL_READ(2, A2, 1);
DIGITAL_READ(2, A3, 2);
// I2C
//DIGITAL_READ(2, A4, 4);
//DIGITAL_READ(2, A5, 8);
// DIGITAL READ not supports on A3 .. A7
#ifdef DEBUG_READ
Serial.print(F("Read 3 bytes: "));
Serial.print(buffer[0]);
Serial.print(' ');
Serial.print(buffer[1]);
Serial.print(' ');
Serial.println(buffer[2]);
#endif
}
void readAnalog(int pin)
{
int val = analogRead(A0 + pin);
len = 2;
buffer[0] = val & 0xFF;
buffer[1] = (val >> 8) & 0b11;
#ifdef DEBUG_READ
Serial.print(F("Read analog pin "));
Serial.println(pin);
#endif
}
void onRequest()
{
Wire.write(const_cast<uint8_t *>(buffer), len);
}
#define CMD_DIGITAL_READ 0x0
#define CMD_WRITE_ANALOG 0x2
#define CMD_WRITE_DIGITAL_HIGH 0x3
#define CMD_WRITE_DIGITAL_LOW 0x4
#define CMD_SETUP_PIN_OUTPUT 0x5
#define CMD_SETUP_PIN_INPUT_PULLUP 0x6
#define CMD_SETUP_PIN_INPUT 0x7
// 8 analog registers.. A0 to A7
// A4 and A5 not supported due to I2C
#define CMD_ANALOG_READ_A0 0b1000 // 0x8
// ....
#define CMD_ANALOG_READ_A7 0b1111 // 0xF
#define CMD_SETUP_ANALOG_INTERNAL 0x10
#define CMD_SETUP_ANALOG_DEFAULT 0x11
void onReceive(int numBytes)
{
#ifdef DEBUG_READ
Serial.print("Received bytes: ");
Serial.println(numBytes);
#endif
int cmd = Wire.read();
switch (cmd)
{
case CMD_DIGITAL_READ:
readDigital();
break;
}
if (cmd >= CMD_ANALOG_READ_A0 && cmd <= CMD_ANALOG_READ_A7)
{
readAnalog(cmd & 0b111);
return;
}
int pin = Wire.read();
switch (cmd)
{
case CMD_WRITE_DIGITAL_HIGH:
case CMD_WRITE_DIGITAL_LOW:
{
bool output = cmd == CMD_WRITE_DIGITAL_HIGH;
digitalWrite(pin, output);
#ifdef DEBUG
Serial.print(F("Pin "));
Serial.print(pin);
Serial.println(output ? F(" HIGH") : F(" LOW"));
#endif
break;
}
case CMD_WRITE_ANALOG:
{
int val = Wire.read() & (Wire.read() << 8);
analogWrite(pin, val);
#ifdef DEBUG
Serial.print(F("Pin "));
Serial.print(pin);
Serial.print(F(" Analog write "));
Serial.println(val);
#endif
break;
}
case CMD_SETUP_PIN_OUTPUT:
pinMode(pin, OUTPUT);
#ifdef DEBUG
Serial.print(F("Pin "));
Serial.print(pin);
Serial.println(F(" OUTPUT"));
#endif
break;
case CMD_SETUP_PIN_INPUT:
pinMode(pin, INPUT);
#ifdef DEBUG
Serial.print(F("Pin "));
Serial.print(pin);
Serial.println(F("INPUT"));
#endif
break;
case CMD_SETUP_PIN_INPUT_PULLUP:
pinMode(pin, INPUT_PULLUP);
#ifdef DEBUG
Serial.print(F("Pin "));
Serial.print(pin);
Serial.println(F("INPUT PULLUP"));
#endif
break;
case CMD_SETUP_ANALOG_INTERNAL:
analogReference(INTERNAL);
#ifdef DEBUG
Serial.println(F("Analog reference INTERNAL"));
#endif
break;
case CMD_SETUP_ANALOG_DEFAULT:
analogReference(DEFAULT);
#ifdef DEBUG
Serial.println(F("Analog reference DEFAULT"));
#endif
break;
}
}
@rcafaro
Copy link

rcafaro commented Oct 30, 2021

I was able to compile in ESPHome. Tks!!!

@ApocWorldwide
Copy link

Up and running, thank you!

@vitucciog
Copy link

vitucciog commented Nov 18, 2021

Hi,
I was searching for a version for Arduino Mega and found this.

Unfortunately it was not working with the latest version of Esphome. I'm not a programmer, but by making a compare beetwen that version and yours, I succeeded to make it work.
I would be nice to make it official and publish in the Esphome site.

I've added the same comment in thebradleysanders github site.

// Must disable logging if using logging in main.cpp or in other custom components for the
//  __c causes a section type conflict with __c thingy
// you can enable logging and use it if you enable this in logger:
/*
logger:
  level: DEBUG
  esp8266_store_log_strings_in_flash: False
  */

//#define APE_LOGGING

// take advantage of LOG_ defines to decide which code to include
#ifdef LOG_BINARY_OUTPUT
#define APE_BINARY_OUTPUT
#endif
#ifdef LOG_BINARY_SENSOR
#define APE_BINARY_SENSOR
#endif
#ifdef LOG_SENSOR
#define APE_SENSOR
#endif

static const char *TAGape = "ape";

#define APE_CMD_DIGITAL_READ 0
#define APE_CMD_WRITE_ANALOG 2
#define APE_CMD_WRITE_DIGITAL_HIGH 3
#define APE_CMD_WRITE_DIGITAL_LOW 4
#define APE_CMD_SETUP_PIN_OUTPUT 5
#define APE_CMD_SETUP_PIN_INPUT_PULLUP 6
#define APE_CMD_SETUP_PIN_INPUT 7
// 16 analog registers.. A0 to A15
// A4 and A5 on Arduino Uno not supported due to I2C
#define CMD_ANALOG_READ_A0 0b1000 // 0x8 = A0
// ....
#define CMD_ANALOG_READ_A15 10111 // 17 = A15 0x11

#define CMD_SETUP_ANALOG_INTERNAL 0x10
#define CMD_SETUP_ANALOG_DEFAULT 0x12

#define get_ape(constructor) static_cast<ArduinoPortExpander *>(constructor.get_component(0))

#define ape_binary_output(ape, pin) get_ape(ape)->get_binary_output(pin)
#define ape_binary_sensor(ape, pin) get_ape(ape)->get_binary_sensor(pin)
#define ape_analog_input(ape, pin) get_ape(ape)->get_analog_input(pin)

class ArduinoPortExpander;

using namespace esphome;

#ifdef APE_BINARY_OUTPUT
class ApeBinaryOutput : public output::BinaryOutput
{
public:
  ApeBinaryOutput(ArduinoPortExpander *parent, uint8_t pin)
  {
    this->parent_ = parent;
    this->pin_ = pin;
  }
  void write_state(bool state) override;
  uint8_t get_pin() { return this->pin_; }

protected:
  ArduinoPortExpander *parent_;
  uint8_t pin_;
  // Pins are setup as output after the state is written, Arduino has no open drain outputs, after setting an output it will either sink or source thus activating outputs writen to false during a flick.
  bool setup_{true};
  bool state_{false};

  friend class ArduinoPortExpander;
};
#endif

#ifdef APE_BINARY_SENSOR
class ApeBinarySensor : public binary_sensor::BinarySensor
{
public:
  ApeBinarySensor(ArduinoPortExpander *parent, uint8_t pin)
  {
    this->pin_ = pin;
  }
  uint8_t get_pin() { return this->pin_; }

protected:
  uint8_t pin_;
};
#endif

#ifdef APE_SENSOR
class ApeAnalogInput : public sensor::Sensor
{
public:
  ApeAnalogInput(ArduinoPortExpander *parent, uint8_t pin)
  {
    this->pin_ = pin;
  }
  uint8_t get_pin() { return this->pin_; }

protected:
  uint8_t pin_;
};
#endif

class ArduinoPortExpander : public Component, public I2CDevice
{
public:
  ArduinoPortExpander(I2CBus *bus, uint8_t address, bool vref_default = false)
  {
    set_i2c_address(address);
    set_i2c_bus(bus);
    this->vref_default_ = vref_default;
  }
  void setup() override
  {
#ifdef APE_LOGGING
    ESP_LOGCONFIG(TAGape, "Setting up ArduinoPortExpander at %#02x ...", address_);
#endif

    /* We cannot setup as usual as arduino boots later than esp8266

            Poll i2c bus for our Arduino for a n seconds instead of failing fast,
            also this is important as pin setup (INPUT_PULLUP, OUTPUT it's done once)
        */
    this->configure_timeout_ = millis() + 5000;
  }
  void loop() override
  {
    if (millis() < this->configure_timeout_)
    {
      bool try_configure = millis() % 100 > 50;
      if (try_configure == this->configure_)
        return;
      this->configure_ = try_configure;

      if (ERROR_OK == this->read_register(APE_CMD_DIGITAL_READ, const_cast<uint8_t *>(this->read_buffer_), 9))   //changed 3 to 9
      {
#ifdef APE_LOGGING
        ESP_LOGCONFIG(TAGape, "ArduinoPortExpander found at %#02x", address_);
#endif
        delay(10);
        if (this->vref_default_)
        {
          this->write_register(CMD_SETUP_ANALOG_DEFAULT, nullptr, 0); // 0: unused
        }

        // Config success
        this->configure_timeout_ = 0;
        this->status_clear_error();

#ifdef APE_BINARY_SENSOR
        for (ApeBinarySensor *pin : this->input_pins_)
        {
          App.feed_wdt();
          uint8_t pinNo = pin->get_pin();
#ifdef APE_LOGGING
          ESP_LOGCONFIG(TAGape, "Setup input pin %d", pinNo);
#endif
          this->write_register(APE_CMD_SETUP_PIN_INPUT_PULLUP, &pinNo, 1);
          delay(20);
        }
#endif
#ifdef APE_BINARY_OUTPUT
        for (ApeBinaryOutput *output : this->output_pins_)
        {
          if (!output->setup_)
          { // this output has a valid value already
            this->write_state(output->pin_, output->state_, true);
            App.feed_wdt();
            delay(20);
          }
        }
#endif
#ifdef APE_SENSOR
        for (ApeAnalogInput *sensor : this->analog_pins_)
        {
          App.feed_wdt();
          uint8_t pinNo = sensor->get_pin();
#ifdef APE_LOGGING
          ESP_LOGCONFIG(TAGape, "Setup analog input pin %d", pinNo);
#endif
          this->write_register(APE_CMD_SETUP_PIN_INPUT, &pinNo, 1);
          delay(20);
        }
#endif
        return;
      }
      // Still not answering
      return;
    }
    if (this->configure_timeout_ != 0 && millis() > this->configure_timeout_)
    {
#ifdef APE_LOGGING
      ESP_LOGE(TAGape, "ArduinoPortExpander NOT found at %#02x", address_);
#endif
      this->mark_failed();
      return;
    }

#ifdef APE_BINARY_SENSOR
    if (ERROR_OK != this->read_register(APE_CMD_DIGITAL_READ, const_cast<uint8_t *>(this->read_buffer_), 9))    //changed from 3 to 9
    {
#ifdef APE_LOGGING
      ESP_LOGE(TAGape, "Error reading. Reconfiguring pending.");
#endif
      this->status_set_error();
      this->configure_timeout_ = millis() + 5000;
      return;
    }
    for (ApeBinarySensor *pin : this->input_pins_)
    {
      uint8_t pinNo = pin->get_pin();

      uint8_t bit = pinNo % 8;
      uint8_t value = pinNo < 8 ? this->read_buffer_[0] : pinNo < 16 ? this->read_buffer_[1] : pinNo < 24 ? this->read_buffer_[2] : pinNo < 32 ? this->read_buffer_[3] : pinNo < 40 ? this->read_buffer_[4] : pinNo < 48 ? this->read_buffer_[5] : pinNo < 56 ? this->read_buffer_[6] : pinNo < 64 ? this->read_buffer_[7] : this->read_buffer_[8];
      bool ret = value & (1 << bit);
      if (this->initial_state_)
        pin->publish_initial_state(ret);
      else
        pin->publish_state(ret);
    }
#endif
#ifdef APE_SENSOR
    for (ApeAnalogInput *pin : this->analog_pins_)
    {
      uint8_t pinNo = pin->get_pin();
      pin->publish_state(analogRead(pinNo));
    }
#endif
    this->initial_state_ = false;
  }

#ifdef APE_SENSOR
  uint16_t analogRead(uint8_t pin)
  {
    bool ok = (ERROR_OK == this->read_register((uint8_t)(CMD_ANALOG_READ_A0 + pin), const_cast<uint8_t *>(this->read_buffer_), 2, 1));
#ifdef APE_LOGGING
    ESP_LOGVV(TAGape, "analog read pin: %d ok: %d byte0: %d byte1: %d", pin, ok, this->read_buffer_[0], this->read_buffer_[1]);
#endif
    uint16_t value = this->read_buffer_[0] | ((uint16_t)this->read_buffer_[1] << 8);
    return value;
  }
#endif

#ifdef APE_BINARY_OUTPUT
  output::BinaryOutput *get_binary_output(uint8_t pin)
  {
    ApeBinaryOutput *output = new ApeBinaryOutput(this, pin);
    output_pins_.push_back(output);
    return output;
  }
#endif
#ifdef APE_BINARY_SENSOR
  binary_sensor::BinarySensor *get_binary_sensor(uint8_t pin)
  {
    ApeBinarySensor *binarySensor = new ApeBinarySensor(this, pin);
    input_pins_.push_back(binarySensor);
    return binarySensor;
  }
#endif
#ifdef APE_SENSOR
  sensor::Sensor *get_analog_input(uint8_t pin)
  {
    ApeAnalogInput *input = new ApeAnalogInput(this, pin);
    analog_pins_.push_back(input);
    return input;
  }
#endif
  void write_state(uint8_t pin, bool state, bool setup = false)
  {
    if (this->configure_timeout_ != 0)
      return;
#ifdef APE_LOGGING
    ESP_LOGD(TAGape, "Writing %d to pin %d", state, pin);
#endif
    this->write_register(state ? APE_CMD_WRITE_DIGITAL_HIGH : APE_CMD_WRITE_DIGITAL_LOW, &pin, 1);
    if (setup)
    {
      App.feed_wdt();
      delay(20);
#ifdef APE_LOGGING
      ESP_LOGI(TAGape, "Setup output pin %d", pin);
#endif
      this->write_register(APE_CMD_SETUP_PIN_OUTPUT, &pin, 1);
    }
  }

protected:
  bool configure_{true};
  bool initial_state_{true};
  uint8_t read_buffer_[9]{0, 0, 0, 0, 0, 0, 0, 0, 0}; //changed from [3]{0, 0, 0}
  unsigned long configure_timeout_{5000};
  bool vref_default_{false};

#ifdef APE_BINARY_OUTPUT
  std::vector<ApeBinaryOutput *> output_pins_;
#endif
#ifdef APE_BINARY_SENSOR
  std::vector<ApeBinarySensor *> input_pins_;
#endif
#ifdef APE_SENSOR
  std::vector<ApeAnalogInput *> analog_pins_;
#endif
};

#ifdef APE_BINARY_OUTPUT
void ApeBinaryOutput::write_state(bool state)
{
  this->state_ = state;
  this->parent_->write_state(this->pin_, state, this->setup_);
  this->setup_ = false;
}
#endif

@vladurash
Copy link

vladurash commented May 25, 2022

``Hi, I have arduino nano which is using D2, D3...D13 for digital pins. Neither source .h file, nor .ino file let me to compile and use these pins with such numerotation. Only 2-13.
Could you please tell me where or what should I need to change in order to work?
I'm using instructions (first time....) from esphome webpage where is stated this:

ape_binary_output(expander1, 2)

Thanks
In file included from src/main.cpp:50: /config/esphome/replace-espeasy.yaml: In lambda function: /config/esphome/replace-espeasy.yaml:64:44: error: 'D9' was not declared in this scope 64 | return {ape_binary_output(expander1, D9), | ^~ src/arduino_port_expander.h:43:69: note: in definition of macro 'ape_binary_output' 43 | #define ape_binary_output(ape, pin) get_ape(ape)->get_binary_output(pin) | ^~~ /config/esphome/replace-espeasy.yaml:65:42: error: 'D10' was not declared in this scope; did you mean 'B10'? 65 | ape_binary_output(expander1, D10), | ^~~ src/arduino_port_expander.h:43:69: note: in definition of macro 'ape_binary_output' 43 | #define ape_binary_output(ape, pin) get_ape(ape)->get_binary_output(pin) | ^~~ /config/esphome/replace-espeasy.yaml:66:42: error: 'D11' was not declared in this scope; did you mean 'B11'? 66 | ape_binary_output(expander1, D11), | ^~~ src/arduino_port_expander.h:43:69: note: in definition of macro 'ape_binary_output' 43 | #define ape_binary_output(ape, pin) get_ape(ape)->get_binary_output(pin) | ^~~ /config/esphome/replace-espeasy.yaml:67:42: error: 'D12' was not declared in this scope 67 | ape_binary_output(expander1, D12)}; | ^~~ src/arduino_port_expander.h:43:69: note: in definition of macro 'ape_binary_output' 43 | #define ape_binary_output(ape, pin) get_ape(ape)->get_binary_output(pin) | ^~~ /config/esphome/replace-espeasy.yaml:67:46: error: could not convert '{<expression error>, <expression error>, <expression error>, <expression error>}' from '<brace-enclosed initializer list>' to 'std::vector<esphome::output::BinaryOutput*>' 67 | ape_binary_output(expander1, D12)}; | ^ | | | <brace-enclosed initializer list> *** [/data/replace-espeasy/.pioenvs/replace-espeasy/src/main.cpp.o] Error 1

@thebradleysanders
Copy link

thebradleysanders commented May 26, 2022 via email

@vladurash
Copy link

Changed board to esp12e and added another power source to arduino. No need to use Dx pins, it works like in documentation.
So is no issue here :)

@thebradleysanders
Copy link

thebradleysanders commented May 27, 2022 via email

@NonaSuomy
Copy link

NonaSuomy commented Oct 17, 2022

Is there a way to get this to support a Rotary Encoder, etc?
Something like this:

sensors:
  - platform: custom
    lambda: |-
      return {ape_sensor(expander1, 2),
              ape_sensor(expander1, 3),
              ape_sensor(expander1, 4)
              };
    sensor:
  # Rotary Encoder
  - platform: rotary_encoder
    name: "Rotary Encoder"
    pin_a: encoderApin
    pin_b: encoderBpin
  # DHT22
  - platform: dht
    pin: dht22pin
    temperature:
      name: "Living Room Temperature"
    humidity:
      name: "Living Room Humidity"
    update_interval: 60s

@kc9ryt
Copy link

kc9ryt commented Mar 18, 2023

@NonaSuomy Did you ever figure out how to get a rotary encoder to work?

@thebradleysanders
Copy link

No I have not, what pin are you using, analog or digital?

@NonaSuomy
Copy link

@kc9ryt I confirmed with @glmnet and they said it is not currently possible to utilize this library like that as it has no way to link up the encoder stuff in ESPHome with the Arduino port expander. You would have to modify the code base to allow it to accept these functions.

I found another Arduino library that allows you to use Rotary Encoders over I2C but in its current state can't just be utilized like this library by including it and would have to be modified to be properly compatible. I ask the maintainer if it was possible to do something like this. They said they may be able to look into it unfortunately their health faltered and couldn't look into it.

RobTillaart/rotaryDecoderSwitch#4

@xion01xion
Copy link

Is there a new code compatible with Home Assistant 2023.8.2. if not, is it possible to modify the code ?

@glmnet
Copy link
Author

glmnet commented Aug 13, 2023

Is there a new code compatible with Home Assistant 2023.8.2. if not, is it possible to modify the code ?

This should work with latest HA and latest ESPHome, I’m still using it. If you need help go to discord server and ask there. When something doesn’t work you should say what the issue is otherwise nobody will know and won’t be able to help you.

@vladurash
Copy link

How can I use this arduino port expander to read a dallas sensor ds18b20 connected to pin D2? is it a binary?

@glmnet
Copy link
Author

glmnet commented Sep 16, 2023

How can I use this arduino port expander to read a dallas sensor ds18b20 connected to pin D2? is it a binary?

Not possible

@Bjoern3003
Copy link

I am using this ino and h file for my ESPHome project.

I have described my Problem here: https://community.home-assistant.io/t/arduino-port-expander-problem-with-analog-and-digital-inputs/640313/2 and I think I have solved the Problem by myself by uncomment this line: https://gist.github.com/glmnet/49ca3d6a9742fc3649f4fbdeaa4cdf5d#file-arduino_port_expander-h-L180

I can use now both Pins (for example D3 for Output and A3 for Analog Input)

@glmnet
Copy link
Author

glmnet commented Nov 14, 2023

can you try the external component version?

it seems custom components are not going to be supported so I created this new version, I'm actively using for binary sensors and outputs, but did also try analog sensors and seems to work.

The arduino code is the same, no need to update it

you need to remove the - include on esphome: and see this example below:

external_components:
  - source: github://glmnet/esphome@ape-external-component
    components: [arduino_port_expander]

arduino_port_expander:
  id: x
  address: 0x08
  analog_reference: DEFAULT

binary_sensor:
  - platform: gpio
    pin:
      arduino_port_expander: x
      number: 12
      mode:
        input: True
        pullup: True
    name: bs-12
    id: ape_12
    on_state:
      then:
        - light.toggle: l1

output:
  - platform: gpio
    pin:
      arduino_port_expander: x
      number: 13
      mode:
        output: True
    id: ape_13

sensor:
  - platform: arduino_port_expander
    id: sx
    pin: A0
    name: Ardu A0
    update_interval: 10s
    filters:
      - multiply: 0.0032258064516129 # x / 1023.0 * 3.3 v

I still need to put some readme somewhere, help welcome btw

For discussion https://discord.com/channels/429907082951524364/1169551049493860371

@Bjoern3003
Copy link

Bjoern3003 commented Nov 14, 2023

Nice, looks like, that it's working :-)

I can not test binary_sensor, but my 8 Outputs and 6 Analog Inputs are working directly.

My big test example:

esphome:
  name: test_arduino

esp8266:
  board: d1_mini

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

api:

ota:

# define i2c device
# for an ESP8266 SDA is D2 and goes to Arduino's A4
#                SCL is D1 and goes to Arduino's A5
i2c:
  id: i2c_component

logger:
  level: DEBUG

external_components:
  - source: github://glmnet/esphome@ape-external-component
    components: [arduino_port_expander]

arduino_port_expander:
  id: ape
  address: 0x08
  analog_reference: DEFAULT

output:
  - platform: gpio
    pin:
      arduino_port_expander: ape
      number: 9
      mode:
        output: True
    id: relay_1
    inverted: true
  - platform: gpio
    pin:
      arduino_port_expander: ape
      number: 8
      mode:
        output: True
    id: relay_2
    inverted: true
  - platform: gpio
    pin:
      arduino_port_expander: ape
      number: 7
      mode:
        output: True
    id: relay_3
    inverted: true
  - platform: gpio
    pin:
      arduino_port_expander: ape
      number: 6
      mode:
        output: True
    id: relay_4
    inverted: true
  - platform: gpio
    pin:
      arduino_port_expander: ape
      number: 5
      mode:
        output: True
    id: relay_5
    inverted: true
  - platform: gpio
    pin:
      arduino_port_expander: ape
      number: 4
      mode:
        output: True
    id: relay_6
    inverted: true
  - platform: gpio
    pin:
      arduino_port_expander: ape
      number: 3
      mode:
        output: True
    id: relay_7
    inverted: true
  - platform: gpio
    pin:
      arduino_port_expander: ape
      number: 2
      mode:
        output: True
    id: relay_8
    inverted: true

switch:
  - platform: output
    name: Relais 1
    id: relais01
    output: relay_1
  - platform: output
    name: Relais 2
    id: relais02
    output: relay_2
  - platform: output
    name: Relais 3
    id: relais03
    output: relay_3
  - platform: output
    name: Relais 4
    id: relais04
    output: relay_4
  - platform: output
    name: Relais 5
    id: relais05
    output: relay_5
  - platform: output
    name: Relais 6
    id: relais06
    output: relay_6
  - platform: output
    name: Relais 7
    id: relais07
    output: relay_7
  - platform: output
    name: Relais 8
    id: relais08
    output: relay_8

    
sensor:
  - platform: arduino_port_expander
    id: analog_input_1_humidity
    pin: A6
    name: Eingang 1 Bodenfeuchtigkeit
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 0
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v
      - calibrate_linear:
          - 0.95 -> 100
          - 2.5 -> 0
      - lambda: |
          if (x < 0) return 0; 
          else if (x > 100) return 100;
          else return (x);
  - platform: arduino_port_expander
    id: analog_input_1_voltage
    pin: A6
    name: Eingang 1 Spannung
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v


  - platform: arduino_port_expander
    id: analog_input_2_humidity
    pin: A7
    name: Eingang 2 Bodenfeuchtigkeit
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 0
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v
      - calibrate_linear:
          - 0.95 -> 100
          - 2.5 -> 0
      - lambda: |
          if (x < 0) return 0; 
          else if (x > 100) return 100;
          else return (x);
  - platform: arduino_port_expander
    id: analog_input_2_voltage
    pin: A7
    name: Eingang 2 Spannung
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v


  - platform: arduino_port_expander
    id: analog_input_3_humidity
    pin: A0
    name: Eingang 3 Bodenfeuchtigkeit
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 0
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v
      - calibrate_linear:
          - 0.95 -> 100
          - 2.5 -> 0
      - lambda: |
          if (x < 0) return 0; 
          else if (x > 100) return 100;
          else return (x);
  - platform: arduino_port_expander
    id: analog_input_3_voltage
    pin: A0
    name: Eingang 3 Spannung
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v


  - platform: arduino_port_expander
    id: analog_input_4_humidity
    pin: A1
    name: Eingang 4 Bodenfeuchtigkeit
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 0
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v
      - calibrate_linear:
          - 0.95 -> 100
          - 2.5 -> 0
      - lambda: |
          if (x < 0) return 0; 
          else if (x > 100) return 100;
          else return (x);
  - platform: arduino_port_expander
    id: analog_input_4_voltage
    pin: A1
    name: Eingang 4 Spannung
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v


  - platform: arduino_port_expander
    id: analog_input_5_humidity
    pin: A2
    name: Eingang 5 Bodenfeuchtigkeit
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 0
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v
      - calibrate_linear:
          - 0.95 -> 100
          - 2.5 -> 0
      - lambda: |
          if (x < 0) return 0; 
          else if (x > 100) return 100;
          else return (x);
  - platform: arduino_port_expander
    id: analog_input_5_voltage
    pin: A2
    name: Eingang 5 Spannung
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v


  - platform: arduino_port_expander
    id: analog_input_6_humidity
    pin: A3
    name: Eingang 6 Bodenfeuchtigkeit
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 0
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v
      - calibrate_linear:
          - 0.95 -> 100
          - 2.5 -> 0
      - lambda: |
          if (x < 0) return 0; 
          else if (x > 100) return 100;
          else return (x);
  - platform: arduino_port_expander
    id: analog_input_6_voltage
    pin: A3
    name: Eingang 6 Spannung
    update_interval: 10s
    filters:
      - multiply: 0.004887585532746823069403714565 # x * 1023.0 / 5.0 v

@fran6120
Copy link

fran6120 commented Dec 8, 2023

I am embarking on a project in which I need to use H711X, which is natively supported by esphome but I do not have more pins for it.

I have quite a few sensors connected via I2C and when I saw this component arduino_port_expander that allows you to expand I/O ports via I2C I thought it would be a very good option.

I had a pro mini lying around the house so I decided to upload the sketch and connect it to the esp32c3.

I currently do not have the H711X, since I am considering the possibility of using it via I2C, but I have a DHT11 at home and I decided to test if the arduino_port_expander component allows me to control it.

First I did tests by configuring an output associated with pin 13 of the Arduino and I was able to control the LED from home assistant perfectly.

Once I verified that arduino_port_expander was working correctly I decided to try the DHT11. First I had to modify the DHT11 component so that it allowed the arduino_port_expander pins to be configured and not just the internal ones of the esp32 running esphome. Change InternalGPIOPin to GPIOPin

in yaml:

arduino_port_expander:
  id: ape1 # must identify somehow to later use
  i2c_id: tca9548a_ch0
  address: 0x08
  analog_reference: DEFAULT

sensor:
  - platform: dht
    pin:
      arduino_port_expander: ape1
      number: 2
    temperature:
      name: "Living Room Temperature"
    humidity:
      name: "Living Room Humidity"
    update_interval: 60s

Unfortunately, although it compiles perfectly, on boot it causes bootloop (I imagine due to wdt)

It seems that the sensors where communication is time critical do not work through I/O I2C expanders.

Still, arduino_port_expander is a cool component.

Thank you!

@glmnet
Copy link
Author

glmnet commented Dec 8, 2023

I forgot to mention here I created a new repo for this. See here

@fran6120 thats a nice story but the end was predicted. If you’re short on pins you can try to use the expander for simple io pins, eg relays switches and save internal gpio for sensors which requiere fast io pins. (And also switching pin mode from input to output quickly)

@Bjoern3003
Copy link

Hi, do you know about the Problem, that neighborhood Analog Inuts have wrong values.

I want to read some of those Sensors: https://de.aliexpress.com/item/1005004819365767.html

Reading a single Sensor has no problems (eg A0). But if I add a second one to A1, the voltage is much higher as its really is.

I have no really solution.

On some Formthreads, they write, that you have to read the ANalog Pin twice and only use the second result:

https://forum.arduino.cc/t/analog-input-affected-by-its-neighbor/88758
or
https://forum.arduino.cc/t/cross-voltages-from-neighbor-analog-input/178708

@glmnet
Copy link
Author

glmnet commented Feb 21, 2024

That’s interesting. Didn’t know

you can try using update interval: none on the sensor s so you disable automatic polling. Then write an interval script which will call component update on the sensors, eg call it twice in A0 and the pick the value on the second call then call twice A1

@Bjoern3003
Copy link

Do you have a small example for me? I will test it shortly.

@glmnet
Copy link
Author

glmnet commented Feb 21, 2024

This does disables polling and calls to update explicitly

https://github.com/glmnet/esphome_devices/blob/master/my_home/eh-energia.yaml

You might want to create another template sensor which you’ll set the value you read the second time

@Bjoern3003
Copy link

Bjoern3003 commented Feb 21, 2024

Thank you,

I think I got it.

I added a dummy analogRead() to the arduino Script before the reading for publishing.

Also I found a thread: firmata/arduino#334 (comment)

I also have added a interval Script to my yaml, to read the Analog Sensors in the correct sequence

  - interval: 60s
    then:
      - component.update: analog_input_3_humidity
      - delay: 1s
      - component.update: analog_input_3_voltage
      - delay: 10s
      - component.update: analog_input_4_humidity
      - delay: 1s
      - component.update: analog_input_4_voltage
      - delay: 10s
      - component.update: analog_input_5_humidity
      - delay: 1s
      - component.update: analog_input_5_voltage
      - delay: 10s
      - component.update: analog_input_6_humidity
      - delay: 1s
      - component.update: analog_input_6_voltage
      - delay: 10s
      - component.update: analog_input_1_humidity
      - delay: 1s
      - component.update: analog_input_1_voltage
      - delay: 10s
      - component.update: analog_input_2_humidity
      - delay: 1s
      - component.update: analog_input_2_voltage```

Now I will test it for a couple of Days :-)

@vicio2008
Copy link

Hola buen dia. Existe la manera de usar el sensor dht11 conectado en arduino. Soy nuevo en esto y no he encontrado la manera.

@balazs111
Copy link

Hello i am trying to use Arduino Port Expander in my home project where reliability is really important .

As a test, temporary I braked the i2c line between the port expander and the esp device. I got a following error immediately (which is good) :

[E][component:112]: Component was marked as failed.
And never recovered (that is not good).

I wonder how can this made more robust? eg. after the transient is gone should recover by them self.

Thank you

@TheAbhor
Copy link

Hello everyone,

I have a little dumb question, I have it all working (the first version was working for me here),
but .... if the i2c bus hangs for some reason, I know the command to reset the ESP8266 ---> "- platform: restart",
but not the NANO (in my case) ... (so the i2c bus won't reset), is there a easy way to reset the NANO too in that same command ?

GreetingZzz

@TheAbhor
Copy link

lol ... I see it now, @balazs111 has the same question :-)

@balazs111
Copy link

Hello,

here is the solution: https://community.home-assistant.io/t/arduino-port-expander-i2c-connection-not-recover-after-failure/745984
In the end decided to use modbus because i needed to breach a longer distance what i2c was not able to handle

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment