When searching for raspberry pi project ideas, most lists stop at magic mirrors, basic weather stations, or retro gaming consoles. If you want to build something that bridges embedded software, power electronics, and mechanical actuation, a Dual-Axis Solar Tracker with real-time power telemetry is the ultimate workbench project. It forces you to deal with I2C bus capacitance, PWM servo jitter, and real-world sensor noise—skills that translate directly to industrial automation and robotics.

This guide walks through building a closed-loop solar tracker that uses light-dependent resistors (LDRs) for fine-tuning and an INA219 shunt monitor to log the actual power yield of a small solar panel. We will use the Raspberry Pi 4 Model B as the brain, offloading the heavy PWM lifting to a dedicated driver to keep the Pi's CPU free for data logging.

Hardware Spec Sheet & Bill of Materials (BOM)

Before we wire anything, let's look at the exact hardware required. Sourcing the right variants is critical; generic clone boards often lack the necessary I2C pull-up resistors, which will cause bus lockups later. The total BOM cost sits around $75-$90 depending on your existing inventory.

Component Exact Model / Variant Qty Est. Price Role in System
Microcontroller Raspberry Pi 4 Model B (4GB RAM) 1 $55.00 Core logic, I2C master, data logging
Servo Driver Adafruit 16-Channel PCA9685 (Product ID: 815) 1 $14.95 Generates hardware PWM for servos via I2C
Current Sensor Adafruit INA219 DC Current Sensor (Product ID: 904) 1 $10.95 Measures panel voltage and current (shunt)
ADC Module Adafruit ADS1115 16-Bit I2C ADC (Product ID: 1085) 1 $9.95 Reads analog LDR voltages (Pi lacks native ADC)
Actuators TowerPro MG996R High-Torque Servos (Metal Gear) 2 $12.00 Pan (Azimuth) and Tilt (Elevation) movement
Sensors GL5528 LDRs (5mm) + 10kΩ 1/4W Resistors 4 $4.00 Form voltage dividers for light direction sensing
Power Supply Mean Well LRS-35-5 (5V 7A Enclosed PSU) 1 $18.00 Dedicated 5V rail for servos (prevents Pi brownout)
Safety Note: If you scale this project to use a solar panel larger than 50W, the short-circuit current (Isc) can exceed 3A and melt standard 22 AWG breadboard jumper wires. Always use an appropriately sized MPPT charge controller and inline fuse for panels exceeding 12V/50W.

Pin Mapping & Wiring Procedure

The Raspberry Pi's 40-pin header is shared across multiple functions. We are using the primary I2C bus (Bus 1) for all three peripherals. Because the PCA9685, INA219, and ADS1115 all have distinct default I2C addresses (0x40, 0x41, and 0x48 respectively), they can coexist on the same SDA/SCL lines without multiplexing.

I2C & Power Pinout Table

Pi 4 Physical Pin BCM GPIO Function Destination Module
Pin 1 3V3 Power VCC (Logic) ADS1115 VDD, INA219 VCC
Pin 3 GPIO 2 (SDA1) I2C Data All Modules (SDA)
Pin 5 GPIO 3 (SCL1) I2C Clock All Modules (SCL)
Pin 6 GND Common Ground All Modules (GND)
Pin 4 5V Power V+ (High Current) PCA9685 V+ (from Mean Well PSU)

Wiring Steps:

  1. Isolate Servo Power: Do NOT power the MG996R servos from the Pi's 5V rail. Connect the Mean Well 5V PSU directly to the PCA9685's green screw terminal block (V+ and GND).
  2. Common Ground: Run a wire from the Mean Well PSU GND to the Raspberry Pi GND (Pin 6). Without a shared ground reference, the I2C logic levels will float and cause erratic behavior.
  3. Build LDR Dividers: Wire the 4 LDRs in voltage divider configurations with the 10kΩ resistors. Route the analog mid-points to ADS1115 channels A0, A1, A2, and A3.
  4. Connect the INA219: Place the INA219 in series with the positive lead of your small test solar panel. Ensure the 'IN' side faces the panel and the 'OUT' side faces your load or charge controller.

Python Telemetry & Tracking Code

Target Board: This code is written for the Raspberry Pi 4 Model B (4GB) running Raspberry Pi OS (Bookworm 64-bit). It utilizes the Adafruit Blinka compatibility layer.

