Live Asset Tracking with the OzzMaker SARA-R5 LTE-M GPS + 10DOF and Traccar

Ever wanted to know where your caravan, boat, trailer or ute is — right now, from your phone — without paying a monthly subscription to a tracking company?

In this guide we will build a complete, self-hosted live asset tracker using:

  • An OzzMaker SARA-R5 LTE-M GPS + 10DOF board on a Raspberry Pi Zero
  • Traccar, a free and open-source GPS tracking server
  • A free “Always Free” VM on Oracle Cloud to host Traccar
  • A PPP data connection over LTE-M to upload positions
  • Geofencing, so you get an alert the moment your asset leaves (or enters) an area you define

Why Traccar?

Traccar Main page

Traccar is a mature, open-source GPS tracking platform used by everyone from hobbyists to commercial fleets — and self-hosting it means you get all of its features for free, with your data staying on your own server. Highlights include:

  • Live tracking — watch all your devices move on a map in real time, from any browser or the free Traccar Manager app (iOS/Android).
  • Geofencing — draw virtual boundaries (circles or polygons) on the map and have Traccar watch them for you. We’ll set this up in Part 7.
  • Alerts & notifications — get notified by email, web popup or push notification when a device exits a geofence, exceeds a speed limit, goes offline, moves after being stationary, or triggers an ignition/motion event.
  • Route replay — scrub back through history and replay exactly where an asset travelled on any day, complete with speed at every point. Great for reviewing a trip, or working out where the trailer went last Tuesday.
  • Reports — built-in trip, stop, distance, and event summary reports over any date range, exportable to Excel.
  • Multiple devices — one server can track as many devices as you like. Build a second tracker for the boat, add the family cars with the free Traccar Client phone app — all on the same map.
  • Users & sharing — create extra accounts with access to only some devices, handy if you want to share the caravan’s location with family without handing over admin access.
  • Full REST API — everything the web interface can do can be scripted, so you can integrate tracking data into your own projects.

We’ll be using live tracking, geofencing, alerts and route replay in this guide — but it’s worth exploring the rest once you’re up and running.

How it all fits together

The SARA-R5 module does two jobs at once. Its integrated M8 GNSS receiver streams NMEA position data to the Pi, while the LTE-M modem provides an internet connection via PPP. A small Python script reads the GPS position and sends it to your Traccar server every few seconds using Traccar’s simple HTTP-based OsmAnd protocol.

GPS Tracker

  1.  NMEA data is sent from GPS to the Pi
  2. A python script sends the location data to the Traccar host on the internet via PPP
  3. The SARA-R5 establishes a PPP connection, giving the Pi internet access

Continue reading Live Asset Tracking with the OzzMaker SARA-R5 LTE-M GPS + 10DOF and Traccar

Build a Pixel-Art LED Matrix Clock, 3d printed diffuser and a web app

Four LED panels behind a 3D-printed diffuser, driven by a Raspberry Pi. Bright white digits on a shifting field of color — or flip it and the numbers go negative.

Clock using four OzzMaker RGFB LED matrices
Clock using four OzzMaker RGFB LED matrices

Underneath it’s just 1,024 LEDs, but in front of them sits a 3D-printed diffuser that gives every LED its own little cell — so the whole thing reads as crisp pixel art instead of a scatter of bright dots. The default look leans right into that: bright white digits on a background color that slowly wanders through a pastel palette. The diffuser’s hard grid between pixels is exactly what makes the contrast pop — white numeral, colored field, a clean edge around every square. And when you want a different mood, one toggle inverts it: the digit LEDs switch off and read as dark cut-outs in the lit field  

The idea: two ways to read the same grid The spark was a blank RGB panel glowing a flat color on the bench. A 64×16 grid of individually LEDs is really just a tiny screen — so what should the time actually look like on it? We landed on two answers and kept both. The headline look lights the digits up in bright white and lets the background carry the color: a soft pastel field that drifts, with clean white numerals on top. Behind the diffuser those white pixels sit in their own crisp cells, and the border between a lit-white digit and the colored surroundings is razor sharp — that contrast is the whole charm. The second look is the inverse: leave the field lit and switch the digit LEDs off, so the time reads as dark holes punched out of the color. That’s the negative mode — quieter, but just as striking. One toggle swaps between them.

Clock using four 16x16 RGB LED panels and a diffuser
Clock using four 16×16 RGB LED panels and a diffuser, powered by a Raspberry Pi
   

The idea: two ways to read the same grid

The headline look lights the digits up in bright white and lets the background carry the color: a soft pastel field that drifts, with clean white numerals on top. Behind the diffuser those white pixels sit in their own crisp cells, and the border between a lit-white digit and the colored surroundings is razor sharp — that contrast is the whole charm. The second look is the inverse: leave the field lit and switch the digit LEDs off, so the time reads as dark holes punched out of the color. That’s the negative mode — quieter, but just as striking. One toggle swaps between them.

The hardware

The canvas is four OzzMaker 16×16 RGB LED panels chained left-to-right into a single 64×16 display — 1,024 pixels. Driving them is a Raspberry Pi through OzzMaker’s LED connector , using Henner Zeller’s excellent rpi-rgb-led-matrix library to actually push bits to the panels at speed. Chain the panels OUT → IN, give them proper power, and the Pi sees them as one wide strip.
OzzMaker RGB LED Clock
Four RGB LED 16×16 panels, Raspberry Pi and diffuser
There’s one more physical layer that makes the whole thing work: a 3D-printed diffuser sits directly in front of the panels — a grid that gives every LED its own little walled-off cell. It softens the raw, glary point-sources into clean, evenly-lit squares, so the display reads as crisp pixel art rather than a scatter of bright dots. Those hard walls between pixels are also what sell the white-digit look: a lit-white numeral and the colored field beside it meet at a sharp, high-contrast edge instead of bleeding into each other. It’s the single cheapest part of the build and the one that does the most work — the difference between “a circuit board with LEDs on it” and “a screen.”  The diffuser also acts as a stand.  
Led diffuser and stand
Led diffuser and stand
  The back of the diffuser is shown below, as you can see, the inner walls are printed with black PLA, these stops light leaking between the LEDs.  This was printed on a Babu Lab  P2S multicolour printer
Led diffuser
LED diffuser
 

Drawing the digits

