Remote appliance control over a CAN bus. Two ESP32 nodes, one twisted pair. Press a button on one "floor", a relay clicks on the other.
Final-year E&TC project — Walchand Institute of Technology, Solapur, 2026-27. Parshwa Gadekar · Laukik Belsare · guide: Mrs. P.S. Shetgar
In a multi-storey building every switch is hardwired to the appliance next to it. Controlling anything on another floor means either walking there or pulling a dedicated wire pair for that one connection, and the pair count grows fast with floors. Cars solved this exact problem decades ago: instead of a wire per device they run one shared differential pair (the CAN bus) that every ECU listens to. Incidentally, it is also the bus inside most modern elevators (CANopen-Lift, CiA 417). We wanted to see if the same trick works for building-scale appliance switching on a student's budget.
This repo is the 2-node proof. Two ESP32s, two SN65HVD230 transceivers, ~₹2,500 of parts.
Docs: the long-form stuff lives in docs/: wiring, protocol
details, a full Windows walkthrough, and a troubleshooting list ordered by how
often each failure actually happens. The same pages are mirrored in the GitHub
Wiki tab (published from wiki/).
firmware/can_node.ino one Arduino sketch for both nodes (flip NODE_ID, flash)
sim/ a Python model of the actual CAN bus + a live animated
schematic that runs on real frames from that model
docs/ wiring, protocol, Windows setup, troubleshooting
wiki/ source for the GitHub Wiki (tools/push-wiki.sh publishes it)
assets/ schematic render
The interesting part of CAN is what the silicon does with the wire. The sim/
model implements real CAN 2.0A behaviour: bit-level dominant-wins arbitration,
CRC-15, bit stuffing, the ACK slot, transmit error counters all the way to
bus-off and recovery. It models the bus layer; the MCU's pins and the
transceiver silicon sit outside its scope. More on that distinction in
docs/protocol.md.
cd sim
python3 can_sim.py --serve # then open http://localhost:8765You get a schematic where the buttons are clickable. Press one and watch a packet travel the CAN_H/CAN_L pair, the remote LED light, and the ACK come back. Two things worth pressing:
- force collision — both nodes transmit in the same arbitration window. The lower ID wins every time; the loser backs off and retries. No queue, no retry logic in software — arbitration resolves on the wire itself.
- unplug node 254 then press a button on 253 — the frame never gets ACKed, the TEC climbs +8 per retry, the node goes error-passive at 128 and bus-off at 256, then recovers on its own. Exactly what the hardware does.
Headless version (console + candump-style trace):
python3 can_sim.pyTwo ESP32 devkits (classic or S3, see the pin note below), two SN65HVD230 modules, one 120 Ω resistor at each end of the bus, buttons, relay modules.
- Set
NODE_ID 253/PEER_ID 254infirmware/can_node.ino, flash board 1. - Flip the two defines, flash board 2.
- Wire per docs/hardware.md. Short version:
TX→CTX,RX←CRX,3V3+GNDto each transceiver, CANH to CANH and CANL to CANL down the trunk, 120 Ω across the pair at both ends, and a third wire tying the grounds together.
Our first diagram assigned TWAI to GPIO 21/22, buttons to 32/33/27/14, relays
to 25/26, a classic-ESP32 pinout that ended up drawn on an ESP32-S3. On the
S3-WROOM-1, GPIO 22-25 do not exist and GPIO 26-33 are in-package
flash/PSRAM lines with no pads. The sketch would have failed at
twai_driver_install before a single frame went out.
The firmware picks the right pin block at compile time:
| Signal | classic ESP32 | ESP32-S3 |
|---|---|---|
| TWAI TX → CTX | 21 | 4 |
| TWAI RX ← CRX | 22 | 5 |
| Buttons | 32, 33, 27, 14 | 6, 7, 15, 16 |
| Relay IN1/IN2 | 25, 26 | 38, 39 |
| ACK confirm LED | 2 | 40 |
If you bought classic DevKits, the original numbers are valid. On S3, use the right-hand column.
One 11-bit standard frame does everything:
identifier = 0x100 | target (0x1FD → node 253, 0x1FE → node 254)
data[0..4] = target · sender · relay# · state · type (0=CMD, 1=ACK)
The target lives in the identifier on purpose. Each node sets its TWAI acceptance filter to its own ID, so frames meant for the other floor never reach the CPU — they are still ACKed on the wire, as the bus requires, then dropped in silicon. Receivers that do act send an ACK frame back so the sender's confirm LED blinks.
A product. The prototype is two nodes on a desk. A real building deployment needs isolated transceivers (an ISO1050, say), TVS protection, shielded pair, proper mains-rated relays and an electrician; the gap list is in docs/hardware.md. CAN also has no security: anything that can touch the pair can inject frames.
This idea is older than us and that is fine:
- Electronics For You built the same thing on Arduino + MCP2515 — "CAN Bus To Control Appliances on Multiple Floors of a Building".
- HAPCAN and Omzlo's NoCAN are complete open CAN home-automation systems.
- Kone has run elevators on CAN since the 1990s.
We picked CAN over RS-485/Modbus for one reason: arbitration, CRC, retry and fault confinement are in the silicon, so a dead node removes itself instead of killing the bus. The comparison lives in docs/protocol.md.
MIT — see LICENSE.

