[How to] UART read without custom component

The UART Bus component provides a write action but no read actions. Typically the UART read is implemented in a custom component that also does the processing. The UART debug sequence can be exploited to act like a call back function whenever there is received data to process. So basically instead of creating a small custom component a large lambda call can be used in the UART debug yaml. This is shared to hopefully help make implementing UART devices a bit easier for average users.

It seems to be well suited for implementing simple UART devices where the code in the lambda would be the majority of the custom component code. If the protocol is more complex with different lengths of types of payload in the packets this isn’t well suited.

Key points are:

  • dummy_receiver: true to make sure the debug logging gets triggered without another uart component to do the reading
  • the rx data is returned in std::vector<uint8_t> bytes
  • delimiter and timeout tweaked so rx data is presented in consistent format
  • direction: RX if uart.write is used to tx data, otherwise tx data gets processed by lambda as well
  • doesn’t have a setup() if you need to perform some initialization (esphome.on_boot could be work around)
  • only gets called when data arrives, additional logic might be required to determine if UART bus is faulted
esphome:
  name: uart-hack

esp32:
  board: esp32dev
  framework:
    type: arduino

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

api:

ota:
  password: !secret esp_home_ota_pw

logger:
  baud_rate: 115200

uart:
  baud_rate: 115200
  tx_pin: 17
  rx_pin: 16
  debug:
    direction: RX
    dummy_receiver: true
    after:
      delimiter: "\r\n"
    sequence:
      - lambda: |-
          UARTDebug::log_string(direction, bytes);  //Still log the data
          
          int sensorID=0;
          float sensorTEMP=0; 
          
          //Example to convert uart text to string
          std::string str(bytes.begin(), bytes.end());

          //watch for potential problems with non printable or special characters in string
          id(rawString).publish_state(str.c_str());
          
          //Sample uart text protocol that sends id:temperature\r\n packets
          if (sscanf(str.c_str(), "%d:%f", &sensorID, &sensorTEMP) == 2 ) {
            if(sensorID==1) 
              id(temp1).publish_state(sensorTEMP); 
            if(sensorID==2) 
              id(temp2).publish_state(sensorTEMP);
          }

          //Sample binary / hex data
          if (bytes.size()==9) {
            if(bytes[0]==0x40)
              id(binData).publish_state( bytes[4] );
          }

text_sensor:
  - platform: template
    name: "Raw String"
    id: "rawString"

sensor:
  - platform: template
    name: "Temp 1"
    id: "temp1"
  - platform: template
    name: "Temp 2"
    id: "temp2"
  - platform: template
    name: "Binary Data"
    id: binData


button:
  - platform: template
    name: "Test Button 1"
    on_press:
      - uart.write: "1:21.1\r\n"
  - platform: template
    name: "Test Button 2"
    on_press:
      - uart.write: "2:21.2\r\n"
  - platform: template
    name: "Test Button 3"
    on_press:
      - uart.write: [0x54, 0x65, 0x73, 0x74, 0x20, 0x33, 0x0A, 0x0D] 
  - platform: template
    name: "Test Button 4"
    on_press:
      - uart.write: "giberish\r\n" 
  - platform: template
    name: "Test Button 5"
    on_press:
      - uart.write: [0x40, 0x6D, 0x75, 0x6C, 0x63, 0x6D, 0x75, 0x0A, 0x0D]          

#if you need to send a request periodically to get response from the uart device 
interval:
  - interval: 1min
    then:
      - uart.write:  [0x54, 0x65, 0x73, 0x74, 0x20, 0x33, 0x0A, 0x0D]      
7 Likes

Nice one for consolidating this info.

I’ve had this need myself and see the same kind of question pop up a lot.

I’m definitely in what I see as a key user group for this:
"Ok yep I’m cool with this ESPHome business now and can fumble through some lamda’s ok, but this cpp stuff has got me awefully confused - I read the thing and pasted the thing and changed the thing but no-worky!

Your post elsewhere actually triggered me to enquire on the ESPHOME Discord about a “UART sensor”.

Here’s what two key Devs had to say… Fair enough.

This is great if the data is text. What if it’s HEX and variable length???

Im struggling trying to figure out how to get the text in yellow into HOME ASSISTANT so I can display it in Lovelace. I think I’ve nailed the hard part which is decyphering what all the codes mean!

Any help appreciated!!! I have a specific discussion for this I created in my Github project:

EDIT: HUGE Thanks to @mulcmu for solving this. Details are in my github link above.

2 Likes