Everything is rendered into a plain in-memory buffer first — a 64×16 list of RGB tuples — and only pushed to the hardware at the very end. The digits themselves are an old-school seven-segment renderer. Each numeral is just a set of little rectangles; each digit says which segments it needs: # which of the 7 segments each digit lights (a=top, g=middle, …) SEGMENTS = { “0”: “abcdef”, “1”: “bc”, “2”: “abged”, “3”: “abgcd”, “4”: “fgbc”, “8”: “abcdefg”, # … } By default we draw those segments in lit white on top of the colored background, so the time The digits themselves are an old-school seven-segment renderer. Each numeral is just a set of little rectangles; each digit says which segments it needs:
# which of the 7 segments each digit lights (a=top, g=middle, ...)
SEGMENTS = {
  "0": "abcdef",  "1": "bc",     "2": "abged",
  "3": "abgcd",  "4": "fgbc",   "8": "abcdefg",  # ...
}

An effects gallery got out of hand

This is the part where a “simple clock” quietly became a hobby. The background doesn’t have to be a flat color — it just has to be something, with the time (white or negative) laid on top. So we kept adding “somethings.” By default the field cycles through an editable pastel palette once an hour, easing between colors with a fade, a diagonal swipe, or a top-to-bottom wipe. Then came the animated backgrounds:
  • Plasma — the classic rolling sine-wave field, colored by your palette.
  • Aurora — soft bands drifting sideways like northern lights.
  • Lava lamp — a few blobs floating and merging (metaballs, if you’re fancy).
  • Clouds — slow, morphing pastel noise.
  • Twinkle — a calm wash with sparse stars blinking in and out.
  • Ripples — two emitters of concentric waves, interfering.
  • Spiral — arms winding out from the center and rotating.
  • Scanner — a single comet sweeping edge to edge on black
And because that still wasn’t enough, there are flourishes you can stack on top of any of them: a slow breathing brightness pulse, a sparkle that shimmers for a second whenever the minute rolls over, and a time-of-day tint that warms the colors in the evening and cools them in the morning. Each effect precomputes its geometry once and then just does cheap math per pixel, so the whole thing holds a steady frame rate on a Raspberry Pi.  

A little web panel to tune it live

Fiddling with settings by editing files and restarting is miserable, so the same program also runs a tiny Flask web panel. Open http://<pi-ip>:8080 from any phone or laptop on the network and you get sliders and dropdowns for brightness, palette, effect, transitions, the lot — with a live preview of the actual frame the panel is drawing. Every change is saved to a settings.json and survives a reboot. Under the hood it’s one process with two threads that never touch each other’s stuff: a render thread that solely owns the matrix and runs at ~20 fps, and the web server. They talk only through a small set of thread-safe objects — settings in, status frames out. Keeping that boundary clean is what made it safe to keep piling on features without the display ever stuttering.    

Software setup

1. Install libraries

Follow this guide to get hzeller’s library installed, which will be used to drive the RGB LED panels.

2. Download and run the code

You can download the cross demo from our GitHub repository:

pberrypi ~ $ git clone https://github.com/ozzmaker/ozzMaker-LED-matrix

cd into the clock folder and run the script

pi@raspberrypi ~ $ cd ozzMaker-LED-matrix
pi@raspberrypi ~ $ cd clock
pi@raspberrypi ~ $ sudo python clock.py

Built with four OzzMaker 16×16 panels, a Raspberry Pi, and hzeller’s rpi-rgb-led-matrix. Digits by seven segments, color by pastels, time by NTP.

LED matrix in cross formation and how to pixel map using AI

In this guide you’ll build a cross-shaped display using five OzzMaker 16×16 RGB LED Matrix Panels — three panels across the middle, one on top and one on the bottom — driven by a Raspberry Pi.
You’ll then run a set of old-school demo effects (plasma, rainbow, expanding rings and more) that flow seamlessly across the whole cross, as if it were one display. 

A straight strip of panels is easy — the hzeller library treats a chain as one long display out of the box. A cross is not a simple rectangle, two of the panels are mounted upside down, and the chain order doesn’t match the visual layout. Solving that is where this guide gets interesting, and it’s also where AI (Claude) did most of the heavy lifting — more on that below.

What you’ll build

A 48×48 pixel cross made from five 16×16 panels:

OzzMaker LED matrix
LED Matrix cross orientation

The above image was provided by Claude The top and bottom panels are deliberately rotated 180 degrees. This lets the FFC cables reach their neighbouring panels and keeps the cables hidden behind the display when viewed from the front. The code corrects for the rotation, so the patterns still appear the right way up.

OzzMaker RGB LED matrix cross back
Rear view showing how panels are connected

 

Bill of materials

Assemble hardware

1. Fit the connector to the Pi

Power off the Pi. Sit the OzzMaker LED connector down onto the 40-pin GPIO header. The connector breaks out the unused GPIO and includes a QWIIC socket for I²C.

2. Lay out the panels

Place the three middle panels (LEFT, CENTRE, RIGHT) face down in a row, all the same way up. Now place the TOP and BOTTOM panels face down above and below the centre panel, but rotated 180 degrees relative to the middle row. Why the rotation? Each panel’s IN and OUT FFC ports sit near opposite edges of the PCB. With the top and bottom panels flipped, their ports end up facing the centre panel, so the short FFC cables can reach — and the whole loom stays tucked behind the display.

3. Chain the panels

Each panel has an IN and OUT FFC port, and the arrow on the back of the PCB shows the direction data flows. The chain order for the cross is: Pi → TOP → LEFT → CENTRE → RIGHT → BOTTOM. Plug an FFC cable from the connector on the Pi into the IN port of the TOP panel, then OUT of TOP into IN of LEFT, and continue through CENTRE and RIGHT, finishing at BOTTOM. Note that the chain order is not the same as the visual order — the software takes care of putting the right pixels on the right panel. A common mistake is getting one of the FFC cables in backwards or 180° rotated. The contacts on the cable should always face the same side of the connector latch. If one panel shows garbage, that’s almost always the cause.

4. Power

