All code up at: https://github.com/leifdroms/tank-level-public
Microcontroller code in the /esp32 folder.
Intro
Having made myself $100 $200 poorer by signing up for a month of a Claude Pro subscription to start evaluating Claude code, it was time to really put it to the test. I had success “vibe coding” a simple app using ChatGPT to read input from a GPIO pin hooked up to a touchless liquid level sensor on an Esp32-WROOM-32 development board I had sitting around, and it was time to scale things up to do something really useful: build a touchless liquid tank level sensing system for my RV wastewater holding tanks that would communicate over Bluetooth to give readings to a companion phone app.
Background
Anyone that owns or has owned an RV knows the struggle of accurately reading their grey and black water holding tank sensors is real. For anyone that’s unfamiliar, most RV’s contain three tanks: one for fresh (potable) water to supply water to the faucets and shower while boondocking (camping away from a water faucet), a grey water tank to collect the wastewater from the sinks and shower, and a black water tank that directly connects to the toilet. While more sophisticated systems exist to properly read the tank levels, they’re not cheap, on the order of several hundred dollars, and I haven’t rolled the dice with them because their interface is stuck in the early 90s: LED and hardwired only, with one exception that offers a Bluetooth model, but users complain of “sensor drift” over time and other inaccuracies. Most of us RVers are stuck relying on a system to detect waste tank levels that’s archaic and unreliable: 4 metal probes are screwed into the sides of our waste holding tanks, that when the waste liquid level rises to meet the lowest probe and 1-3 of the probes mounted above it, a simple circuit is formed with the probe in the bottom of the tank, and 1-3 of the probes above it, and a display located in the living area of the RV will display whether the tank is “1/3”, “2/3”, or “Full” depending on which probes are connected together by the conductive liquid connecting them (wastewater).
Now why are these sensors notoriously unreliable, you ask? Well, if you have ever cleaned out a toilet or a sink drain, you are aware at how quickly a truly disgusting, and electrically conductive biofilm can form. Once that forms, or something else causes a “short circuit” like toilet paper bridging two sensors, you end up getting false readings. Ways to deal with this involve chemical treatments that aren’t cheap and not always effective to break up and release solids and films, or a series of “sprayers” you either put down the toilet attached to a garden hose through the window, or installed into the side of the holding tank, that hopefully clean things out and prevent false readings on the sensors.
I personally couldn’t stomach the idea of passing a nasty hose connected to a nasty sprayer through my living area and hosing out my black tank that way, so I installed a tank sprayer into the side of the tank. While it does seem to help a bit, my black tank still seems to be perpetually stuck at reading at least 2/3 full, even after I’ve gotten rid of everything in there. My grey tank doesn’t like to drop below a reading of 1/3 full, even if it is completely empty, due to the thick biofilm that has likely developed inside of there. I have completely replaced the sensors, twice, but quickly false readings crop up and we more or less judge when to dump the tanks based on personal experience of how many days and/or showers we can go before filling the tanks up. And then there’s the anxiety that we’ll overdo it one of these days and have nasty shower drain water (or worse) crop up into the RV.
So. Now that the reader knows more about RV wastewater than they ever intended, or if they are an RV-er themselves the rumblings of PTSD are starting to creep over their spine, we ask: “What if there was a nice “Do-It-Yourself” solution I could come up with, where I knew exactly how it worked, I had access to the source code because I wrote it, and if any part broke down, I could easily find a replacement one that wasn’t manufacturer-locked?” This solution would have to be a “non-contact” so any sensors would not get fouled up by waste and solids in the holding tank. Fortunately, “non-contact” sensors, already bundled into simple packages that either turn “ON” or “OFF” a wire when connected to a power source and can be easily read by a microcontroller like an ESP32, are cheap and plentiful on Amazon.com or Alibaba.com. Their operation is simple: a fluid like wastewater coming near the sensor changes the capacitance of the circuitry, which is easily detected and tripped. Three sensors per tank would give a resolution of “1/3”, “2/3”, or “Full”, which is adequate for the application; there’s no need to know that exactly 54.2323% of your grey water tank is full. Just a ballpark figure of when it’s time to move on.
Stumbling Blocks
Why not try and build one? Well, I’m no expert in C (or a variant), which is a necessity when working with microcontrollers, but, since we’ve entered the “vibe coding” area, why not give it a whirl? The phrase “liquid courage” is used to describe someone who feels much more confidant in their abilities after drinking a couple units of alcohol, and it’s high time that we recognize “vibe courage”, because it’s a thing. On this wave of vibe courage, I gave Claude code a ridiculously complicated prompt, using the Opus 4.1 model:
It thought for a while, and was happy to announce its progress:
Pretty cool. Let’s hope.
Ok, the initial folder structure looks ok:
But oh my, we’re getting the red squiggles in Vs Code:
And…it looks like nothing is set up as a properly configured ESP-IDF project that will actually build. Maybe it would have been a better idea to set up a separate project first using the ESP-IDF VS Code plugin and their “new project wizard”, but I digress. Here we are.
After some prompting and playing around, the file structure looks a little more realistic:
But sadly,
Still red squiggle city.
Well, ok then Claude Code.
Oops. Didn’t compile. Errors.
Yes, please, get rid of the unnecessary variables that are causing the compiler to not compile.
Fingers crossed…and it builds! And Flashes! Holy smokes, when I put my finger next to the sensor to trip it, seems to be working!:
But alas, still have those red squiggles. Hmmm.
After prodding Claude a bit and it making suggestions to some IDE configuration files, my squiggles are gone! I’m not thrilled with Claude Code’s approach of everything being in a nearly 700 line main.c file, so I asked Claude politely to backup main.c and do a refactor.
And we’re more organized, but oof looks like ble_gatt.c, the file necessary for setting up how the microcontroller sends its data over Bluetooth, is unhappy.
Prodding Claude several times on the command line to inquire about the line with the red squiggles.
Claude’s explanation of why seems to make sense:
But now it won’t build:
When asked why and what changed in the refactor:
I suppose Claude Code is more human than I thought, it even forgets to include headers when refactoring.
The build then failed, but after cleaning the build folder with the “Full Clean” command in the ESP-IDF extension, …we’re still failing. Oh. It had a naming conflict and named two functions “config”. Ok cool. Fixed that.
It built and uploaded to the ESP32! Hooray!
Now this wasn’t without issues. I installed a BLE Utility on my Mac (LightBlue), connected to the ESP32 over Bluetooth, and every time it tried to read the data it would throw an error. Prodded Claude a bunch, and it decided to abandon its approach of storing the sensor values in the Bluetooth stack database, and rather directly send them via a callback. Then it started working:
Which is slightly funny. I wonder why Claude decided to store readings, which changed all the time, in the database and then create a response calling on this data, and it responded it was slightly more efficient as far as the CPU goes. Not sure if this reasoning is accurate or a hallucination meant to please me, but the code is working now, I’m reading hex values over Bluetooth, and I’m feeling very happy.
Security
In my original prompt, I had told Claude I wanted an optional PIN number implemented. Of course, it didn’t do anything other than setting a password called “AUTH”. I decided that a PIN number shouldn’t be necessary to read values, since I wasn’t that concerned about some weird person going near my campsite with a Bluetooth sniffer and discerning how much human waste I had in my holding tanks, but since there are some real odd people that show up where RV’s can be found, that it would be a good idea to have a PIN in order to set configuration settings on the ESP32 (so far, the only settings in mind being whether or not to enable the bank of black or grey tank sensors, as well as setting/resetting the PIN itself). It took some wrangling but Claude and I eventually made it work.
Additionally, while the original version merely reported what the level of the tank was at (1/3, 2/3, or Full represented by the integers 1,2,3), I decided to report raw sensor data along with tank levels to the client app – this way I could add in code to detect an abnormal condition, like the “full” sensor was registering but the 2/3 level sensor was not, indicating a potential problem with the 2/3 sensor because on planet Earth with its gravity, the adage “s*** always runs downhill” holds true. In other words, it should be impossible for the “full” sensor to register without 1/3 and 2/3 both indicating their level as well.
Refinement
Lastly, instead of having to buy another momentary on/off switch in order to rig it to a GPIO pin in order to reset the PIN number (or do something like upload new firmware whenever I wanted to reset the PIN number), I decided to have Claude rig this to the “BOOT” switch on the ESP32 development board. It took some finagling and trial and error, but Claude eventually got the right GPIO pin, and I was left with a nice little working package where if a user held down the “BOOT” switch for 10 seconds it would reset the PIN code back to “000000”.
Oh, and now the README file was inaccurate. Fixed that.
You can lead an AI to a prompt, but you can’t force it to pick the right Bluetooth stack.
One last point of contention – it looks like Claude ended up using the Bluedroid stack to implement Bluetooth, rather than the smaller and more lightweight “NimBLE” stack, but I don’t really care. The package compiles, it fits onto my development board’s memory, and seems to run great.
I went through everything and while I am no expert in C, it looked fine, and all variables and pointers were on the stack rather than the heap so I didn’t have to be concerned about long-term heap corruption after I let it run for months or years on end off my RV batteries.
Euphoria
At this point, I am ecstatic. I have a fairly professional little ESP32 package that reads tank sensors and reports them over Bluetooth. Even if I didn’t develop the phone companion app, I could still read tank levels using a Bluetooth sniffer and interpreting the hex string sent to Bluetooth to indicate tank levels. All in a nice, neat DIY package where I know exactly how it works and can fix on the road as needed.
Our little mobile home on wheels now allows us to function just a little bit more like we do at home, without guessing our wastewater levels.
I feel…human again. Thanks to a happy robot in the cloud named Claude.
Now in the next blog entry, on to the React-Native companion app...
Top comments (0)