YoBatt - Bidirectional Battery Charging
Our boat Yoto has two battery banks — a 48V LiFePO4 bank that gets charged by solar, and a 12V AGM bank connected to the alternator. The problem is getting energy between them. When the sun's out, the 48V bank fills up but the 12V needs topping off. When the engine's running, the alternator is pumping into 12V — energy that could be going back to 48V storage instead.
There are off-the-shelf solutions for one-way DC-DC charging, but I wanted something bidirectional — automatically deciding which direction to transfer based on what each bank actually needs. And critically, it can never drain the 12V bank. Lose 12V on a boat and you lose engine start, bilge pumps, and navigation. That's a bad day.

The Hardware
The heart of it is a Calex 48S12 DC-DC converter, which can move power in either direction — stepping 48V down to charge 12V (buck mode) or boosting 12V up to charge 48V (boost mode). It takes commands over CAN bus, which is where the Raspberry Pi comes in. I'm using a Pi 4 with a PICAN Duo CAN hat to talk to the converter and make all the charging decisions.
The Software
Using AI to help, the controller is a Python asyncio application. It runs a state machine that evaluates both battery banks and decides what to do:
BUCK: Solar bank is full, 12V needs a charge — step down and top off the AGM
BOOST: 12V is healthy and 48V could use more — harvest alternator energy up to 48V
IDLE: Neither bank needs attention, or conditions aren't right for a transfer
The charging follows proper multi-stage profiles for each chemistry. AGM gets bulk, absorption, and float. LiFePO4 gets bulk and absorption (no float needed). Rather than implementing complex current regulation in software, I just set voltage and current targets over CAN and let the Calex's built-in regulation handle it. The controller decides what to ask for and when to change stages.

Keeping the 12V Safe
This is where I got a little obsessive. Three independent layers prevent the system from ever draining the 12V bank:
The charge logic never even recommends boost mode if 12V is below 12.4V
The state machine has its own guard that blocks boost entry
A separate safety monitor runs every 100ms and will emergency-stop boost if 12V drops
The safety monitor also watches for CAN communication timeouts, hardware faults from the converter, and high temperatures. It runs independently from the main controller — if the control logic has a bug, the safety task can still shut things down.
One detail I'm particularly happy with: the CAN heartbeat to the converter is sent by the Linux kernel's broadcast manager, not by Python. If the Python process stalls for any reason — garbage collection, a bug, whatever — the kernel keeps talking to the converter. No single point of failure.

The Dashboard
I built a web UI so I can monitor everything from my phone. It shows both battery banks with live voltage, current, and power readings, plus the converter's status and what mode it's in. There are charts for voltage trends, temperature, and energy transferred over time. I can also manually idle the system or hit an emergency stop from the browser. It pushes updates over WebSocket so the dashboard feels real-time without constant polling.