The FFC cable carries 5 V and GND alongside the data lines, and five panels is within what the FFC can deliver for typical animations. However, at 100% brightness with lots of LEDs lit, five panels can pull around 3.5 A — far more than the Pi’s 5 V rail can supply. The demo script defaults to 50% brightness for this reason. If you see flickering or garbage on panels at the end of the chain, feed 5 V and GND directly to the 5V and GND pads on the back of one of the later panels in the chain, using the same supply that powers the Pi (or a second 5 V supply with a common ground — never two supplies without a common ground). If you’re using a second supply, cut the trace between the green-highlighted pads on the back of the panel where you have connected the second power supply. See the LED matrix overview guide for the pad locations.

5. Soldering panels together

First place some solder on the pads near the edge of the panels, then align the panels together and add more solder to each pair of pads until the solder flows across both pads, bridging them. For the cross you’ll solder along four joints: TOP-to-CENTRE, LEFT-to-CENTRE, RIGHT-to-CENTRE and BOTTOM-to-CENTRE. Take extra care aligning the top and bottom panels — remember they are rotated 180°, so double-check the LED side lines up square before the solder goes on.

OzzMaker 16x16 RGB LED panel
Example of soldering two panels together using Solder-joinable tabs

Software setup

1. Install libraries

Follow this guide to get hzeller’s library installed, which will be used to drive the RGB LED panels.

2. Download and run the code

You can download the cross demo from our GitHub repository:

pberrypi ~ $ git clone https://github.com/ozzmaker/ozzMaker-LED-matrix

cd into the pacman folder and run the script

pi@raspberrypi ~ $ cd ozzMaker-LED-matrix
pi@raspberrypi ~ $ cd cross
pi@raspberrypi ~ $ sudo python cross.py

sudo is required by the rpi-rgb-led-matrix library so it can use realtime scheduling on the GPIO. If you see flicker or ghosting, the two knobs to turn are –gpio-slowdown (a Pi 4 likes 2–4, a Pi 3 likes 1–2, a Pi Zero usually 0–1) and the hardware mapping setting — see the getting-started guide above.

OzzMake LED matrix
4 LED panels connected in a cross

How the pixel mapping works

This is the clever part of the project. As far as hzeller’s library is concerned, the five panels are just one long 80×16 strip — it has no idea they’re arranged in a cross, and no idea two of them are upside down. The solution is a logical canvas. All of the animation code draws onto a simple 48×48 buffer using normal (x, y) coordinates, exactly as if the cross were one square display. A small mapping layer then translates each logical pixel to its physical location in the chain on every frame. Each panel is described by one line in a table: where it sits in the FFC chain, where its top-left corner lands on the 48×48 canvas, and whether it’s rotated:

Don’t worry if below appears complex, we don’t have to worry about it as Claude did all the work for us.

# ---------------------------------------------------------------------------
# Cross canvas
# ---------------------------------------------------------------------------
# Each panel's top-left corner in "cross-space" coordinates.
# Centre panel sits at (0, 0).
PANEL_OFFSETS = {
    "centre": ( 0,          0),
    "top":    ( 0,         -PANEL_SIZE),
    "bottom": ( 0,          PANEL_SIZE),
    "left":   (-PANEL_SIZE, 0),
    "right":  ( PANEL_SIZE, 0),
}
CROSS_MIN_X = -PANEL_SIZE
CROSS_MAX_X =  PANEL_SIZE * 2
CROSS_MIN_Y = -PANEL_SIZE
CROSS_MAX_Y =  PANEL_SIZE * 2
CROSS_W     = CROSS_MAX_X - CROSS_MIN_X   # 48
CROSS_H     = CROSS_MAX_Y - CROSS_MIN_Y   # 48

def _make_cross_mask():
    mask = np.zeros((CROSS_H, CROSS_W), dtype=bool)
    for ox, oy in PANEL_OFFSETS.values():
        px = ox - CROSS_MIN_X
        py = oy - CROSS_MIN_Y
        mask[py:py + PANEL_SIZE, px:px + PANEL_SIZE] = True
    return mask

CROSS_MASK = _make_cross_mask()

class CrossCanvas:
    """
    A 48x48 numpy RGB buffer representing the full cross.
    Only pixels inside CROSS_MASK are physically illuminated.
    """
    def __init__(self):
        self.buf = np.zeros((CROSS_H, CROSS_W, 3), dtype=np.uint8)
    def clear(self):
        self.buf[:] = 0
    def set(self, x, y, r, g, b):
        bx = int(x) - CROSS_MIN_X
        by = int(y) - CROSS_MIN_Y
        if 0 &amp;amp;lt;= bx &amp;amp;lt; CROSS_W and 0 &amp;amp;lt;= by &amp;amp;lt; CROSS_H and CROSS_MASK[by, bx]:
            self.buf[by, bx] = (r, g, b)
    def fill(self, r, g, b):
        self.buf[CROSS_MASK] = (r, g, b)
    def fill_panel(self, panel, r, g, b):
        ox, oy = PANEL_OFFSETS[panel]
        px = ox - CROSS_MIN_X
        py = oy - CROSS_MIN_Y
        self.buf[py:py + PANEL_SIZE, px:px + PANEL_SIZE] = (r, g, b)
    def apply_mask(self):
        """Zero out pixels outside the cross shape, then apply global brightness."""
        self.buf[~CROSS_MASK] = 0
        if BRIGHTNESS &amp;amp;lt; 1.0:
            self.buf[CROSS_MASK] = (
                self.buf[CROSS_MASK].astype(np.float32) * max(0.0, min(1.0, BRIGHTNESS))
            ).astype(np.uint8)

For an upright panel, a logical pixel maps straight through to the same position on the physical panel. For a rotated panel, the coordinates are mirrored in both axes — (x, y) becomes (15 – x, 15 – y) within the panel — which is exactly what a 180° rotation looks like in code. The physical x position is then the panel’s chain index × 16 plus the local x.

The corners of the 48×48 canvas don’t exist physically (that’s what makes it a cross), so the mapping layer also keeps a mask of which logical pixels fall on a real LED, and simply skips the rest. The animations don’t need to care — they can draw to the full square and only the cross lights up.

The payoff is that every effect in the script — plasma, rainbow, expanding rings, the lot — is written in plain, readable code against a square canvas, with not a single mention of chains, panels or rotations.

 

Using AI (Claude) to create the patterns

This project was built with the help of Claude. I didn’t write the mapping layer or the animations by hand — I described the physical setup and let Claude work out the geometry.

