<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Leif</title>
    <description>The latest articles on Forem by Leif (@mergewithcare).</description>
    <link>https://forem.com/mergewithcare</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3417594%2Ff6888399-24e4-4669-90fc-9faa61894339.jpeg</url>
      <title>Forem: Leif</title>
      <link>https://forem.com/mergewithcare</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mergewithcare"/>
    <language>en</language>
    <item>
      <title>DIY Holding Tank Sensors Part 2: "The React Native App"</title>
      <dc:creator>Leif</dc:creator>
      <pubDate>Fri, 03 Oct 2025 22:25:00 +0000</pubDate>
      <link>https://forem.com/mergewithcare/diy-holding-tank-sensors-part-2-the-react-native-app-496n</link>
      <guid>https://forem.com/mergewithcare/diy-holding-tank-sensors-part-2-the-react-native-app-496n</guid>
      <description>&lt;p&gt;In my previous article, &lt;a href="https://dev.to/mergewithcare/diy-holding-tank-sensors-part-1-or-mommy-the-ai-made-me-code-in-c-4llg"&gt;https://dev.to/mergewithcare/diy-holding-tank-sensors-part-1-or-mommy-the-ai-made-me-code-in-c-4llg&lt;/a&gt; , I went over the process of "vibe coding" a working ESP32 app in C using the Espressif toolchain. &lt;/p&gt;

&lt;p&gt;I'm going to go over the journey of "vibe coding" out a React Native app to read values from the sensors on the microcontroller as described in Part 1, and as always my code is up at &lt;a href="https://github.com/leifdroms/tank-level-public" rel="noopener noreferrer"&gt;https://github.com/leifdroms/tank-level-public&lt;/a&gt; for anyone that so desires to download it themselves.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ce03a1fhnfru0riqyeu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ce03a1fhnfru0riqyeu.png" alt=" " width="800" height="116"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are off to a rocky start.&lt;/p&gt;

&lt;p&gt;Claude thinks our React Native project structure is incorrect, and asks us to eject the project. I don’t want to do that- I want to build it with the latest snazzy version of Expo, the framework that sits on top of React Native to make things easier. It suggests that we install the package expo-dev-client, and work from there, which I agree with because a limitation of using React Native with Expo Go: Expo Go on its own only allows you to work with native modules included with Expo Go. Unfortunately, a Bluetooth client is not one of them.&lt;/p&gt;

&lt;p&gt;…And unfortunately things did not work out.&lt;/p&gt;

&lt;p&gt;Each time my bundler would crash, Claude would take some feedback and output something like the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9hhftrgt4fteei8ug30o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9hhftrgt4fteei8ug30o.png" alt=" " width="800" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There came a point where I stopped auditing each change it made, crossed my fingers, and hoped that it would all sort itself out. This message was the result of the culmination of several changes Claude made without explicitly approving each one as I had been doing for the microcontroller app section of the project:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mji3f56ins5dvcorpmg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mji3f56ins5dvcorpmg.png" alt=" " width="428" height="826"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It was to no avail.&lt;/p&gt;

&lt;p&gt;Why are files in my node_modules folder corrupted?? Something isn’t right. Maybe Claude should stick to its “day job” and not try to replicate the function of command line tooling to bootstrap projects, when that tooling is available, mature, and free. I think to myself to pivot at this point, start over from scratch with a proper React Native project, and have Claude do what it does best – work from there. I will take a peek at the app structure as we have it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn2z0bxygb8lrbxodnja4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn2z0bxygb8lrbxodnja4.png" alt=" " width="800" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And again we have one giant application file that’s about 700 lines of code long as well:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywb9o343xk4afbuei43c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywb9o343xk4afbuei43c.png" alt=" " width="420" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have finally hit a point where I’m putting on my judgement and deciding to get things done “the traditional way” and bootstrap the app in the traditional means rather than hoping an LLM will replicate this on its own. Maybe someday when context windows and training data is much better, but for now I can feel myself getting into an AI-driven “coding death spiral” and it’s important to notice when you’re going in circles and to pull yourself out. Sorry Claude, I’ll be back to you in a moment.&lt;br&gt;
And now we’re back to sanity, after setting up Expo and the dev client the “normal” way on the command line:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fad7pj3u0wd58ez2w9gos.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fad7pj3u0wd58ez2w9gos.png" alt=" " width="450" height="894"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ok. Let’s let Claude do some of its automagic. I’m in a salty mood while writing this blog entry, so I’m going to make Claude earn its keep today. Before I created a new app with the official command line utilities, I changed the old app’s folder name to mobile-app-backup. Opening up the new app folder and a new Claude instance in the VS Code terminal, the “annoyed Dad” side of me comes out:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fruhsvcysrmh8vbx9ros4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fruhsvcysrmh8vbx9ros4.png" alt=" " width="800" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At least Claude can pretend to understand my feelings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfyjcyltouv6091wlt6z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfyjcyltouv6091wlt6z.png" alt=" " width="800" height="75"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dedbr62850xweih0zaf.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dedbr62850xweih0zaf.jpeg" alt=" " width="602" height="1306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hmm doesn’t look bad after some iteration, although we don’t need the “Explore” tab anymore that came with Expo. Will get rid of that in the next iteration, and probably drop the "tank history" view because it's really not that important.&lt;/p&gt;

&lt;p&gt;Which brings up an interesting point - why did Claude put that in there? It just sort of hallucinated a product requirement, and now I have the associated cruft along with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flwqboqz33kdhd2r1qfq5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flwqboqz33kdhd2r1qfq5.png" alt=" " width="770" height="68"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Oh no! We can’t connect to Bluetooth!&lt;/p&gt;

&lt;p&gt;And unfortunately Claude wasn’t able to get it spinning after a few more prompts. Time for me to get my hands dirty on my own. &lt;/p&gt;

