A LIN (Local Interconnect Network) bus component for ESPHome, for communicating with the many cheap LIN actuators, sensors, fans, flaps, motors, pumps and solenoids found in vehicles.
Platform: ESP32 family only (ESP-IDF framework). Compile-validated with ESPHome 2026.6.2 on ESP32-C3, ESP32-C6, and ESP32.
v2 — rewrite from scratch. Previous versions are discarded and not backward-compatible (YAML key, action names, and internal API all changed).
The implementation lives upstream on the lin_bus branch of swifty99/esphome and is mirrored here as an ESPHome external component so you can use it without building from a fork.
Add this to your ESPHome YAML:
external_components:
- source: github://swifty99/linbus
components: [linbus]See example.yaml for a complete, runnable configuration. Minimum master-mode setup:
esp32:
board: esp32-c3-devkitm-1
framework:
type: esp-idf
linbus:
id: lin_master
tx_pin: GPIO10
rx_pin: GPIO6
cs_pin: GPIO7
baud_rate: 19200
uart_port: UART_NUM_1
mode: master
schedule:
- lin_id: 33
interval: 200ms
on_frame:
- lin_id: 33
then:
- lambda: |-
ESP_LOGD("linbus", "Got %d bytes for id 33", data_length);linbus:
id: lin_master # Optional
tx_pin: GPIO10 # Required
rx_pin: GPIO6 # Required
cs_pin: GPIO7 # Optional — LIN transceiver chip-select
baud_rate: 19200 # Optional (default 19200). Common: 9600, 19200, 20000
uart_port: UART_NUM_1 # Optional (default UART_NUM_1)
mode: master # Optional — master | slave | listener (default master)
schedule: # Optional - master-only scheduling table (max 16)
- lin_id: 34
interval: 100ms
responses: # Optional - pre-buffered publisher data (max 16; master/slave only)
- lin_id: 33
data: [0x00, 0x00, 0x00, 0x00]
checksum: enhanced # enhanced (LIN 2.x, default) | classic (LIN 1.x)
on_frame: # Optional — automations per LIN ID
- lin_id: 33
then:
- lambda: ...Frame IDs are 6-bit decimal values (0–63). IDs 60–63 are diagnostic / reserved IDs. Raw frame scheduling and responses can still use them, but NAD/SID diagnostic services are not implemented.
Configured responses use the selected checksum mode. Use enhanced for normal LIN 2.x frames and classic for LIN 1.x devices that require it. Diagnostic IDs use classic checksum behavior per LIN rules.
- master — drives the bus. Sends frame headers on the configured
schedule. Also publishes data for anylin_idinresponses. - slave — listens passively for headers. When a header matches one of this node's
responses, the node transmits the buffered data. - listener — receives only, never transmits. Useful for sniffing an existing bus or bench-testing.
Validation enforces those roles: schedule is only valid in master mode, and responses are rejected in listener mode.
LIN has no collision detection. If two nodes respond to the same lin_id, the bus corrupts and stops working. You must design your bus so that each lin_id has exactly one publisher. The master's schedule simply sends headers — whoever is configured to respond to that ID puts the bytes on the wire.
Lay this out explicitly before you configure anything:
ID Publisher Purpose Update Rate
-- --------- ----------------- -----------
10 Slave A Temperature 200ms
11 Master Pump Command 100ms
12 Slave B Status 100ms
Communication directions all use the same mechanism:
- Slave → master: master schedules header for a slave's ID; slave publishes; master receives via
on_frame. - Master → slave: master schedules header for an ID the master itself publishes (listed in its own
responses); slaves receive viaon_frame. - Slave → slave: master schedules the header; publisher slave transmits; listener slaves receive.
Fires when a complete, valid frame with the given lin_id is received. Exposed variables:
data(std::vector<uint8_t>) — up to 8 bytes received.data_length(uint8_t) — actual length.
on_frame:
- lin_id: 33
then:
- lambda: |-
if (data.size() >= 2) {
float temp = ((data[0] << 8) | data[1]) / 10.0f;
ESP_LOGD("linbus", "temp=%.1f", temp);
}At 10–50 ms intervals, on_frame runs thousands of times a minute. Heavy logic inside it will overwhelm ESPHome. Keep the automation trivial (copy into a global) and read at a controlled rate from a template sensor:
globals:
- id: lin_frame_33
type: std::vector<uint8_t>
restore_value: no
initial_value: '{}'
linbus:
# ...
on_frame:
- lin_id: 33
then:
- lambda: id(lin_frame_33) = data;
sensor:
- platform: template
name: "LIN Temperature"
unit_of_measurement: "°C"
update_interval: 1s
lambda: |-
auto d = id(lin_frame_33);
if (d.size() >= 2) return ((d[0] << 8) | d[1]) / 10.0f;
return {};Updates the pre-buffered data this node publishes for a given lin_id. The new bytes are sent the next time that header appears on the bus. If the response already exists in YAML, the action preserves that response's checksum mode. Responses created only by the action default to enhanced checksum.
number:
- platform: template
name: "Pump Speed"
min_value: 0
max_value: 100
step: 1
optimistic: true
on_value:
then:
- linbus.update_response:
id: lin_master
lin_id: 48
data: !lambda |-
uint8_t speed = (uint8_t) x;
return {speed, 0x01, 0x00, 0x00};Parameters:
id— thelinbuscomponent ID (optional when only one is configured).lin_id— required, 0–63, templatable.data— required, 0–8 bytes, static list or lambda returningstd::vector<uint8_t>. An empty list clears the configured response for that ID.
The component test fixtures under tests/components/linbus load the local component through ESPHome external_components, so they compile the code in this repository.
Compile checks:
esphome compile tests/components/linbus/test_esp32c3.yaml
esphome compile tests/components/linbus/test_esp32c6.yaml
esphome compile tests/components/linbus/test_master.yaml
esphome compile tests/components/linbus/test_slave.yaml
esphome compile tests/components/linbus/test_sensors.yaml
esphome compile tests/components/linbus/test_actions.yaml
esphome compile tests/components/linbus/test_classic_checksum.yamlValidation checks that should fail:
esphome config tests/components/linbus/test_listener_with_responses_invalid.yaml
esphome config tests/components/linbus/test_slave_with_schedule_invalid.yamlBefore any ESP32-C6 hardware session, confirm the attached chip with a read-only probe:
python3 -m esptool --chip esp32c6 -p /dev/cu.usbmodem1101 chip_idIf python3 -m esptool is not installed in the active Python environment, the PlatformIO environment usually provides the same check with:
$HOME/.platformio/penv/bin/esptool --chip esp32c6 -p /dev/cu.usbmodem1101 chip_idDo not upload firmware until the actual LIN transceiver wiring and selected pins have been confirmed. The C6 fixture pins are compile-only placeholders.
Expose bus-health counters as standard ESPHome sensors:
sensor:
- platform: linbus
linbus_id: lin_master
master_requests:
name: "LIN Master Requests"
id_requests_answered:
name: "LIN Requests Answered"
data_bytes_received:
name: "LIN Data Bytes"
checksum_errors:
name: "LIN Checksum Errors"
send_failures:
name: "LIN Send Failures"
bus_load:
name: "LIN Bus Load" # percent, MEASUREMENT
error_rate:
name: "LIN Error Rate" # percent, MEASUREMENTAll fields are optional — add only the ones you need.
- No event-triggered (spontaneous) frames.
- No NAD/SID diagnostic services.
- No sleep / wake-up signalling.
- No node configuration services (LIN 2.x config).
- ESP32 / ESP-IDF only. RP2040, ESP32 Arduino, and ESP8266 from the v1 codebase are not supported.
If you used the old lin: component from this repo, nothing carries over:
| v1 | v2 |
|---|---|
lin: YAML key |
linbus: |
uart_id + separate uart: block |
tx_pin / rx_pin / uart_port / baud_rate on linbus: directly |
linbus.send action |
linbus.update_response |
linbus.request_pid action |
removed — use schedule: instead |
Custom forked uart: component |
removed — uses ESP-IDF UART driver directly |
| ESP32 Arduino / RP2040 / ESP8266 | ESP-IDF on ESP32 only |
- Original slave code inspiration: Fabian-Schmidt/esphome-truma_inetbox
- LIN 2.1 specification: https://lin-cia.org/fileadmin/microsites/lin-cia.org/resources/documents/LIN-Spec_Pac2_1.pdf
- Upstream source branch: swifty99/esphome @ lin_bus
- API shape mirrors ESPHome's CAN bus component.
GPLv3 — see LICENSE.