This was the actual prompt:

I am using the RGB LED matrix in the attached URL. I have 5 setup in a cross formation. The panels would be setup in this order. Top is first, left is second, centre is third, right is fourth, bottom is fifth, and the top panel and bottom panel are upside down. Create some old school animation effects to show off the panels, like rainbow, plasma, etc.. Add a setting for brightness. Power for these panels will come from the FFC cable connected to the raspberry Pi. Show an image of how the panels should be connected.

A few things worth noting about how that went:

  • Claude read the product page first. By including the URL, Claude picked up the panel specs and even checked the FFC power limits against the page before writing any code.
  • It flagged the power problem unprompted. Claude pointed out that five panels at full white can draw around 4 A through the FFC — more than the Pi can supply — and made the brightness default conservative (50%) on its own.
  • The chain-order and upside-down mapping came out right first go. The sentence “the top panel and bottom panel are upside down” was enough for Claude to build the coordinate remapping table above, including the 180° flip and the cross mask.
  • It drew the wiring diagram. The connection diagram showing the Pi and the five panels in chain order was generated by Claude as part of the same conversation.

 

Build a scrolling Pac-Man on a 8-panel OzzMaker LED strip

In this guide you’ll build the Pac-Man chase animation shown on the 16×16 RGB LED matrix panel product page: Pac-Man and the four classic ghosts running across a long horizontal strip of 8 panels chained end-to-end (128 × 16 pixels), driven by a Raspberry Pi.

It’s a great first project for the panels because it covers everything you need for bigger displays later — chaining, managing power, the driver library, and sprite-based animation in Python.

https://youtu.be/mdd0pqUy4kg

 

What you’ll build

A 8-panel horizontal strip — 128 pixels wide, 16 pixels tall — showing Pac-Man chasing the four ghosts (Blinky, Pinky, Inky and Clyde) across the display. Animation runs at around 25 FPS and loops forever.

 

Bill of materials

  • 8 × OzzMaker 16×16 RGB LED Matrix Panel (each ships with one FFC cable)
  • 1 × OzzMaker RGB LED Matrix Connector for Raspberry Pi
  • 1 × Raspberry Pi 4, Pi 3, Pi Zero 2 W, or Pi Zero
  • 1 × 5 V 3 A power supply
  • A microSD card with Raspberry Pi OS

Assemble hardware

1. Fit the connector to the Pi

Power off the Pi. Sit the OzzMaker LED connector down onto the 40-pin GPIO header so it covers all 40 pins. The connector breaks out the unused GPIO and includes a QWIIC socket for I²C.

2. Chain the panels

Each panel has an IN and OUT FFC port. The arrow on the back of the PCB shows the direction data flows. Plug an FFC cable from the connector on the Pi into the IN port of panel 1, then from OUT of panel 1 to IN of panel 2, and so on, all the way to panel 8.

A common mistake: getting one of the FFC cables in backwards or 180° rotated. The contacts on the cable should always face the same side of the connector latch. If a panel half-way down the chain shows garbage, that’s almost always the cause.

3. Power

The FFC cable carries 5 V and GND alongside the data lines and you should be able to run this animation without needing a separate power supply for some of the LED panels as most of the LEDs are off during the animation. If you see flickering or garbage towards the end of the chain, this would most likely mean the power supply powering your Pi and LED panels isn’t reliable or powerful enough. From the panel before you see the problem, feed 5 V and GND directly to the 5V and GND pads on the back of panel 7. Run those wires from the same supply that’s powering the Pi (or a second 5 V supply with a common ground — never two supplies without a common ground).

If you’re using a second supply, cut the trace between the green-highlighted pads on the back of panel where you have connected the second power supply. See the LED matrix overview guide for the pad locations.

4. Soldering panels together

OzzMaker RGB LED matrix
Back of panels

Before permanently soldering the panels together for a rigid display, you should run a test to make sure everything is connect properly. Once it is confirmed working, you can now begin soldering. First place some solder on the pads near there edge of the panels, then aligned them together and place more solder on the two pads until the solder flows across both pads, as show in the image below.

Software setup

1. Install libraries

Follow this guide to get hzeller’s library installed which will be used to run the RGB LED panels.

2.  Download code

You can download the pacman demo from our github repositry

pi@raspberrypi ~ $ git clone https://github.com/ozzmaker/ozzMaker-LED-matrix

cd into the pacman folder and run the script

pi@raspberrypi ~ $ cd ozzMaker-LED-matrix
pi@raspberrypi ~ $ cd pacman
pi@raspberrypi ~ $ sudo python pacman.py

Below is the main python script, which is well document and easy to follow.

#!/usr/bin/env python
from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics

#from samplebase import SampleBase
import time
import math
import colorsys
from PIL import Image



options = RGBMatrixOptions()
options.rows = 16
options.cols = 16
options.chain_length = 10
options.gpio_slowdown = 2
options.brightness = 30
matrix = RGBMatrix(options = options)
def clearMatrix():
    for x in range(matrix.width):
        for y in range( matrix.height):
            double_buffer.SetPixel(x, y, 0,0,0)
