Een Bluetooth-printerdriver vanaf nul: van Python op de laptop naar C++ op een ESP32-C3
Dat werkt prima, maar voor mij wordt zo’n apparaat vooral interessant wanneer ik de originele software helemaal uit de keten haal. Kan ik rechtstreeks met de printer communiceren? Kan ik achterhalen welk protocol wordt gebruikt? En kan ik de printer uiteindelijk autonoom laten werken zonder telefoon of computer?
Ik pak het project bewust in twee stappen aan. Eerst ontwikkel ik op mijn laptop een werkende printerdriver in Python. Python geeft me de vrijheid om snel te experimenteren met Bluetooth Low Energy (BLE), services, characteristics, printercommando’s en rasterdata. Als die aansturing betrouwbaar werkt, port ik dezelfde functionaliteit naar C++ op een ESP32-C3.
Die tweede stap verandert het karakter van het project. De Python-versie is mijn ontwikkel- en reverse-engineeringomgeving. Met de ESP32-C3 ontstaat vervolgens een klein embedded platform dat zelfstandig alerts, notificaties en werkbonnen kan printen.
Het uitgangspunt is een eenvoudige Hi.22.052 Pocket Printer die normaal via een app op de telefoon wordt aangestuurd.
Hardware-onderzoek
Zoals wel vaker begint het project met het verwijderen van een paar schroeven. Binnenin vind ik een thermische printkop, papiertransport met een klein motortje, een 3,7 volt Li-ion accu en een verrassend compacte printplaat met de complete besturing.
Ik haal de elektronica verder uit elkaar om te zien hoe de verschillende onderdelen met elkaar verbonden zijn. De thermische printkop en motor zijn via flexprints met de besturing verbonden. Onder de microscoop bekijk ik vervolgens printbanen, componenten en verschillende delen van de elektronica.
De hardware vertelt me hoe het apparaat fysiek is opgebouwd, maar nog niet welke data ik naar de printer moet sturen. Daarvoor moet ik naar de softwarekant.
Reverse engineering van het protocol
Een officiële SDK of protocolbeschrijving heb ik niet. Ik gebruik daarom de bestaande software en de Bluetooth-communicatie als uitgangspunt voor mijn eigen onderzoek.
De printer meldt zich als: B23a7F36BLE. In de app kan ik daarnaast het serienummer en firmwareversie V2.0.4 terugvinden. Met een BLE-browser onderzoek ik de GATT-structuur en zoek ik naar services, write-characteristics en notifications. Vervolgens gebruik ik de nRF BLE-browser om te bekijken welke services en characteristics het apparaat aanbiedt. Ik zoek daarbij vooral naar characteristics waarop ik kan schrijven en naar notifications waarmee de printer informatie terugstuurt.
Mijn exemplaar heeft het volgende BLE MAC-adres: CC:99:17:57:7F:36.
In mijn uiteindelijke driver gebruik ik de volgende configuratie:
- Service:
FF00(0000ff00-0000-1000-8000-00805f9b34fb) - Data verzenden: Characteristic
FF02 - Notifications ontvangen: Characteristic
FF03
Langzaam ontstaat daarmee mijn eigen documentatie van een printer waarvoor ik geen bruikbare programmeerhandleiding heb.
Stap 1: Een werkende driver in Python
Ik begin bewust nog niet met de ESP32-C3. Eerst wil ik op mijn laptop aantonen dat ik de printer volledig vanuit mijn eigen software kan bedienen. Daarvoor gebruik ik Python met Bleak voor Bluetooth Low Energy. Python is voor dit soort onderzoek prettig omdat ik snel kan aanpassen en testen.
Een vereenvoudigd stukje van deze eerste driver ziet er zo uit:
import asyncio
from bleak import BleakClient
PRINTER = "CC:99:17:57:7F:36"
FF02 = "0000ff02-0000-1000-8000-00805f9b34fb"
FF03 = "0000ff03-0000-1000-8000-00805f9b34fb"
ready = asyncio.Event()
def notification_handler(sender, data):
if data == bytes([0x01, 0x01]):
ready.set()
async def send(client, data):
ready.clear()
await client.write_gatt_char(
FF02, data, response=False
)
try:
await asyncio.wait_for(
ready.wait(), timeout=0.15
)
except asyncio.TimeoutError:
pass
Via FF02 verstuur ik data. Tegelijkertijd luister ik via FF03 naar de printer. Wanneer daar 01 01 binnenkomt, kan mijn programma doorgaan met het versturen van de volgende data.
Het moment waarop het motortje voor het eerst vanuit mijn eigen Python-code begint te draaien, is een belangrijke mijlpaal. De route klopt: Python → BLE → eigen printercommando’s → papier.
Van commando’s naar pixels
De thermische printkop is 384 pixels breed. Omdat iedere pixel één bit vertegenwoordigt, bestaat één horizontale regel uit $384 / 8 = 48$ bytes. Voor het versturen van rasterdata gebruik ik een commando dat begint met: 1D 76 30 00.
Daarna volgen de breedte, hoogte en de bitmapdata. Iedere groep van acht pixels wordt daarbij één byte. Een paar bytes versturen blijkt echter eenvoudiger dan een complete grafische bon betrouwbaar over BLE krijgen.
Flow control en betrouwbaarheid
Wanneer ik probeer grotere hoeveelheden rasterdata te versturen, ontstaan incomplete prints omdat ik de printer simpelweg zo snel mogelijk van te veel bytes voorzie. De notifications via FF03 blijken hiervoor de sleutel. De printer stuurt 01 01 terug wanneer hij weer verder kan.
Ik deel de datastroom daarom op in kleinere stukken. In de ESP32-versie gebruik ik uiteindelijk chunks van maximaal 120 bytes, gecombineerd met een timeout van 150 milliseconden.
Conceptueel ziet de communicatie er als volgt uit: ESP32-C3 $\xrightarrow{\text{data (FF02)}}$ B23a ESP32-C3 $\xleftarrow{\text{01 01 (FF03)}}$ B23a ESP32-C3 $\xrightarrow{\text{volgende data (FF02)}}$ B23a
Met die flow control wordt het printen van grotere afbeeldingen betrouwbaar.
Stap 2: Van Python naar C++ op de ESP32-C3
Nu ik een werkende Python-driver heb, weet ik hoe ik verbinding moet maken, welke characteristics ik nodig heb, hoe de printercommando’s zijn opgebouwd en hoe ik rekening moet houden met de verwerkingssnelheid. Nu port ik de driver naar C++ op een ESP32-C3.
Voor de embedded versie gebruik ik de volgende libraries:
Arduino.h: Basis framework.NimBLEDevice.h: Voor de BLE-communicatie.Adafruit_GFX.h: Voor de grafische opbouw.qrcode.h: Voor het genereren van QR-codes.
De flow control die ik eerst in Python ontwikkelde, komt vrijwel letterlijk terug in C++:
bool sendChunk(const uint8_t* data, size_t length) {
flowReady = false;
bool result = ff02->writeValue(
data, length, false
);
if (!result) return false;
unsigned long start = millis();
while (!flowReady) {
delay(1);
if (millis() - start > 150) {
break;
}
}
return true;
}
De callback van FF03 zet flowReady weer op true zodra 01 01 binnenkomt. De laptop kan nu definitief uit de keten.
Zelf een complete bon renderen
Nu de ESP32-C3 zelfstandig kan printen, wil ik ook de complete bon op de microcontroller zelf opbouwen. Daarvoor gebruik ik GFXcanvas1 uit Adafruit GFX:
GFXcanvas1 canvas(
PRINTER_WIDTH,
CANVAS_HEIGHT
);
Op dit monochrome canvas teken ik teksten, lijnen, waarschuwingssymbolen, KPI-indicatoren, een trendgrafiek en een QR-code. De iconen worden gebouwd met functies als drawLine(), fillRect(), drawCircle() en drawTriangle(). De QR-code wordt met qrcode.h gegenereerd en rechtstreeks op het canvas getekend.
Voor het printen verwerk ik de afbeelding in stroken van 32 pixels hoog. Eén strook bevat $48 \times 32 = 1536$ bytes rasterdata. Met de acht bytes rasterheader komt een volledige tijdelijke buffer op ongeveer 1544 bytes.
De complete route is daarmee: Adafruit GFX → 1-bit canvas → rasterbuffer → BLE-chunks → FF02 → printer.
Fouten en optimalisatie
Tijdens het uitbreiden van de C++-code kwamen er compilerfouten voor, zoals:
'canvas' was not declared in this scope'drawWarningIcon' was not declared in this scope
De oorzaak was dat ik een deel van renderTicket() op de verkeerde plaats in de sketch had geplaatst, waardoor objecten en functies werden gebruikt voordat ze bekend waren. De oplossing was de code logisch te structureren: eerst libraries en globale objecten, vervolgens callbacks en tekenfuncties, en daarna pas het renderen en printen.
Ook fysieke testprints waren essentieel. Een icoon dat op een beeldscherm prima lijkt, kan op thermisch papier ineens te klein zijn. Teksten moesten groter, de QR-code duidelijker en de layout moest vooral op het echte papier functioneel zijn.
Toepassing: Predictive Maintenance-alert
De eerste echte toepassing is een sample voor een 'Predictive Maintenance-alert'. De ESP32-C3 bouwt zelfstandig een bon met:
- Machine-ID, locatie, eventnummer en anomaly score.
- KPI’s voor trillingen, temperatuur, stroom en toerental.
- Een grafiek die de trend toont.
- Een QR-code waarmee de papieren melding aan aanvullende digitale informatie kan worden gekoppeld.
Hiermee kan de machine operator meteen aan de slag; het is actiegericht en helder als opdrachtbon.
Conclusie: Sensor in, papier uit
De ESP32-C3 heeft naast BLE ook WiFi. Daarmee kan het platform later worden gekoppeld aan bijvoorbeeld MQTT, een REST-interface, een machinecontroller of een andere IoT-omgeving. Een Anomaly Detection-model kan dan ergens in de keten afwijkend gedrag constateren, waarna de ESP32-C3 de melding ontvangt, de bon opbouwt en deze direct bij de machine print. Geen app, geen telefoon en geen laptop meer nodig.
Dit project combineert elektronica, reverse-engineering, Python, Bluetooth Low Energy, embedded C++, grafische vormgeving, IoT en data-analyse. Het resultaat is tastbaar: de ESP32-C3 stuurt zijn laatste pakket, het motortje draait en er ligt een Predictive Maintenance-alert op tafel.
Sensor in, papier uit.
Groetjes,