&lt;p&gt;After some juggling and coaxing, we’re connected! We have an app that shows tank levels and changes based on sensor input!&lt;/p&gt;

&lt;p&gt;But now we have even more questions, like - &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Why is it asking me to authenticate with a pin at the beginning, but this seems to do absolutely nothing?!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Why can’t I set an “admin” pin?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Why can’t I change configuration settings on the ESP32 (which was the whole point of an admin pin in the first place)?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thus we are really starting to see the pitfalls of “vibe coding”. This thing, while completely, wholly impressive and saving me days or weeks, will NOT crank out a production-ready app just by waving my fingers and making a wish.&lt;/p&gt;

&lt;p&gt;Let's focus on the bright side though, and maybe I'm being more critical of the phone app/"front end", because that's been the direction my career has taken the past few years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Mid-way Conclusions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I was able to go from zero to a working microcontroller and companion phone app that would serve my purposes well in about 2-3 days. This is insane and my mind is still a little blown. The initial product WAS pretty rough and nowhere near production grade, and required quite a bit of human intervention, but I was particularly impressed with how AI was able to handle the embedded/C code. This was a huge weight off my shoulders since it is out of my usual comfort zone, although upon inspection it appears fine without memory leaks. Like my other “vibe coding” project on my blog thus far, the overall risk surface area is pretty low and in fact lower for this project since there’s no exchange of secret keys other than a PIN number over Bluetooth, and this would require a hacker being within about 30-90 feet of the RV while the PIN was being sent sniffing the Bluetooth traffic somehow, just to have the privilege of turning off tank monitoring for that particular bank of tank sensors (grey, black, or both). The obvious way to mitigate this risk would be to set the pin making sure that nobody is within about 30-90 feet of you while setting the PIN. &lt;/p&gt;

&lt;p&gt;In a future iteration I may implement encrypted Bluetooth traffic, but for now it’s just fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;And On To Encryption&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
I declared the future to be &lt;em&gt;NOW&lt;/em&gt; and that I was going to implement encryption, so we're going to journey back for a moment into microcontroller land in C. &lt;/p&gt;

&lt;p&gt;Claude gave me several options as far as implementing encryption, and I picked #1, “MITM” (“Man In The Middle”) protection, being the most secure. Changes were made and I rebuilt the app, but when connecting on my mobile app…there was no dialog that popped up for pairing. I asked Claude about this and:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3c1p22qzvfg8yte2l4pa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3c1p22qzvfg8yte2l4pa.png" alt=" " width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now. At the end of the day, this would only be a problem in my app if someone nearby sniffed my Bluetooth traffic to get my admin PIN, and then was able to turn off my tank sensors, which I trust completely at this point, and then allow my sewage tank to overflow leaking human waste everywhere. Ok, that probably wouldn’t actually happen since it would be apparent I’m reaching the top of the tank when using the restroom, but it’s an unpleasant thought.&lt;/p&gt;

&lt;p&gt;Point I’m making, is if I was working on an application where I was truly concerned about security, I’d be really, really concerned right now! Claude, we have some trust issues to work on.&lt;/p&gt;

&lt;p&gt;Now, I’m feeling lazy again, so let’s go back to React Native land and see how much of this we can vibe code, starting with refactoring the app to use a global context with a reducer function rather than having state management all reside in index.tsx and be dependent on that component (or at least through prop drilling which isn’t how the app is setup).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsq3dj4lglirquvxrwgwy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsq3dj4lglirquvxrwgwy.png" alt=" " width="598" height="1296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;WHAT THE HECK HOMEY! AHHH! LAST TIME I TRUST YOU TO IMPLEMENT A GLOBAL CONTEXT WITHOUT ADULT SUPERVISION!&lt;/p&gt;

&lt;p&gt;In fact, maybe it’s time we got a second opinion. On a suggestion from someone else, I installed Codex, OpenAI’s answer to Claude Code, fired it up in my IDE, and posed a question:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fddqxwe61uxq7uwrgexwp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fddqxwe61uxq7uwrgexwp.png" alt=" " width="800" height="822"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5s04ra4rylgz1ahx10ea.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5s04ra4rylgz1ahx10ea.png" alt=" " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Well goll-ey. Doesn’t seem like bad advice at all.&lt;/p&gt;

&lt;p&gt;Going back to Claude Code, and purposely introducing it with a hostile tone (per someone’s suggestion), I asked Claude Code:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv4k6lch9kv7yillh9bp2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv4k6lch9kv7yillh9bp2.png" alt=" " width="800" height="82"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude disagreed with me. At least I know now it will give me honesty, even when I need to hear it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjahr09nlgpyvayh250rg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjahr09nlgpyvayh250rg.png" alt=" " width="800" height="51"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And one more prompt just to be sure it wasn’t a fluke:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fss8qnonzrn6ce0ng0x0b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fss8qnonzrn6ce0ng0x0b.png" alt=" " width="800" height="37"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To which Claude Code replied:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqjnrmmo2fv1o1oq73m2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqjnrmmo2fv1o1oq73m2.png" alt=" " width="800" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And Codex isn’t budging:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6jd6cu6l86yw0vfpv0l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6jd6cu6l86yw0vfpv0l.png" alt=" " width="800" height="649"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And Claude Code backtracks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxzdzaswtfrxd6iziy88.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxzdzaswtfrxd6iziy88.png" alt=" " width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let’s examine reality, shall we:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Futjz6p7olevppyra8zhr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Futjz6p7olevppyra8zhr.png" alt=" " width="800" height="56"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9ew6uf2a6yo5b4402hj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9ew6uf2a6yo5b4402hj.png" alt=" " width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4w322zlqse2xg6ptxur1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4w322zlqse2xg6ptxur1.png" alt=" " width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Indeed. Claude Code was complaining about nothing, and “subscription” is provided as a ref in the global context.&lt;/p&gt;

