Ongoing Build Notes for LoRa/Meshtastic Arduino Rangefinder Ultrasonic

by snotty in Circuits > Microcontrollers

33 Views, 0 Favorites, 0 Comments

Ongoing Build Notes for LoRa/Meshtastic Arduino Rangefinder Ultrasonic

heltec lora v3.2.png

This is a living document which details the build log of an ultrasonic sonar rangefinder integration project. Part of a larger integration to datalog well water depth in Home Assistant in 2026.


Sensor similar or identical to Large Ultrasonic (Sonar) Sensor with Horn and UART Output https://cdn-shop.adafruit.com/product-files/4664/4664_datasheet.pdf

https://www.adafruit.com/product/4664

Operating Voltage: 3.3~5V

Average Current Draw : ≤ 15mA

Peak Current Draw: ≤ 50mA

Blind Zone Distance: 0 ~ 28cm

Detection Range without bell-mouth cone: 28 ~ 450 cm

Sensing Angle without bell-mouth cone: 75º

Detection Range with bell-mouth cone: 28 ~ 750 cm (we were able to test it up to 6 meters)

Sensing Angle with bell-mouth cone: 40º

Precision: ±1cm + 0.5%

Output: 9600 baud TTL UART

Response Time: 100ms

Probe Center Frequency: 40KHz ±1.0KHz

Operating Temperature: -15 ~ 60 degrees C at <80% humidity

Storage Temperature: -25 ~ 80 degrees C at <90% humidity


