Vision-enhanced Robot ‘Find Best Direction’ Issue

WallE, my autonomous wall-following robot had a nominal capability called “run to daylight”. The idea was to use this as a last resort when all the other ‘find a wall and follow it’ tricks failed. Unfortunately, I never could get this to work and never figured out why. Fast forward to the present, and Grok and I were trying to implement the same kind of capability into the new vision-enhanced version, to address the issue of what to do when the robot gets stuck.

Basically, the algorithm is the same as it was for the wall-following version; back up, look around and turn to the heading that provides the most freedom (‘largest depth’ in vision processing language). We (Grok and I) implemented the “back up and look around” operations using the new ‘L/Rxxx’ (where ‘xxx’ is in degrees), and then the ‘turn to the best heading’ utilizes a new ‘Txx’ command which is implemented via the already-existing ‘TurnToHdgDeg()’ function.

Unfortunately, although the ‘back up and look around’ part worked almost immediately, the ‘turn to the best heading’ part failed miserably, with the end result consistently going off in a completely different direction. I was somewhat mystified by this, as I was suffering from the (soon-to-be-debunked) belief that my MPU6050 IMU/DMP was actually delivering accurate heading measurements, and the problem must be in the ‘TurnToHdgDeg()’ function. However, data beats belief, so I decided to go back to basic ‘show me’ experiments.

To validate the MPU6050 IMU function operation, I set up an experiment where I manually rotated the robot on a printed compass rose, and measured the output of the IMU at each point, as shown in the following Excel table and resulting plot:

The above plot looks ugly, but a lot of the ugliness is due to the way IMU heading output jumps from +180 to -179; this is normal. What isn’t normal is the way the IMU reported heading diverges from the actual heading – the +180 to -179 transition occurs between 150 and 160 degrees rather than 180, and the -179 to +180 transition occurs between -140 and -150 degrees on the other side.

At first I thought this must be due to a bad IMU module, but changing out the module for a brand-new one produced the same results – so not that (further validation of my “hardware never fails – its ALWAYS the software” mantra).

So, I made another plot, this time manually changing the ‘opposite sign’ measurements back into the proper range by adding/subtracting 360 degrees. This produced the following plot:

Just looking at the ‘positive’ half from 0 to 180 deg, it is clear that the measured values are consistently higher than the actual physical headings. Looking at the first five values (before the first breakpoint in the plot) we see the differential goes from zero to about 156.8-120 = 36.8 deg over a 150 deg range, or about 36.8/150 = 0.25 deg/deg. Assuming the difference is linear, then the required multiplication factor should be 156.8X = 120 –> X = 120/156.8 = 0.769. For example, a multiplicative factor of 0.787 yielded the following plot:

Good enough for government work ;).

This factor can’t be applied directly to the IMU yaw output, as that value has already been corrected for the +/- 180 arrangement, so Grok suggested an algorithm to hold a ‘running total’ heading that uses IMU heading changes to produce a separate heading value that is corrected for the slope error and only afterwards applies the +/- 360 adjustment for the +/-180 scheme, as follows:



AC Power Generator Issues During Recent House Power Loss

Posted 31 August

On 11 August 2026 we had a multi-hour night-time AC power loss at our home, and although I was able to get our backup power generator running and AC power partially restored, there were some ‘issues’.

When our house was built in 2000, I had the electrician install a 240-volt twist-lock receptacle in the garage and an auxiliary breaker panel at the main AC breaker location to support the use of a backup AC power generator. Then I fabricated a 50′ jumper cable so the generator could be connected to the 240-volt connector while still outside the garage.

I didn’t actually buy the generator shown below until 2022, as AC power in our area has been very reliable, and other than testing the unit and transfer switch, it was never used ‘in anger’ until a couple of weeks ago when we had a night-time multi-hour power interruption.

I have a hobbyist engineering lab in our home, with several PC’s protected from short AC interruptions with typical single-PC UPS units. So, when the power loss occurred all the lights went out, but our PCs (and their associated displays) stayed up so we weren’t totally in the dark, but I knew that wasn’t going to last long – I needed to get that generator going before we were totally in the dark.

Using my phone as a flashlight (VERY helpful!) I went out into the garage, where I was immediately faced with the problem of getting the garage door open with no AC power. After finally getting the door open, I got the generator moved outside and tried to start it up – no dice; the electric starter solenoid just chattered without actually engaging the motor. A quick visual inspection showed that the onboard 12V SLA batter was grossly swollen – most likely due to my use of an old-style battery charger to keep the battery topped off. Fortunately, I was able to get the generator started using the manual pull cord – yay!

After getting the generator started, I got the 50′ extension cable connected to the 240-volt power receptacle in the garage and toggled the transfer switch on the emergency power sub-panel in the utility room, again using my phone as a flashlight. After toggling the transfer switch, I got lights and PC power back in my engineering lab, but there were still some issues.

  • Both the in-house refrigerator and the freezer in the garage were still unpowered (this was later found to be due to me finger-fumbling the transfer switch)
  • The house internet router and LAN ethernet switches were still unpowered. This was unexpected, but not a big deal as I was able to run an extension cord from my engineering lab space to the router/switch power strip.
  • The garage door was still unpowered.

Utility power was restored after about 3 hours, so I was able to shut down the generator, toggle the transfer switch back to the default position, and generally put everything back together before going to bed. Overall an exciting evening.

31 August Update:

The next day I was able to review the previous night’s activities and make some adjustments for the next time. I ordered a new battery for the generator, and a new modern SLA-compatible float charger, as shown below:

Interestingly, I couldn’t find a replacement battery with the same terminal arrangement as the one that came with the generator. The original had the positive terminal on the left, and all compatible offerings from Amazon had the positive terminal on the right. Fortunately I was able to confirm that the attachment cables had enough slack to reach.

After getting the battery replaced and charged, I went through the entire process again – getting the garage door open, getting the generator outside the garage and started up, getting the 240-volt cable connected to the garage 240-volt receptacle, and toggling the transfer switch on the emergency power panel. This time both the house refrigerator and the in-garage freezer had power, and I realized that I had fumble-fingered the transfer switch during the actual power failure.

Building an Improved Under-monitor Accessories Tray

Posted 25 July 2026

For quite a while I have used the accessories tray shown below, but I have become increasingly unhappy with the quality with it; in particular the small tray sections aren’t quite big enough to handle all my USB sticks, micro-SD cards and holders, spring-clips, etc. So, after trying in vain to find a better model on-line, I decided I would design and 3D print one for myself, using my 300×300 Voron 3D printer in the ‘no_dock’ configuration.

Here’s where I am at the moment:

preliminary design for under-monitor accessory tray. Note that this is only 1/2 of the finished product

I started out with the idea of using removable partitions to fix the problem of having too-small or too-large bins for whatever I wanted to store. The available free distance under the riser is about 270mm so I made the width (the ‘Y’ axis above) slightly wider than original at 260mm. The above model is about 237mm long (the ‘X’ direction above), but is only 1/2 length, as the full length won’t fit on the Voron’s 300x300mm print bed, so I’ll have to figure out some way of connecting the two halves.

26 July 2026 Update:

The idea I came up with to connect the two halves of the accessory tray was to place holes in the ‘front’ half of the tray, and matching posts in the ‘back’ half of the tray, as shown in the screenshot below:

Now that I have my ‘no_dock’ printer configuration working properly, I printed up the small section of the back half of the tray, along with just a small section of the mating end of the front half of the tray, just to make sure the mating idea would work. It seemed to work pretty well, as shown in the following photos:

The posts on the back half came out a little scruffy due to small diameter and their length, but still worked well after a bit of cleanup. In the final version I think I will increase the diameter a bit and reduce the length. The gap shown in the mated photo above I believe was due to the way I printed the part, and shouldn’t be a factor in the finished product – we’ll see!

The next step was to create the rear section sketch. First I tried do this by simply copy/pasting the entire ‘2-bin_Tray’ section, but the result was ‘too many’ broken references, as shown below:

‘Too many’ broken references

But after some consultation with Grok about using the ‘use’ feature, I simply created a new sketch on the same plane as the original ‘2-bin Tray’ sketch, then activated the ‘Use’ feature and dragged a rectangle to select the entire original sketch. This produced a new, unlinked sketch which I moved to a non-interfering location with the transform tool, as shown below:

A better ‘copy/paste’ using the ‘Use’ tool, selecting everything, and then using the ‘Transform’ tool to move it

The rear section of the tray will only be 120mm long instead of 240, as the total length of the tray is 360mm. Here are some photos of the constituent parts:

Removable partitions in three lengths

09 August 2026 Update:

After the ‘Gloop’ arrived I was able to glue the front & back sections together with a liberal application of ‘gloop’ to both sides of the junction and to the top and bottom of the tray once the sections were joined. Then transferred all my junk from my old tray to the new one and installed it under my monitor riser, as shown in the following photos:

Moving Vision Processing to My 4-Wheel Robot, Part 1

Posted 08 July 2026

In this previous post, I describes my work with Grok Code to successfully demonstrate the use of vision processing with a Luxonis OAKD-Lite camera and a Raspberry Pi5 to generate ‘best clear direction’ advisories for robot motion control.

After achieving the above milestone, I started thinking about the next steps. Clearly I want to move on to achieve actual robot navigation utilizing the above ‘best direction’ information, but the current 2-wheel robot battery pack doesn’t really provide enough power for the Pi5, the OAKD-Lite camera, and the wheel motors. Consequently, I started thinking about moving the vision processing project from the 2-wheel robot onto my existing 4-wheel platform to take advantage of the latter’s larger battery pack. Looking at the pinouts for the Teensy 3.6 and Teensy 4.1 along with the 2-wheel and 4-wheel schematics, I developed the following pin assignment spreadsheet:

Pin assignments for the 2-wheel to 4-wheel transfer

In the above (hopefully self-explanatory) spreadsheet there was only one significant conflict. On the current 2-wheel robot pins 29-32 are occupied by the four Hall-effect wheel encoders, and on the current 4-wheel robot these pins are used by four of the six battery charge indicator LED’s, and that function carries over into the new vision-enhanced robot. Easy enough resolution, just move the wheel encoder pins to 7-10 (actually even easier, I don’t need to move the encoders over until I actually have software/firmware that needs them, and that might be a while).

I started out thinking I would 3D print another ‘2nd deck’ plate so I wouldn’t have to disassemble the original one. Then I came to my senses and realized that I had originally constructed the 2nd deck expressly so it *could* be disassembled – duh! Here’s the mostly disassembled 2nd deck plate:

And the bottom contains all the motor hardware and drivers, the battery and charging circuitry. The ‘deck’ of the bottom deck already has a 5V regulator module (more on that below), the MPU5060 MPU and the charger unit homing module. Why re-invent when it is already there.

15 July 2026 Update:

I launched DipTrace, opened the schematic file for my current 4 wheel robot, then saved a copy as ‘Vision-Enhanced Robot Schematic’ and then edited it to reflect the following changes:

  • removed the Wixel OTA module; OTA is now handled through the Pi5 wifi link.
  • removed all but the rear-facing element of the 7-element VL53L1X LIDAR array and the Teensy 3.5 that managed it.
  • added the Raspberry Pi5 and the OAKD-Lite camera to the second deck, utilizing pins freed up by the removal of the Teensy 3.5 LIDAR array manager
  • Replaced the Teensy 3.6 main system controller with a Teensy 4.1 for additional processing power.

I also replaced the approximately 45cm long USB cable that came with the camera with a 15cm one as the extra length was just adding mass (and mess) to the installation. When the new USB cable arrived, I hooked it and the Pi5 stock power cube up to the Pi5 and used the previously developed Python script and associated shell scripts to confirm that the cable worked.

20 July 2026 Update:

In the process of moving the Pi5 and OAKD-Lite camera to the 4-wheel robot, I managed somehow to lose the Pi5’s SD card. I tried my best to find it – including dumpster-diving with the hope that I could find in my last trash bag – but no luck. Attempting to make lemonade out of lemons I decided to try and document the process of building up a new SD card from scratch.

RaspberryPi Imager:

20 July 2026 Update:

In the process of moving the Pi5 and OAKD-Lite camera to the 4-wheel robot, I managed somehow to lose the Pi5’s SD card. I tried my best to find it – including dumpster-diving with the hope that I could find in my last trash bag – but no luck. Attempting to make lemonade out of lemons I decided to try and document the process of building up a new SD card from scratch.

RaspberryPi Imager:

Choose Ubuntu Server 24.04 LTS (64-bit) (Server version is recommended — Desktop images sometimes cause issues with the customization wizard). I actually had no problem with the ‘desktop’ version, but I think it included a lot more ‘crapware’ than the ‘server’ version. Also, when the ‘server’ version is chosen, the imager includes the ‘Customization’ page for wifi setup, username/password, etc.

  • Hostname: RobotPi5.
  • Capital City: Washington D.C
  • Time Zone: America/New York
  • Username: Pi
  • Wifi: Choose ‘Secure Network, fil in SSID & password.
  • SSH: Choose ‘Enable SSH’ and ‘Use Password authentication.
  • Password: xxxx (note that on subsequent passes of the imager, it will offer to use a saved password)
  • Write: The image below shows the situation just before writing the image to the SD card

Write to the SD card (takes a few minutes).

First Boot (Monitor + Keyboard):

Insert the SD card into the Pi 5 and power it on.Complete the initial Ubuntu setup prompts (this can take a few minutes).Log in with username pi and the password you set. I had to reboot the Pi a couple of times to get it to go all the way through the startup sequence to present the login screen.

Connect from Your PC via SSH:

To confirm that the Pi can see the wifi network, run ‘ssh pi@RobotPi5’ from a Windows Command Window and enter your password. This should connect right away. If it doesn’t, then check the Pi’s IP address (DHCP can assign it a different one) with the ‘ip addr’ command on the Pi. If the address was changed (like from 192.168.1.200 to 192.168.1.201), you can change it back by editing your local router to give the Pi5 a static IP address reservation as shown below, and then rebooting the Pi. When the Pi comes back up it will get the ‘reserved’ address from the router, and life will be good again, except…

When you try to connect from your PC using ssh, you will probably get the following error message:

This occurs because the new Ubuntu installation generated a new SSH key, and the on one in your PC’s ‘host’ file no longer matches. To fix this, you must first remove the old key, with the following in a command window on your PC:

Then try to connect to the Pi from the PC with ‘ssh RobotPi5’. You should now see this:

Enter ‘yes’ (can’t enter just ‘y’) to accept the new key. Then you should be able to connect to the Pi with your username and password, and you should see this:

Connect using VS Code:

Now you should also be able to connect to the PI from VS Code with Cmd – Shift – P -> Remote SSH: Connect to host ->’RobotPi5′. This should result in the following screenshot:

Ignore the misleading warning in the blue field above and enter the password for the Pi5. This should log you in, but it may still take a while for VS Code to connect the first time (you will see ‘opening RobotPi5’ with the spinner in the bottom left-hand corner, and then this will change to ‘SSH: RobotPi5’ as shown below:

or this:

At this point you have a fully functional Ubuntu ‘server’ installation on the Pi, but none of the software packages (ROS2 Jazzy) needed to actually use the OAKD-Lite camera, and none of the already-developed user software. The user software can be retrieved from your GitHub repo (you DID create a GitHub repo and save your software, right?).

Install ROS 2 Jazzy (Jazzy is a version nickname):

From a VS Code Terminal Window, run these commands in order:

sudo apt update && sudo apt upgrade -y
sudo apt install software-properties-common curl -y
sudo add-apt-repository universe -y
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
sudo apt update
sudo apt install ros-jazzy-desktop -y

You will probably need Grok’s help with getting through the entire package installs. We tried the Ubuntu 24.04LTS ‘server’ edition because it has less bloatware, but ros2-Jazzy would not install. Finally went back to Ubuntu 24.04LTS ‘desktop’ edition, and this worked – yay!!

After flashing the SD card with Ubuntu 24.04LTS ‘desktop’ edition, I booted it up on the Pi5 and went through the initial setup process. The ‘desktop’ edition skips all the customization stuff in the SD card load because it can be done by the user in the desktop.

The first hurdle to overcome was the inability to connect to the Pi with SSH, even though I ‘enabled’ SSH in the desktop settings.

the first two commands above succeeded, but the stataus was still ‘unable’. However after some tangential stumbling around with (almost) trying to kill some interfering processes, it turned out that just waiting a minute or two did the trick and SSH started working – Yay!

At this point we started the process of getting ros2 ‘Jazzy’ installed, as follows:

So at this point ros2-Jazzy is installed and active, and my working software has been restored from Github – WHEW!

Tomorrow we will continue through to getting the ‘clearest_direction_node.py’ program running (yet again)

While I can’t really say that losing that micro-SD card was much fun, it *was* educational, and now I think I have a better understanding of how the startup process works, and much better documentation.

22 July 2026 Update:

I renamed the Grok session to “260722 Pi5 Boot ROS and OAKD-Lite Initialization” to be more recognizable in the future.

OK, back to the effort to recover from my disastrous loss of my original micro-SD card. When we left it last night, we had gotten to the point of successfully flashing the ‘desktop’ version of Ubuntu 24.04, gotten ros2-Jazzy installed, cloned the ‘my_vision_robot’ GitHub repo and switched to the ‘vision-to-motion-demo’ branch.

2 Wheel Robot with Vision Processing, Part IV

Posted 06 July 2026

After wrapping up the Wifi_OTA project described in this post, Grok Code and I moved on to the next big step – integrating the Luxonis OAKD-Lite camera integrated into ROS (Robot Operating System) so the robot could take advantage of the ROS ‘topic/subscriber’ strategy where a sensor like the OAKD-Lite camera can ‘publish’ formatted sensor data to a ‘topic’ that programs can ‘subscribe’ to, thereby making sensor data available to inform robot navigation. Amazingly, this trick actually worked, and now the robot can ‘see’ the best direction to move. Below is a summary of the work involved getting to this point, produced by Grok Code at my request. (note: When Grok uses ‘I’ below, he really means ‘we’, as I was very much the junior partner in this adventure. Think of me more as somewhat intelligent clerk,just barely smart enough to follow direction and report results ;).

From Grok Code: Summary for “2 Wheel Robot with Vision Processing, Part IV”

After struggling for a while with custom ROS 2 package structures and running into repeated setup.py issues, I decided to take a step back and follow a known working example as closely as possible. I came across Jesse Stevens’ blog post about setting up the OAK-D Lite with ROS 2 Jazzy on a Raspberry Pi 5, and it looked very close to what I was trying to achieve. Rather than continuing to fight with my own over-engineered layout, I made the deliberate choice to start fresh using his structure and naming.

We began by backing up my existing src/ folder and creating a clean myrobot package exactly as described in the blog. This meant using the standard ros2 pkg create command, adding a config/ directory, and modifying setup.py to properly install YAML files using data_files and glob. Following the blog’s oak_run.yaml example, I configured the camera for a Depth-only pipeline with conservative settings (no subpixel, RGB disabled, HIGH_ACCURACY preset initially).

Getting the camera running wasn’t entirely smooth — we hit the familiar X_LINK_DEVICE_ALREADY_IN_USE error a few times — but once we established a reliable restart process, the camera would come up and begin publishing depth data. However, early captures showed suspicious results: the minimum and maximum depth values were often stuck at the same numbers across different scenes. This told us the depth data wasn’t yet reliable enough for motion planning.

To make progress visible and testable, we built a capture utility (save_depth_frame.py) that could save both raw depth values as CSV and, later, visual PNG heatmaps. Being able to generate side-by-side comparisons between the depth map and the actual scene was incredibly helpful. Over time, with adjustments to the YAML (switching to the DEFAULT preset, enabling subpixel, and using only the bottom half of the image for processing), the depth maps started to look structurally meaningful. Features in the real world began appearing clearly in the heatmaps.

With usable depth data in hand, we shifted focus toward the higher-level goal: using vision to help the robot decide where to go. We created a new node called clearest_direction_node.py that divides the lower half of the depth image into seven sectors, labeled intuitively as clock positions from 9:00 to 3:00. The node calculates the average valid depth in each sector and identifies the “clearest” direction — the one with the highest average depth. It then suggests a turn angle toward that direction.

This node proved to be surprisingly effective right from the start. When I moved objects into different parts of the camera’s view, the recommended direction updated immediately and logically. Seeing the output change in real time gave me confidence that the depth data was now good enough to build motion logic on top of.

Throughout this process, we also invested time in workflow improvements. I created several small shell scripts (restart_camera.sh, capture_depth.sh, start_viewer.sh) to reduce friction when restarting the camera or capturing data. These scripts, along with the analysis node, were eventually moved into my my_vision_robot GitHub repository under a clean structure (software/ros2/ for the ROS package and software/scripts/ for the utilities). We also updated the project .gitignore to properly exclude ROS 2 build artifacts and temporary test data.

By the end of this phase, I had moved well beyond simply getting the camera to publish data. I now had a working system that could look at a depth image and suggest a sensible direction for the robot to face — all running on the Pi 5 and reacting to real changes in the environment. While there’s still work ahead to turn these direction suggestions into actual motor commands on the Teensy, this felt like a genuine milestone: the vision processing side had gone from “not yet reliable” to “usable for basic navigation decisions.”

Because I know I won’t remember the details of how we got from the start to a functioning “Turn this way” demo, I asked Grok for a short description of the relevant files, based on the latest GitHub file structure for the ‘vision-to-motion-demo’ branch, as shown in the following screenshot:

1. Camera Launch & Configuration (the foundation)

  • software/ros2/my_vision_robot/config/oak_run.yaml The main parameter file passed to depthai_ros_driver. Defines the Depth pipeline, 480P stereo resolution, HIGH_ACCURACY (or DEFAULT) preset, bottom-half-friendly settings, RGB disabled, and all the i_* parameters that make depth actually usable.
  • software/ros2/my_vision_robot/launch/oak_camera.launch.py Simple launch file that starts depthai_ros_driver’s camera_node and loads oak_run.yaml via –params-file. This is what restart_camera.sh calls.

2. Core Depth Capture Script

  • software/scripts/save_depth_frame.py The workhorse script. Subscribes to /camera/stereo/image_raw, converts the 16UC1 depth image with CvBridge, saves the raw depth values as a timestamped CSV, and generates a matplotlib heatmap PNG (with stats in the title). This is what produces the nice side-by-side depth maps you’ve been using for validation.

3. Convenience / Workflow Scripts

  • software/scripts/capture_depth.sh Tiny wrapper that sources both ROS 2 environments (/opt/ros/jazzy + your workspace) and then runs save_depth_frame.py. Prevents the classic “No module named ‘rclpy’” error.
  • software/scripts/restart_camera.sh One-command way to kill any old camera processes, source the environments, and relaunch the camera via the launch file above. You use this every time you reboot the robot.
  • software/scripts/start_viewer.sh Sources the environments and launches rqt_image_view pointed at the depth topic so you can visually confirm the camera is streaming.

4. Depth Analysis / “Clearest Direction” Node

  • software/scripts/clearest_direction_node.py The first real vision-to-motion node. Subscribes to the depth image, looks only at the bottom half (rows 240–479), divides it into 7 clock-position sectors (9:00–3:00), computes average valid depth per sector, identifies the “clearest” direction, and prints a live table + recommended turn angle. This is the node that actually reacts when you move obstacles in front of the camera.

5. Supporting Package Files (mostly standard but customized)

  • software/ros2/my_vision_robot/package.xml & setup.py Standard ament_python package files. We updated the package name from myrobot → my_vision_robot and made sure config/ and launch/ get installed correctly.
  • software/ros2/my_vision_robot/my_vision_robot/__init__.py & resource/my_vision_robot Required empty marker files so the package is recognized by ROS 2.
  • software/ros2/my_vision_robot/setup.cfg & test/ files Boilerplate that came with ros2 pkg create; left mostly untouched.

Summary of the “depth measurement stack”

  1. restart_camera.sh → starts the camera with the tuned oak_run.yaml
  2. capture_depth.sh (or save_depth_frame.py directly) → grabs a frame and saves CSV + pretty PNG
  3. start_viewer.sh → lets you watch the live depth image
  4. clearest_direction_node.py → turns the raw depth into actionable “which way is clearest?” information

All of these now live under version control in the repo, so “future Frank” (or anyone else) can clone the branch and have a working depth pipeline in a few minutes.

Additions by the not-so-smart human:

This was the part of the project that transformed my somewhat unrealistic idea of enhancing my robot’s navigation capabilities with vision processing into a real, concrete capability even in its current nascent state. The Raspberry Pi5 combined with the OAKD-Lite camera easily processed 30 frames/sec of depth information to generate ‘turn this much in this direction’ outputs for the robot. This part of the overall project didn’t actually send commands to the Teensy for movement, but the ability to do so was demonstrated with an earlier ‘end-to-end-serial’ project and the earlier Wifi_OTA project. There is still A LOT more to do to get to the point where I can set the robot loose on the world (or at least the house), but getting even the most basic vision processing going was a HUGE milestone.

In addition to the above progress, I have been learning a huge amount about the OAKD-Lite camera, the ROS (Robot Operating System) environment, Visual Studio Code, and Git repository creation & maintenance. Early on in this phase of the project, Grok Code recommended VS Code for working with the Raspberry Pi in general, and for Python programming in general. I have always used Visual Studio on my PC, and by adding the Python extension I could easily generate Python code on my PC. However, this project required that the actual code has to run on the Raspberry Pi5 on the robot. In addition, there is a lot that has to be done at the Linux (bash) command line on the Pi5. What I needed was a way of doing all the work on my dual-screen PC setup without having to keep transferring files and programs back and forth between the PC and the Pi5. It turns out that ‘that something’ is VS Code, and now that I have had some time to work my way up the learning curve, it is WONDERFUL!

Once VS Code is setup properly to connect via SSH over Wifi to my Pi5, it does so every time it is launched, and from then on, the environment is very much like using Visual Studio. Files can be created, edited, moved etc using a File Explorer and editor window very similar to VS and it is easy to lose track of the fact that all the action is actually taking place on that small Pi5 board over there on my robot.

Another milestone of sorts that was achieved with this project was making a Git repository a central part of the overall project. Although I have Tortoise Git on my PC and have used it on and off for years, this is the first time I decided to set up a logical repo layout and keep it up-to-date as the project moved along. The Git repo has three different pieces – a local repo on the Pi5 for all the shell scripts, Python scripts and ROS-specific configuration files, a local repo on the PC for the teensy stuff, and a GitHub remote repo that ties the two together. Here’s a sketch of the repo layout (produced in about 3 seconds by Grok Code):

I had to insert this as an image, as WordPress doesn’t handle the 3rd level indent properly

This same layout exists in all three places (Pi5, PC, Github), but obviously the pi5 repo deals with the ‘Software’ side and the PC deals with the ‘Firmware side. Both ‘local’ repos are clones of the same repo on Github, so both local repos also contain all the material produced by the other one. So a typical work flow on either the PC or the Pi5 would be:

  • Git status – to see if anything unusual is happening
  • Git pull origin [branch] <– started out on ‘main’, now working on ‘vision-to-motion-demo’. This synchs the local repo with the GitHub repo, and any commits from the ‘other local’ repo
  • Git status – again to see what changed during this session
  • Git commit -m “[Message]”
  • Git status – again to catch any problems
  • Git push origin [branch] <- synchs the GitHub repo with the local repo

06 August 2026 Update:

Here is a ‘structured index of the Grok chat that produced most of the progress shown above:

Structured Index – OAK-D Lite + ROS 2 Jazzy on Pi 5

Chat focus: Getting reliable depth data and a “clearest direction” node working, then organizing everything into a proper Git repo.

1. Project Context & Philosophy

  • Goal: Vision-enhanced 2-wheel (later 4-wheel) robot using OAK-D Lite + Raspberry Pi 5 + Teensy 4.1
  • Key decision: Stop fighting custom structures and follow Jesse Stevens’ blog post exactly as a known-working template
  • Philosophy: “BIG RED BUTTON” approach — get something known to work first, then adapt

2. Initial Clean Setup (July 5–6)

  • Backed up and cleared ~/ros2_ws/src
  • Created package: ros2 pkg create –build-type ament_python myrobot –dependencies launch_ros rclpy
  • Added config/ folder and oak_run.yaml
  • Modified setup.py to install launch files and YAML configs
  • Successfully launched camera with: ros2 run depthai_ros_driver camera_node –ros-args –params-file …
  • Confirmed depth topic /camera/stereo/image_raw was publishing at ~30 Hz (16UC1 encoding)

3. Key Files Created

FilePurposeFinal Location
oak_run.yamlCamera configuration (Depth pipeline, resolution, stereo settings)software/ros2/my_vision_robot/config/
oak_camera.launch.pyLaunch file for the camera nodesoftware/ros2/my_vision_robot/launch/
restart_camera.shKill old processes + source environments + launch camerasoftware/scripts/
start_viewer.shLaunch rqt_image_view (requires monitor)software/scripts/
capture_depth.shWrapper that sources ROS and runs the capture scriptsoftware/scripts/
save_depth_frame.pyCaptures one depth frame → CSV + PNG heatmapsoftware/scripts/
clearest_direction_node.pyAnalyzes bottom half of depth image into 7 clock sectors and recommends turn directionsoftware/scripts/

4. Depth Data Validation

  • Early problem: Fixed min/max values (358 mm / ~34 m) that didn’t change with scene
  • Solution path:
    • Enabled subpixel, changed depth preset to DEFAULT
    • Restricted analysis to bottom half of the image (camera is highest point on robot)
    • Confirmed with side-by-side photo + heatmap that real structure was visible (cup handle, toolbox edge, doorframe, etc.)
  • Capture method: Press Enter in save_depth_frame.py to grab one frame on demand

5. Clearest Direction Node Design Decisions

  • 7 sectors labeled as clock positions: 9:00 → 3:00
  • Only processes the bottom half of the depth image (rows 240–479)
  • Scoring: Average valid depth per sector
  • Output: Live table showing valid pixel count, average depth, best sector, and recommended turn angle
  • First successful test: Correctly switched from 3:00 to 1:00 when a cardboard box was moved into the view

6. Git Repository Organization

  • Repo: paynterf/my_vision_robot
  • Branch used: vision-to-motion-demo
  • Final structure:textmy_vision_robot/ ├── software/ │ ├── ros2/ │ │ └── my_vision_robot/ ← ROS 2 package │ └── scripts/ ← All utility scripts ├── docs/ ├── firmware/ └── .gitignore
  • Important: Scripts now live inside the repo. Old copies in ~/ were cleaned up.
  • .gitignore updated to exclude ROS 2 build artifacts, CSVs, PNGs, and backup folders

7. Verification After Moving to 4-Wheel Robot

  • Switched from ~18″ USB cable to a short 6″ high-speed cable
  • Camera still launched cleanly and produced usable depth heatmaps
  • Confirmed the entire stack still works on the new platform

8. Current Recommended Workflow

Bash

# Start camera
cd ~/my_vision_robot/software/scripts
./restart_camera.sh

# Capture depth frames (in another terminal)
./capture_depth.sh

# Run direction analysis
python3 clearest_direction_node.py

9. Open Items / Next Logical Steps

Clean residual old myrobot package from ~/ros2_ws/src/ once fully confident

Send recommended turn commands to the Teensy

Add hysteresis / minimum valid-pixel thresholds

Possibly improve sector scoring

A Flagpole for the Hobbit House

Posted 09 April 2026

Last month I decided it was time to put up a real flagpole to display the American flag in all its glory. Being an engineer, I had to research the heck out of the subject to make sure we got something we would be proud to have in our front yard. A neighbor has a flagpole and he had gotten a flag pole kit from Admiral Flags, right here in Columbus Ohio. I also asked Grok, and he (It?) came up with the same company, so we ordered a 20′ kit from them.

Reading through the instructions on Admiral’s website, I was amazed at the amount of concrete needed for the flagpole foundation. They called for 6-8 80lb bags of quick setting concrete (480 – 640lbs!) for an 18″ diameter by 29″ deep hole – yikes! Being a septuagenarian, I wasn’t very eager to manage 80lb bags, so I decided to start with 10ea 50lb bags (500lbs) and add more if needed. The Admiral website also strongly recommended calling Ohio811 ‘call before you dig’ utility mapping service, so I did that right away. As it turned out that was one of my better moves, as my first intended location was right on top of a utility run to the house. Dodged the bullet on that one!

My planned location in our front yard is at least 150′ away from any electrical outlet and the nearest water faucet, so I had some logistics issues to work through. I needed an additional 100′ water hose, and I decided to use my backup electric generator for electric power. Here’s a short video showing the distances.

From watching some videos regarding quick setting concrete I decided to go with the 5-gallon bucket and electric drill mixing paddle route, so I loaded my brand-new Toyota Tundra pickup with 500lbs of QuickCrete, 2ea Lowes 5-gallon buckets, a cheap 100′ water hose and the mixing paddle.

I had conned a neighbor into helping, so we started digging mid-morning, and had a hole pretty much done in a couple of hours. I was amazed that the first 6-10″ was pretty rocky, but after that it was pretty easy going. Here are some photos of the hole:

Hole about 3/4 full, using 50lb QuickCrete bags and some rods to keep flagpole sleeve vertical

We had to stop at this point because my 20-year-old 1/2″ drill started smoking badly. I couldn’t really complain as I had gotten a LOT of use out of it, but it was apparently time for a new one. The next day I came back with a new 1/2″ hammer-drill and finished the foundation work. I poured concrete up to grade level so we could mow around it without having to come back and weed-whack the base.

The next step (literally) was to install the flag pole itself. My stepson was visiting from St. Louis, so helped me assemble the pole kit and set it into the sleeve.

And here is a short video of the completed flag pole with the U.S. flag proudly flying. Sure wish I had done this 20 years ago, but better late than never!

Proud to be an American!

Frank

Wall-E2’s many words for ‘obstacle’

In a manner analogous to the many words for ‘ice’ and ‘snow’ in the Inuit language, Wall-E2 has many words for ‘obstacle’, as follows:

  • Wall-E2 gets too close to an obstacle in front
  • Wall-E2 gets too close to the currently-tracked wall during tracking operations
  • Wall-E2 senses an upcoming wall while tracking the current wall
  • Wall-E2 hits something that causes it to get stuck and stop moving forward but the obstacle doesn’t register on the forward sensor
  • Wall-E2 gets too close to an obstacle to the rear

Wall-E2 gets too close to an obstacle in front

If Wall-E2 isn’t currently tracking a wall and senses an upcoming obstacle, it stops, backs up, and turns away from the nearest wall. If there is no ‘nearest wall’ it simply turns one way or the other and hopes for the best ;-).

