LEGO DUPLO train control with esphome

This is my approach to mimic non connected controller behavior, while connected to BT. Train will react to action bricks as well as pushes and stops.
I use the vision sensor in “COLOR” mode which returns one of the predefined colors. “RGB” mode may drain battery faster due to more frequent changes and more frequent BLE transmission.
Speed sensor returns the raw value converted to integer, but I haven’t yet implement ed any interpolation to make sure it is somewhere between -100 and 100.
Now at least we can redefine or even extend behavior for action bricks. The good thing that the sensor in “COLOR” mode supports more colors than the 5 original action bricks.

Enjoy.

substitutions:
  service_uuid: '00001623-1212-efde-1623-785feabcd123'
  characteristic_uuid: '00001624-1212-efde-1623-785feabcd123'

esphome:
  name: duplo-steam-train
  friendly_name: Duplo Steam Train
  includes:
    - duplo.h

esp32:
  board: esp32dev
  framework:
    type: arduino

logger:

api:
  encryption:
    key: !secret enc_key
ota:
  password: !secret ota_pwd

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true

globals:
   - id: motor_on
     type: bool
     restore_value: no
     initial_value: 'false'
   - id: white_on
     type: bool
     restore_value: no
     initial_value: 'false'

esp32_ble_tracker:

ble_client:
  - mac_address: "6C:B2:FD:83:AA:31"
    id: train_hub
    on_connect:
      then:
        - lambda: |-
            ESP_LOGD("train", "Connected to Train");
        - delay: 1s
        # activate vision sensor in COLOR mode
        - script.execute:
            id: activate_single
            port: !lambda "return VISION_SENSOR_PORT_ID;"
            mode: !lambda "return VISION_SENSOR_MODE_COLOR;"
        # activate speed sensor in SPEED mode
        - script.execute:
            id: activate_single
            port: !lambda "return SPEED_SENSOR_PORT_ID;"
            mode: !lambda "return SPEED_SENSOR_MODE_SPEED;"
    on_disconnect:
      then:
        - lambda: |-
            ESP_LOGD("train", "Disconnected from Train");

sensor:
  - platform: ble_client
    type: characteristic
    id: ble_sensor
    ble_client_id: train_hub
    service_uuid: ${service_uuid}
    characteristic_uuid: ${characteristic_uuid}
    lambda: |-
      ESP_LOGD("train", "Sensor raw bytes: [{%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d}]", x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[11], x[12]);
      
      switch(x[3]) {
        case VISION_SENSOR_PORT_ID:
          {
            int color = x[4];
            if (x[4] == 1 || x[4] == 5) {
              color = color + 1;
            }
            if (color == 255) {
              color = 11;
            }
            id(hub_color).publish_state((float)color);
            break;
          }
        case SPEED_SENSOR_PORT_ID:
          {
            int16_t speed = x[4] | (int16_t)(x[5] << 8);
            id(hub_speed).publish_state((float)speed);
            break;
          }
      }
      return 0;
    notify: true

  - platform: template
    id: hub_color
    filters:
      - debounce: 0.2s
    on_value:
      then:
        - lambda: |-
            int value = (int)x;
            id(hub_color_text).publish_state(COLORS[value]);
            switch (value) {
              case RED:
                id(brake).press();
                id(stop).press();
                break;
              case BLUE:
                id(brake).press();
                id(stop).press();
                id(fill_water).press();
                delay(4000);
                id(speed_2).press();
                break;
              case YELLOW:
                id(horn).press();
                break;
              case GREEN:
                id(brake).press();
                if (id(hub_speed).state < 0) {
                  id(stop).press();
                  id(speed_2).press();
                } else {
                  id(stop).press();
                  id(backward).press();
                }
                break;
              case WHITE:
                if (id(white_on)) {
                  id(light_off).press();
                  id(white_on) = false;
                } else {
                  id(light_white).press();
                  id(white_on) = true;
                }
                break;
            }

  - platform: template
    id: hub_speed
    name: Speed
    on_value:
      then:
        - lambda: |-
            if (!id(motor_on)) {
              if (x > 10) {
                id(speed_2).press();
                id(motor_on) = true;
                return;
              }
              if (x < -10) {
                id(backward).press();
                id(motor_on) = true;
                return;
              }
            }
            if (x == 0.0) {
              id(motor_on) = false;
            }

text_sensor:
  - platform: template
    id: hub_color_text
    name: Color

switch:
  - platform: ble_client
    ble_client_id: train_hub
    name: Train