Hi
Thanks for your good topic and nice suggestion.
I’m in basic level and new on Esphome and Home assistant.
I conneced a Sim800l module to Esp32 and used Sim800L Component and every thing is working fine. sometimes Sim800l disconnnects and my controller doesn’t undrestand. I need to send AT command to Sim800l continusly (Like this uart.write: “AT\r\n”) and if the answer was “ok” it means every thnig is okay but if not, so the controller should reset the sim800 module. would you please help me through your own method, how can send and receive uart code directly. Please explain to me in a simple way.
I need a code like this in my program. would you please help me execute it through your method:

action:
      - uart.write: 
          id: uart_bus
          data: "AT\r\n"
      - delay: 500ms
      - if:
          condition:
            - sensor.condition.string_contains:
                id: sim800l_response
                substring: "OK"
          then:
            - logger.log: "SIM800L module is working fine"
          else:
            - logger.log: "SIM800L module is not responding, resetting..."
            - uart.write: 
                id: uart_bus
                data: "AT+CFUN=1,1\r\n"
            - delay: 5s

The Sim800L module will have lots of RX/TX traffic on the UART. Adding the debug processing will add extra processing time that could cause long delays and interfere with the normal module operation. Also sending the AT command can get the module out of sync ESPHome: /opt/build/esphome/esphome/components/sim800l/sim800l.cpp Source File. It will get and try to process the extra OK responses.

I’d try to find another way to detect that the Sim800l is operating okay. Maybe if the RSSI is same value for some time period it indicates module needs reset.

Can you help with O2 sensor , how get data from sensor via esphome uart
Sensor sends out data when recives letter Z via uart
What i am doing wrong?




uart:
  id: mod_uart
  tx_pin: GPIO17
  rx_pin: GPIO16
  baud_rate: 9600
  stop_bits: 1
  parity: none
  debug:
    direction: BOTH
    dummy_receiver: false
    after:
      delimiter: "\n"
    sequence:
    - lambda: |-
          UARTDebug::log_string(direction, bytes);  //Still log the data
            std::string str(bytes.begin(), bytes.end());
            id(rawString).publish_state(str.c_str());



text_sensor:
  - platform: template
    name: "UART Received Data"
    id: uart_received_data

  - platform: template
    name: "Raw String"
    id: "rawString"

interval:
  - interval: 10sec
    then:
      - uart.write: Z\r

For the uart delimiter try "\r\n". Likewise for the interval send Z\r\n. \r is the <CR> and \n is <LF> used in the manual. Then see if anything starts showing up in the logs.

Hi, i try to send data via UART to my own MCU since days, but it will not work. I connect pin 17 (RX2) and 16(TX2) from my NodeMCU32.

This is my code. Is there anything wrong?

substitutions:
  device_name: hw-kg-vorraum-stromzahler
    
esphome:
  name: ${device_name}

esp32:
  board: nodemcu-32s

api:
  encryption:
    key: !secret api_encryption

ota:
  password: !secret ota_password

logger:
  baud_rate: 0 #disable logging over uart
  level: DEBUG  

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "${device_name}"
    password: !secret hotspot_password

captive_portal:

web_server:
  port: 80
#################################################################################

uart:  
  id: uart_bus
  rx_pin: GPIO16
  tx_pin: GPIO17
  baud_rate: 9600
  data_bits: 8
  stop_bits: 1
  parity: none
  debug:
    direction: BOTH
    dummy_receiver: false
    after:
      delimiter: "\r\n"
    sequence:
      - lambda: |-
          UARTDebug::log_string(direction, bytes);  //Still log the data
            std::string str(bytes.begin(), bytes.end());
            id(rawString).publish_state(str.c_str());

text_sensor:
  - platform: template
    name: "UART Received Data"
    id: uart_received_data

  - platform: template
    name: "Raw String"
    id: "rawString"

interval:
  - interval: 1s
    then:
      - uart.write: "Hello World\r\n"

I’m nowhere done making my esphome controller for my xpelair heater/airconditioner unit, but this actually got me to read data without making me vomit .h files until something stuck. Thank you!

1 Like

Just providing this little example.

