The Arduino Mini Camera OV2640 2MP Plus And The Pico

An Arduino Mini Camera OV2640 2MP Plus

I’m starting to realise that “compatible with the Pico” might have as much meaning as “sure it’s possible to ride a tiger”: “compatible” is not the same as “conducive” in the same way that “possible” is not the same as advisable.

Perhaps I was boosted by the “ease” with which I got the Pico and e-ink display working, but for my next project I have my sights on a wildlife camera for the garden. I realise there are plenty on the market, but they all seem to require a row batteries, or need charging every few days, or a mains power cable. All of these are possible, but for me not ideal.

My ideal wildlife camera would be charged from solar (which may or may not be possible) and would automatically upload images to a server. Both these things mean that once I put it up somewhere I’ll never have to touch it again. (Hah!)

There seemed to be no reason why a Pico W, motion sensor and camera couldn’t do most of the work, and then I’d assess the viability of solar power later. Battery power, as I said, is not ideal, but not a deal-breaker. I’ll also need to make sure that if the Pico is out in the garden that it can still connect to the Wifi, because I didn’t want to have to store images locally.

Knowing that I had to take small steps, I decided to get the camera part working first. I bought a 2mp camera that claims compatibilty with the Pico, the Arduino Mini OV2640 2MP Plus, and instantly hit the first hurdle when I tried to find code to run it on.

To cut a long story short, I found I needed to be using CircuitPython (what’s that?) instead of Micropython. That proved to be a non-issue, although slightly annoying as I was just getting used to the libraries available in Micropython.

The official example worked, once I’d followed the instructions to the letter, but all it did was display the output of the camera in a bespoke Windows app. At least it told me that the camera was wired up properly and the code on the Pico was working, but it didn’t give me much of a clue as to how to get images into a state where I could do something with them, preferably something like a JPG file, but at least a byte array.

More Googling brought back some clues, and eventually I had something that appeared to be reading from the camera. Except it was hard to tell what I was reading as there’s nowhere to save the images. Circuitpython mounts the Pico memory as an external drive, and that locks out any attempts in code to write to it. It’s possible to change it to writeable mode, but then it’s a pain to change it back again to put more code onto it.

In my simple little mind I’d thought I could solve the problem by POSTing the output of the camera to a lightweight web server somewhere. Except the problem of base64 encoding bits reared its head. No problem, I thought: installing a base64 encoding module will fix that. Except, even a 7kb JPG caused an out of memory error when I tried to encode it. Base64 encoding seemed to be right out.

Like Inception, I needed to go deeper, so I dug into the Sockets documentation, both for Python and CircuitPython. Half a day later I was getting somewhere, managing to pipe JPG pixels straight from the camera to a tiny socket connection on my PC and save them to the hard drive. The problem was that only about one in four images seemed to be valid.

More long story to cut short, but just after I capture the image in the code I’ve added

time.sleep(1)

and that seems to have “fixed” it. Le shrug.

For posterity, here is the v1 of “click run and it’ll send a picture somewhere” code, with Wifi and IP details placeholdered, and using the Arducam library:

import time
import os
from digitalio import DigitalInOut
import wifi
import socketpool

#import wifi
import gc
from Arducam import *

gc.enable()

led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
led.value = False

wifi.radio.connect(ssid='SSID',password='WIFI PASSWORD")

mycam = ArducamClass(OV2640)
mycam.Camera_Detection()
mycam.Spi_Test()
mycam.Camera_Init()
time.sleep(1)
mycam.clear_fifo_flag()
mycam.OV2640_set_JPEG_size(OV2640_1600x1200)

def get_still(mycam):
    mycam.flush_fifo()
    mycam.clear_fifo_flag()
    mycam.start_capture()
    once_number = 1024
    buffer=bytearray(once_number)
    count = 0
    finished = 0
    
    time.sleep(1)

    pool = socketpool.SocketPool(wifi.radio)
    s = pool.socket(pool.AF_INET, pool.SOCK_STREAM)
    s.connect(("SERVER IP ADDRESS", SERVER PORT))
    
    length = mycam.read_fifo_length()
    mycam.SPI_CS_LOW()
    mycam.set_fifo_burst()
    
    while finished == 0:
        mycam.spi.readinto(buffer, start=0, end=once_number)
        count += once_number
        s.send(buffer)
        
        if count + once_number > length:
            count = length - count
            mycam.spi.readinto(buffer, start=0, end=count)
            s.send(buffer)
            mycam.SPI_CS_HIGH()
            finished = 1
    s.close()
    gc.collect()

get_still(mycam)

And the Python server code to receive and save the image with consecutive numbered file names, in the most basic POC (piece of crap?) form possible:

from os import curdir
from os.path import join as pjoin
import socket

HOST = IP ADDRESS  # IP of this server
PORT = PORT NUMBER  # Port to listen on
photo_num = 0

while True:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind((HOST, PORT))
        s.listen()
        conn, addr = s.accept()
        with conn:
            with open(pjoin(curdir, 'photo_{0}.jpg'.format(photo_num)), 'ab+') as fh:
                while True:
                    part = conn.recv(1024)
                    if len(part) <= 0:
                        break
                    fh.write(part)
            fh.close()
            conn.sendall(b"Complete")
    photo_num = photo_num + 1

So far so feasible at least, so the next step is to prototype movement sensing. In the latest edition of “famous last words”: the movement (or PIR) sensor part looks like it might be quite easy. On to that next!