LCTech ESP32 Relay Board x8 Modbus

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:
- Write to 74HC595 (relays)
- Read from 74HC165 (inputs)
- 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_relaysGPIO Pinout
| GPIO | Function | Chip Pin |
|---|---|---|
| GPIO13 | OE (Enable, Active LOW) | 74HC595 pin 13 |
| GPIO25 | LATCH (shared) | 74HC595 pin 12, 74HC165 pin 1 |
| GPIO26 | CLOCK (shared) | 74HC595 pin 11, 74HC165 pin 2 |
| GPIO27 | DATA IN | 74HC165 pin 9 |
| GPIO33 | DATA OUT | 74HC595 pin 14 |
Relay Mapping (74HC595)
| Relay | Bit | Hex |
|---|---|---|
| 1 | 0 | 0x01 |
| 2 | 1 | 0x02 |
| 3 | 2 | 0x04 |
| 4 | 3 | 0x08 |
| 5 | 4 | 0x10 |
| 6 | 5 | 0x20 |
| 7 | 6 | 0x40 |
| 8 | 7 | 0x80 |
Input Mapping (74HC165)
| Input | Bit | Hex |
|---|---|---|
| IN1 | 4 | 0x10 |
| IN2 | 3 | 0x08 |
| IN3 | 5 | 0x20 |
| IN4 | 2 | 0x04 |
| IN5 | 6 | 0x40 |
| IN6 | 1 | 0x02 |
| IN7 | 7 | 0x80 |
| IN8 | 0 | 0x01 |
Optocoupled Inputs
- Optocoupler: P785 (PC817 compatible)
- Series resistor: 4.7kΩ (472)
- Recommended voltage: 12-24V DC (5V marginal)
| Voltage | Current | Status |
|---|---|---|
| 5V | 0.8mA | ⚠️ Marginal |
| 12V | 2.3mA | ✅ OK |
| 24V | 4.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) │ GNDBoot Sequence
To prevent relay activation on power-up, the configuration uses two-stage boot:
| Stage | Priority | Action |
|---|---|---|
| 1 | 900 | Set OE = HIGH (outputs disabled) |
| 2 | 600 | Wait 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"