geoffwilliams@home:~$

Raspberry Pi IP camera - Part II

Lets put the learnings from last weeks research into practice by making a useful “dumb” IP webcam with pan/tilt support

Step 1: Hardware upgrade

I treated myself to the Raspberry Pi AI Camera, because it supports the project and has some seriously cool features like AI models applying inside the camera.

The camera itself is still tiny and weighs only a few grams. It easily secures to my 3d printed pan/tilt mechanism with some double sided foam tape and doesn’t overload the servos. This is a huge time saver vs designing a camera mount from scratch. I might have a go one day though.

Step 2: FFMPEG

FFMPEG expects to work with video4linux devices, but in bookworm the kernel driver is removed and raspberry pi specific userland drivers are used instead so we must RTFM.

Note that /dev/video0 still exists and has some level of functionally but its a shim rather then a device thats expected to work, so we can find out infos but attempting to capture with ffmpeg fails with IOCTL errors because there is no backend device.

Instead, to use FFMPEG, you must use rpicam-vid and pipe the results to ffmpeg. Highly confusing and disappointing a useable V4L device does not manifest. I don’t consider switching to the “old” camera stack a workable alternative.

Step 3: MediaMTX

Reading further down the manual, MediaMTX directly supports raspberry pi cameras which means we can actually skip FFMPEG altogether now. This is a welcome improvement.

I made MediaMTX start on reboot and configure itself for Raspbery Pi Camera very easily:

/etc/mediamtx.yml

paths:
  cam:
    source: rpiCamera

/etc/systemd/system/mediamtx.service

[Unit]
Wants=network.target
[Service]
ExecStartPre=/bin/sleep 30
ExecStart=/usr/local/bin/mediamtx /etc/mediamtx.yml
[Install]
WantedBy=multi-user.target

Then enable the service and reboot

systemctl enable mediamtx

Test in google chrome, camera feed working

Step 4: Quick REST API for pan/tilt

The camera needed moving to look at the 3d printer so immediately needed a quick way to pan and tilt. I knocked up a very simple API to move in 1 degree increments:

from fastapi import FastAPI
from fastapi.responses import JSONResponse
import uvicorn
import board
import busio
from adafruit_pca9685 import PCA9685
from adafruit_motor import servo
import time

app = FastAPI()

position = {
    "pan": {
        "min": 10,
        "max": 120,
        "current": 60
    },
    "tilt": {
        "min": 10,
        "max": 170,
        "current": 45
    }
}


# Create the I2C bus interface
i2c = busio.I2C(board.SCL, board.SDA)

# Create the PCA9685 instance
pca = PCA9685(i2c)
pca.frequency = 50  # Standard servo frequency

# Servo on channel 0
servo_pan = servo.Servo(pca.channels[0])

# Servo on channel 1
servo_tilt = servo.Servo(pca.channels[1])

def move_servo(servo, new_angle):
    servo.angle = new_angle
    time.sleep(0.01)

# tilt is inverted
@app.get("/down")
def move_down():
    position["tilt"]["current"] 
    want = min(position["tilt"]["current"] + 1, position["tilt"]["max"] )
    move_servo(servo_tilt, want)
    position["tilt"]["current"] = want
    return JSONResponse({"action": "up", "want": want})


@app.get("/up")
def move_up():
    want = max(position["tilt"]["current"] - 1, position["tilt"]["min"] )
    move_servo(servo_tilt, want)
    position["tilt"]["current"] = want
    return JSONResponse({"action": "down", "want": want})


@app.get("/left")
def move_left():
    want = min(position["pan"]["current"] + 1, position["pan"]["max"] )
    move_servo(servo_pan, want)
    position["pan"]["current"] = want
    return JSONResponse({"action": "left", "want": want})


@app.get("/right")
def move_right():
    want = max(position["pan"]["current"] - 1, position["pan"]["min"] )
    move_servo(servo_pan, want)
    position["pan"]["current"] = want
    return JSONResponse({"action": "right", "want": want})


@app.get("/")
def root():
    return {"message": "Use /up, /down, /left, /right to move."}


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

This works great and I can operate it on the command line very easily:

curl http://pispy.untrusted.asio:8000/up
curl http://pispy.untrusted.asio:8000/down
curl http://pispy.untrusted.asio:8000/left
curl http://pispy.untrusted.asio:8000/right

For now, I just move the camera and then exit the API.

Step 5: Testing

To test, I kicked off a 17 hour print and am watching on Chrome. So far, so good:

videoframe

Post comment

Markdown is allowed, HTML is not. All comments are moderated.