&lt;p&gt;Codex, you get to refactor everything. &lt;/p&gt;

&lt;p&gt;Now will you suggest more when running on the command line rather than in the sandbox? Indeed!:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft97o83ddbwb1g98wztta.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft97o83ddbwb1g98wztta.png" alt=" " width="800" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Well. That’s a mouthful. But go for it. I agree, we shouldn’t duplicate the functionality of our code base if we can avoid it.&lt;/p&gt;

&lt;p&gt;…15 minutes later, it finished. This seems a lot slower than Claude Code, but whatever. No red errors popping out except for one- that’s a good sign:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk292vr4rpy6lqifo5065.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk292vr4rpy6lqifo5065.png" alt=" " width="796" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just a little Typescript boo-boo! All better now:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F44t7gnz2ydqocoms55vm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F44t7gnz2ydqocoms55vm.png" alt=" " width="800" height="55"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Things seem ok for now, so why not write some tests?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2l2w9gq0zf688vesbz3b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2l2w9gq0zf688vesbz3b.png" alt=" " width="800" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Oh no they’re failing! And failing! And each time I prompt Codex to fix it, it tries to catch the error when maybe we should be asking it a question:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuhbz5m7vnv9mv4n7ju5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuhbz5m7vnv9mv4n7ju5d.png" alt=" " width="800" height="143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvykfoh9t1jzevw3n1zpp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvykfoh9t1jzevw3n1zpp.png" alt=" " width="800" height="66"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu28gg2pqihjs3ph24r6a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu28gg2pqihjs3ph24r6a.png" alt=" " width="800" height="47"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Well, better:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqyu1b8f7b1ytem72vwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqyu1b8f7b1ytem72vwu.png" alt=" " width="684" height="136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But we’re still not having passing tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9xtsdv77q77j9tgrnjv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9xtsdv77q77j9tgrnjv.png" alt=" " width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hmm. We seem to be having a memory leak when mocking these components.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6tblkqhaibhhn14ombn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6tblkqhaibhhn14ombn.png" alt=" " width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3jcgaj5qqmh2ot5mirb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3jcgaj5qqmh2ot5mirb.png" alt=" " width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Welp, let’s hope the AI isn’t just trying to be agreeable. &lt;/p&gt;

&lt;p&gt;But we’re still failing tests. Maybe it’s time to close the window and create a new one with ample context.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft1uw7owpaze3lynbxs2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft1uw7owpaze3lynbxs2e.png" alt=" " width="534" height="166"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And now we’re passing, hooray!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Conclusions and Final Thoughts&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What grand wisdom have I gleaned from this…thinking…thinking…&lt;/p&gt;

&lt;p&gt;I wish I had code snippets and proof for all of my thoughts here, and maybe I will go through that effort in future blog posts (if anyone cares), but I decided I wasn’t going to be so methodical on a personal project. In that regard, I apologize for lack of objective proof for any statements going forward, but convince me it’s worth my time and I will do so in the future.&lt;/p&gt;

&lt;p&gt;As I sit here and gather my thoughts on the experience of creating an embedded app for a microcontroller in C that relies on Bluetooth to communicate with a React Native phone app within a couple weeks time, part-time (!!! YEAH!), I feel it’s necessary to highlight just how COOL this is. &lt;/p&gt;

&lt;p&gt;If I had to judge based on the output, what I was able to do with my time probably would’ve taken a team of developers about 3-4 weeks, so I’m thinking roughly two standard sprints worth of time, to accomplish.&lt;/p&gt;

&lt;p&gt;The code itself probably would have been functional but a lot rougher; there wouldn’t have been such neatly memoized functions and abstracted classes and libraries and so on.&lt;/p&gt;

&lt;p&gt;Security for the Bluetooth would have been much simpler.&lt;/p&gt;

&lt;p&gt;In other words, the Ais supercharged my output as one single developer and allowed me to come up with gorgeous code closer to the speed of my thoughts and wishes, as a sole developer, rather than at the speed of programming things by hand. This is assuming familiarity with the various components that I had zero familiarity to begin with, such as programming with the Bluetooth stack, which the AI shaved off several days or weeks for me in pouring through documentation and deciding which parts were relevant.&lt;/p&gt;

&lt;p&gt;At the end of the day though, it still took ME, a “real human”, to stand over the AI generated code and come up with a judgement of whether it smelled right, whether it was overengineered for the application (A DIY hobbyist liquid level sensor where there is no risk to life or finances if it doesn’t work properly), and whether it actually did what it intended.&lt;/p&gt;

&lt;p&gt;A lot of it was wrong. A lot of it was unnecessary – I don’t need to know what the readings of the sensor were 5 minutes ago or at 3 am while we were sleeping vs. now; I just need to know if I can use the sink or the restroom without overfilling my tanks &lt;em&gt;when I use the sinks or the restroom&lt;/em&gt;. For some reason Claude Code decided this was necessary and threw it in there, and it was up to me to take it out.&lt;/p&gt;

&lt;p&gt;I was thoroughly impressed with Codex’s ability to analyze documentation and create clean, elegant, testable and readable code, but also found it creating code that looked wonderful, but actually failed in its objective at optimization: there was one spot where it memoized a function to only run when the black or grey tank sensor levels changed, which should save a re-render if non-UI related state variables change independent of this, but it was constructed in a way that it would still re-render on every state change…because it ingested the entire state (rather than just those two values). In addition, the memoization itself added a small performance penalty, so in essence…the performance was probably negligibly worse.&lt;/p&gt;

