Have you ever wanted to create a physical display that responds to real-world interactions? In this tutorial, we’ll build a fascinating IoT project that combines Bluetooth Low Energy (BLE), WiFi, and LED matrix displays to create a responsive system where rotating a physical device instantly updates a display. You need a timeular tracker and a cosmic unicorn 32x32 matrix to actually be able to build this. But even if you do not own any of these components you can still learn how to setup a web server on a Raspberry PI Pico w and how to use the bartolome-ble-toolkit to connect to bluetooth periphals.
What We’re Building
Our project creates a seamless connection between three components:
- Timeular Tracker - A physical octagonal device that detects which side is facing up
- Go Application - A desktop/server application that reads the tracker via BLE using the Bartolome BLE Toolkit
- Raspberry Pi Pico W - A microcontroller running a web server that controls the LED display
- Cosmic Unicorn 32x32 LED Matrix - A vibrant display that shows large digits
When you rotate the Timeular tracker to show side 5, the number “5” instantly appears on the LED matrix. It’s like having a physical remote control for your display!
Where is the code?
Here: https://github.com/coded-aesthetics/timeular-cosmic-unicorn
The Architecture
[Timeular Tracker] --BLE--> [Go App] --HTTP--> [Pico W Server] --GPIO--> [LED Matrix]
(Physical) (Computer) (WiFi/Web) (Display)
The data flow is beautifully simple:
- Timeular tracker broadcasts its current side via BLE
- Go application captures this data using Bluetooth
- Go app immediately sends HTTP GET request to Pico W
- Pico W receives the request and displays the digit on the LED matrix
Hardware Requirements
- Timeular Tracker - The octagonal time-tracking device
- Raspberry Pi Pico W - Microcontroller with WiFi capability
- Pimoroni Cosmic Unicorn - 32x32 RGB LED matrix display
- Computer with Bluetooth - To run the Go application (Windows, macOS, or Linux)
Setting Up the Cosmic Unicorn LED Display
First, let’s set up our LED matrix display server on the Pico W. This creates a web server that accepts HTTP requests to display digits.
Installing MicroPython on Pico W
- Download the latest MicroPython UF2 file for Pico W from micropython.org
- Hold the BOOTSEL button while connecting your Pico W via USB
- Copy the UF2 file to the RPI-RP2 drive that appears
- Install the Pimoroni MicroPython libraries for Cosmic Unicorn
The LED Display Server Code
The Pico W runs a complete web server that can display any digit (0-9) in large, readable format across the entire 32x32 matrix:
from cosmic import CosmicUnicorn
from picographics import PicoGraphics, DISPLAY_COSMIC_UNICORN
import network
import socket
# Initialize the display
cu = CosmicUnicorn()
graphics = PicoGraphics(display=DISPLAY_COSMIC_UNICORN)
cu.set_brightness(0.5)
# Define colors
WHITE = graphics.create_pen(255, 255, 255)
RED = graphics.create_pen(255, 0, 0)
GREEN = graphics.create_pen(0, 255, 0)
BLACK = graphics.create_pen(0, 0, 0)
def draw_thick_digit(digit, color=WHITE, thickness=3):
"""Draw a large digit filling most of the 32x32 display"""
graphics.set_pen(BLACK)
graphics.clear()
graphics.set_pen(color)
# Coordinates for well-proportioned digits
left, right = 6, 26
top, bottom = 2, 30
middle = 14
width = right - left
height = bottom - top
# Draw different digit patterns
if digit == 1:
x = 16 - thickness // 2
graphics.rectangle(x, top, thickness, height)
graphics.rectangle(x - 3, top + 2, 3, thickness)
elif digit == 2:
graphics.rectangle(left, top, width, thickness)
graphics.rectangle(right - thickness, top, thickness, middle - top)
graphics.rectangle(left, middle - thickness//2, width, thickness)
graphics.rectangle(left, middle, thickness, bottom - middle)
graphics.rectangle(left, bottom - thickness, width, thickness)
# ... (additional digit patterns)
cu.update(graphics)
Web Server with Query Parameter Handling
The server accepts HTTP requests with parameters to control what’s displayed:
def web_server():
# Connect to WiFi
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect('YOUR_WIFI_NAME', 'YOUR_PASSWORD')
while not wlan.isconnected():
time.sleep(1)
ip = wlan.ifconfig()[0]
print(f'Server running on http://{ip}')
# Create web server
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.bind(addr)
s.listen(1)
while True:
try:
cl, addr = s.accept()
request = cl.recv(1024).decode('utf-8')
# Parse query parameters
params = parse_query_params(request)
if 'num' in params:
digit = int(params['num'])
if 0 <= digit <= 9:
# Choose color based on parameter
color = WHITE
if 'color' in params:
if params['color'] == 'red':
color = RED
elif params['color'] == 'green':
color = GREEN
draw_thick_digit(digit, color, 3)
# Send response
cl.send('HTTP/1.1 200 OK\r\n\r\nOK')
cl.close()
except Exception as e:
cl.close()
print(f'Error: {e}')
The server responds to URLs like:
http://192.168.1.100/?num=5
- Display white “5”http://192.168.1.100/?num=3&color=red
- Display red “3”
Setting Up the Bartolome BLE Toolkit
The Bartolome BLE Toolkit provides a clean, Go-based interface for working with Bluetooth Low Energy devices, specifically designed for Timeular trackers.
Installing the Toolkit
go mod init timeular-display
go get github.com/coded-aesthetics/bartolome-ble-toolkit
Why Bartolome BLE Toolkit?
Traditional BLE programming involves complex connection management, device discovery, and protocol handling. Bartolome abstracts all this complexity:
- Automatic Connection Management - Handles reconnections when devices disconnect
- Device-Specific APIs - Pre-built support for Timeular protocol
- Robust Error Handling - Graceful handling of Bluetooth stack issues
The Go Bridge Application
Our Go application serves as the intelligent bridge between the BLE world and the HTTP world:
package main
import (
"fmt"
"net/http"
"time"
"github.com/coded-aesthetics/bartolome-ble-toolkit/pkg/ble"
"github.com/coded-aesthetics/bartolome-ble-toolkit/pkg/timeular"
)
func main() {
// Create Timeular device with custom polling
timeularDevice := timeular.NewDeviceWithConfig(timeular.Config{
Name: "Timeular Tra", // Your device name
PollInterval: 500 * time.Millisecond,
})
// Create BLE manager
manager := ble.NewManager()
// Handle side changes
timeularDevice.OnSideChange(func(deviceName string, side byte) error {
// Validate the detected side
if !timeular.IsValidSide(side) {
fmt.Printf("⚠️ Invalid side: %d\n", side)
return fmt.Errorf("invalid side: %d", side)
}
// Send HTTP request to Pico W
picoURL := fmt.Sprintf("http://192.168.0.185/?num=%d", side)
response, err := http.Get(picoURL)
if err != nil {
fmt.Printf("❌ Error updating display: %v\n", err)
return err
}
defer response.Body.Close()
fmt.Printf("🎲 Side %d -> LED Display updated\n", side)
return nil
})
// Handle disconnections gracefully
manager.SetDisconnectHandler(func(deviceName, address string, err error) {
fmt.Printf("⚠️ Device disconnected: %v\n", err)
fmt.Println("🔄 Attempting to reconnect...")
timeularDevice.Reset()
})
// Configure and connect
deviceConfig := ble.DeviceConfig{
Name: timeularDevice.GetName(),
ServiceUUID: timeularDevice.GetServiceUUID(),
CharacteristicUUID: timeularDevice.GetCharacteristicUUID(),
NotificationHandler: timeularDevice.ProcessNotification,
}
if err := manager.ConnectDevices([]ble.DeviceConfig{deviceConfig}); err != nil {
log.Fatalf("❌ Connection failed: %v", err)
}
fmt.Println("✅ Connected! Rotate your Timeular to see changes on the LED display")
// Keep running until interrupted
select {}
}
The Magic in Action
Here’s what happens when you rotate your Timeular tracker:
- Physical Rotation - You flip the Timeular from side 3 to side 7
- BLE Detection - The tracker’s internal accelerometer detects the new orientation
- Bluetooth Broadcast - Timeular sends BLE notification with new side data
- Go Processing - Bartolome toolkit receives and validates the side change
- HTTP Request - Go app sends
GET http://192.168.0.185/?num=7
- Display Update - Pico W parses the request and draws large “7” on LED matrix
- Visual Feedback - Bright, colorful “7” appears instantly on the 32x32 display
The entire process happens in under 100 milliseconds, creating a satisfying, real-time connection between physical and digital worlds.
Advanced Features and Customization
Color-Coded Sides
You can add color coding to make different sides more meaningful:
timeularDevice.OnSideChange(func(deviceName string, side byte) error {
var color string
switch side {
case 1, 2, 3:
color = "green" // Work tasks
case 4, 5, 6:
color = "red" // Break time
case 7, 8:
color = "white" // Other activities
}
url := fmt.Sprintf("http://192.168.0.185/?num=%d&color=%s", side, color)
// ... send request
})
Multiple Display Support
The architecture easily scales to control multiple displays:
displays := []string{
"http://192.168.0.185", // Kitchen display
"http://192.168.0.186", // Office display
"http://192.168.0.187", // Living room display
}
for _, display := range displays {
go func(url string) {
http.Get(fmt.Sprintf("%s/?num=%d", url, side))
}(display)
}
Time-Based Automation
Add scheduling logic to your bridge application:
timeularDevice.OnSideChange(func(deviceName string, side byte) error {
now := time.Now()
// Only update display during work hours
if now.Hour() >= 9 && now.Hour() <= 17 {
// Update display
} else {
// Maybe dim the display or show a clock
http.Get("http://192.168.0.185/?mode=clock")
}
return nil
})
Troubleshooting Common Issues
Bluetooth Connection Problems
- Permission Issues: Ensure your Go application has Bluetooth permissions
- Device Discovery: Make sure the Timeular is powered on and not connected to other apps
- Range Issues: Keep the Timeular within 10 meters of your computer
WiFi and HTTP Issues
- Network Connectivity: Verify Pico W and computer are on the same network
- Firewall: Check that port 80 is accessible on the Pico W
- IP Address Changes: Consider using mDNS or static IP for the Pico W
Display Problems
- Power Supply: Ensure adequate power for the LED matrix
- Library Issues: Verify Pimoroni libraries are correctly installed
- Memory: Monitor Pico W memory usage with complex displays
Extending the Project
This foundation opens up countless possibilities:
Integration with Productivity Tools
Connect your Timeular sides to:
- Toggl/Harvest - Automatic time tracking
- Slack Status - Update your status based on current activity
- Philips Hue - Change room lighting based on activity type