from PIL import Image
import time
# Define constants
MATRIX_WIDTH = 160
MATRIX_HEIGHT = 16
SPRITE_WIDTH = 16  # Assuming each sprite is 16 pixels wide
PACMAN_WIDTH = 16  # Assuming Pacman is also 16 pixels wide
GAP = 8  # Gap between ghosts
INITIAL_PACMAN_GAP = 80  # Initial gap between Pacman and the first ghost
FINAL_PACMAN_GAP = -SPRITE_WIDTH  # Final gap where ghosts overlap Pacman
ANIMATION_DELAY = 0.03  # Delay between frames in seconds
PACMAN_DIES_LOCATION = 130 
PACMAN_DIES_DELAY = 0.07  # Delay between frames in the Pacman death animation
# Load sprite images
ghostRed1 = Image.open('ghostRed1.ppm').convert('RGB')
ghostRed2 = Image.open('ghostRed2.ppm').convert('RGB')
ghostPink1 = Image.open('ghostPink1.ppm').convert('RGB')
ghostPink2 = Image.open('ghostPink2.ppm').convert('RGB')
ghostLightBlue1 = Image.open('ghostLightBlue1.ppm').convert('RGB')
ghostLightBlue2 = Image.open('ghostLightBlue2.ppm').convert('RGB')
ghostOrange1 = Image.open('ghostOrange1.ppm').convert('RGB')
ghostOrange2 = Image.open('ghostOrange2.ppm').convert('RGB')
# Load Pacman images
pacman1 = Image.open('pacman1.ppm').convert('RGB')
pacman2 = Image.open('pacman2.ppm').convert('RGB')
pacman3 = Image.open('pacman3.ppm').convert('RGB')
# Load PacmanDies images
pacmanDies = [
    Image.open(f'pacmanDies{i+1}.ppm').convert('RGB') for i in range(11)
]
# Initialize double buffer and matrix
double_buffer = matrix.CreateFrameCanvas()
# List of sprites, their positions, and individual frame counters
sprites = [
    {'images': [pacman1, pacman2, pacman3], 'x': -PACMAN_WIDTH, 'frame': 0, 'switch_rate': 4},
    {'images': [ghostRed1, ghostRed2], 'x': -PACMAN_WIDTH - INITIAL_PACMAN_GAP - SPRITE_WIDTH, 'frame': 0, 'switch_rate': 5},
    {'images': [ghostPink1, ghostPink2], 'x': -PACMAN_WIDTH - INITIAL_PACMAN_GAP - 2*(SPRITE_WIDTH + GAP), 'frame': 0, 'switch_rate': 7},
    {'images': [ghostLightBlue1, ghostLightBlue2], 'x': -PACMAN_WIDTH - INITIAL_PACMAN_GAP - 3*(SPRITE_WIDTH + GAP), 'frame': 0, 'switch_rate': 9},
    {'images': [ghostOrange1, ghostOrange2], 'x': -PACMAN_WIDTH - INITIAL_PACMAN_GAP - 4*(SPRITE_WIDTH + GAP), 'frame': 0, 'switch_rate': 11}
]
# Function to play the Pacman death animation
def play_pacman_dies(x_position):
    for image in pacmanDies:
        double_buffer.Clear()
        double_buffer.SetImage(image, x_position)
        matrix.SwapOnVSync(double_buffer)
        time.sleep(PACMAN_DIES_DELAY)
