Continuous ultra sonic level measurement - DS1603L

Hi,
has anyone used the DS1603L so far?
It has an UART serial interface.
I am considering to use it for water (plastic tank) ,black water (stainless steel 2mm) and Diesel tan (stainless steel).

Would be interested in complexity to integrate it.
So far I only use Deconz connected sensors.

Best regards,
Joerg

Digging an old post up here, I’ve been playing around with the DS1603L and esphome using the custom uart device configs with no success so far, anyone else?

Hi

Old post but it is never to late.

I needed to use this DS1603L sensor for a project and the only option was to create an esphome external component but my knowledge in coding is zero.
After some digging in GitHub - jesserockz/esphome-external-component-examples to figure out the files structure and with the use of Chat GTP I managed to create the external component for DS1603L-V1 ultrasonic sensor.

Below the codes for .h, .cpp, .py and yaml.

DS1603L.h

#pragma once

#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"

namespace esphome {
namespace DS1603L {

class DS1603L : public sensor::Sensor, public PollingComponent, public uart::UARTDevice {
 public:
  DS1603L() = default;

  void setup() override;       // Called during setup
  void update() override;      // Called periodically
  void loop() override;        // Called frequently to process data
  void dump_config() override; // Prints configuration info

 private:
  uint8_t rx_buffer_[4];  // Buffer for incoming data

  void parse_data_();  // Parse received data
};

}  // namespace DS1603L
}  // namespace esphome

DS1603L.cpp

#include "esphome/core/log.h"
#include "DS1603L.h"

namespace esphome {
namespace DS1603L {

static const char *TAG = "DS1603L.sensor";

void DS1603L::setup() {
  ESP_LOGCONFIG(TAG, "Setting up DS1603L sensor...");
}

void DS1603L::update() {
  // Updates are primarily handled in the loop
}

void DS1603L::loop() {
  // Check if enough bytes are available
  while (this->available() >= 4) {
    // Read 4 bytes of data
    this->read_array(this->rx_buffer_, 4);

    ESP_LOGD(TAG, "Raw Data: %02X %02X %02X %02X", 
             this->rx_buffer_[0], this->rx_buffer_[1], 
             this->rx_buffer_[2], this->rx_buffer_[3]);

    // Verify the header byte
    if (this->rx_buffer_[0] != 0xFF) {
      ESP_LOGW(TAG, "Invalid header received");
      continue;
    }

    // Parse the received data
    this->parse_data_();
  }
}

void DS1603L::dump_config() {
  ESP_LOGCONFIG(TAG, "DS1603L Sensor:");
  LOG_SENSOR("", "Liquid Level", this);
}

void DS1603L::parse_data_() {
  uint8_t header = this->rx_buffer_[0];
  uint8_t data_h = this->rx_buffer_[1];
  uint8_t data_l = this->rx_buffer_[2];
  uint8_t checksum = this->rx_buffer_[3];

  // Validate header
  if (header != 0xFF) {
    ESP_LOGW(TAG, "Invalid header: Received 0x%02X, expected 0xFF", header);
    return;
  }

  // Compute checksum
  uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;

  ESP_LOGD(TAG, "Data: Header=0x%02X, Data_H=0x%02X, Data_L=0x%02X, Checksum=0x%02X", 
           header, data_h, data_l, checksum);
  ESP_LOGD(TAG, "Checksum: Computed=0x%02X, Received=0x%02X", computed_checksum, checksum);

  if (checksum != computed_checksum) {
    ESP_LOGW(TAG, "Checksum mismatch: Received 0x%02X, expected 0x%02X", checksum, computed_checksum);
    return;
  }

  // Calculate liquid level
  int liquid_level = (data_h << 8) | data_l;  // Combine high and low bytes
  ESP_LOGI(TAG, "Liquid Level: %d cm", liquid_level);

  // Publish the state
  this->publish_state(liquid_level);
}

}  // namespace DS1603L
}  // namespace esphome

sensor.py

import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart, sensor
from esphome.const import CONF_ID, CONF_NAME, UNIT_EMPTY, ICON_EMPTY

# Define the namespace for DS1603L
DEPENDENCIES = ["uart"]
DS1603L_ns = cg.esphome_ns.namespace("DS1603L")
DS1603L = DS1603L_ns.class_("DS1603L", cg.PollingComponent, uart.UARTDevice)

# Configuration schema for DS1603L
CONFIG_SCHEMA = (
    sensor.sensor_schema(
        unit_of_measurement=UNIT_EMPTY,  # Unit is customizable in YAML
        icon=ICON_EMPTY,                # Icon is customizable in YAML
        accuracy_decimals=1,            # Default to 1 decimal place
    )
    .extend(cv.polling_component_schema("60s"))  # Default update interval is 60s
    .extend(uart.UART_DEVICE_SCHEMA)  # Include UART configuration options
    .extend({
        cv.GenerateID(): cv.declare_id(DS1603L),  # Declare sensor ID
        cv.Optional(CONF_NAME, default="DS1603L Sensor"): cv.string,  # Default name
    })
)

