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:
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.
NMEA data is sent from GPS to the Pi
A python script sends the location data to the Traccar host on the internet via PPP
The SARA-R5 establishes a PPP connection, giving the Pi internet access
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
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 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 panelschained 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.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 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 printerLED 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:
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:
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.
1 × 5 V 3 A power supply (or a battery with a 5 V boost board if you want a portable build, as shown in the photos)
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. 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.
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:
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.
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;lt;= bx &amp;lt; CROSS_W and 0 &amp;lt;= by &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;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.
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
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 ~ $ 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 < 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'] >= 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'] > 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)
We’ve just added three new products to the OzzMaker shop: a 16×16 RGB LED Matrix Panel, and small Raspberry Pi connector boards for our matrix, one which supports a LiPo battery built specifically to drive it.
The panel
The 16×16 RGB LED Matrix Panel uses the HUB75 signalling protocol, but with one important twist — instead of the awkward 16-pin IDC ribbon you’d normally wrestle with, our panels use a flat FFC cable for connections. That makes chaining panels together a lot less fiddly.
You can chain 50+ panels in a long strip for scrolling text, or tile them into bigger square displays — 64×64 and beyond. Each panel has solder pads along its edges that act as anchor points: line two panels up side by side, match the pads, and flow solder across them to join the panels together — a bit like spot-welding. The result is a single rigid display rather than a loose array of separate boards
50 panels
The panels are driven with hzeller’s open-source library, which is well-documented, actively maintained, and works with Python or C. We also have plenty of examples available for scrolling text, animations, (yes, including a Pac-Man animation), clocks, patterns, etc..
Raspberry Pi Connectors
We have two type of connectors which are used to connect the 16×16 RGB LED Matrix Panels to a Raspberry Pi.
OzzMaker RGB LED Matrix Connector for Raspberry Pi
OzzMaker RGB LED Matrix Connector for Raspberry Pi with Lipo/Ii-Ion support
Each connector is a small board that sits on the 40-pin GPIO header and gives you a clean, solder-free way to plug a panel straight into a Pi. No jumper wires, no IDC ribbon. It also breaks out the unused GPIO pins and adds a QWIIC connector for I²C accessories, so you can keep building on the same Pi. The second connector above has support for a Lipo/Li-Ion battery which allows you to power your Raspberry Pi and OzzMaker RGB LED matrix from a single 3.7V LiPo or Li-Ion battery. It also includes a charger!
Raspberry Pi connector which supports Lipo battery
They work with the Pi 3, Pi 4, Pi Zero and Pi Zero 2. Pi 5 support is in progress.
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.
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:
a low-g accelerometer for precise everyday motion sensing (tilt, orientation, vibration), and
a 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 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.
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
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
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.
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 thenNetwork connection menus.
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.
This is what the default view looks like when connected and the GPS has a fix.
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
Ground Track This window will show you where the satellites are as well as what time.
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
Deviation Map This map shows the average of all previously measured positions.
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.
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
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.
This is an all in one module which can provide location tracking and LTE-M services such as data, text and SMS to your project. It comes in the same form factor as a Raspberry Pi Zero, which makes it nice and compact when used with a Raspberry Pi Zero.
It is a feature packed board, which includes the following components;
LTE-M cellular modem
GPS receiver (including assisted GPS)
Accelerometer
Gyroscope
Magnetometer (Compass)
Barometric/pressure sensor (altitude)
Temperature sensor
This board includes a SARA-R510M8S module from uBlox, this module can be used for LTE-M connectivity and includes an integrated GPS receiver. Using both these features together results in obtaining a GPS fix in seconds, using Assisted GPS.
The below video shows a comparison between assisted and normal GPS.
Asisted GPS takes 19secs to get a fix
Normal GPS takes 8min 22Sec to get a fix
Typically, a GPS module can take a few minutes to get Time To First Fix(TTFF), or even longer if you are in built up areas. This is because the Almanac needs to be downloaded from satellites before a GPS fix can be acquired and only a small portion of the Almanac is sent in each GPS update.
Assisted GPS speeds this up significantly by downloading ephemeris, almanac, accurate time and satellite status over the network, resulting in faster TTTF, in a few seconds. This is very similar how to GPS works on a smartphone.
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 BerryIMv3 via SPI
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.
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