script:
  # activate updates for given port and given mode
  - id: activate_single
    parameters:
      port: char
      mode: char
    then:
      - ble_client.ble_write:
          id: train_hub
          service_uuid: ${service_uuid}
          characteristic_uuid: ${characteristic_uuid}
          value: !lambda |-
            std::vector<unsigned char> result { 0x0a, 0x00, 0x41, port, mode, 0x01, 0x00, 0x00, 0x00, 0x01 };
            return result;
  - id: prepare_ble
    then:
      - ble_client.ble_write:
          id: train_hub
          service_uuid: 00001623-1212-efde-1623-785feabcd123
          characteristic_uuid: 00001624-1212-efde-1623-785feabcd123
          value: [0x0a, 0x00, 0x41, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01]
  
  - id: switch_speed
    parameters:
      value: char
    then:
      - script.execute:
          id: prepare_ble
      - ble_client.ble_write:
          id: train_hub
          service_uuid: 00001623-1212-efde-1623-785feabcd123
          characteristic_uuid: 00001624-1212-efde-1623-785feabcd123
          value: !lambda |-
            std::vector<unsigned char> v = {0x08, 0x00, 0x81, 0x00, 0x01, 0x51, 0x00};
            v.push_back(value);
            return v;

  - id: make_sound
    parameters:
      value: char
    then:
      - script.execute:
          id: prepare_ble
      - ble_client.ble_write:
          id: train_hub
          service_uuid: 00001623-1212-efde-1623-785feabcd123
          characteristic_uuid: 00001624-1212-efde-1623-785feabcd123
          value: !lambda |-
            std::vector<unsigned char> v = {0x08, 0x00, 0x81, 0x01, 0x11, 0x51, 0x01};
            v.push_back(value);
            return v;

  - id: lights
    parameters:
      value: char
    then:
      - script.execute:
          id: prepare_ble
      - ble_client.ble_write:
          id: train_hub
          service_uuid: 00001623-1212-efde-1623-785feabcd123
          characteristic_uuid: 00001624-1212-efde-1623-785feabcd123
          value: !lambda |-
            std::vector<unsigned char> v = {0x08, 0x00, 0x81, 0x11, 0x11, 0x51, 0x00};
            v.push_back(value);
            return v;

button:
  - platform: template
    name: "Speed 1"
    id: speed_1
    icon: mdi:play
    on_press:
      - script.execute:
          id: switch_speed
          value: 0x1e

  - platform: template
    name: "Speed 2"
    id: speed_2
    icon: mdi:step-forward
    on_press:
      - script.execute:
          id: switch_speed
          value: 0x32

  - platform: template
    name: "Speed 3"
    id: speed_3
    icon: mdi:step-forward-2
    on_press:
      - script.execute:
          id: switch_speed
          value: 0x64

  - platform: template
    name: "Stop"
    id: stop
    icon: mdi:stop
    on_press:
      - script.execute:
          id: switch_speed
          value: 0x00

  - platform: template
    name: "Backward"
    id: backward
    icon: mdi:step-backward
    on_press:
      - script.execute:
          id: switch_speed
          value: 0xce

  - platform: template
    name: "Sound: Brake"
    id: brake
    icon: mdi:stop
    on_press:
      - script.execute:
          id: make_sound
          value: 0x03

  - platform: template
    name: "Sound: depart"
    id: depart
    on_press:
      - script.execute:
          id: make_sound
          value: 0x05

  - platform: template
    name: "Sound: fill water"
    id:  fill_water
    icon: mdi:water
    on_press:
      - script.execute:
          id: make_sound
          value: 0x07

  - platform: template
    name: "Sound: horn"
    id: horn
    icon: mdi:bugle
    on_press:
      - script.execute:
          id: make_sound
          value: 0x09

  - platform: template
    name: "Sound: steam"
    id: steam
    icon: mdi:kettle-steam
    on_press:
      - script.execute:
          id: make_sound
          value: 0x0A

  - name: "Light: Off"
    platform: template
    on_press:
      - script.execute:
          id: lights
          value: 0x00

  - name: "Light: Pink"
    platform: template
    on_press:
      - script.execute:
          id: lights
          value: 0x01

  - name: "Light: Purple"
    platform: template
    on_press:
      - script.execute:
          id: lights
          value: 0x02

  - name: "Light: Blue"
    platform: template
    on_press:
      - script.execute:
          id: lights
          value: 0x03

  - name: "Light: Light Blue"
    platform: template
    on_press:
      - script.execute:
          id: lights
          value: 0x04

  - name: "Light: Cyan"
    platform: template
    on_press:
      - script.execute:
          id: lights
          value: 0x05

  - name: "Light: Green"
    platform: template
    on_press:
      - script.execute:
          id: lights
          value: 0x06

  - name: "Light: Yellow"
    platform: template
    on_press:
      - script.execute:
          id: lights
          value: 0x07

  - name: "Light: Orange"
    platform: template
    on_press:
      - script.execute:
          id: lights
          value: 0x08

  - name: "Light: Red"
    platform: template
    on_press:
      - script.execute:
          id: lights
          value: 0x09

  - name: "Light: White"
    platform: template
    on_press:
      - script.execute:
          id: lights
          value: 0x0A

duplo.h

static const char VISION_SENSOR_PORT_ID = 0x12;
static const char VISION_SENSOR_MODE_COLOR = 0x00;
static const char VISION_SENSOR_MODE_CTAG = 0x01;
static const char VISION_SENSOR_MODE_REFLECTIVITY = 0x02;
static const char VISION_SENSOR_MODE_RGB = 0x03;

static const int BLACK = 0;
static const int PINK = 1;
static const int PURPLE = 2;
static const int BLUE = 3;
static const int LIGHTBLUE = 4;
static const int CYAN = 5;
static const int GREEN = 6;
static const int YELLOW = 7;
static const int ORANGE = 8;
static const int RED = 9;
static const int WHITE = 10;
static const int NONE = 255;
static const char *COLORS[12] = {"black", "pink", "purple", "blue", "lightblue", "cyan", "green", "yellow", "orange", "red", "white", "none"};

static const char SPEED_SENSOR_PORT_ID = 0x13;
static const char SPEED_SENSOR_MODE_SPEED = 0x00;
static const char SPEED_SENSOR_MODE_COUNT = 0x01;

static const char VOLTAGE_SENSOR_PORT_ID = 0x14;
5 Likes