# Generate the C++ code from YAML
async def to_code(config):
    var = cg.new_Pvariable(config[CONF_ID])  # Create a new DS1603L instance
    cg.add(var.set_name(config[CONF_NAME]))  # Set the sensor name
    await cg.register_component(var, config)  # Register the component
    await sensor.register_sensor(var, config)  # Register it as a sensor
    await uart.register_uart_device(var, config)  # Register UART communication

Inside the component folder you also need to put an empty __init__.py file

YAML

esphome:
  name: heating-oil
  friendly_name: Heating OIL

esp32:
  board: esp32dev
  framework:
    type: arduino
    
external_components:
  - source:
      type: local
      path: my_components

# Enable logging
logger:
  level: INFO  # Set the default log level

# Enable Home Assistant API
api:
  encryption:
    key: # Your api key here

ota:
  - platform: esphome
    password: # Your ota password here

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.30.15
    gateway: 192.168.30.1
    subnet: 255.255.255.0

# Enable hotspot if case wifi connection fails
  ap:
    ssid: "heating-oil fail hotspot"
    password: !secret wifi_password

# Sync time with Home Assistant.   
time:
  - platform: homeassistant
    id: homeassistant_time

captive_portal:

web_server:
  port: 80

uart:
  id: uart_bus
  baud_rate: 9600
  tx_pin: 17
  rx_pin: 16

sensor:
  - platform: DS1603L
    name: "Heating OIL Level"
    id: ds1603l_sensor_id
    unit_of_measurement: "mm"
    icon: "mdi:water"
    accuracy_decimals: 1
    update_interval: 5s

button:
  - platform: restart
    icon: mdi:power-cycle
    name: "ESP Reboot"

As i mention i have zero knowledge in coding so PLEASE check the code before use it.

PS. Sorry for my poor English.

Hi.
I noticed that after power cycles the sensor reported
[W][DS1603L.sensor:029]: Invalid header received
and it needed a restart of esp to fix it.

Ιn order to solve the problem, the following changes have been made to the code.

Added 2 sec delay for sensor initialization.

Added UART Buffer flush to clear any trash data in buffer

After startup, the first 2 seconds of data are not processed.

I have tested the sensor with both metallic and plastic container filled with water and works

IMPORTANT note. For the sensor to work you need to add a kind of coupling compound like silicone grease between sensors head and container. Without this the sensor cannot take measurements.

Below is the modified code.

DS1603L.cpp

#include "esphome/core/log.h"
#include "DS1603L.h"

namespace esphome {
namespace DS1603L {

static const char *TAG = "DS1603L.sensor";

void DS1603L::setup() {
  ESP_LOGCONFIG(TAG, "Setting up DS1603L sensor...");

  // Add a delay to allow the sensor to initialize after power-up
  delay(2000); // 2 seconds

  // Flush any residual data in the UART buffer
  while (this->available() > 0) {
    this->read();
  }
}

void DS1603L::update() {
  // Updates are primarily handled in the loop
}

void DS1603L::loop() {
  static bool initialized = false;
  static unsigned long start_time = millis();

  // Ignore invalid data during the first 2 seconds after startup
  if (!initialized && (millis() - start_time < 2000)) {
    while (this->available() > 0) {
      this->read(); // Clear any initial invalid data
    }
    return;
  }
  initialized = true;

  // Process incoming data
  while (this->available() >= 4) {
    // Read 4 bytes of data
    this->read_array(this->rx_buffer_, 4);

    ESP_LOGD(TAG, "Raw Data: %02X %02X %02X %02X", 
             this->rx_buffer_[0], this->rx_buffer_[1], 
             this->rx_buffer_[2], this->rx_buffer_[3]);

    // Verify the header byte
    if (this->rx_buffer_[0] != 0xFF) {
      ESP_LOGW(TAG, "Invalid header received");
      continue;
    }

    // Parse the received data
    this->parse_data_();
  }
}

void DS1603L::dump_config() {
  ESP_LOGCONFIG(TAG, "DS1603L Sensor:");
  LOG_SENSOR("", "Liquid Level", this);
}

void DS1603L::parse_data_() {
  uint8_t header = this->rx_buffer_[0];
  uint8_t data_h = this->rx_buffer_[1];
  uint8_t data_l = this->rx_buffer_[2];
  uint8_t checksum = this->rx_buffer_[3];

  // Validate header
  if (header != 0xFF) {
    ESP_LOGW(TAG, "Invalid header: Received 0x%02X, expected 0xFF", header);
    return;
  }

  // Compute checksum
  uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;

  ESP_LOGD(TAG, "Data: Header=0x%02X, Data_H=0x%02X, Data_L=0x%02X, Checksum=0x%02X", 
           header, data_h, data_l, checksum);
  ESP_LOGD(TAG, "Checksum: Computed=0x%02X, Received=0x%02X", computed_checksum, checksum);

  if (checksum != computed_checksum) {
    ESP_LOGW(TAG, "Checksum mismatch: Received 0x%02X, expected 0x%02X", checksum, computed_checksum);
    return;
  }

  // Calculate liquid level directly
  int liquid_level_mm = (data_h << 8) | data_l;

  ESP_LOGI(TAG, "Liquid Level: %d mm", liquid_level_mm);

  // Publish the state
  this->publish_state(liquid_level_mm);
}

}  // namespace DS1603L
}  // namespace esphome

Regards