Arduino Duemilanove (chip transferred to protoboard with crystal (maybe not necessary) A cheap arduino permanently installed would be better.

This proved necessary for using Meshtastic and could have otherwise been omitted by running the arduino code on the Heltec.

Arduino is for removing headers from the sensor's onboard uart serial outputs.

And also for reducing sensor use from ~10hz to 10 seconds, maybe reducing wear. Used the Adafruit sleepy dog library.


Heltec LoRa v3.2 WiFi LoRa 32(V3), ESP32S3 + SX1262 LoRa Node, Meshtastic

Two units running meshtastic.



Supplies

Ultrasonic rangefinder sonar with uart output

Arduino (only if using meshtastic)

Sleep library https://github.com/adafruit/Adafruit_SleepyDog

LoRa Heltec WiFi LoRa 32 V3

Arduino to Interpret Sensor

With arduino intermediary between sensor and heltec.


#include <SoftwareSerial.h> //add another serial port
#include <Adafruit_SleepyDog.h> //sleep library
SoftwareSerial mySerial(2, 1); //rx pin WHITE WIRE, tx pin
//YELLOW CONTROL WIRE TO GND OR DATA READS AS ZERO

#define SENSOR_POWER_PIN 3
bool sensorToggle = false;
int loopcounter;
int16_t distance; // The last measured distance
bool newData = false; // Whether new data is available from the sensor
uint8_t buffer[4]; // our buffer for storing data
uint8_t idx = 0; // our idx into the storage buffer

void setup() {
// Open serial communications and wait for port to open:
// To monitor: USB CDC On Boot: “Enabled” (under Tools)
Serial.begin(9600);
mySerial.begin(9600);
pinMode(SENSOR_POWER_PIN, OUTPUT);
digitalWrite(SENSOR_POWER_PIN, LOW);
}


void loop() {
//delay(10); //sensor stabilize
// Serial.print("Main loop number: ");

// loopcounter ++;
// Serial.println(loopcounter);

if (sensorToggle == false) {
digitalWrite(SENSOR_POWER_PIN, HIGH); //sensor power supply on
// mySerial.begin(9600); //restart softwareSerial
sensorToggle = true;
// Serial.println("sensor on");
//Serial.println("delay");
}

for (int i = 1; i <= 4; i++) {
delay(5);
// Serial.print("ultrasonic read loop ");
// Serial.println(i);
//ultrasound
mySerial.begin(9600); //restart softwareSerial
if (mySerial.available()) {
uint8_t c = mySerial.read();
//Serial.println(c, HEX);

// See if this is a header byte
if (idx == 0 && c == 0xFF) {
buffer[idx++] = c;
}
// Two middle bytes can be anything
else if ((idx == 1) || (idx == 2)) {
buffer[idx++] = c;
}
else if (idx == 3) {
uint8_t sum = 0;
sum = buffer[0] + buffer[1] + buffer[2];
if (sum == c) {
distance = ((uint16_t)buffer[1] << 8) | buffer[2];
newData = true;
}
idx = 0;
}
}
//write out new data
if (newData) {
Serial.println(distance);
newData = false;
loopcounter = 0;
mySerial.end(); //(maybe not needed) turn off software serial to clear buffer
digitalWrite(SENSOR_POWER_PIN, LOW); //turn off sensor power supply
sensorToggle = false;
// Serial.println("sensor off");
}
}//end for
sleep();

sleep();

//Serial.println("end main loop");
}

void sleep(){
//Serial.println("delay here should sleep for several seconds");
delay(100); //minimum 80ms or serial corrupts
//delay here after serial print causes corrupt serial
int sleepMS = Watchdog.sleep(); //should sleep for like 8 seconds max, despite args
}


Cleaner version that still needs debugging

#include <SoftwareSerial.h> //add another serial port
#include <Adafruit_SleepyDog.h> //sleep library
SoftwareSerial mySerial(2, 1); //rx pin WHITE WIRE, tx pin
//YELLOW CONTROL WIRE TO GND OR DATA READS AS ZERO
void setDefaults(); //maybe fixes not declared in scope error

#define SENSOR_POWER_PIN 3
//int loopcounter;
int16_t distance; // The last measured distance
bool newData = false; // Whether new data is available from the sensor
uint8_t buffer[4]; // our buffer for storing data
uint8_t idx = 0; // our idx into the storage buffer

void setup() {
// Open serial communications and wait for port to open:
// To monitor: USB CDC On Boot: “Enabled” (under Tools)
Serial.begin(9600);
mySerial.begin(9600);
pinMode(SENSOR_POWER_PIN, OUTPUT);
digitalWrite(SENSOR_POWER_PIN, LOW);
delay(3000);//let heltec get settled
}

void loop() {

//first look for data
readSensor();

//send out new data only. Don't contaminate datalogger with zeros if no data
sendData();

//sleep no mater what and turn off sensor
sleep();//for about 8 seconds by default on atmega328
sleep();//sleep some more
}

void readSensor(){
digitalWrite(SENSOR_POWER_PIN, HIGH); //sensor power supply on
for (int i = 1; i <= 4; i++) {
delay(5);
// Serial.print("ultrasonic read loop ");
// Serial.println(i);
//ultrasound
mySerial.begin(9600); //restart softwareSerial
if (mySerial.available())
{
uint8_t c = mySerial.read();
//Serial.println(c, HEX);

// See if this is a header byte
if (idx == 0 && c == 0xFF) {
buffer[idx++] = c;
}
// Two middle bytes can be anything
else if ((idx == 1) || (idx == 2)) {
buffer[idx++] = c;
}
else if (idx == 3) {
uint8_t sum = 0;
sum = buffer[0] + buffer[1] + buffer[2];
if (sum == c) {
distance = ((uint16_t)buffer[1] << 8) | buffer[2];
newData = true;
}
idx = 0;
}
}
}
}

void sendData(){
if (newData) {
Serial.println(distance);
newData = false;
mySerial.end(); //(maybe not needed) turn off software serial to clear buffer
}
}

void sleep(){
digitalWrite(SENSOR_POWER_PIN, LOW); //turn off sensor power supply
//Serial.println("sleep");
delay(100); //minimum 80ms or serial corrupts
int sleepMS = Watchdog.sleep(); //should sleep for like 8 seconds max, despite args
}


Without extra arduino but no meshtastic


#include <HardwareSerial.h> // Reference the ESP32 built-in serial port library
#include <Wire.h>
#include "HT_SSD1306Wire.h"

//Lora
#include "LoRaWan_APP.h"
#include "Arduino.h"

#define RF_FREQUENCY 915000000 // Hz

#define TX_OUTPUT_POWER 5 // dBm

#define LORA_BANDWIDTH 0 // [0: 125 kHz,
// 1: 250 kHz,
// 2: 500 kHz,
// 3: Reserved]
#define LORA_SPREADING_FACTOR 7 // [SF7..SF12]
#define LORA_CODINGRATE 1 // [1: 4/5,
// 2: 4/6,
// 3: 4/7,
// 4: 4/8]
#define LORA_PREAMBLE_LENGTH 8 // Same for Tx and Rx
#define LORA_SYMBOL_TIMEOUT 0 // Symbols
#define LORA_FIX_LENGTH_PAYLOAD_ON false
#define LORA_IQ_INVERSION_ON false


#define RX_TIMEOUT_VALUE 1000
#define BUFFER_SIZE 30 // Define the payload size here

char txpacket[BUFFER_SIZE];
char rxpacket[BUFFER_SIZE];

double txNumber;

bool lora_idle=true;

static RadioEvents_t RadioEvents;
void OnTxDone( void );
void OnTxTimeout( void );

//Display
static SSD1306Wire display(0x3c, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED); // addr , freq , i2c group , resolution , rst

HardwareSerial ultrasoundSerial(1); // Using serial port 1
#define RXD2 46 // This is the WHITE wire
#define TXD2 45 // This is the YELLOW wire, can be any data line. Actually, just tie yellow to ground.
#define CONTROL_PIN 45 // This is the YELLOW wire, can be any data line Actually, just tie yellow to ground.

int16_t distance; // The last measured distance
bool newData = false; // Whether new data is available from the sensor
uint8_t buffer[4]; // our buffer for storing data
uint8_t idx = 0; // our idx into the storage buffer

void setup() {
// Open serial communications and wait for port to open:
// To monitor: USB CDC On Boot: “Enabled” (under Tools)
Serial.begin(115200);
Mcu.begin(HELTEC_BOARD,SLOW_CLK_TPYE);//lora?
//more lora
txNumber=0;

RadioEvents.TxDone = OnTxDone;
RadioEvents.TxTimeout = OnTxTimeout;
Radio.Init( &RadioEvents );
Radio.SetChannel( RF_FREQUENCY );
Radio.SetTxConfig( MODEM_LORA, TX_OUTPUT_POWER, 0, LORA_BANDWIDTH,
LORA_SPREADING_FACTOR, LORA_CODINGRATE,
LORA_PREAMBLE_LENGTH, LORA_FIX_LENGTH_PAYLOAD_ON,
true, 0, 0, LORA_IQ_INVERSION_ON, 3000 );

// while (!Serial) {
// delay(10); // wait for serial port to connect. Needed for native USB port only
// }
// set the data rate for the Serial port, 9600 for the sensor
ultrasoundSerial.begin(9600, SERIAL_8N1, RXD2, TXD2); // Initializing serial port
pinMode(CONTROL_PIN, OUTPUT);
digitalWrite(CONTROL_PIN, HIGH);

// Initialising the UI will init the display too.
VextON(); // Turn on external voltage
delay(100); // Wait for display power stabilization
display.init();
display.setContrast(255);
display.setFont(ArialMT_Plain_16);
}


void VextON(void)
{
pinMode(Vext,OUTPUT);
digitalWrite(Vext, LOW);
}


void loop() {

//ultrasound
if (ultrasoundSerial.available()) {
uint8_t c = ultrasoundSerial.read();
//Serial.println(c, HEX);

// See if this is a header byte
if (idx == 0 && c == 0xFF) {
buffer[idx++] = c;
}
// Two middle bytes can be anything
else if ((idx == 1) || (idx == 2)) {
buffer[idx++] = c;
}
else if (idx == 3) {
uint8_t sum = 0;
sum = buffer[0] + buffer[1] + buffer[2];
if (sum == c) {
distance = ((uint16_t)buffer[1] << 8) | buffer[2];
newData = true;
}
idx = 0;
}
}
//Screen
if (newData) {
// Serial.print("Distance: ");
// Serial.print(distance);
// Serial.println(" mm");
Serial.println(distance);

display.clear();
display.setTextAlignment(TEXT_ALIGN_LEFT);

char distancestring[50];
sprintf(distancestring, "Dist: %d mm\n", distance);

// display.drawString(0, 0, "Dist: ");
display.drawString(0, 0, distancestring );
display.display();
newData = false;
}

//LoRa
if(lora_idle == true)
{
Serial.println("LoRa active");
delay(1000);
txNumber += 0.01;
sprintf(txpacket, "%d",distance); //start a package
// sprintf(txpacket,"Distance: %d",distance); //start a package

Serial.printf("\r\nsending packet \"%s\" , length %d\r\n",txpacket, strlen(txpacket));
Serial.println(txpacket);
Radio.Send( (uint8_t *)txpacket, strlen(txpacket) ); //send the package out
//Radio.Send(txpacket); //send the package out //must have two parameters
lora_idle = false;
}
Radio.IrqProcess( );

/*
uint8_t buf[9] = {0}; // An array that holds data
if (ultrasoundSerial.available() > 0) {
ultrasoundSerial.readBytes(buf, 9); // Read 9 bytes of data
if( buf[0] == 0x59 && buf[1] == 0x59)
{
uint16_t distance = buf[2] + buf[3] * 256;
uint16_t strength = buf[4] + buf[5] * 256;
int16_t temperature = buf[6] + buf[7] * 256;
Serial.print("Distance: ");
Serial.print(distance);
Serial.print(" cm, strength: ");
Serial.print(strength);
Serial.print(", temperature: ");
Serial.println(temperature / 8.0 - 256.0);

display.clear();
display.setTextAlignment(TEXT_ALIGN_LEFT);

char distancestring[50];
char strengthstring[50];
char temperaturestring[50];
float corrtemperature = temperature / 8.0 - 256.0;
sprintf(distancestring, "Dist: %d cm\n", distance);
sprintf(strengthstring, "Strength: %d \n", strength);
sprintf(temperaturestring, "Temp: %2.1f degC\n", corrtemperature);

// display.drawString(0, 0, "Dist: ");
display.drawString(0, 0, distancestring );
display.drawString(0, 20, strengthstring );
display.drawString(0, 40, temperaturestring );
display.display();
}
}
delay(50);
*/
}

//LoRa

void OnTxDone( void )
{
Serial.println("TX done......");
lora_idle = true;
}

void OnTxTimeout( void )
{
Radio.Sleep( );
Serial.println("TX Timeout......");
lora_idle = true;
}



Meshtastic Sender Settings

Settings -> Serial ->

Enabled

Echo doesn't seem do do anything?

Baud 9600

Timeout, is not an interval timer.

Mode Text Message

GPIO rxd pin 45 (Don't use pin 46, breaks bluetooth)

GPIO txd pin 42 (not used)


There doesn't seem to be any way to see outgoing messages.

Traps

Connecting to pin 46 breaks Bluetooth.

Booting with serial input into rxd pin 42 causes a boot hang

Use rdx pin 45 instead.

Access Directories With Sambashare + Integrating a Sonoff Zigbee Energy Monitor

File access and editing can be had via https://www.home-assistant.io/common-tasks/os/


As of Sept 2026 the Sonoff MINI-ZB1GP needs extra work to enjoy all features. Here's how.


Pair as a regular Zigbee device. Notice voltage etc. aren't displayed properly.

Add the python script provided on the Sonoff product page. https://help.sonoff.tech/docs/MINI-ZB1GP

This file goes in config as /config/custom_zha_quirks/mini_zb1gp.py

Notice the underscore is not a dash.

To access config, set up samba on Home Assistant and create a login/passw.

Then on linux mount to a folder by

sudo mkdir /mnt/samba_share
sudo mount -t cifs //server_name/share_name /mnt/samba_share -o username=your_username

Refer to https://linuxvox.com/blog/linux-connect-to-samba-share/

And https://github.com/noobosaurus-r3x/Cheat-sheets/blob/main/SMBClient%20Cheat%20Sheet.md

Remember after a reboot you need to restart sambashare at Settings > Apps > Sambashare (or set to start automatically after reboot)


Make a folder using something like

smbclient //192.168.1.10/config -U user%pass -c "mkdir custom_zha_quirks"


Then copy the above python script into the custom folder and name the file mini_zb1gp.py


Next, edit /config/configuration.yaml to include

zha:
database_path: /config/zigbee.db
enable_quirks: true
custom_quirks_path: /config/custom_zha_quirks


And that should do it. Restart and delete/re-add the MINI-ZB1GP device if necessary.

E-Reader Network Safety

Kobo firewall rule.png

Using an old e-reader as a display. In this case an old Kobo.

In OpenWRT set up a network rule so it can't reach out.


Probably something to prevent other things too.

Flipping a Graph

rightside up graph.png
rightside up graph options.png
rightside up graph math.png

Well water distance readings are backward in that a bigger distance to water means a smaller amount of water available.

To fix this edit the entity template(?)


(Optional) Create a new helper: Settings > Devices & Services > Helpers tab > Create Helper > Template > Number.


Click the entity that displays the sensor data.

Gear icon >> Template Options

Edit the code that calls the state

{{ states('sensor.heltecrec_welllevelraw') }}


This is text by default so to get math to work properly you need to change the data type with:

{{ states('sensor.heltecrec_welllevelraw')|float(0) *-1 }}


The | float(0) part converts the text to a number. The 0 is a fallback: if the conversion fails (maybe the sensor is offline), the template uses 0 instead of crashing.

See https://www.home-assistant.io/integrations/serial/#value_template-for-template-sensor