uart:
  id: uart_bus
  tx_pin: GPIO21
  rx_pin: GPIO20 
  baud_rate: 9600
  debug:
    direction: BOTH
    dummy_receiver: true
    after:
      # bytes: 9 
      # timeout: 10ms
      delimiter: [0x7E] #Specific end byte
    sequence:     
      - lambda: |-
          UARTDebug::log_int(direction, bytes, ',');                 // Log the message as int. 
          UARTDebug::log_hex(direction, bytes, ',');                 // Log the message in hex.
          ESP_LOGD("custom", "Bytes size: %d", bytes.size());        // Log how many bytes in the message.
          if (direction == UART_DIRECTION_RX)                        // Check message direction (Change to TX if required)
            {
                if (bytes.size() == 9)                               // Check number of bytes
                  {
                      if ( bytes[0] == 0xF2 &&                       // Check the first three bytes and the last byte
                           bytes[1] == 0xF2 && 
                           bytes[2] == 0x01 &&
                           bytes[8] == 0x7E
                          )       
                        {
                          int height = (bytes[4] * 256) + bytes[5];  // Do some operations with some bytes.
                          id(desk_height).publish_state(height);     // Publish results to a sensor.
                        }
                  }
            }
1 Like

legends, that has the M701 - 7 in 1 air sensor working. I get the following error in the logs tho: “Component uart took a long time for an operation (93 ms)” and “Components should block for at most 30 ms.”
I think I need to add a delay or something.
Would it be easy to turn this to an external component now to make it easier for others to use this air sensor?
here is the .yaml

logger:
  baud_rate: 9600

uart:
  id: uart_bus
  baud_rate: 9600
  tx_pin: 16
  rx_pin: 17
  debug:
    direction: RX
    dummy_receiver: true
    after:
      delimiter: "\r\n"
    sequence:
      - lambda: |-
          UARTDebug::log_hex(direction, bytes, ',');                 // Log the message as int. 
          ESP_LOGD("custom", "Bytes size: %d", bytes.size());        // Log how many bytes in the message.
          if (direction == UART_DIRECTION_RX)                        // Check message direction (Change to TX if required)
            {
                if (bytes[0] == 0x3C && bytes[1] == 0x02)                       
                  {
                    int eCO2 = (bytes[2] << 8) | bytes[3];
                    int eCH2O = (bytes[4] << 8) | bytes[5];
                    int TVOC = (bytes[6] << 8) | bytes[7];
                    int PM2_5 = (bytes[8] << 8) | bytes[9];
                    int PM10 = (bytes[10] << 8) | bytes[11];
                    float temperature = bytes[12] + bytes[13] / 100.0;
                    float humidity = bytes[14] + bytes[15] / 100.0;
                    id(eCO2_sensor).publish_state(eCO2);
                    id(eCH2O_sensor).publish_state(eCH2O);
                    id(TVOC_sensor).publish_state(TVOC);
                    id(PM2_5_sensor).publish_state(PM2_5);
                    id(PM10_sensor).publish_state(PM10);
                    id(temperature_sensor).publish_state(temperature);
                    id(humidity_sensor).publish_state(humidity);
                  }
            }

sensor:
  - platform: template
    name: "eCO2 reading"
    id: eCO2_sensor
    device_class: carbon_dioxide
    state_class: measurement
    unit_of_measurement: "ppm"
  - platform: template
    name: "eCH2O reading"
    id: eCH2O_sensor
    device_class: volatile_organic_compounds
    state_class: "measurement"
    unit_of_measurement: "µg/m³"
  - platform: template
    name: "TVOC reading"
    id: TVOC_sensor
    device_class: volatile_organic_compounds
    state_class: "measurement"
    unit_of_measurement: "µg/m³"
  - platform: template
    name: "PM2.5 reading"
    id: PM2_5_sensor
    unit_of_measurement: μg/m³
    accuracy_decimals: 0
    device_class: PM25
    state_class: "measurement"
  - platform: template
    name: "PM10 reading"
    id: PM10_sensor
    unit_of_measurement: μg/m³
    accuracy_decimals: 0
    device_class: PM10
    state_class: "measurement"
  - platform: template
    name: "Temperature reading"
    id: temperature_sensor
    unit_of_measurement: "°C"
    device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 1
  - platform: template
    name: "Humidity reading"
    id: humidity_sensor
    unit_of_measurement: "%"
    device_class: "humidity"
    state_class: "measurement"
1 Like


After one of the Esphome updates, this happen in different components.
see:

It depends how much c++ experience you have. Probably pretty easy if you have some, or not a bad learning project if you have limited.

Last time I tried it was still beyond me (despite the more proficient suggesting it is “easy”:slight_smile: ) and I stuck with yaml and lots of lamdas, which I am comfortable with.

The devs over on Discord would probably give some guidance through the process if you are patient and willing.