&lt;p&gt;I do give lots of credit for Codex for thinking to do a memoization in the first place, which if I’m being honest, I probably would have skipped, because this is a personal project I’m not getting paid for, I’m not trying to actively maintain it for 10 years, I’m not passing it to other developers, and I have a vested interest in getting out “something that just works while I’m on the road” and don’t personally care if there’s a slight inefficiency in renders, for example. &lt;/p&gt;

&lt;p&gt;I think this is the biggest value add of AI going forward though – the excuses to NOT push clean, elegant, and efficient code, going forward, are going to be wearing extremely thin. Used with care rather than blindly trusting the output of the LLM, we are entering an era where the gap between publishing really gorgeous code vs. “do it dirty and on time” is becoming very narrow. &lt;/p&gt;

&lt;p&gt;I ridiculed “vibe coding” at first because it threatened me, I still ridicule it when the term means people pushing code to production that hasn’t been vetted and is the result of a non-coder beating up LLM prompts until it “looks right”, but now I find myself very optimistic going forward that AI will allow me to accomplish what I never have been able to on my own: to create things, really, really useful things, closer to the speed of my thoughts and creativity. &lt;/p&gt;

&lt;p&gt;All of the boxes of unopened crap I have in my garage, the piles of microcontrollers and electrical components and this and that and the other that I bought once thinking I’d find the time to learn how to use it, might actually be put to use going forward.&lt;/p&gt;

&lt;p&gt;I’m ecstatic and I can only hope my thus-far extremely patient wife will be so as I empty out the garage and actually make use of the things collecting dust in there.&lt;/p&gt;

&lt;p&gt;Regarding which tool is the “best” right now, I can say that I will be likely reaching for Claude Code when I want it to accomplish the “quick and dirty” and test a feasibility of an idea, and Codex when I really want to hone something down and consider multiple angles. &lt;/p&gt;

&lt;p&gt;This might already be outdated advice to myself, because people are hyping Claude Code’s model that was released a few days ago, and I greatly look forward to seeing how THAT compares to Codex! I just have to make sure no matter which tool I use, that if I find myself and the AI “going in circles”, to close the current context window and reopen a new one, and pray that this takes us out of an AI-driven "coding death spiral".&lt;/p&gt;

&lt;p&gt;If you got this far, I strongly encourage you to download the code posted in the repo at the top of the blog entry, and let me know if you love it, hate it, or are indifferent. I’m looking forward to people tearing it to pieces so please do your worst.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>vibecoding</category>
      <category>rv</category>
    </item>
    <item>
      <title>DIY Holding Tank Sensors Part 1: Or "Mommy, the AI made me code in C"</title>
      <dc:creator>Leif</dc:creator>
      <pubDate>Wed, 01 Oct 2025 01:04:40 +0000</pubDate>
      <link>https://forem.com/mergewithcare/diy-holding-tank-sensors-part-1-or-mommy-the-ai-made-me-code-in-c-4llg</link>
      <guid>https://forem.com/mergewithcare/diy-holding-tank-sensors-part-1-or-mommy-the-ai-made-me-code-in-c-4llg</guid>
      <description>&lt;p&gt;All code up at: &lt;a href="https://github.com/leifdroms/tank-level-public" rel="noopener noreferrer"&gt;https://github.com/leifdroms/tank-level-public&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Microcontroller code in the /esp32 folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intro&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Having made myself &lt;del&gt;$100&lt;/del&gt; $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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Background&lt;/strong&gt;&lt;br&gt;
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).&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stumbling Blocks&lt;/strong&gt;&lt;br&gt;
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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faljk7cnvjrr0s6b8b9v3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faljk7cnvjrr0s6b8b9v3.png" alt=" " width="800" height="644"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It thought for a while, and was happy to announce its progress:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frghrf9iiv2attnh97q2s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frghrf9iiv2attnh97q2s.png" alt=" " width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pretty cool. Let’s hope.&lt;/p&gt;

&lt;p&gt;Ok, the initial folder structure looks ok:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzd8fcwm65folowifa5r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzd8fcwm65folowifa5r.png" alt=" " width="636" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But oh my, we’re getting the red squiggles in Vs Code:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1qkz6ggd3zhhw7a1y25c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1qkz6ggd3zhhw7a1y25c.png" alt=" " width="704" height="682"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk51ifmv0fg6hou3ga2qh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk51ifmv0fg6hou3ga2qh.png" alt=" " width="800" height="73"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Famgmt675zz6m8r7m95d4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Famgmt675zz6m8r7m95d4.png" alt=" " width="468" height="50"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After some prompting and playing around, the file structure looks a little more realistic:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fraxtr1gsnodpv7qrqoun.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fraxtr1gsnodpv7qrqoun.png" alt=" " width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But sadly, &lt;br&gt;
Still red squiggle city.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6m0yre22fw7b5mn5d806.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6m0yre22fw7b5mn5d806.png" alt=" " width="686" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz8yy2z3qwas32gl9wbg4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz8yy2z3qwas32gl9wbg4.png" alt=" " width="800" height="56"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuece71fo10n7qc7umvuc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuece71fo10n7qc7umvuc.png" alt=" " width="800" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Well, ok then Claude Code.&lt;/p&gt;

&lt;p&gt;Oops. Didn’t compile. Errors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhrhyq7bryt0cd3odxpbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhrhyq7bryt0cd3odxpbi.png" alt=" " width="800" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yes, please, get rid of the unnecessary variables that are causing the compiler to not compile. &lt;/p&gt;

&lt;p&gt;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!:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnqsuy5u9yl7s3we4a3pk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnqsuy5u9yl7s3we4a3pk.png" alt=" " width="616" height="62"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But alas, still have those red squiggles. Hmmm.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1qejgk2yjhlkv6u9vx1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1qejgk2yjhlkv6u9vx1.png" alt=" " width="800" height="721"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Prodding Claude several times on the command line to inquire about the line with the red squiggles.&lt;/p&gt;

