Skip to content

Repository files navigation

ESPHome LIN Bus Component

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.

Status

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.

Installation

Add this to your ESPHome YAML:

external_components:
  - source: github://swifty99/linbus
    components: [linbus]

Quickstart

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);

Configuration Reference

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.

Modes

  • master — drives the bus. Sends frame headers on the configured schedule. Also publishes data for any lin_id in responses.
  • 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.

Schedule & Responses — the one rule that matters

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 via on_frame.
  • Slave → slave: master schedules the header; publisher slave transmits; listener slaves receive.

on_frame Trigger

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);
          }

High-frequency frames: use template sensors

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 {};

Action: linbus.update_response

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 — the linbus component ID (optional when only one is configured).
  • lin_id — required, 0–63, templatable.
  • data — required, 0–8 bytes, static list or lambda returning std::vector<uint8_t>. An empty list clears the configured response for that ID.

Development Verification

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.yaml

Validation 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.yaml

Before any ESP32-C6 hardware session, confirm the attached chip with a read-only probe:

python3 -m esptool --chip esp32c6 -p /dev/cu.usbmodem1101 chip_id

If 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_id

Do not upload firmware until the actual LIN transceiver wiring and selected pins have been confirmed. The C6 fixture pins are compile-only placeholders.

Diagnostic Sensors

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, MEASUREMENT

All fields are optional — add only the ones you need.

Current Limitations

  • 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.

Migrating from v1

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

Credits & References

License

GPLv3 — see LICENSE.

Releases

Packages

Contributors

Languages