Wall-E2 gets too close to the currently-tracked wall during tracking operations

This item in the above list was an unintended consequence of Wall-E2’s new parallel offset tracking ability. After finding the parallel orientation at a distance greater than the desired offset, Wall-E2 makes a cut toward the wall to capture the offset. Unfortunately this also means the forward LIDAR measures the slant distance to the near wall, NOT the distance to any upcoming walls. In some cases this leads Wall-E2 to believe it is running into an obstacle, and instead of continuing the offset capture maneuver, it instead executes an obstacle avoidance maneuver. The fix for this problem is to recognize that when Wall-E2 is deliberately angling toward the wall, and reduce the ‘obstacle avoidance distance’ OAD accordingly. So, for a desired wall offset distance Doff and for a maximum approach angle corresponding to a steering value of -Smax (negative steering values are ‘toward the wall’), the ‘effective obstacle avoidance distance’ OADeff should be less by some factor, (arbitrarily selected here to be 2). This gives OADeff = Doff/2 for a steering value of -Smax, linearly increasing to Doff when S = 0. This is a straight line with slope 1/(2*Smax) and y-intercept of Doff for all S values <= 0. For S > 0, OADeff = Doff. Here’s a graph of the equation for Stgt between 0 & -0.3

This image has an empty alt attribute; its file name is 201112-Calc-OAD-vs-SteerValTgt.jpg

