Skip to main content
Device Type:relay
Electrical Standard:global
Board:esp32

LCTech ESP32 Relay Board x8 Modbus

Product

This is an 8-relay board with an ESP32-WROOM-32E using 74HC595 (outputs) and 74HC165 (inputs) shift registers on a shared bus.

Each relay has COM+NO+NC exposed and supports 10Amp max load.

⚠️ Important: Shared Bus Architecture

Unlike older ESP32 relay boards where each relay was connected to a direct GPIO, this version uses shift registers with shared Clock and Latch lines.

You cannot trigger relays by simply setting a GPIO to HIGH. You must "shift out" the data through the shared bus.

Why Lambda Instead of Standard ESPHome Components?

This board uses a shared bus architecture where 74HC595 (outputs) and 74HC165 (inputs) share the same Clock (GPIO26) and Latch (GPIO25) lines.

The Problem

When using standard ESPHome components:

sn74hc595:
- id: relay_hub
...
sn74hc165:
- id: input_hub
...

Result: Components interfere with each other. Relay 1 controls all relays, inputs don't work.

The Solution

Manual control via lambda with proper timing:

  1. Write to 74HC595 (relays)
  2. Read from 74HC165 (inputs)
  3. Restore relay state after reading
switch:
- platform: template # not gpio!
lambda: return id(relay_state) & 0x01;
turn_on_action:
- lambda: id(relay_state) |= 0x01;
- script.execute: write_relays

GPIO Pinout

GPIOFunctionChip Pin
GPIO13OE (Enable, Active LOW)74HC595 pin 13
GPIO25LATCH (shared)74HC595 pin 12, 74HC165 pin 1
GPIO26CLOCK (shared)74HC595 pin 11, 74HC165 pin 2
GPIO27DATA IN74HC165 pin 9
GPIO33DATA OUT74HC595 pin 14

Relay Mapping (74HC595)

RelayBitHex
100x01
210x02
320x04
430x08
540x10
650x20
760x40
870x80

Input Mapping (74HC165)

InputBitHex
IN140x10
IN230x08
IN350x20
IN420x04
IN560x40
IN610x02
IN770x80
IN800x01

Optocoupled Inputs

  • Optocoupler: P785 (PC817 compatible)
  • Series resistor: 4.7kΩ (472)
  • Recommended voltage: 12-24V DC (5V marginal)
VoltageCurrentStatus
5V0.8mA⚠️ Marginal
12V2.3mA✅ OK
24V4.9mA✅ Best

Wiring

+12V/24V ──► INx
GND ──► GND (near input)

Optional indicator LED

+12V/24V ──┬──► INx

LED + 1kΩ (for 12V) or 2.2kΩ (for 24V)

GND

Boot Sequence

To prevent relay activation on power-up, the configuration uses two-stage boot:

StagePriorityAction
1900Set OE = HIGH (outputs disabled)
2600Wait 500ms, initialize pins, clear register, set OE = LOW

Basic Config

esphome:
name: ESP32 relayboard
on_boot:
- priority: 900
then:
- lambda: |-
pinMode(13, OUTPUT);
digitalWrite(13, HIGH);
- priority: 600
then:
- delay: 500ms
- lambda: |-
pinMode(25, OUTPUT);
pinMode(26, OUTPUT);
pinMode(33, OUTPUT);
pinMode(27, INPUT);
digitalWrite(25, LOW);
shiftOut(33, 26, MSBFIRST, 0xFF);
digitalWrite(25, HIGH);
digitalWrite(13, LOW);

esp32:
board: esp32dev
framework:
type: arduino

globals:
- id: relay_state
type: uint8_t
initial_value: '0'
- id: input_state
type: uint8_t
initial_value: '0xFF'
- id: last_input
type: uint8_t
initial_value: '0xFF'

script:
- id: write_relays
then:
- lambda: |-
digitalWrite(25, LOW);
shiftOut(33, 26, MSBFIRST, ~id(relay_state));
digitalWrite(25, HIGH);

interval:
- interval: 100ms
then:
- lambda: |-
digitalWrite(25, LOW);
delayMicroseconds(10);
digitalWrite(25, HIGH);

uint8_t v = 0;
for (int i = 0; i < 8; i++) {
v <<= 1;
if (digitalRead(27)) v |= 1;
digitalWrite(26, HIGH);
delayMicroseconds(2);
digitalWrite(26, LOW);
delayMicroseconds(2);
}

