Hey everyone,
So, I’ve got this little thermal printer, a MakeID L1, and I’m trying to get it to print custom images directly from an ESP32. The end goal is to be able to print whatever I want without using the official app.
I’ve spent some time digging into how it works and managed to figure out the basics of the Bluetooth communication. I can connect to it, I know the structure of the data packets it expects, and I’ve even figured out the checksum calculation. I can successfully make it print by replaying the exact data I captured from the official Android app.
Here’s where I’m stuck: the image data.
The app doesn’t just send a standard bitmap. It uses some kind of proprietary compression or encoding on the image payload, and for the life of me, I can’t figure out what it is. It’s just a chunk of bytes that I can’t decipher.
Without understanding how to format my own images into this compressed format, I can’t actually print anything custom.
I’ve dumped all my findings, code, and the Bluetooth traffic logs into a GitHub repo. It has the ESP32 code, the raw data captures, and the original images I used for testing. The code works when replaying btsnoop_hci3.log frames, but I just don’t know exactly how should I translate bitmap into the encoding. (for more info check out my repo)
ble_printer_manager.h
#include <NimBLEDevice.h>
class PrinterAdvertisedDeviceCallbacks : public NimBLEScanCallbacks {
void onResult(NimBLEAdvertisedDevice *advertisedDevice) {
Serial.print("Found device: ");
Serial.println(advertisedDevice->toString().c_str());
if (PRINTER_MAC[0] != '\0' &&
advertisedDevice->getAddress().toString() == std::string(PRINTER_MAC)) {
Serial.println("Found target printer, stopping scan...");
NimBLEDevice::getScan()->stop();
}
}
};
void beginBLESniffer();
void startPrintJob();
void setBitmapFrame(std::vector<uint8_t> framecontent);
void setExampleBitmapFrame();
ble_printer_manager.cpp
#include "ble_printer_manager.h"
static const char *PRINTER_MAC = "58:8C:81:72:AB:0A";
NimBLERemoteService *pPrinterService = nullptr;
NimBLEClient *pClient = nullptr;
// globals
NimBLERemoteCharacteristic *pWriteChar;
NimBLERemoteCharacteristic *pNotifyChar;
volatile bool ackReceived = false;
std::vector<uint8_t> lastAck; // stores last notification bytes
int currentFrame = 0;
bool printingInProgress = false;
std::vector<uint8_t> frame1;
std::vector<uint8_t> frame2;
std::vector<uint8_t> frame3;
std::vector<uint8_t> frame4;
void setExampleBitmapFrame() {
frame1 = {0x66, 0x35, 0x00, 0x1b, 0x2f, 0x03, 0x01, 0x00, 0x01, 0x00, 0x01,
0x33, 0x01, 0x55, 0x00, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x3d, 0x03, 0x00, 0xff, 0x3f, 0xff, 0x28, 0x00, 0x00, 0x35,
0x2e, 0x00, 0x00, 0x38, 0xf3, 0x08, 0x03, 0x00, 0x00, 0x20, 0x00,
0x00, 0x00, 0x89, 0x2c, 0x00, 0x11, 0x00, 0x00, 0x63};
frame2 = {0x66, 0x2f, 0x00, 0x1b, 0x2f, 0x03, 0x01, 0x00, 0x01, 0x00,
0x01, 0x33, 0x01, 0x55, 0x00, 0x02, 0x00, 0x02, 0x00, 0x38,
0x00, 0x00, 0x00, 0x80, 0x00, 0x02, 0x03, 0x00, 0x00, 0x38,
0x00, 0xc3, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0xc5, 0x2c, 0x00, 0x11, 0x00, 0x00, 0xb1};
frame3 = {0x66, 0x2f, 0x00, 0x1b, 0x2f, 0x03, 0x01, 0x00, 0x01, 0x00,
0x01, 0x33, 0x01, 0x55, 0x00, 0x01, 0x00, 0x02, 0x00, 0x38,
0x00, 0x00, 0x00, 0x80, 0x00, 0x02, 0x03, 0x00, 0x00, 0x38,
0x00, 0xc3, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0xc5, 0x2c, 0x00, 0x11, 0x00, 0x00, 0xb2};
frame4 = {0x66, 0x44, 0x00, 0x1b, 0x2f, 0x03, 0x01, 0x00, 0x01, 0x00,
0x01, 0x33, 0x01, 0x34, 0x00, 0x00, 0x00, 0x02, 0x00, 0x38,
0x00, 0x00, 0x00, 0x80, 0x00, 0x02, 0x03, 0x00, 0x00, 0x38,
0x00, 0xc3, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x00, 0x08,
0x2f, 0x00, 0xff, 0x3f, 0xff, 0x28, 0x00, 0x00, 0x35, 0x2c,
0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0xaa};
}
// Notification callback
void notifyCallback(NimBLERemoteCharacteristic *chr, uint8_t *data, size_t len,
bool isNotify) {
// copy ack
lastAck.assign(data, data + len);
ackReceived = true;
Serial.print("Notification [");
Serial.print(chr->getUUID().toString().c_str());
Serial.print("] : ");
for (size_t i = 0; i < len; ++i)
Serial.printf("%02X ", data[i]);
Serial.println();
// When printing, use each notification as a signal to send the next frame
if (printingInProgress && chr == pNotifyChar) {
delay(50); // slight delay to allow internal buffer clear
currentFrame++;
const uint8_t *nextFrame = nullptr;
size_t lenNext = 0;
switch (currentFrame) {
case 1:
nextFrame = &frame2[0];
lenNext = frame2.size();
break;
case 2:
nextFrame = &frame3[0];
lenNext = frame3.size();
break;
case 3:
nextFrame = &frame4[0];
lenNext = frame4.size();
break;
default:
Serial.println("All frames sent. Printing should complete.");
printingInProgress = false;
return;
}
Serial.printf("Sending frame %d...\n", currentFrame + 1);
Serial.printf("%s", nextFrame);
pWriteChar->writeValue(nextFrame, lenNext, false);
}
}
void startPrintJob() {
if (!pWriteChar) {
Serial.println("No write characteristic available!");
return;
}
currentFrame = 0;
printingInProgress = true;
Serial.println("Starting print job (frame 1)...");
pWriteChar->writeValue(&frame1[0], frame1.size(), false);
}
void startScanner() {
// Scan
NimBLEScan *pScan = NimBLEDevice::getScan();
pScan->setScanCallbacks(new PrinterAdvertisedDeviceCallbacks());
pScan->setInterval(45);
pScan->setWindow(15);
pScan->setActiveScan(true);
pScan->start(5, false);
if (PRINTER_MAC[0] == '\0')
return;
}
void startConnectionFindServices() {
// Connect
NimBLEAddress addr(std::string(PRINTER_MAC), BLE_ADDR_PUBLIC);
pClient = NimBLEDevice::createClient();
Serial.print("Connecting to printer: ");
Serial.println(addr.toString().c_str());
if (!pClient->connect(addr)) {
Serial.println("Failed to connect.");
return;
}
Serial.println("Connected!");
// Negotiate MTU
uint16_t mtu = pClient->getMTU();
Serial.printf("Negotiated MTU: %u\n", mtu);
// Find printer service (UUID 0xABF0)
pPrinterService = pClient->getService("ABF0");
if (!pPrinterService) {
Serial.println("Printer service (0xABF0) not found!");
return;
}
Serial.println("Printer service found.");
// Get write characteristic (ABF1)
pWriteChar = pPrinterService->getCharacteristic("ABF1");
if (!pWriteChar) {
Serial.println("Write characteristic ABF1 not found!");
return;
}
Serial.println("Write characteristic ABF1 found.");
// Get notify characteristic (ABF2)
pNotifyChar = pPrinterService->getCharacteristic("ABF2");
if (!pNotifyChar) {
Serial.println("Notify characteristic ABF2 not found!");
return;
}
Serial.println("Notify characteristic ABF2 found.");
// Subscribe to ABF2 notifications
if (pNotifyChar->canNotify()) {
if (pNotifyChar->subscribe(true, notifyCallback)) {
Serial.println("Subscribed to ABF2 notifications.");
} else {
Serial.println("Subscribe to ABF2 failed.");
}
}
}
void beginBLESniffer() {
Serial.println("Starting BLE sniffer...");
NimBLEDevice::init("ESP32-BLE-Sniffer");
startScanner();
if (PRINTER_MAC[0] != '\0')
startConnectionFindServices();
}
main.cpp
#include <Arduino.h>
#include <ble_printer_manager.h>
void setup() {
Serial.begin(115200);
NimBLEDevice::init("ESP32-BLE-Sniffer");
beginBLESniffer();
if (PRINTER_MAC[0] != '\0') {
setExampleBitmapFrame(); // replay btsnoop_hci3.log frames
startPrintJob();
}
}
void loop() { delay(1000); }
If anyone has experience with reverse-engineering printer protocols or recognizes weird compression formats, I’d be super grateful for any pointers or ideas!
Thanks!



