RC Submarine 4.0 – laser distance sensor (6/10)

While testing the pressure sensor, I started thinking is there other ways to measure depth underwater. Sonar? I could reflect sonar waves from the surface to measure depth. I tried to look for parts but couldn’t find any inexpensive sonars. There were ultrasonic distance sensors available, but would they work underwater, through the hull walls, and with directionality? Probably not.

But a laser distance sensor might work. It uses electromagnetic radiation, like our eyes. As long as the water is clear enough to see through, the laser sensor can also, right? The hull is made of transparent acrylic plastic, therefore the sensor doesn’t even have to be waterproof, you can just shoot laser through the walls. But it probably cannot measure depth, as the water surface doesn’t reflect light when shot perpendicular. I have to just measure distance to the bottom. That could still be useful when moving close to the bottom of a lake. I picture in my mind a time lapse video of a submarine speeding through water as it automatically lowers and raises to keep close to the bottom. Kind of like the flying scenes from the beginning of Star Wars V: The Empire Strikes Back.

The first laser sensor

The first laser distance sensor I bought was Whadda WPSE337. It uses the STMicroelectronics VL53L0X Time-of-Flight module. It cost 18 euros in a Finnish retail shop, link: https://www.radioduo.fi/p/p/WPSE337/.

The sensor is very small and it communicates through I2C. Range is 30-2000 mm. The purpose is to keep the submarine in about 20 cm distance to the bottom, so 2 meters would be enough. Sampling time is 30 ms, that means about 33 samples/s, which is not great but maybe enough for PID control. Accuracy is +/- 3 % which would be 0.6 cm at 20 cm distance. That would be enough.

I installed the sensor against the bottom of the hull using Lego parts and a small rubber band.

Laser distance sensor pressed against the bottom of the hull. The hull is upside down in this picture.

The sensor has four wires: GND, Vin (3.3V), SDA, SCL. After connecting the wires to Raspberry, I downloaded a Python library for the sensor: https://github.com/johnbryanmoore/VL53L0X_rasp_python

I wrote a simple program to test the sensor. I selected “high accuracy” ranging profile that has a timing budget of 200 ms. Light travels slower in water, so compensation was needed.

I tested the sensor in a jar filled with water. The hull was pressed against the water surface so that there was no air in between plastic and water. I lowered a Lego plate in to the water and started moving it up and down to try different distances.

At first the data looked accurate. Distance was correct when compared to the measuring tape next to the container. When the plate was pressed against the hull, having less than 1 cm distance to the sensor, it gave 2-3 cm readings, which was too much. But that was expected as the range minimum is 3 cm. But then when I lowered the plate to 15 cm, I noticed the readings stayed at 12 cm.

I tried again with a 35 cm deep water container to verify the problem: max distance is 12 centimeters. Disappointing. What was worse, the sensor didn’t stop giving data, but instead it gave wrong data. It gave random values between 6-12 cm when the actual distance was more than 12 cm. That basically made all data is unusable.

Wavelength affects water absorption

Why is 2 meter range limited to 12 cm underwater? That baffled me. The water I tested against was crystal clear and I could see through it easily. Then I got an idea. The laser uses an infrared wavelength of 940 nm. Our eyes see a range of 400-700 nm. That is the only difference I could think of.

I looked up water absorption spectrum curves online. The absorption coefficient does climb very steeply after leaving the visible spectrum at 700 nm. It is about 0.6 at 700 nm and increases to 40 at 940 nm (unit 1/m). So almost 100 times increase in absorption. No wonder the sensor range is limited. Below are the curves I looked at. Note that both have logarithmic scale on y-axis.

Source: Kebes, Wikipedia.
Source: Zhun310, Wikipedia.

Based on the curves, visible light 400-700 nm or ultraviolet light between 300-400 nm would be best for underwater measurements. Unfortunately, I couldn’t find any sensors in that range. But I found one sensor that uses 850 nm wavelength. The absorption coefficient for that wavelength is about 5. That is almost 10 times better than the first sensor. Let’s try that.