Wall-E2 senses an upcoming wall while tracking the current wall

When tracking a wall, which by definition means in MODE_WALLTRACKING with a target steering value between -S and +S, Wall-E2’s desired response to an upcoming wall is to stop and make a 90º ‘spin turn’ away from the currently-being-tracked wall at the desired wall tracking offset value, on the theory that this will place Wall-E2 in the right place to start tracking the next wall.

Wall-E2 hits something that causes it to get stuck and stop moving forward but the obstacle doesn’t register on the forward sensor

This is the classic ‘I’m stuck!’ situation detected by monitoring the mathematical variance of front distance measurements over time, as described in this post. The recovery technique used up to now has been to back up for 1-2 seconds, and then turn 90º However, recovery from this condition can be problematic, as the robot will sometimes run backwards into another obstacle and execute the reverse ‘tractor scare maneuver’ of ‘Cars’ fame. With the addition of a 7th VL53L0X time-of-flight sensor, Wall-E2 can now detect a rear obstacle before it tries to climb it, leading to a better experience for the robot and it’s owner (that would be me). Now the ‘ExecuteStuckRecoveryManeuver()’ function first assesses whether or not there is enough room behind to successfully back up. If there is, it does the following:

  • Backs up a predetermined distance using the most suitable (forward or rear) distance sensor
  • Makes a 90º turn away from nearest wall
  • Moves forward a predetermined distance using either the forward or rear distance sensor
  • Makes another 90º turn to parallel the nearest wall again

The idea here is that if Wall-E2 is stuck on a shoe or a chair leg, it might be able to go around it and continue wall tracking. If not, it can repeat this procedure until it eventually works its way around the obstacle.

Wall-E2 gets too close to an obstacle to the rear

This condition rarely/never occurs in isolation; it happens during one of the other obstacle avoidance maneuvers, and only then when Wall-E2 fails to check it’s rear clearance before starting to back up.

Cheap Yellow Display – based Solfeggio Tone Generator

Posted 21 January 2026

My wife recently asked me if I could build a ‘Solfeggio’ tone generator that she could listen to as a sleep aid. A quick net search yielded the following:

Solfeggio frequencies are a set of specific tones believed to promote healing, relaxation, and balance. The primary nine frequencies range from 174 Hz to 963 Hz, each associated with different benefits, such as emotional healing, stress reduction, and chakra alignment.

963 Hz: Connection to the divine and awakening.
These frequencies can be used in sound therapy and meditation practices to enhance well-being.

174 Hz: Pain relief and grounding.

285 Hz: Healing tissue and organs.

396 Hz: Liberating guilt and fear.

417 Hz: Facilitating change and undoing situations.

528 Hz: Transformation and DNA repair.

639 Hz: Enhancing communication and relationships.

741 Hz: Awakening intuition and problem-solving.

852 Hz: Returning to spiritual order.

As a result of a previous project to create a GPS-enabled DashCam Power Switch, I thought this would be a perfect ‘Cheap Yellow Display project. The Cheap Yellow Display (CYD) is a ESP32-2432S028R development board integrated with a 240×320 pixel color touch-sensitive display. In the case of my DashCam project, this meant that I only needed a GPS module, a low-Rds MOSFET, and a bit of circuitry to make it all work – neat. For this tone generation project, it looked like I might be able to do the whole thing with just a CWD and a pair of BT earbuds, as the ESP32-2432S028R dev board incorporates BT capability. The wife already had a pair of BT earbuds, and I had acquired two more CYD modules after it worked so well for my DashCam, so I was all set.

As I did with the DashCam project, I started consulting with Grok right away. If you haven’t yet taken advantage of this – you should – Not only can Grok code in any language, it is also mind-bogglingly fast for internet searches on particular topics. So I started out with this conversation:

Me: I want to build a rechargeable battery-operated tone generator using the ESP32/CYD configuration. I would like the battery to be the same (or smaller) width/length as the CYD. The input to the charging circuit would be +5V from the Type-C connector on the CYD. What battery and charging circuit would you suggest?

Grok: For a rechargeable battery-operated tone generator using the ESP32-based Cheap Yellow Display (CYD), which measures approximately 50mm x 86mm, you’ll want a compact 3.7V LiPo battery that fits within those dimensions for easy integration (e.g., mounted behind the board). The tone generator can use the ESP32’s built-in DAC (8-bit resolution) or PWM for audio output, connected to a small speaker or amp. Power management involves charging the battery via the CYD’s USB-C (which supplies 5V) and boosting the battery voltage to 5V for the board when unplugged, as the CYD expects 5V input on its power pins.Suggested BatteryA 3.7V 1000mAh LiPo battery with JST connector, such as a 603450 cell or equivalent.