&lt;p&gt;Claude’s explanation of why seems to make sense:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcsup0gdfe51zys3kw3p9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcsup0gdfe51zys3kw3p9.png" alt=" " width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But now it won’t build:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F707rlr0ygwgj83h4760h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F707rlr0ygwgj83h4760h.png" alt=" " width="800" height="114"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When asked why and what changed in the refactor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9lznf77tnezc17h81dg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9lznf77tnezc17h81dg.png" alt=" " width="800" height="42"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftm28x2ogsstg3zalwe6w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftm28x2ogsstg3zalwe6w.png" alt=" " width="800" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I suppose Claude Code is more human than I thought, it even forgets to include headers when refactoring.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It built and uploaded to the ESP32! Hooray! &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs4mhaavsc9ft5cz8ma2z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs4mhaavsc9ft5cz8ma2z.png" alt=" " width="800" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo2tf3wjtbl4wy39naqet.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo2tf3wjtbl4wy39naqet.png" alt=" " width="432" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;br&gt;
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. &lt;br&gt;
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. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refinement&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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”. &lt;/p&gt;

&lt;p&gt;Oh, and now the README file was inaccurate. Fixed that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can lead an AI to a prompt, but you can’t force it to pick the right Bluetooth stack.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Euphoria&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;I feel…human again. Thanks to a happy robot in the cloud named Claude.&lt;/p&gt;

&lt;p&gt;Now in the next blog entry, on to the React-Native companion app...&lt;/p&gt;

</description>
      <category>esp32</category>
      <category>vibecoding</category>
      <category>rv</category>
    </item>
    <item>
      <title>More Adventures in Vibe Coding...</title>
      <dc:creator>Leif</dc:creator>
      <pubDate>Thu, 04 Sep 2025 22:32:44 +0000</pubDate>
      <link>https://forem.com/mergewithcare/more-adventures-in-vibe-coding-1ggp</link>
      <guid>https://forem.com/mergewithcare/more-adventures-in-vibe-coding-1ggp</guid>
      <description>&lt;p&gt;It looks like the LLM totally forgot to actually do anything with the Apple Watch background scheduler and I didn't catch this on the first iteration. And this is why we need to check, check, and check again every line we "vibe code":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import WatchKit