Before running the script, install the required libraries:

sudo apt install python3-pip i2c-tools
pip3 install adafruit-blinka adafruit-circuitpython-pca9685 adafruit-circuitpython-ina219 adafruit-circuitpython-ads1x15

Save the following code as solar_tracker.py. Notice the explicit error handling wrapped around the I2C initialization—a necessary precaution when dealing with physical wiring on a workbench.

import time
import board
import busio
import adafruit_pca9685
import adafruit_ina219
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn

# --- PIN & I2C DEFINITIONS ---
# Using default Pi I2C pins: SDA = GPIO2 (Pin 3), SCL = GPIO3 (Pin 5)
i2c = busio.I2C(board.SCL, board.SDA)

# Servo PWM mapping (PCA9685 channels)
AZIMUTH_SERVO_CH = 0
ELEVATION_SERVO_CH = 1

# LDR mapping (ADS1115 channels)
# Top-Left, Top-Right, Bottom-Left, Bottom-Right
LDR_TL = ADS.P0
LDR_TR = ADS.P1
LDR_BL = ADS.P2
LDR_BR = ADS.P3

def init_hardware():
    """Initialize I2C peripherals with explicit error handling."""
    try:
        pca = adafruit_pca9685.PCA9685(i2c, address=0x40)
        pca.frequency = 50 # Standard 50Hz for MG996R servos
        
        ina = adafruit_ina219.INA219(i2c, addr=0x41)
        ina.bus_adc_resolution = ina.ADCResolution_12BIT
        ina.shunt_adc_resolution = ina.ADCResolution_12BIT
        ina.calibration_32V_1A() # Adjust based on your solar panel specs
        
        ads = ADS.ADS1115(i2c, address=0x48)
        ads.gain = 1 # +/- 4.096V range
        
        print("[OK] All I2C devices initialized successfully.")
        return pca, ina, ads
        
    except ValueError as e:
        # Catches missing devices (e.g., wrong address or disconnected)
        print(f"[FATAL] I2C Device Missing: {e}")
        print("Check physical wiring and run 'sudo i2cdetect -y 1'.")
        exit(1)
    except OSError as e:
        # Catches bus lockups or missing pull-ups
        print(f"[FATAL] I2C Bus Error: {e}")
        print("Check SDA/SCL pull-up resistors and cable lengths.")
        exit(1)

def move_servo(pca, channel, angle):
    """Convert 0-180 degree angle to PCA9685 PWM duty cycle."""
    # MG996R typically expects 1ms to 2ms pulse width (approx 2.5% to 12.5% duty at 50Hz)
    # Blinka handles the raw 12-bit PWM conversion via the Servo class, 
    # but for raw channel access: duty_cycle = int((angle / 180.0) * 65535)
    # Using a simplified linear map for this demo (calibrate for your specific servo)
    min_duty = 3276  # ~1ms
    max_duty = 8192  # ~2ms
    duty = int(min_duty + (angle / 180.0) * (max_duty - min_duty))
    pca.channels[channel].duty_cycle = duty

def track_light(ads):
    """Read LDRs and calculate pan/tilt error."""
    tl = AnalogIn(ads, LDR_TL).voltage
    tr = AnalogIn(ads, LDR_TR).voltage
    bl = AnalogIn(ads, LDR_BL).voltage
    br = AnalogIn(ads, LDR_BR).voltage
    
    # Calculate horizontal and vertical imbalance
    # Positive values mean light is stronger on the Right/Bottom
    horiz_error = (tr + br) - (tl + bl)
    vert_error = (bl + br) - (tl + tr)
    
    return horiz_error, vert_error

if __name__ == "__main__":
    pca, ina, ads = init_hardware()
    
    current_az = 90  # Start centered
    current_el = 90
    
    try:
        while True:
            # 1. Read Power Telemetry
            bus_voltage = ina.bus_voltage
            current_ma = ina.current
            power_mw = bus_voltage * current_ma
            
            # 2. Track Light
            h_err, v_err = track_light(ads)
            
            # 3. Apply Proportional Control (P-Controller)
            # Deadband of 0.2V prevents jitter from ambient light noise
            if abs(h_err) > 0.2:
                current_az += 1 if h_err > 0 else -1
            if abs(v_err) > 0.2:
                current_el += 1 if v_err > 0 else -1
                
            # Clamp angles to physical servo limits
            current_az = max(10, min(170, current_az))
            current_el = max(10, min(170, current_el))
            
            move_servo(pca, AZIMUTH_SERVO_CH, current_az)
            move_servo(pca, ELEVATION_SERVO_CH, current_el)
            
            print(f"Pwr: {power_mw:.1f}mW | Az: {current_az}° | El: {current_el}°")
            time.sleep(0.5)
            
    except KeyboardInterrupt:
        print("\n[INFO] Halting tracker and disabling servos.")
        pca.deinit()