The second laser sensor

The second laser distance sensor I bought was Sparkfun TFMini-S Micro. It cost 55 euros, link: https://www.elfadistrelec.fi/fi/p/30216198.

As mentioned, this laser has a wavelength of 850 nm, which should provide 10 times less absorption compared to the previous sensor. Range is max 12 meters, a hefty increase from the previous sensor. Range minimum is 10 cm, which might be a problem, as the plan is to keep submarine distance to 10 or 20 cm from bottom. Frame rate is stated to be 100 samples/s in this datasheet table, but it actually can go up to 1000 samples/s, so improvement there. Accuracy is 6 cm, a little bit lower than the previous sensor. Have to see how that works with PID control.

Power consumption has increased considerably. The previous sensor consumed 10 mA but this sensor can draw up to 140 mA in average based on the specs. Actually, I measured the current flow myself, and it is 93 mA when pointing the sensor at a far away object or black paper or something other difficult, and drops to 50 mA when measuring close distance. It seems to have those two states it switches between automatically. I didn’t find any commands that would limit the power consumption, even lowering frame rate has no effect, except for 1 Hz frame rate that puts it into a 13 mA low power mode.

High current might bring problems with the Lego battery box. It can output max 1A, and it has many other components to supply. I have to keep an eye on that. Also, the 3v3 output pin on Raspberry Pi Zero 2 is rated for only 50 mA, but luckily this sensor uses 5V input, so I can power it directly from a 5V regulator. The regulator is rated for 1A.

Field-of-view is only 2.2 degrees, much less from 25 degrees in the first sensor. That brings me to another potential problem. This sensor is fairly big compared to the first one, and the emitter and receiver are 10 mm apart. If you lay the sensor sideways to the direction of the hull, the curvature of the hull will cause refraction when laser beam moves from air to plastic and from plastic to water. Actually, I did some quick calculations with the refraction formula and, if I’m correct, a laser beam that leaves the emitter straight will come back to the receiver at a 3.5 degree angle. That would be too much for the 2.2 degree FOV. Have to test that.

The sensor has four wires: GND, 5V, RXD/SDA and TXD/SCL. The package included two wires to connect the strange plug this sensor has. The first thing I had to do is cut one of the wires and solder female DuPont connectors to it. Then I could connect it to Raspberry.

The sensor supports both I2C and UART Serial communication. I first tried to use I2C, but the sensor stayed silent. Looks like UART mode is on by default and you need UART to change the mode. Luckily I have UART pins on Raspberry. I connected those and got a response.

I read the specs and looked up examples online to figure out how to communicate with the sensor. Pretty simple otherwise, but the command checksums were annoying to calculate by hand. Output data includes distance, signal strength and temperature.

from serial import Serial

#prepare laser distance sensor
serial = Serial("/dev/serial0", 115200, timeout=0.01)
serial.write([0x5A,0x05,0x05,0x06,0x6A])  #set unit to mm
serial.write([0x5A,0x06,0x03,0x64,0x00,0x39])  #set frame rate to 100 Hz
serial.write([0x5A,0x09,0x3B,0x00,0x00,0x00,0x00,0x00,0x62])  #set I/O mode to standard

def readLaserSensor():
    #skip old serial data
    FRAME_SIZE = 9
    while (serial.inWaiting() > FRAME_SIZE):

    #read data
    data = serial.read(FRAME_SIZE)
    #parse data
    distance = float('nan')
    strength = float('nan')
    temperature = float('nan')
    if len(data) == FRAME_SIZE and data[0] == 0x59 and data[1] == 0x59:
        checksum = 0
        for i in range(0, FRAME_SIZE - 1):
            checksum = checksum + data[i]
        checksum = checksum & 0xFF
        if checksum == data[FRAME_SIZE - 1]:
            distance = data[2] | data[3] << 8  #[mm]
            strength = data[4] | data[5] << 8  #[0-65536]
            temperature = data[6] | data[7] << 8
            distance = distance / 1000   #[m]
            temperature = temperature / 8 - 256  #[C]
    return distance, strength, temperature