Why this? It provides 3-5 hours of runtime for a low-power tone generator (ESP32 + display draws ~200-300mA at 5V under load; enable deep sleep modes to extend life). It’s widely available, rechargeable, and includes built-in protection (PCM) against overcharge/discharge. Smaller options like 300-500mAh (e.g., 402030 at 40mm x 20mm x 3mm) work if runtime isn’t critical, but 1000mAh balances size and capacity for ESP32 projects. ezsbc.shop +3 Avoid larger capacities (e.g., 3000mAh) if they exceed ~65mm x 35mm, as they may not fit neatly.

Dimensions: Approximately 60mm x 34mm x 5mm (fits well within 86mm x 50mm, leaving room for wiring).

Suggested Charging Circuit: Use a TP4056-based module with integrated battery protection (BMS).

  • Why this? It’s a simple, inexpensive ($1-2) 1A charger for single-cell LiPo batteries, accepting 5V input directly from the CYD’s USB-C (via wires to the module’s IN+ / IN- pads). It handles charging to 4.2V safely and includes overcharge/over-discharge protection. When USB-C is plugged in, it charges the battery; when unplugged, the module’s OUT+ / OUT- pads provide battery voltage (3.7V)
  • Additional component needed: Pair it with a small boost converter like the MT3608 (adjustable to 5V output) connected to the TP4056’s OUT pads. This boosts the 3.7V battery voltage to 5V for the CYD’s 5V pin (avoid direct 3.7V connection, as the board’s regulator expects 5V for stable operation). github.com +2 Add a switch on the boost output for power control.

There was some back-and-forth to nail down the actual modules I would need, but pretty soon I had the battery (li-ion 603450), charger (TP4056) and DC-DC converter (MT3608) on order from Amazon. The reason I went with separate charger and up-converter modules instead of the IP5306 all-in-one is due the IP5306 requiring a minimum load current to remain active, and this would conflict with my plan to put the ESP32 to sleep during idle times to extend battery life.

After getting the hardware issues resolved, the next step was the ESP32 software. I had Grok develop the software using Arduino code, with the following specs:

Me: Referencing our discussion of my ESP32 CYD Sinewave Tone Generator Project, I would like to see your take on Arduino code for the ESP32, with the following specs:

  1. Non-K&R bracket convention (beginning bracket on new line)
  2. Nine (9) touch-sensitive radio buttons for the ‘Solfeggio frequencies’ (https://www.bing.com/search?q=complete+list+of+solfeggio+frequencies&FORM=R5FD1).
  3. A volume slider (horizontal at the bottom or vertical on the side)
  4. Tone output directed to Bluetooth transmitter
  5. Sleep mode after 10 minutes from coming out of sleep mode
  6. Battery charge indicator based on A/D measurements of the LiPo battery voltage
  7. Whatever other obvious requirement that I have failed to mention

Within a few seconds, Grok produced the following code:

Not only did Grok produce initial code, but it helped me work through some subtle issues with the ‘AudioTools’ library as I had not used this library before. Grok would offer code, then I would run (or try to run) and report back on any errors encountered. Pretty soon I had code that compiled cleanly and ran, and then it was a case of exercising the code and reporting any debug output back to Grok. This went around for a while, but we eventually got to working code. I learned that while Grok is very fast, he’s not always all that accurate. For anything other than the most trivial programming task, the human at the other end of the conversation must have relevant experience. At one point in the debugging process, Grok was recommending some hardware changes on the CYD board, but I knew from prior experience with the DashCam project that no such mods were required for detecting user touches on the touch-screen. So I was able to provide Grok with a small working program to do this and Grok was happy to incorporate the new information into the complete program. This, however, brought up another ‘gotcha’. Grok takes input from the human, thinks for a few seconds, and then disgorges another complete program – without any accompanying explanation. So, I said “what was the problem with the previous code?” and got a pretty comprehensive answer- yay.

Grok really doesn’t do a very good job of troubleshooting or code debug at all – that’s where the human’s engineering experience comes in. At one point I tried to describe the ‘cut the world in two’ troubleshooting technique I have been using for decades, but I’m not sure if Grok has any ability to remember things like that, or any ability to apply them, so I suspect that was a waste of time. Grok will continue to produce code without explanation if you let it – kind of a ‘random walk’ shotgun approach AFAICT. At the end of this hours-long session, I got kind of frustrated with this behavior and terminated the session. I put up a DM to @xAI, but never got a response.

Eventually, over a 2-3 day period we got a working program that would connect via BT to my wife’s earbuds and play the tone (with some ‘reverb’ added, even!) selected by tapping a button on the CYD screen.

This program worked fine, but had a few problems:

  • The ESP32 ‘deep sleep’ mode was problematic. The mode itself worked fine (and dropped the current drain down to just a few mA, but when the system came back awake, it wouldn’t reconnect to the BT earbuds. We eventually wound up using the ‘light sleep’ mode, which would reliably reconnect to the earbuds, but drew significantly more current (less battery life).
  • The earbuds themselves were an issue. When/if the system failed to reconnect, then only way to do so was to put the earbuds back in their charging container and force them back into ‘pairing’ mode so the tone generator box could ‘see’ them again, and this was a PITA.

These issues led my wife to ask if I could change the project to use wired earbuds rather than wireless. This made the design both simpler and more complex. Simpler because now ‘sleep’ wouldn’t be an issue as the wired earbuds eliminated the need to connect via BT. More complex, because the CYD has no native stereo output, so an additional module to convert I2S protocol stereo audio data to human-audible stereo sound was required, and in turn this required finding a third output-enabled GPIO line (the CYD configuration uses most of the ESP32’s GPIO lines to run the display), and this required cutting a board trace and running a jumper to access GPIO26.

Here’s my statement to Grok:

Me: referencing our CYD Tone Generator project, I want to modify it to use plug-in wired earbuds rather than BT. I believe this will require outputting the audio on two GPIO lines (22 & 27)? and implementing an audio amplifier and a volume control. Can you recommend a good audio amplifier package for this?

Grok immediately identified a really nice module:

Grok: I recommend the PCM5102A I2S DAC module (often labeled as “PCM5102 DAC Decoder” or similar). It’s a compact, affordable breakout board widely used in ESP32 audio projects for its simplicity and good sound quality (32-bit, up to 384kHz sampling). Many versions include a built-in 3.5mm headphone jack, making it plug-and-play for wired earbuds without additional soldering for the connector.

Why this one?

Directly interfaces with ESP32 I2S output (no need for the internal DAC on GPIO 25/26).

Provides line-level analog output that can drive most earbuds (16–32Ω impedance) at reasonable volumes without distortion—users commonly report success with direct connection for low-power applications like tones.

Low noise, high SNR (112dB), and minimal external components needed.

Cheap and available: ~$5–10 on Amazon, AliExpress, or electronics sites like Adafruit/SparkFun (search for “PCM5102 I2S DAC module”).

If your earbuds are very sensitive or you need more headroom, some variants include an integrated headphone amplifier (e.g., based on LM4880M—look for “PCM5102 with headphone amp” on Tindie or AliExpress for ~$10–15).

Connections (assuming standard I2S remapping in code):

VCC: 3.3V from ESP32.

GND: Shared ground.

BCLK (bit clock): Connect to your GPIO 22 (or remap as needed).

LRCK/WS (word select/left-right clock): You’ll likely need a third GPIO (e.g., 25 or 26) for this—add it if not already in your setup.

DIN/SDIN (data in): Connect to your GPIO 27.

FMT, FLT, DMP, SCL: Ground these (or leave default for standard I2S format).

XMT/XSMT: 3.3V (unmutes the output).

Analog out: Directly to 3.5mm jack for earbuds.

So I got a couple of the PCM5102A I2S DAC modules from Amazon, and wired them up according to Grok’s instructions. 3.3V, Gnd, BCLK, DIN all came from CN1 on the CYD, but I had to cut a trace to free GPIO26 from it’s normal connection through an audio amp to the ‘speaker’ connector so I could use it for LRCK. See below for the details:

Here’s the program:

This all worked really well, except for one last major ‘gotcha’. We used GPIO35 (input only) to monitor the battery voltage, and implemented a small ‘slider’ style battery charge indicator at the very top of the display. A call to ‘analogRead()’ in loop() measured battery voltage, and this value was depicted by the length and color of the ‘slider’ display. Unfortunately it turned out that the ESP32 library required to run the PCM5102A DAC module conflicted with the Arduino library used for ‘analogRead()’ calls. As hard as this was to believe, it was real, and neither Grok nor I could figure any way around it. So, we wound up using ‘digitalRead()’ instead (which obviously will only return a ‘0’ or ‘1’) and displaying either a full-length green slider (battery good) or a full-length red slider (battery bad).

And the schematic:

GPS-enabled Dash Cam Power Switch

Posted 10 November 2025

A while ago I got a dash cam for my truck, thinking that would be a good thing to have if someone tries to cash in on my insurance by deliberately backing into me on the road. So, I got this dash cam, mounted it on the windshield of my truck, and life was good. Then my wife and I went out of town for a few days and when we came back (at 2am), the truck battery was dead and it took us a while (and a new car battery) to recover. This happened one more time, fortunately in my garage, and I decided I was going to have to come up with a way of killing the power to the dash cam when (or shortly thereafter) the truck stopped moving.

I researched the power outlets in my truck (a 2011 Ford F-150) and found that the power to the SYNC system was (at least theoretically) on only when the engine was running. Unfortunately, I had pulled the fuse for this circuit years before to keep SYNC from grabbing my phone’s Bluetooth signal even though I had long ago UNpartnered SYNC and my phone, so that was out.

My next idea was to construct a power switch around one of the recent GPS modules available cheaply, such as this one. I looked around a little bit, and found the nice ‘TinyGPS++’ library, and used a Sparkfun Pro Micro (the Arduino version) from my parts box to interface to the GPS module. Another search and I found an example sketch, and I was in business. After the usual number of errors I got the sketch working, with data displayed in the serial window of my Windows 11/Visual Studio/Visual Micro setup.

Unfortunately, all the data came back as ‘INVALID’, mostly because we live in an earth-sheltered house, and that pretty much snuffs out any GPS signals. The next day I took my laptop and the breadboard for a spin, and the GPS module started spitting out valid data about 30 seconds after I backed out of the garage. The data looks like this:

This isn’t particularly useful for my project, as I had planned to use speed < MPH_THRESHOLD for some time > ELAPSED_TIME_THRESHOLD, maybe something like 10-20 minutes. The above data has the time I need, but not the speed. Maybe the TinyGPS++ library has that capability?

Yes, the TinyGPS++ library does have the methods I want. Here’s the code I came up with:

Now to test the code. I found a free web-based app called NMEASimulator by panazzolo.com that allows me to set a course with heading and speed, and then output the corresponding NMEA sentences to a COM port. With a USB-to-Serial adapter I should be able to replace the data lines from the GPS module with the simulator output and test the code.

12 November 2025 Update:

My USB-to-Serial adapter arrived today, and I was able to connect it to my GPS switching board, bypassing the actual GPS. Then I was able to use the NMEA simulator to transmit NMEA messages to the Sparkfun Pro Micro and test the Dash Cam power switch algorithm.

This combination worked great! I started the action, NMEA sentences started flowing to the Pro Micro which output decoded speed values to the debug window. I could run the speed up and down and watch the program toggle power to the load (39-Ohm power resistor) in response to the speed changes. The only things left to do are to build an enclosure and change the time and speed threshold parameters. Here’s the ‘final’ code:

26 November 2025 Update:

Thinking about how to test my GPS-enabled power switch project, I decided to add a cheap LCD display so I could monitor GPS output and power switch status in real time. So, I reached into my parts box and what came out was a pair of ‘Cheap Yellow Display’ modules had been laying around for some time now. As I started working with the display, I realized it wasn’t just a display; the ESP32-2432S028R development board that runs the touch-sensitive display incorporates a ESP32-WROOM-32 module as its main microcontroller. This module features a dual-core Xtensa LX6 processor running at up to 240 MHz, 520 KB SRAM, 448 KB ROM, integrated Wi-Fi (802.11 b/g/n) and Bluetooth (v4.2 BR/EDR and BLE), and 4 MB flash memory. In addition, the board has a standard micro-USB port for power and programming, a USB-C port for power, three 4-pin micro-JST 1.25mm pitch connectors for UART and GPIO, and a 2-pin micro-JST connector for a speaker. Looking at the specs, I realized I could conceivably eliminate the Sparkfun Pro Micro module entirely, and have just the display/CYD connected to the GPS via CN1 (4-pin GPIO/power/GND) and a single GPIO output (plus GND) to run the power switch.

A not-so-insignificant hitch in the getalong for this project was the type of connector required to interface to the sockets on the CYD. It turns out these are ‘micro JST 1.25mm’ connectors, and it took me three tries with Amazon to get the right ones. These are the correct ones.

I was able to easily connect the GPS input to CN1 and use the NMEA simulator to test the code, ported over from the Pro Micro project. However, I hit a roadblock when I went looking for a GPIO port to use as a switch signal to the power switch board; it turns out the display occupies most of the GPIO lines, and the two ports needed for GPS Txd & Rxd took the only remaining output-capable lines (IO35 is available, but it is input-only). Some more research with the help of Grok led me to this project site that describes how to free up ports via hardware mods. There I found directions for freeing up IO21, the port that normally controls display backlight brightness by PWM-ing the backlight LED. This involved removing a surface-mount transistor and re-orienting a TINY! surface-mount 3.9 Ω current-limiting resistor, as shown in the screenshot below:

As you might guess, this took some doing, as these parts are TINY!

But after a couple of false starts, I got it done:

The photo below shows the CYD module with GPS data displayed, and with my newly-liberated IO21 line connected to the power switch board (the illuminated red LED shows that the power switch is ON)

So now we are pretty close to being done; all that is left is to mount the display/controller into its box, mount both the display & power switch boxes onto their custom-printed carrier, and test the whole thing in my truck. The ‘final’ (to the extent that anything I do is actually final) code is shown below:

Here is a short video of the CYD display and power switch during a driving test in my local development loop. At the start of the video, the GPS module has already detected 8 sats (hard to see), so as soon as the computed speed goes over 5mph, the power switch changes from OFF to ON (red LED on the power switch and ‘ON’ on the CYD.

Unfortunately it was at this point that I discovered that the BUZ76A MOSFET power switch had too high of an Rds (around 4Ω) to actually turn on the camera -bummer!

Some discussion with Grok netted me a low-Rds MOSFET – the AOD424G AOD424 TO252 with Rds in the milli-Ohms. This has a pretty low Vds(max) of around 20V, but this is fine for my needs as it will only be switching 5V. Here’s the new schematic:

01 December 2025 Update:

As things are coming together on the electronics side, I took some time to investigate mounting options in my truck. After fiddling around a bit, I came up with the following possibility:

This would actually work out great, as the PETG plastic boxes wouldn’t be directly exposed to sunlight, and, the display would still be very visible to the driver, and it wouldn’t block vision to my time and temperature display at the top of the radio stack. As a huge bonus, it turns out there is enough slack in the USB power cable to the dash camera that I could plug it directly into the switch box (green in the photo above, if I changed out one of the USB type-C chassis connectors for a USB2/3 type. $7 for 5 connectors from Amazon – delivery in less than a week – yay!

Of course this means I need to change the box design, but that won’t take long.

08 December 2025 Update:

I got my new USB2 connector, and redesigned the box & lid to accommodate it, creating a conundrum in the process.

See the problem?

When I started wiring everything in, I realized I had created a big problem. If I ever wanted to remove the proto-board from the enclosure, I would have to unsolder all the wires to the USB connectors first – UGH!! I thought I could solve this problem by adding connectors in the lines to the USB connectors, but I hated that idea as it just adds complexity (“a connector is a source of trouble between two other sources of trouble”), so I put the whole thing aside to await a better solution. This morning while exercising it came to me – redesign the enclosure so the parting line between box and lid is right at the top edge of the connector modules. Then the proto-board and connectors can be lifted right out after removing the two proto-board mounting screws – genius! Only one small ‘gotcha’ the USB-C connectors will have to be flipped 180𝆩 so the PCB slot is on top rather than on the bottom. Since USB-C connectors are orientation insensitive anyway, this shouldn’t be an issue. Here’s a screenshot of the ‘final’ (as final as anything gets in my world) enclosure, with the connector cutouts moved to the top so removing the lid will allow the PCB – with the connectors still attached – to be lifted out.

Here are some photos of the ‘finished’ product:

After installing it in my truck, I made a couple of test runs around my little development’s circle, and it seems to be working great. Here are some photos showing the installation in my truck:

Here is the ‘finished’ version of the firmware:

15 December 2025 Update:

Now that things seem to be working, I’m trying to clean up the installation a bit. Here are some photos showing some of the current temporary installation details:

I also took the opportunity to measure the current load with the display and switch box electronics powered, but with the dashcam OFF. This is important, as this load determines how long the battery will last in this configuration. I used my handy-dandy USB2 ‘Charger Doctor’ pass-through device that alternately displays output voltage and current. As shown in the following photo, the ‘OFF’ current draw is about 0.19A.

Showing current draw in the ‘dashcam OFF’ configuration. The meter shows 0.19A (190 mA).

The 12V car battery in this truck is a ‘DieHard’ AGM Technology battery as shown below:

So I asked Grok how long this battery would last with the dashcam OFF, but the display (and it’s imbedded MCU) still powered up with a 0.2A draw, and this is what I got back:

So almost a month – that sounds about right, and it means I wouldn’t have had a dead battery at 2 am when my wife and I got home from last summer after a week at the 2025 Senior Games in Des Moines, Iowa!

Just as a sidenote, I have a ‘premium’ subscription to X not because I think X posts are worthwhile, but because Grok *IS* worthwhile. As a long-time electrical engineer, I was familiar with the property of lead-acid batteries that extended the discharge time under very small loads compared to the normal A-Hr rating, but I had never heard of the ‘Peukert effect’, which allows for a much better estimate of discharge times for low loads. This took Grok about 30 seconds – what a *great* engineering assistant!

And, speaking of Grok, I also asked him (it?) for the name and purpose of the ‘cubbyhole’ I’m using as the mounting cavity for the dashcam switch project. Turns out this was probably originally intended for an ashtray, and then repurposed as the mounting cavity for a trailer brake module. And, he also came up with dimensions, which should allow me to design a cradle that will fit snugly in the hole and keep the dashcam electronics from sliding around. We’ll see!

17 December 2025 Update:

A couple of interesting things happened in the last two days. The first thing is I have been cursing at the maker of the SuperONE cigar-lighter automobile power adapter for providing a USB-C retractable cable that doesn’t deliver +5V to power my dashcam switch module.

Because of this, I gave the product a ‘1’ review and described my methodology in determining that the retractable USB-C cable just wasn’t providing +5V. This got the company’s attention, and they offered to send me a new one, free of charge. I said, ‘OK’ but if the new one doesn’t work either, then my review will be edited to say that. Well, the new one arrived, and the same thing happened – no +5V to my project box. However, this time I was smart enough to try plugging the cable into my BT headset, and voila – it started charging the headset! Hmm… After some back and forth with Grok I learned that a ‘Power Delivery’ (PD) device doesn’t deliver power to a connection until it is asked to do so. The BT headset did, my cheap USB-C chassis receptacle didn’t – oops! So, I wound up editing my review from a 1 to a 5 and explained why; I suspect that will make the SuperONE folks a lot happier ;). Also, I ordered a different USB-C receptacle – one that has the smarts to request power from the USB-C cable.

The second thing that happened is I’m trying to figure out how to mount my dashcam power switch project into the ashtray/trailer brake ‘cubbyhole’ (see the photos above). And because I have not only one – but two 3D printers it naturally involves 3D printed parts. I designed and printed a shape that fits nicely into the cavity, and then I’ll need to figure out exactly how the combined assembly can be fastened into the cavity in a way that can be removed easily, if necessary, but also won’t just fall out at the slightest provocation.

1;8 December 2025:

After getting the enclosure issues resolved, I turned my attention to the GPS module, which is kind of hanging around on the shelf above the central audio panel. I’d like to build a small enclosure just to hide it away, leaving only the small GPS antenna itself and the even smaller RF cable leading down to the GPS module visible.

22 December 2025 Update:

I received and installed the new PD-enabled USB-C module, and it went in OK, with the usual curse-words and adaptations:

I printed up a small right-angle adaptor to mate the holes on the USB-C module with the holes on the box, but I had to mount the module on the inside wall – I decided not to make a new box just for this issue. So now The PD-enabled retractable cable on the cigar-lighter adaptor delivers 5V to the module and life is good!

Here’s the ‘final’ installation (nothing is every really ‘final’ in the Paynter world)

23 January 2026 Update:

I’ve been using the DashCam power switch for about a month now, and it seems to be working great. A couple of weeks ago I modifed the ‘Too Slow’ and ‘No Data’ readouts to change the unit from ‘sec’ to ‘min’ and ‘hours’ as appropriate; trying to figure out what ‘83520 sec’ meant was too hard for my brain, so I changed the code so this number would read ‘23.2 hours’

I also noticed that when I drive into my earth-sheltered garage, I get some strange status display readings. The number of sats will go to zero, but the clock time will often stay running (and stay accurate). Also, since the clock is active, the speed value will actually start to climb (divide by-almost-zero?) and so the speed criteria for keeping the power ON will be met. I think the way to handle this is to have a separate block in the logic for sats < min_satellites to force everything but the ‘Too Slow’ and ‘No Data’ timers to show “n/a”

26 January 2026 Update:

The dashcam switch is working well, but I’m not happy with the physical installation. I thought I had a great idea by putting it into the space originally designed for an ashtray, but it turns out not to be such a hot idea. The gear selector lever blocks out a significant amount of the display in ‘D’, and looking down at the display (and peering around the gear shifter lever) isn’t great. So, I decided to ‘re-imagine’ the entire installation.

The original installation features separate boxes for the display and the switch, which wastes a LOT of space. Starting from a clean sheet, I laid out the the switch circuitry and GPS module on a plug board, as shown in the following photo.

Required hardware for complete GPS-to-CYD implementation.

As shown above, the power input (USB-C), power output (USB2), switch (AOD424 MOSFET) and GT-U7 GPS modules easily fit into the footprint of the CYD display. I should be able to put everything into one box with the footprint of the CYD and sufficient depth to accommodate the switch & GPS hardware

However, the display area where I want to install the new version of the dash cam features a deeply recessed display area, and a complex profile. So, I needed to somehow model the profile so the box design would work. As the following photos show I used a combination of ‘cut & try’ for the first rough estimate, and then a contour gauge to get an estimate of the rest of the profile.

Then I used my 3D printer magic to print out successive profile estimates until I got reasonably close to the actual profile. This then can be used for the actual box design. The following photo shows the progression from left to right.

After getting the profile reasonably close, I printed a prototype box by extruding out the profile a few tens of millimeters, and got this result:

After a few more iterations I printed a full-size box:

Here’s a photo showing the old and new installations:

The above photo shows the old and new installations. The new installation will actually move to the left a bit as shown below, as it will incorporate the GPS module internally.

This will still hide the truck’s clock display, but since the new unit will also display time, that’s not a loss of functionality.

The next step was to disassemble the old installation to reuse the parts:

As can be seen from the above photo, there is quite a bit of wasted space in the power switch box, and I also figured out that one of the two USB-C chassis-mount connectors can also be removed, as the power connection from the power-switch circuit to the display will be routed internally – no need for a separate USB-C cable. I breadboarded the power switch circuit again, this time with compactness in mind, and wound up with this:

and then transferred the parts to a small piece of DIY circuit board. Then the whole thing fits nicely into the new enclosure, as shown in the following photos:

After more than a few hours of frustration due to the lack of a reliable GPS signal in the house, I think I finally got everything running properly, and tested it on my subdivision loop:

Here’s the updated schematic, with a missing ground wire added and with signal/control GPIO numbers added:

Stay tuned,

Frank