id(input_state) = v;
uint8_t pressed = (v ^ id(last_input)) & ~v;

if (pressed & 0x10) id(relay1).toggle();
if (pressed & 0x08) id(relay2).toggle();
if (pressed & 0x20) id(relay3).toggle();
if (pressed & 0x04) id(relay4).toggle();
if (pressed & 0x40) id(relay5).toggle();
if (pressed & 0x02) id(relay6).toggle();
if (pressed & 0x80) id(relay7).toggle();
if (pressed & 0x01) id(relay8).toggle();

id(last_input) = v;

digitalWrite(25, LOW);
shiftOut(33, 26, MSBFIRST, ~id(relay_state));
digitalWrite(25, HIGH);

switch:
- platform: template
name: "Relay 1"
id: relay1
restore_mode: ALWAYS_OFF
lambda: return id(relay_state) & 0x01;
turn_on_action:
- lambda: id(relay_state) |= 0x01;
- script.execute: write_relays
turn_off_action:
- lambda: id(relay_state) &= ~0x01;
- script.execute: write_relays

- platform: template
name: "Relay 2"
id: relay2
restore_mode: ALWAYS_OFF
lambda: return id(relay_state) & 0x02;
turn_on_action:
- lambda: id(relay_state) |= 0x02;
- script.execute: write_relays
turn_off_action:
- lambda: id(relay_state) &= ~0x02;
- script.execute: write_relays

- platform: template
name: "Relay 3"
id: relay3
restore_mode: ALWAYS_OFF
lambda: return id(relay_state) & 0x04;
turn_on_action:
- lambda: id(relay_state) |= 0x04;
- script.execute: write_relays
turn_off_action:
- lambda: id(relay_state) &= ~0x04;
- script.execute: write_relays

- platform: template
name: "Relay 4"
id: relay4
restore_mode: ALWAYS_OFF
lambda: return id(relay_state) & 0x08;
turn_on_action:
- lambda: id(relay_state) |= 0x08;
- script.execute: write_relays
turn_off_action:
- lambda: id(relay_state) &= ~0x08;
- script.execute: write_relays

- platform: template
name: "Relay 5"
id: relay5
restore_mode: ALWAYS_OFF
lambda: return id(relay_state) & 0x10;
turn_on_action:
- lambda: id(relay_state) |= 0x10;
- script.execute: write_relays
turn_off_action:
- lambda: id(relay_state) &= ~0x10;
- script.execute: write_relays

- platform: template
name: "Relay 6"
id: relay6
restore_mode: ALWAYS_OFF
lambda: return id(relay_state) & 0x20;
turn_on_action:
- lambda: id(relay_state) |= 0x20;
- script.execute: write_relays
turn_off_action:
- lambda: id(relay_state) &= ~0x20;
- script.execute: write_relays

- platform: template
name: "Relay 7"
id: relay7
restore_mode: ALWAYS_OFF
lambda: return id(relay_state) & 0x40;
turn_on_action:
- lambda: id(relay_state) |= 0x40;
- script.execute: write_relays
turn_off_action:
- lambda: id(relay_state) &= ~0x40;
- script.execute: write_relays

- platform: template
name: "Relay 8"
id: relay8
restore_mode: ALWAYS_OFF
lambda: return id(relay_state) & 0x80;
turn_on_action:
- lambda: id(relay_state) |= 0x80;
- script.execute: write_relays
turn_off_action:
- lambda: id(relay_state) &= ~0x80;
- script.execute: write_relays

binary_sensor:
- platform: template
name: "Input 1"
lambda: return !(id(input_state) & 0x10);

- platform: template
name: "Input 2"
lambda: return !(id(input_state) & 0x08);

- platform: template
name: "Input 3"
lambda: return !(id(input_state) & 0x20);

- platform: template
name: "Input 4"
lambda: return !(id(input_state) & 0x04);

- platform: template
name: "Input 5"
lambda: return !(id(input_state) & 0x40);

- platform: template
name: "Input 6"
lambda: return !(id(input_state) & 0x02);

- platform: template
name: "Input 7"
lambda: return !(id(input_state) & 0x80);

- platform: template
name: "Input 8"
lambda: return !(id(input_state) & 0x01);

- platform: status
name: "Status"