Here is the adjustment for water.

#scale for water
SPEED_IN_AIR = 299000  #[km/s]
SPEED_IN_WATER = 225000  #[km/s]

While testing close distances through water, I noticed a systematic error: 18 cm is actually 20 cm, 10=15, 5=10, 2=5, 0=0. So I added a little compensation formula for close distances.

#fix laser data for close distance (when sensor says 0.05m it is actually 0.10m)
laserDistanceFixed = laserDistance
if laserDistanceFixed < 0.25:
    laserDistanceFixed += 0.5 * (0.25 - laserDistanceFixed) * \
                          math.sqrt(laserDistanceFixed / 0.25)

Later when I tested the submarine in nature, I started getting a lot of unreliable data. Of course, a muddy river floor will not reflect the laser well. The signal strength seemed to be below 300 when the data was very noisy.

The signal strength is a number between 0-65535, higher better. The spec says lower than 100 is unreliable and the sensor will send -1 as distance. Well, the value I received was 49.316 meters, don’t know where that comes from, and it may come even when strength is a little bit larger than 100. Whatever, I just use both strength and distance to omit bad values. In case of bad data, I will pass old data to the PID controller for max 1 second. After 1 sec I will give NaN to indicate no distance can be found.

#omit unreliable laser data
if laserStrength < 300 or laserDistance > 10:
    laserDistanceFixed = float('nan')
    if (perf_counter_ns() - laserDistanceValidTime) / 1e9 >= 1.0:
        laserDistanceFiltered = float('nan')
    if math.isnan(laserDistanceFiltered):
        laserDistanceFiltered = laserDistanceFixed
    laserDistanceValidTime = perf_counter_ns()

Lastly a filter. I chose 0.95 ratio instead of 0.90 as the laser is noisier than the pressure sensor.

#filter laser distance (exponential moving average)
if not math.isnan(laserDistanceFixed):
    laserDistanceFiltered = 0.05 * laserDistanceFixed + 0.95 * laserDistanceFiltered

I tested it in a water container, similar way to the first sensor, using a red Lego plate to change distance.

Tests were done for both sideways orientation and lengthwise orientation. The Lego plate was measured at four distances: 33 cm, 20 cm, 10 cm, and 1 cm. As you see in the graphs below, sideways orientation does give correct readings but the data is noisier and signal strength is below 2000. In lengthwise orientation strength is much higher between 1000 and 20000. Interestingly, strength is higher in 33 cm distance than in 20 cm distance, no idea why, maybe the sensor switched to high power mode. No filter was used in these tests, the data is raw, only corrected for water and close distances.

So, refraction through the curved hull does have an effect. Less than I expected based on my calculations, but still. The sensor orientation needs to be lengthwise. I first thought that would cause a problem pushing the sensor too far to the tungsten pellet area, but it was fine. I made a little support construct from Lego parts to hold the sensor still.

The hull is upside down in this picture.

Later I tested the submarine in a swimming pool that is 1.2 meters deep and the floor is painted with bluish white/gray paint that reflects light fairly well. Water is clear with visibility to maybe 10 meters. Below is a graph that shows pressure sensor and laser data on top of each other. The swimming pool floor is even with only slight tilt, therefore depth and distance to bottom should be relative to each other. From that graph we can see that the maximum distance is 50 cm. Above that the graph doesn’t show lines, as the measured value is NaN.

And here are tests in a small river, about 30 cm deep, and it has a brown muddy bottom with branches and rocks laying around. Water is dirty with visibility to maybe 3 meters. As you see, the data is very patchy and not really usable for control, even though distances are quite small.

In the end, I didn’t use the laser much while playing with the submarine. The data was too unreliable for that.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s