# Main animation loop
while True:
    double_buffer.Clear()
    # Calculate the current gap between Pacman and the first ghost based on Pacman's position
    pacman_position = sprites[0]['x']
    if pacman_position &lt; PACMAN_DIES_LOCATION:
        # Linearly decrease the gap as Pacman approaches 2/3 of the way across the matrix
        current_gap = INITIAL_PACMAN_GAP - (INITIAL_PACMAN_GAP - FINAL_PACMAN_GAP) * (pacman_position / PACMAN_DIES_LOCATION)
    else:
        # Once Pacman is beyond 2/3, the gap is at its minimum (overlap)
        current_gap = FINAL_PACMAN_GAP
    # Update positions of the ghosts based on the current gap
    sprites[1]['x'] = sprites[0]['x'] - current_gap - SPRITE_WIDTH
    sprites[2]['x'] = sprites[1]['x'] - (SPRITE_WIDTH + GAP)
    sprites[3]['x'] = sprites[2]['x'] - (SPRITE_WIDTH + GAP)
    sprites[4]['x'] = sprites[3]['x'] - (SPRITE_WIDTH + GAP)
    # Check if the red ghost catches up to Pacman
    if sprites[1]['x'] &gt;= pacman_position - 2:
        play_pacman_dies(pacman_position)
        break
    for sprite in sprites:
        # Update the frame counter for each sprite
        sprite['frame'] += 1
        
        # Switch between the two images for ghosts or three images for Pacman based on the individual sprite's frame counter
        image = sprite['images'][(sprite['frame'] // sprite['switch_rate']) % len(sprite['images'])]
        
        # Draw the sprite at the current position
        double_buffer.SetImage(image, sprite['x'])
        
        # Move sprite to the right
        sprite['x'] += 1
        
        # Reset sprite position when it moves out of the right side of the matrix
        if sprite['x'] &gt; MATRIX_WIDTH:
            # Reset positions, with Pacman leading and ghosts following with the initial gap
            sprite['x'] = -PACMAN_WIDTH - INITIAL_PACMAN_GAP - (SPRITE_WIDTH + GAP) * (sprites.index(sprite) - 1)
        
    # Swap the buffers to display the new frame
    matrix.SwapOnVSync(double_buffer)
    
    # Wait before the next frame
    time.sleep(ANIMATION_DELAY)

New product – BerryIMU-320G

We have released a new product, the BerryIMU-320G, which is an inertial measurement unit, or IMU, that measures and reports on velocity, orientation, shock events and gravitational forces using a combination of two accelerometers, a gyroscope, a magnetometer and a barometric/altitude sensor.

 

BerryIMU-320G top

 

At the heart of BerryIMU-320G is ST’s LSM6DSV320X, a smart 6-axis IMU that integrates one gyroscope and two separate accelerometers on the same die:

  • low-g accelerometer for precise everyday motion sensing (tilt, orientation, vibration), and
  • high-g accelerometer capable of measuring extreme impacts up to ±320 g.

This means BerryIMU-320G can capture fine movements and violent shocks with one device, without needing a separate high-g sensor. The low-g channel provides high resolution and low noise for normal motion, while the high-g channel is reserved for crashes, drops and other intense events. Because they are independent, saturating the high-g accelerometer does not disturb the low-g readings, so you don’t lose detail on everyday motion while still being able to catch rare extreme impacts.

BerryIMU-320G bottom

 

BerryIMU-320G is also fitted with a Bosch BMP581 barometric pressure sensor which can be used to calculate altitude & temperature , and a LIS3MDL magnetometer for heading/compass measurements. Temperature readings are available from the on-board sensors.

BerryIMU-320G QWIIC

Using u-Center to connect to the GPS on the OzzMaker SARA-R5 LTE-M GPS + 10DOF

u-Center from u-Blox is a graphical interface which can be used to monitor and configure all aspects of the GPS module on a OzzMaker SARA-R5 LTE-M GPS + 10DOF

u-Center from uBlox
U-Center

 

u-Center only runs on Windows. It can connect over the network to a Raspberry Pi.  This will require us to redirect the serial interface on the Raspberry Pi to a network port using ser2net.

Pi Setup

You will first need to have multiplexing enabled for the serial interface and have GPS data streaming to the /dev/ttyGSM2 virtual serial interface, which is covered in these two guides

  1. How to enable multiplexing on the Raspberry Pi Serial interface
  2. Using the GPS on OzzMaker SARA-R5 LTE-M GPS + 10DOF

 

Then, do an upt-get update and then install ser2net;

pi@raspberrypi ~ $ sudo apt-get update
pi@raspberrypi ~ $ sudo apt-get install ser2net

Edit the ser2net config file and add the serial port redirect to a network port. We will use network port 6000

pi@raspberrypi ~ $ sudo nano /etc/ser2net.yam

And add these line at the bottom;

connection: &con1197
    accepter: tcp,6000
    enable: on
    options:
      banner: *banner
      kickolduser: true
      telnet-brk-on-sync: true
    connector: serialdev,
              /dev/ttyGSM2,
              115200n81,local

you can now restart ser2net using;

pi@raspberrypi ~ $ sudo systemctl restart ser2net

If you need to disable it, you can disable it using;

pi@raspberrypi ~ $ sudo systemctl disable ser2net

And you can use the below command to check if it is running by seeing if the port is open and assigned to the ser2net process;

pi@raspberrypi ~ $ sudo netstat -ltnp | grep 6000

If it is running, you should see something similar to the output below;

check result of ser2net

Windows PC Setup and Connecting to the GPS module

You can download u-Center from here.

Once installed, open u-Center. You will get the default view as shown below.  No data will be shown as we are not connected to a GPS.

u-Center default view

The next step, is to create a new network connection and connect to the GPS which is connected to our Raspberry Pi. You can create a new connection under the Receiver and then Network connection menus.

u-Center connect to Raspberry Pi
In the new window, enter the IP address of the Raspberry Pi and specify port 6000. This is the port we configured in ser2net on the Raspberry Pi.
u-Center Raspberry Pi Address

This is what the default view looks like when connected and the GPS has a fix.u-Center connected

 

u-Center

Below I will list of the more useful windows/tools within u-Center.
You can also click on the images below for a larger version.

Data View
This window will show you the longitude, latitude, altitude and fix mode. It will also show the HDOP, which is the Horizontal Dilution of Precision.  Lower is better, anything below 1.0 means you have a good signal.

u-Center Data View
u-Center Data View

Ground Track
This window will show you where the satellites are as well as what time.

u-Center Ground Track
u-Center Ground Track

Skye View
Sky view is an excellent tool for analyzing the performance of antennas as well as the conditions of the satellite observation environment.

u-Center Sky View
u-Center Sky View

Deviation Map
This map shows the average of all previously measured positions.

u-Center Deviation Map
u-Center Deviation Map

Continue reading Using u-Center to connect to the GPS on the OzzMaker SARA-R5 LTE-M GPS + 10DOF

Using CellLocate with OzzMaker SARA-R5 LTE-M GPS + 10DOF

What do you do if you have a poor or no GNSS signal, such has being indoors, inside a parking garage or in urban canyons? You could try CellLocate.

CellLocate

In a nutshell

CellLocate provides an estimated location based on visible network cell information reported by the cellular module. When CellLocate is activated, a data connection to the CellLocate server is established and the network cell information is passed to the server which provides an estimation of the device position based on the cell information.

CellLocate is fully integrated into the SARA-R5 which is on the OzzMaker SARA-R5 LTE-M GPS 10DOF board. The technology enables stand-alone location data based on surrounding mobile network information as well as hybrid technology that works in conjunction with GNSS. Through the single AT command interface, it is possible to define all the location settings for optimized performance.

When using CellLocate, the position accuracy is not predictable and is determined by the availability in the database of previous observations within the same area. CellLocate does not require a GNSS receiver to be present or active.

CellLocate requires a data connection (PDP) from the SARA-R5 module to the carrier.

 

Getting started

Connect to the SARA-R5 cellular module

pi@raspberrypi ~ $ minicom 115200 -D /dev/serial0

Setup the Packet Data Protocol (PDP) context.

First task is to setup the connection parameters for the PDP context using AT+CGDCONT. Any setting applied with this commend is persistent over power cycles. This means it only needs to be done once. You will however need to enter it again if you do a factory reset.

First, turn off the radio

 AT+CFUN=0

Then set up a connection profile with the APN for your network operator, using the AT+CGDCONT  command (Packet Switch Data configuration). In this example we are using a Hologram SIM, so the APN would be hologram.

AT+CGDCONT=1,”IP”,””hologram”

Now turn the radio back on;

 AT+CFUN=1

Once your SARA-R5 connects to the carrier, you can use AT+CGDCONT? to get your current IP address

AT+CGDCONT?
+CGDCONT: 1,”IP”,”hologram.mnc050.mcc234.gprs”,”10.170.92.244″,0,0,0,2,0,0,0,0,0,0,0

Now active the PDP context

AT+CGACT=1,1

Set the PDP type to IPv4

AT+UPSD=0,0,0

Profile #0 is mapped on CID=1

AT+UPSD=0,100,1

Activate the PSD profile

 AT+UPSDA=0,3

 

Your SARA-R5 should now have internet access. If you want to test the data connection, you can use AT+UPING

AT+UPING=”www.google.com”
OK
+UUPING: 1,32,”www.google.com”,”142.250.179.228″,113,617
+UUPING: 2,32,”www.google.com”,”142.250.179.228″,113,637
+UUPING: 3,32,”www.google.com”,”142.250.179.228″,113,636
+UUPING: 4,32,”www.google.com”,”142.250.179.228″,113,637

 

Using CellLocate

When using CellLocate, There are two modes to choose from:

normal scan: the cellular module reports the serving cell and the neighboring visible cells designated by the network operator, which are normally collected by the module during its “network” activity. This configuration is suggested for a quick update of location

deep scan: the cellular module scans and reports all visible cells providing in addition to serving and neighboring cells by the serving network operator, also the cells of all other available (visible) network operators, thus increasing the probability of obtaining a successful position estimation. Although this takes a bit longer (approximately 30 sec to 2 minutes is needed to perform a deep scan), uses more data (each reported cell requires a few bytes), and more power, coverage and reliability are potentially better in corner cases.

Continue reading Using CellLocate with OzzMaker SARA-R5 LTE-M GPS + 10DOF

Accessing GPS via I2C on a BerryGPS-IMU

The BerryGPS-IMU uses a  CAM-M8C U-Blox GPS module, this GPS module includes a DDC interface which is fully I2C compatible.

This guide will show how to read NMEA sentences from the GPS module via I2C, using a Raspberry Pi. This leaves the serial interface on the Raspberry Pi free for other uses. You can also use a QWIIC connector to connect the BerryGPS-IMU to the Raspberry Pi

We will create a virtual node where we will send the NMEA sentences, we will then configure GPSD to read this virtual node.

 

Caveat: There is a well know I2C clock stretching bug on the Raspberry Pi which will be encountered when trying to communicate with the  uBlox module via native I2C.  This results with random characters appearing in the retrieved data.
We will cover two methods of how to get around this;

Method 1 – Include a lot of checks to ignore NMEA sentences with corrupt data.
The overall amount of NMEA sentences which will be corrupt is very small(20 out of 1,000), which still makes this method very usable.

Method 2 – Using bit banging to overcome the clock stretching bug. This will result in zero errors, but requires I2C to be disabled on the Raspberry Pi.

 

I2C Jumpers

By default, the GPS module on the BerryGPS-IMU is not connected to the I2C bus.  This can be fixed by placing a solder blob on the jumpers JP11 and JP10 on the back of the PCB.

BerryGPS-IMU I2C GPS

 

Method 1 – Using native I2C

Enable I2C on your Raspberry Pi and set the speed to 400Khz.

pi@raspberrypi ~ $ sudo nano /boot/config.txt

Near the bottom, add the following line

dtparam=i2c_arm=on,i2c_arm_baudrate=400000

Now reboot.

You can confirm if you see the GPS module by using the below command.

pi@raspberrypi ~ $ sudo i2cdetect -y 1

 

Here is the output when a BerryGPS-IMU is connected. 42 is the GPS module

pi@raspberrypi ~ $ sudo i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: — — — — — — — — — — — — —
10: — — — — — — — — — — — — 1c — — —
20: — — — — — — — — — — — — — — — —
30: — — — — — — — — — — — — — — — —
40: — — 42 — — — — — — — — — — — — —
50: — — — — — — — — — — — — — — — —
60: — — — — — — — — — — 6a — — — — —
70: — — — — — — — 77

 

Create the python script which will read the data via I2C from the GPS module

pi@raspberrypi ~ $ nano i2c-gps.py

Copy in the below code;

#! /usr/bin/python
import time
import smbus
import signal
import sys
BUS = None
address = 0x42
gpsReadInterval = 0.03
def connectBus():
    global BUS
    BUS = smbus.SMBus(1)
def parseResponse(gpsLine):
  if(gpsLine.count(36) == 1):                           # Check #1, make sure '$' doesnt appear twice
    if len(gpsLine) < 84:                               # Check #2, 83 is maximun NMEA sentenace length.
        CharError = 0;
        for c in gpsLine:                               # Check #3, Make sure that only readiable ASCII charaters and Carriage Return are seen.
            if (c < 32 or c > 122) and  c != 13:
                CharError+=1
        if (CharError == 0):#    Only proceed if there are no errors.
            gpsChars = ''.join(chr(c) for c in gpsLine)
            if (gpsChars.find('txbuf') == -1):          # Check #4, skip txbuff allocation error
                gpsStr, chkSum = gpsChars.split('*',2)  # Check #5 only split twice to avoid unpack error
                gpsComponents = gpsStr.split(',')
                chkVal = 0
                for ch in gpsStr[1:]: # Remove the $ and do a manual checksum on the rest of the NMEA sentence
                     chkVal ^= ord(ch)
                if (chkVal == int(chkSum, 16)): # Compare the calculated checksum with the one in the NMEA sentence
                     print gpsChars
def handle_ctrl_c(signal, frame):
        sys.exit(130)
#This will capture exit when using Ctrl-C
signal.signal(signal.SIGINT, handle_ctrl_c)
def readGPS():
    c = None
    response = []
    try:
        while True: # Newline, or bad char.
            c = BUS.read_byte(address)
            if c == 255:
                return False
            elif c == 10:
                break
            else:
                response.append(c)
        parseResponse(response)
    except IOError:
        connectBus()
    except Exception,e:
        print e
connectBus()
while True:
    readGPS()
    time.sleep(gpsReadInterval)

 

You can test the script with python i2c-gps.py. If you have a GPS fix, you will get output similar to below.

pi@raspberrypi ~ $ python i2c-gps.py
$GNRMC,071423.00,A,3254.18201,S,15243.27916,E,0.252,,110721,,,A*72
$GNVTG,,T,,M,0.252,N,0.466,K,A*3C
$GNGGA,071423.00,3254.18201,S,15243.27916,E,1,10,1.01,29.0,M,22.6,M,,*63
$GNGSA,A,3,30,14,07,17,13,19,15,,,,,,1.66,1.01,1.31*17
$GNGSA,A,3,73,74,72,,,,,,,,,,1.66,1.01,1.31*1C
$GPGSV,3,1,12,01,21,124,,06,13,009,20,07,11,048,21,13,47,286,34*70
$GPGSV,3,2,12,14,54,143,27,15,25,253,31,17,84,132,28,19,70,328,31*7D
$GPGSV,3,3,12,21,09,139,,24,08,225,18,28,,,29,30,49,052,26*45
$GLGSV,3,1,10,65,14,243,16,71,00,330,,72,15,287,21,73,39,099,22*65
$GLGSV,3,2,10,74,53,176,19,75,16,227,,80,00,070,,83,24,149,*64

 

Create a virtual node, this is where we will send the NMEA sentences from the GPS module to.

pi@raspberrypi ~ $ mknod /tmp/gps p

 

Now run the python script and redirect the output to the virtual node we just created.
We will also use stdbuf so the output from the script is sent to the virtual node one line at a time. Without this, the output is buffered and only sent to the virtual node when the buffer is full.

pi@raspberrypi ~ $ stdbuf -oL python i2c-gps.py > /tmp/gps
Configure GPSD to use the virtual buffer

No you can configure GPSD to point to the virtual buffer.

pi@raspberrypi ~ $ sudo nano /etc/default/gpsd

Look for
DEVICES=””
and change it to
DEVICES=”/tmp/gps”

Restart GPSD so the new settings take effect.

pi@raspberrypi ~ $ sudo systemctl restart gpsd.socket

 

You can now start using your GPS module with your Raspberry Pi

Method 2 – Bit Bang I2C

Confirm that you do not have I2C enabled. There should be no i2c devices under /dev/

pi@raspberrypi ~ $ ls /dev/i2c*
ls: cannot access ‘/dev/i2c*’: No such file or directory

 

If you do have I2C enabled, the above command will return a file under the /dev/ directory.
You can disable I2C in /boot/config.txt

pi@raspberrypi ~ $ sudo nano /boot/config.txt

 

Look for the below line and comment it out by adding a “#” in front.

dtparam=i2c_arm=on

Now reboot.

Create the python script which will read the data by bit banging I2C from the GPS module

pi@raspberrypi ~ $ nano i2c-gps.py

 

Copy in the below code;

import time
import signal
import sys
import pigpio
address = 0x42
gpsReadInterval = 0.03
SDA=2
SCL=3
pi = pigpio.pi()
pi.set_pull_up_down(SDA, pigpio.PUD_UP)
pi.set_pull_up_down(SCL, pigpio.PUD_UP)
pi.bb_i2c_open(SDA,SCL,100000)
def handle_ctrl_c(signal, frame):
        pi.bb_i2c_close(SDA)
        pi.stop()
        sys.exit(130)
#This will capture exit when using Ctrl-C
signal.signal(signal.SIGINT, handle_ctrl_c)
def readGPS():
    c = None
    response = []
    while True: # Newline, or bad char.
        a=pi.bb_i2c_zip(SDA, [4, address, 2, 6, 1])  # Bit bang I2C read. 2 = Start, 6 = read, 1= How many bytes to read
        c = ord(a[1])
        if c == 255:
            return False
        elif c == 10:
            break
        else:
            response.append(c)
    gpsChars = ''.join(chr(c) for c in response)  #Convert list to string
    print gpsChars
while True:
    readGPS()
    time.sleep(gpsReadInterval)

 

Create a virtual node, this is where we will send the NMEA sentences from the GPS module to

pi@raspberrypi ~ $ mknod /tmp/gps p

 

Now run the python script and redirect the output to the virtual node we just created.
We will also use stdbuf so the output from the script is sent to the virtual node one line at a time. Without this, the output is buffered and only sent to the virtual node when the buffer is full.

pi@raspberrypi ~ $ stdbuf -oL python i2c-gps.py > /tmp/gps
Configure GPSD to use the virtual buffer

No you can configure GPSD to point to the virtual buffer

pi@raspberrypi ~ $ sudo nano /etc/default/gpsd

Look for
DEVICES=””
and change it to
DEVICES=”/tmp/gps”

Restart GPSD so the new settings take effect.

pi@raspberrypi ~ $ sudo systemctl restart gpsd.socket

 

You can now start using your GPS module with your Raspberry Pi

Using the BerryIMUv3 on a Raspberry Pi Pico with MicroPython

In this guide we will show how to get the a BerryIMUv3 working with a Raspberry Pi Pico, using MicroPython. This code example supports I2C and SPI.

Hook up Guide

The two images below show how to hook up the BerryIMUv3 via I2C or SPI. (If using SPI, you will need to place a solder “blob” on JP7 on the BerryIMUv3 to complete the SPI connection, as shown here)

 

Raspberry Pi Pico and BerryIMU
Raspberry Pi Pico and BerryIMv3 via SPI

 

Raspberry Pi Pico and BerryIMU
Raspberry Pi Pico and BerryIMv3 via I2C

Thonny and the MicroPython code

The MicroPython code can be downloaded from our GitHub repository. The code for this example can be found under the PicoMicroPython directory.

We will be using Thonny to program the Raspberry Pi Pico.

If you are new to the Raspberry Pi Pico and Thonny, we suggest viewing these excellent tutorials from our friends at Core Electronics;
1. Getting started with Raspberry Pi Pico
2. Pico and Thonny

 

The sample code supports both I2C and SPI communications.  Comment out the protocol which will not be used as shown below. Below we have commented out I2C as we will be using SPI

import utime
import math
from LSM6DSL import *
import machine
#Comment out one of the below lines
import IMU_SPI as IMU
#import IMU_I2C as IMU

 

BerryIMU Pico Thonny
BerryIMU Pico Thonny

 

Connecting BerryIMUv3 via SPI to a Raspberry Pi

The accelerometer and gyroscope on the BerryIMUv3 can output data at a rate of 6,664 times a second!  I2C is too slow to read the output at this rate, this is where SPI comes in.

Buy default, BerryIMUv3 is setup to use I2C.  You can complete the SPI interface by placing a solder blob on jumper 7 (JP7).

BerryIMU SPI
Blob on jumper 7

 

SPI uses 4 pins,  and depending on what device you are using these pins could be named differently, which causes confusion.

The most common pin names are;

  • MOSI  (Master out Slave In)
  • MISO  (Master In Slave Out
  • SCLK (Serial Clock)
  • CS (chip select)  This is CE0 or CE1 on the Raspberry PI.

These pins have been highlighted below

Raspberry Pi SPI pins
Raspberry Pi 40 pin header

 

On the BerryIMUv3, they are called;

  • SDI (Slave Data In)
  • SDO (Slave Data Out)
  • SPC (Serial Port Clock)
  • CS (Chip Select)

When we connect a BerryIMUv3 to a Raspberry Pi using SPI, the Raspberry Pi will be acting as a master and the BerryIMUv3 will be acting as a slave. This is how they are connected logically.

BerryIMU SPI
BerryIMU and Raspberry Pi SPI

Here is the physical wiring

Raspberry Pi BerryIMU SPI
Raspberry Pi SPI and BerryIMUv3

 

You can enable SPI on the Raspberry Pi using raspi-config

pi@raspberrypi ~ $ sudo raspi-config

Raspberry Pi SPIGo into “Interfacing Options”

Raspberry Pi SPI
Then select “SPI”

When asked if you want to enable SPI, select “yes”

 

The code for SPI can be found here https://github.com/ozzmaker/BerryIMU/tree/master/python-BerryIMUv3-SPI

Blip, blop, bloop…