Debugging: I2C Faults and Servo Jitter

When building hardware projects, things will fail on the first boot. The most common failure mode in multi-device I2C setups is bus contention or voltage sag. If your script crashes, look for these exact error strings in your terminal.

Ranked Causes for I2C Failures

Error String 1: ValueError: No I2C device at address: 0x40

  • Cause A (Most Likely): The PCA9685 is not receiving 3.3V logic power. You may have wired VCC to the 5V servo rail instead of the Pi's 3.3V Pin 1.
  • Cause B: The I2C address jumper on the PCA9685 board is bridged, shifting the address from 0x40 to 0x41, colliding with the INA219.

Error String 2: OSError: [Errno 121] Remote I/O error

  • Cause A (Most Likely): Missing pull-up resistors on the SDA/SCL lines. The Adafruit breakouts include 10kΩ pull-ups, but if you are using raw clone modules, you must add 4.7kΩ resistors between SDA/SCL and 3.3V.
  • Cause B: I2C bus capacitance is too high. If your wires exceed 30cm (12 inches), the signal edges degrade. Switch to a lower I2C baud rate or use an I2C bus extender like the PCA9615.
The First 3 Things to Check When It Fails:
  1. Run the bus scan: Execute sudo i2cdetect -y 1 in the terminal. You should see a grid with 40, 41, and 48 populated. If you see dashes everywhere, your wiring or ground is wrong.
  2. Verify SDA/SCL orientation: It is incredibly easy to swap Pin 3 and Pin 5. Swap them and run the scan again.
  3. Measure the 5V rail under load: If the servos twitch and the Pi reboots, you are experiencing a brownout. Use a multimeter to measure the Mean Well PSU output while moving the servos. If it drops below 4.8V, upgrade your PSU or wire gauge.

Fixing Servo Jitter

If your MG996R servos hum or jitter when they should be holding still, the issue is rarely the code. It is almost always power supply noise coupling into the PWM signal. Ensure your servo power ground is tied directly to the PCA9685 GND terminal, not daisy-chained through a breadboard. Breadboard contacts have enough resistance to create ground loops that manifest as PWM jitter.

Scaling the Build: Extensions and Simplifications

Not every workbench needs a full dual-axis closed-loop system. Here is how you can adapt this build based on your constraints.

How to Simplify the Build

If you want to reduce the BOM cost and wiring complexity, drop the ADS1115 and the LDRs entirely. Instead, implement Astronomical Sun Tracking. By installing the pysolar Python library, you can calculate the exact azimuth and elevation of the sun based on your GPS coordinates and the current RTC time. This eliminates analog noise, cloud-tracking errors, and the need for an ADC. The INA219 can still be used purely as a data logger to verify the panel's output curve against the theoretical solar path.

How to Extend the Build

For those looking to push this into a production-grade IoT node:

  • Add MQTT Telemetry: Integrate the paho-mqtt library to push the INA219 power readings to a local Mosquitto broker. Feed this into Grafana via InfluxDB to visualize your solar yield over time.
  • Upgrade to Stepper Motors: MG996R servos have internal plastic potentiometers that wear out and drift over months of continuous use. Swap them for NEMA 17 stepper motors driven by TMC2209 silent drivers. You will need to implement limit switches to establish a 'home' position on boot, but the holding torque and precision will vastly outclass servos.
  • Implement MPPT Logic: Use the Pi to dynamically adjust the load on the solar panel to find the Maximum Power Point Tracking (MPPT) sweet spot, rather than just pointing at the sun. This requires adding a digitally controlled buck converter to the INA219's output side.

Building a solar tracker bridges the gap between writing software and moving physical mass. By logging the actual power data via the INA219, you transition from a 'blinking LED' hobbyist to an embedded systems engineer capable of measuring real-world efficiency. Grab your multimeter, verify your I2C pull-ups, and start tracking.