class ExtensionDelegate: NSObject, WKExtensionDelegate {
    func handle(_ backgroundTasks: Set&amp;lt;WKRefreshBackgroundTask&amp;gt;) {
        for task in backgroundTasks {
            switch task {
            case let backgroundRefreshTask as WKApplicationRefreshBackgroundTask:
                Task {
                    print("Performing background refresh for TempStick data")
                }
                backgroundRefreshTask.setTaskCompletedWithSnapshot(false)

            case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
                urlSessionTask.setTaskCompletedWithSnapshot(false)

            default:
                task.setTaskCompletedWithSnapshot(false)
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;chmod a+x ./facepalm.sh; ./facepalm.sh&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;**Note: there is no actual shell script in the app called facepalm.sh&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import WatchKit

class ExtensionDelegate: NSObject, WKExtensionDelegate {
  func handle(_ backgroundTasks: Set&amp;lt;WKRefreshBackgroundTask&amp;gt;) {
    for task in backgroundTasks {
      switch task {
      case let backgroundRefreshTask as WKApplicationRefreshBackgroundTask:
        Task {
          defer { backgroundRefreshTask.setTaskCompletedWithSnapshot(false) }
          print("Performing background refresh for TempStick data")

          await WatchModel.shared.fetchAllReadings()

          WatchModel.shared.scheduleBackgroundRefresh()
        }

      case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
        urlSessionTask.setTaskCompletedWithSnapshot(false)

      default:
        task.setTaskCompletedWithSnapshot(false)
      }
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's better! Now we should actually be scheduling background tasks in the Apple Watch, which is important...because for battery purposes and so on, Apple doesn't let you arbitrarily run long running processes in the watch (like continuously polling our temperature sensors) in the background; at most you can "request" a background task lasting no more than a few seconds is executed at a given interval and the watch will decide whether or not to let it happen. Currently configured to &lt;em&gt;try&lt;/em&gt; at the max given interval specified in the user settings per sensor.&lt;/p&gt;

&lt;p&gt;Code fixes pushed to public repo &lt;a href="https://github.com/leifdroms/TempStickMonitor-public" rel="noopener noreferrer"&gt;https://github.com/leifdroms/TempStickMonitor-public&lt;/a&gt;. Thank you for listen to this public service message.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>swift</category>
    </item>
    <item>
      <title>"Vibe Grokking" My Way Into an Apple Watch App</title>
      <dc:creator>Leif</dc:creator>
      <pubDate>Wed, 03 Sep 2025 23:08:57 +0000</pubDate>
      <link>https://forem.com/mergewithcare/vibe-grokking-my-way-into-an-apple-watch-app-2e18</link>
      <guid>https://forem.com/mergewithcare/vibe-grokking-my-way-into-an-apple-watch-app-2e18</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftj28cuxgnq5luh5pljdd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftj28cuxgnq5luh5pljdd.png" alt=" " width="344" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonam2qnmi8j6u1xk7xvq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonam2qnmi8j6u1xk7xvq.png" alt=" " width="774" height="826"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tl;dr: &lt;/p&gt;

&lt;p&gt;I needed an Apple Watch app to check the temperature sensors in my RV while my dog was in there, which didn’t exist. The company that made the sensors (TempStick, &lt;a href="https://tempstick.com" rel="noopener noreferrer"&gt;https://tempstick.com&lt;/a&gt;) graciously opens up their APIs to developers, so you can build your own software as needed. The only problem? I don’t know SwiftUI and there’s no cross-platform JavaScript/Typescript library to build watch apps that I’m aware of - you HAVE to do it in Swift. I heavily relied on partial “vibe coding” to get a prototype down, and am pretty happy with the results, although uneasy about potential security problems I don’t know to catch due to lack of experience with building Swift apps, although this is balanced against the low-risk nature of the project itself. &lt;/p&gt;

&lt;p&gt;If you would like to work with me, please reach out - I’m always looking for new opportunities, primarily in the JS/TS domain, but am happy to branch out. Pictures of the doggie doing what she loves to do, which is sleep on the travel trailer couch, for the algorithms. I am not paid by the TempStick company but it’s not for lack of trying; unfortunately their paid affiliate program is not accepting new applicants at this time but I’d love to pump their product and get a commission.&lt;/p&gt;

&lt;p&gt;Link to public github repository with the Apple Watch and companion iOS code:&lt;br&gt;
&lt;a href="https://github.com/leifdroms/TempStickMonitor-public" rel="noopener noreferrer"&gt;https://github.com/leifdroms/TempStickMonitor-public&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Intro:&lt;/p&gt;

&lt;p&gt;Whew! What a journey this has been, “vibe coding” my first Apple Watch App and companion iOS app.. Well - not entirely. I didn’t exclusively beat up an LLM with prompts until I had a fully functioning Apple Watch App loaded onto my Apple Watch, but I did manage to use Claude and ChatGPT to get things going and prompt as needed to fill gaps in my knowledge, and the end result is a fairly simple, but functional Apple Watch app that does exactly what I wanted it to do. And some more - which was unnecessary, but harmless (I will dive into that).&lt;/p&gt;

&lt;p&gt;Background:&lt;/p&gt;

&lt;p&gt;My wife and I love traveling in our travel trailer and are not afraid to admit it. It provides most of the comforts of home, and our extremely lazy, chunky dog “Luna” (pictured above, hiding under a blanket) is more than happy to curl up on the RV couch for a nap with either the air conditioner or the heat pump going if we are out and about without her (no, we don’t burn propane in the furnace with her alone in the trailer - it gets shut off when we’re away for safety).&lt;/p&gt;

&lt;p&gt;Which brings up some issues of safety. For those who are unaware, most RV’s are just plywood boxes, maybe with some aluminum siding, with minimal insulation and a rubber roof. It doesn’t take long if the air conditioner or heater fails for the inside to either get dangerously hot or cold depending on the weather, either through an equipment fault or power loss where we are staying. As a measure for peace of mind, like many RV-ers with pets, we keep temperature sensors in the RV which monitor and report environmental conditions inside the vehicle back to the cloud, which we can then check with our phones, and are programmed to give SMS and app alerts if something is amiss so we can rush back and take care of it before the situation gets dangerous.&lt;/p&gt;

&lt;p&gt;Because I tend to be a paranoid person, we were as until recently running 3 different temperature sensors in the trailer: two by one manufacturer, TempStick (&lt;a href="https://tempstick.com" rel="noopener noreferrer"&gt;https://tempstick.com&lt;/a&gt;), which I built the Apple Watch app for and will get into in a moment, and another by a competitor that shall remain unnamed. The problem with the competitor’s sensor is that the subscription was very expensive for what it does (report temperature and humidity to the cloud), compounded by the fact that it relies on an internal cell modem, whereas Tempstick monitors are Wi-Fi only. Now, that would normally be a feature, not a bug, but as mentioned before, the siding on our RV is aluminum, which functions as a makeshift Faraday cage blocking cell signals pretty well, and the competitor’s temperature monitor has no place to attach an external antenna. At this point, the “Wi-Fi” only monitors are actually more fail safe in our situation, since I have attached an external 4x4 MIMO cell antenna to the roof of the RV to circumvent the signal blocking effects of the external aluminum siding, plus our travel router not only connects to cell towers, but has the feature of allowing backup WAN sources (Wide Area Network; i.e. connections to the Internet) to function in the case of a failure of the primary WAN. Most of the time we stay at a campground or park with some rudimentary, weak, and overloaded free Wi-Fi, but offering more than enough bandwidth for sensor uploads to the cloud as a backup. If that’s not available, we also have our Starlink we can set up which would also offer more than enough bandwidth for these purposes. All in all, at this point, there is not as much value in a “standalone” device with its own internal cell modem given our current setup with multiple fail-safes to provide internet access to our devices. Except for one thing the competitor did great that unfortunately TempStick has not implemented yet: An Apple Watch App to check sensor information.&lt;/p&gt;

&lt;p&gt;Normally, there is no issue because I’m usually near my iPhone and can check the monitors anytime, but while doing things like swimming or tubing (as we love to do in the hot hot Texas heat), my phone is usually stashed in a pool locker or back at the truck, and I like to check in on the RV every half an hour or hour just to be safe. Not to mention, the less I pull out my phone while hiking with my wife, the less annoyed she is by all the distractions it offers that inevitably suck me in, which an Apple Watch is not quite as overloaded with. In these situations it would be very beneficial to have an Apple Watch app so I could check in on the RV right on my wrist, but sadly TempStick hasn’t implemented one yet, and didn’t have immediate plans to implement one after contacting them. &lt;/p&gt;

&lt;p&gt;I did have the sensors configured to send alerts in case things went awry, and in testing I did in fact receive these on my Apple Watch, but I would only do so if something was wrong. I didn’t want to only get notified if something was wrong, but also to have the power to check that everything was alright and functioning properly. Not to mention, I frequently pause alerts when the trailer is unoccupied and sitting on the storage lot, and while I try to remember to turn them back on, I am human and sometimes forget. &lt;/p&gt;

&lt;p&gt;Which leads us to the interesting part of this blog post: “vibe coding” to get this project done.&lt;/p&gt;

&lt;p&gt;Well, not purely vibe coding, I suppose. I did look over the first 2 weeks or so of Paul Hudson’s excellent and free course, Hacking with Swift UI (&lt;a href="https://www.hackingwithswift.com/100/swiftui" rel="noopener noreferrer"&gt;https://www.hackingwithswift.com/100/swiftui&lt;/a&gt;), so I would have SOME understanding of what the code was doing and the concepts that were unfamiliar to me coming from a JS/TS background. Additionally, while the definition of “vibe coding” is not solidified in popular culture yet, I’m going to define it somewhat strictly and as “completely divorcing yourself from the code, allowing the LLM to handle everything, only supplying the prompts, and stopping when the results match what you intend”. I did not strictly do this, and to the best of my knowledge I could not do this at this time, because there’s no LLM that’s hooked up to fully release an Apple Watch app that I’m aware of like there currently are to release Web apps and handle all of the supporting infrastructure. But, credit is due where credit is due, so I’ll call what I have been doing as “grokking with vibes”, because that’s how I perceive it: using the LLM’s to get started, iterate from there, intuitively make my way through the code based on heuristics, and when the thing fails to compile, asking it for its opinion on what to change.&lt;/p&gt;

&lt;p&gt;While I did not document every detail of this journey, it’s been an interesting one and I’ve had some take homes from it, in no particular order of importance:&lt;/p&gt;

&lt;p&gt;Vibe coding has been a great starting point when coming out of my “comfort zone” and doing something I haven’t done before in earnest- Swift development. Outside of tweaking some configuration files and installing libraries via CocoaPods in Xcode to make a React Native project work, I was nearly completely new to the idea of poking around in Xcode to build something, and certainly completely new to developing an Apple Watch app. Plugging in your requirements into ChatGPT or Claude and saying “now build me an Apple Watch App” is a great way to jump off, and probably saved me several months of churn as I developed into a real SwiftUI developer, which was great, because - at this time I don’t have a need to be a “real” SwiftUI developer. I need to fake it well enough to develop one Apple Watch app for personal reasons, and probably won’t need to develop another Apple Watch app again, or at least for a long while. And now I have my watch app, without having to take time to become an expert.&lt;/p&gt;

&lt;p&gt;Vibe coding is great to get a sketch of an idea and build on it, but relying on it completely would lead me into complicated loops of prompting the AI that were completely unnecessary. I was able to understand when this was happening based on my previous coding experience in other languages and frameworks, stop what I was doing, analyze the code and realize, “oh, this is simple, I just need to pass in a dictionary rather than converting it to an array like the first iteration of the AI was attempting to do”, but if I had no clue what any of this meant because I had never programmed anything and was unfamiliar with the concepts, I’d likely be stuck in these frustrating loops for days before eventually giving up (or paying someone to fix it if the end result would potentially make me money).&lt;/p&gt;

&lt;p&gt;When deciding to build something where I knew “just enough” to get it going, security was in the back of my mind. Frequently with “purely vibe coded” projects done by people with zero coding experience, major security problems are introduced, including but not limited to leaking sensitive credentials like API keys in cleartext in the final product, configuring databases with no security, and so on. The worst secret that could get leaked in this project would be my sensor API key, which could potentially allow a hacker to see what the temperature and humidity in my RV is, or change my configuration without my consent, deleting sensors, etc. Both of these situations would be “pretty darn annoying” but neither would potentially ruin my life and could be rectified by creating a new account with TempStick to get a new API key, or contacting customer service. Since my sensor lacks a GPS receiver, I’m not running the risk of some weirdo hacker tracking my movements. That being said, I did look at the code that Claude/ChatGPT spat out, and did my best to decide whether or not it “made sense” and was reasonably secure for what I was doing; the LLM’s had made the decision to store the API key in secure Apple keychain storage and their implementation of this looked ok to my eyes. Additionally, I did check to make sure that communication between the iPhone app and the Apple Watch app through Apple’s Watch Connectivity framework is secure and encrypted (&lt;a href="https://support.apple.com/en-in/guide/security/secc7d85209d/web" rel="noopener noreferrer"&gt;https://support.apple.com/en-in/guide/security/secc7d85209d/web&lt;/a&gt;). I’d say the biggest glaring loopholes to security that I was able to spot were pretty obvious: after developing, it was important to erase any calls to “print()” that would output credentials to the debugger. If someone had absolutely no clue about programming they might not catch this and it could be a problem.&lt;br&gt;
Frequently, the LLM would do something I was unfamiliar with, and it was useful to prompt it to ask “why” it was doing so. For example, when implementing an observer in the Notification center in order to “do something” when either the watch app or the phone app would communicate with each other, ChatGPT-5 came up with some code using “weak self”. I didn’t know what this was or why, and could ask it. Was it a hallucination? Possibly, but considering it’s trained on the wider internet (including countless StackOverflow answers), its explanation appears plausible and correct, and I didn’t see an issue with going with its suggestion (ChatGPT-5):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj6wwh7wv86n491yzszmb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj6wwh7wv86n491yzszmb.png" alt=" " width="512" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff97bp3ncksmboszf4epa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff97bp3ncksmboszf4epa.png" alt=" " width="800" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Different LLM’s had different strengths and weaknesses. I found Claude was great for getting the initial project structure out, but it lacks the ability to output everything as a .zip file and simply load it in; the best you’d get was a pdf of all your files. I could then plug that into ChatGPT and split things up, as well as ask for its opinion on the code Claude had spat out. &lt;br&gt;
I’m fairly impressed with the UI the LLM’s generated on the first try. While a lot of it was unnecessary for the use case in that I only really needed the companion iOS app in order to input my TempStick API key and adjust the polling settings for each sensor, I decided to keep it for simplicity and aesthetics. It recreates a lot of the functionality that the official TempStick phone app already created; i.e. displaying sensor names along with online status, battery level, temperature and humidity, but…it looks good and I don’t feel the need to cut it out since the LLM’s generated it with little effort on my part.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkt51ats747oh7y2t4ipb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkt51ats747oh7y2t4ipb.png" alt=" " width="632" height="1256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A lot of the code that the LLM’s output is messy and repetitive - which is not a good practice. Ideally the shared code between the watch app and the phone companion app would appear in one place, rather than be duplicated for each target, but I didn’t want to spend time to clean things up and make it look nice. The goal was to get a watch app out as quickly as possible for personal reasons, with a minimum amount of time spent on it. “In real life” at a “real company”, this should not and would not fly - this is a common way for nasty bugs to come up in the future as different people work on different parts of the codebase but don’t update everything as needed, especially as it grows. If I get really bored I will probably refactor this for my own purposes, but for now I’m going to purposely leave things as is, if anything to demonstrate one of the pitfalls of “vibe coding” as things stand as of the time of this writing.&lt;br&gt;
Some of the functionality seems redundant but I’m going to leave it as is; pressing “Save Settings” in the iPhone app settings also functions to “Sync to Apple Watch”. &lt;br&gt;
I’m not sure I trust some of the functionality but it appears pretty benign and not worth my time to correct for now; for example it appears that enabling Global Polling and setting the interval doesn’t actually change the interval of the sensors to hit the API on the iPhone app and they can only be changed individually, although it does appear to affect things on the watch app. Again, if I get bored, I’ll probably just cut out Global Polling completely to prevent confusion and require individual sensor configuration.&lt;br&gt;
The LLM’s are NOT great at crawling documentation you link to in order to generate a functioning app that connects with the backend provided, or at least weren’t at the time I was developing the app. Maybe if I had copied and pasted all of TempStick’s API documentation directly it would have come up with something better on the first go, but the AI’s solution to this was to find a wild repo on the internet that made use of TempStick’s API, crawl that, and make its decisions from there. Which, as you may guess, didn’t work at this time (although maybe it would have worked with a previous incarnation of the TempStick API that the repo was based on). I had to look in the documentation myself and fix the problems. I still feel that this saved me some time vs. doing everything from scratch, but again, I’m relying on my ability to program or at least grok through something new-ish to me, whereas I’d be at a dead halt if I had none of that in the back of my brain. Interestingly, I tried again today, asking it to “make a demo web app” using the URL of the TempStick API documentation, which it spat right out, but when pressed (ChatGPT-5):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faa5cc84r4rgmf9x09083.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faa5cc84r4rgmf9x09083.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;-Certain critical parts of functionality, like having the Apple Watch app poll the API on its own (rather than relying on data sharing between the phone app and it) were strangely commented out at the beginning and marked as “optional”. If I wasn’t looking for this in the code, everything would “appear” to work fine at first since generally speaking, my phone and watch are on at the same time, and the phone usually has a connection to the internet. If it were stuffed inside a metal locker in a concrete building at a pool or boathouse, there’s a good chance it wouldn’t be able to connect to the cell towers though, my watch wouldn’t receive updates on its own, and thus the whole purpose of the project would be defeated since in its initial form the watch app wasn’t configured to poll the backend on its own.&lt;/p&gt;

&lt;p&gt;Summary:&lt;/p&gt;

&lt;p&gt;As many have said before, AI is a great tool for those that already have foundational programming concepts under their belt, both in order to properly articulate what they’re looking for in addition to spotting and dealing with potential problems as they come up. In my opinion if I hadn’t had the experience I already had, I would have never been able to bring this specific personal project to fruition. For a purely web app, I do think it is possible to purely “vibe code” something and in this narrow regard the hype is indeed real, but except for the simplest client-side only web app with no authentication, no database access, no API usage, etc., doing so runs a huge risk of security problems, or at a minimum vendor lock…unless someone was able to vibe code their way around the cloud as well to set up the necessary infrastructure, and trusted the results (although I remain extremely skeptical this is possible at this point in time). At the end of the day, it’s important to remain vigilant, skeptical of the AI output, and to not use it as a substitute for your own reasoning and decision making, as Open AI warns about itself, right in the chat prompt box:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F39nqjomhrz8ohxvm0akd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F39nqjomhrz8ohxvm0akd.png" alt=" " width="626" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Although at the time of writing, it appears we are starting to hit a point of diminishing returns with new LLM’s and a plateauing of progress, I do think AI will continue to be a great resource for developers to brainstorm and sketch out rough ideas as well as slap together some boilerplate, greatly speeding up the process of development. Especially in a situation like I found myself in, where I NEEDED to learn and adopt a technology I didn’t have much experience with, I did not want to take months to become an expert in this domain because I will probably not use it much outside of this one project (at least for now), but didn’t want to wait for the company to produce their own Apple Watch companion app which doesn’t appear to be happening soon. I remain optimistic that I’ll see a more polished, “official” app though, since now one exists in the wild: mine!&lt;/p&gt;

&lt;p&gt;Those are my thoughts for now, feel free to comment, and if you’d like to download the code and run it yourself, I created a public repo containing just the Swift files and the assets: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/leifdroms/TempStickMonitor-public" rel="noopener noreferrer"&gt;https://github.com/leifdroms/TempStickMonitor-public&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’d like to thank in particular the TempStick customer service team as well as the company in general for graciously opening up their APIs to developers. They make a great product and I’d recommend it to RV-ers or anyone else that needs to do cloud based environment monitoring. Gavin in customer support, if you’re reading this, thank you in particular.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>swift</category>
      <category>swiftui</category>
    </item>
  </channel>
</rss>
