Need some help reverse-engineering a thermal printer's image format!

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!

I think your best chances are to take https://github.com/Konloch/bytecode-viewer/releases and dig into the Android app. Look for anything that has to do with Bitmap / picture conversion into byte arrays or the L1 printer specifically.

Some interesting pieces of code:

  • com\wewin\wewinprinter_api\printer\CreateL1DotArray.class
import org.minilzo.common.LZOUtil;

public ArrayList getDotArrayFromPixelsForL1(wewinPrinterLabelParamHelper var1, int var2, int var3, int var4, float var5, float var6, int var7) {
  //...
  var46 = Bitmap.createBitmap(var438, 0, var36, var34, var29);
  var450 = new byte[var26 * var29];
  //...
  wewinPrinterByteArrayHelper var463 = new wewinPrinterByteArrayHelper();
  var463.setWidth(var34);
  var463.setHeight(var29);
  var463.setLeft(0);
  var463.setTop(var36);
  var463.setByteArray(LZOUtil.lzo1xCompress(var450));
  var452.add(var463);
  //...
}

..but also other pieces of code where there’s no compression..

   private ArrayList getDotArrayFromBitmap(SplitBitmapType var1, Bitmap var2, int var3, boolean var4) {
    //...
            Bitmap var13 = Bitmap.createBitmap(var2, var10, var6, var7, var8);
            var12 = wewinPrinterCommonUtil.getBitmapPixelsToByteArray(var13, wewinPrinterManager.dotConvertValue, true, var4);
            if (!var13.isRecycled()) {
               var13.recycle();
            }

            wewinPrinterByteArrayHelper var17 = new wewinPrinterByteArrayHelper();
            var17.setWidth(var7);
            var17.setHeight(var8);
            var17.setLeft(var10);
            var17.setTop(var6);
            if (var1 == com.wewin.wewinprinter_api.entry_configure.wewinPrinterUpdateEntryPackage.SplitBitmapType.Horizontal) {
               var17.setByteArray(var12);
            } else {
               var17.setByteArray(LZOUtil.lzo1xCompress(var12));
            }

            var11.add(var17);
//..

hey maxgerhardt, I will try using bytecode-viewer, previously I used the command jadx MakeID-Life_2.2.0_APKPure.xapk , it returned

INFO  - loading …
INFO  - processing …
ERROR - finished with errors, count: 6

from that, the only thing that was visible was checksum function, but the payload data compression algorithm was unprocessed due to errors

I’m not sure whether I’m hallucinating or not, but trying to use the same minilzo decoder

https://github.com/yuhaoth/minilzo

And taking each bluetooth frame and trying different offsets in the data, I happen to find that at a constant starting offset of 17 bytes (and giving it all the remaining data), the decoder only complains about not having consumed all input but so far having produced 1020 bytes of data:

Trying offset 0, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 53 B
Trying offset 1, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 36 B, Input 52 B
Trying offset 2, LZO1X decompress returned: -8 (LZO_E_INPUT_NOT_CONSUMED) Dest Len 45 B, Input 51 B
Trying offset 3, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 10 B, Input 50 B
Trying offset 4, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 30 B, Input 49 B
Trying offset 5, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 6 B, Input 48 B
Trying offset 6, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 4 B, Input 47 B
Trying offset 7, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 19 B, Input 46 B
Trying offset 8, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 4 B, Input 45 B
Trying offset 9, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 19 B, Input 44 B
Trying offset 10, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 4 B, Input 43 B
Trying offset 11, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 34 B, Input 42 B
Trying offset 12, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 4 B, Input 41 B
Trying offset 13, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 40 B
Trying offset 14, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 21 B, Input 39 B
Trying offset 15, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 6 B, Input 38 B
Trying offset 16, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 20 B, Input 37 B
Trying offset 17, LZO1X decompress returned: -8 (LZO_E_INPUT_NOT_CONSUMED) Dest Len 1020 B, Input 36 B
Trying offset 18, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 35 B
Trying offset 19, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 34 B
Trying offset 20, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 33 B
Trying offset 21, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 32 B
Trying offset 22, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 31 B
Trying offset 23, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 30 B
Trying offset 24, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 6 B, Input 29 B
Trying offset 25, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 28 B
Trying offset 26, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 27 B
Trying offset 27, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 26 B
Trying offset 28, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 25 B
Trying offset 29, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 24 B
Trying offset 30, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 23 B
Trying offset 31, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 22 B
Trying offset 32, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 21 B
Trying offset 33, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 20 B
Trying offset 34, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 19 B
Trying offset 35, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 18 B
Trying offset 36, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 17 B
Trying offset 37, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 16 B
Trying offset 38, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 11 B, Input 15 B
Trying offset 39, LZO1X decompress returned: -6 (LZO_E_LOOKBEHIND_OVERRUN) Dest Len 6 B, Input 14 B
Trying offset 40, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 13 B
Trying offset 41, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 12 B
Trying offset 42, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 11 B
Trying offset 43, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 10 B
Trying offset 44, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 9 B
Trying offset 45, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 8 B
Trying offset 46, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 7 B
Trying offset 47, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 6 B
Trying offset 48, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 5 B
Trying offset 49, LZO1X decompress returned: -8 (LZO_E_INPUT_NOT_CONSUMED) Dest Len 0 B, Input 4 B
Trying offset 50, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 3 B
Trying offset 51, LZO1X decompress returned: -4 (LZO_E_INPUT_OVERRUN) Dest Len 0 B, Input 2 B

Then, cutting off the end of the data in every increasing steps, I find that with a cutoff of the last 18 bytes:

  • frame 1 LZO decodes into 1020 bytes of data
  • frame 2 LZO decodes into 1020 bytes of data
  • frame 3 LZO decodes into 1020 bytes of data
  • frame 4 LZO decodes into 624 bytes of data

Though trying to interpret these bytes as pixels, I have had no sensible interpretation of them yet.

See:

Frame 1

00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 FF 3F FF FF  FF FF FF FF FF FF FF FF  |  .....?..........
FF 3F FF FF FF FF FF FF  FF FF FF FF FF 3F FF FF  |  .?...........?..
FF FF FF FF FF FF FF FF  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8...... 
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8.......... 
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8.......... 
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00              |  .8..........

Frame 2

00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8.......... 
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8.......... 
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8.......... 
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00              |  .8..........

Frame 3

00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8.. 
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8.......... 
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00              |  .8..........

Frame 4

00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8.......... 
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8.......... 
00 38 00 00 00 00 00 00  00 00 03 00 00 38 00 00  |  .8...........8..
00 00 00 00 00 00 03 00  00 38 00 00 00 00 00 00  |  .........8......
00 00 03 00 00 38 00 00  00 00 00 00 00 00 03 00  |  .....8..........
FF 3F FF FF FF FF FF FF  FF FF FF FF FF 3F FF FF  |  .?...........?..
FF FF FF FF FF FF FF FF  FF 3F FF FF FF FF FF FF  |  .........?......
FF FF FF FF 00 00 00 00  00 00 00 00 00 00 00 00  |  ................

ByteViewer is a thousand times better than that because it includes Fernflower, Procryon and other decompilers, which decompile it differently. Some tricky decompiles are better done with Procyon, for other cases, FernFlower produces better results. At least one of them code decompile every file I threw at it.

1 Like

I have summarized all the hex stream into a higher level view so we can compare between different payloads that I have captured using Wireshark so far as seen in this newly added makeid-l1-printer-sample/hex_stream_and_image_comparison.md (turn on dark mode for better view of picture white area)

Hah. With the all black image, I get a LZO decompress that decodes to all 0xFF (white pixel, so maybe inverted).

Trying start offset 17 cutoff 1, LZO1X decompress returned: 0 (LZO_E_OK) Dest Len 1020 B, Input 16 B
Buf after decompress
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................ 
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................ 
FF FF FF FF FF FF FF FF  FF FF FF FF FF FF FF FF  |  ................
FF FF FF FF FF FF FF FF  FF FF FF FF              |  ............
1 Like

Equally with the all-white image, frame 1, I get a LZO decompress to all 0x00 (no light, black pixel, so surely inverted).

Trying start offset 17 cutoff 1, LZO1X decompress returned: 0 (LZO_E_OK) Dest Len 1020 B, Input 16 B
Buf after decompress
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................ 
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................ 
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................ 
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................ 
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................
00 00 00 00 00 00 00 00  00 00 00 00              |  ............

So close! Trying to decode the “btsnoop_hci1.log” picture.

Original picture:

Every frame LZO decodes correctly with offset = 17 and cutoff = 1 with respect to the full bluetooth frame.

The black image with the white stripe at the right side produced 3468 Byte of total decompressed output. The first 3456 bytes were 0xFF and the very last 12 bytes were 0x00.

Since 12 byte = 96 bits, and the 12 0xff bytes should represent the white column on the very far right of the black picture, this leads to me assume that the input picture is scanned column wise, left to right, up-down.

When I do that, I indeed get the picture back with dimensions 289 x 96 pixels (4 pixels shorter than the original 289 x 100.. maybe cut off or resized).

When I try the same reconstruction with the decompressed data above, I get something almost right

thermal_reconstructed

from PIL import Image
import numpy as np

# Simulated input: all black except last 12 bytes (rightmost column) = white
#data = b"\xFF" * (3468 - 12) + b"\x00" * 12
# decompressed from btsnoop_hci1
frame1 = "FF 0F E0 FF 1F 07 03 FF 9F FF FF FF FF 0F E0 FF 1F 0F 03 FF 9F FF FF FF FF 0F E0 FF 1F 0F 03 FF 9F FF FF FF FF 0F E3 FF 1F CF FF FF 1F FF FF FF 00 0F E3 01 03 CF FE FF 1E 38 01 00 00 0F E3 01 03 CF FE FF 1E 38 01 00 1F 0F E3 F1 1F C7 FF FF 1E F8 F1 3F 1F 0F E0 F9 1F 00 1F C7 1E F8 F1 3F 1F 0F E0 F9 1F 00 1F C7 1E F8 F1 3F 1F 0F E0 F9 1F 00 1F C7 1E FC F1 3F 1F 0F E3 F9 1E FF 00 7F 9E 07 F1 3F 1F 0F E3 F9 1E FF 00 7F 9E 07 F1 3F 1F 0F E3 F9 1E FF 00 7F 9E 07 F1 3F 1F 0F E0 F9 00 78 F0 0F 9E 07 F1 3F 1F 0F E0 F9 00 78 F0 07 9E 07 F1 3F 1F 0F E0 F9 00 78 F0 07 9E 07 F1 3F 1F 0F E3 F1 FE F8 FE 07 1E 3F F1 3F 00 0F E3 01 FE F8 1E 00 1E 38 01 00 00 0F E3 01 FE F8 1E 00 1E 38 01 00 FF 0F E3 FF FE FF FF 38 1F FF FF FF FF 0F E3 FF 1E CF F3 78 9F C7 FF FF FF 0F E3 FF 1E CF F3 78 9F C7 FF FF FF 0F E3 FF 1E CF F3 78 9F C7 FF FF 00 00 00 00 F0 7F F3 78 80 FF 00 00 00 00 00 00 F0 7F F3 78 80 FF 00 00 00 00 00 00 F0 7F F3 78 80 FF 00 00 FF 0F FF F9 F0 F8 00 00 F1 07 8E C7 FF 0F FF F9 F0 F8 00 00 F1 07 8E C7 FF 0F FF F9 F0 F8 00 00 F1 07 8E C7 FF 0F FF FF FF FF 00 FF FF 07 8E FF 1F 0F 1E CF 1F 0F 00 FF 1F 00 8E FF 1F 0F 1E CF 1F 0F 00 FF 1F 00 8E FF 1F 0F FF 8F 1F CF 1F FF FF C0 FE FF 1C 00 FF 01 03 CF 1F FF F1 C0 F0 C7 1C 00 FF 01 03 CF 1F FF F1 C0 F0 C7 3C 00 FF 01 03 C7 1F FF F3 C0 F0 E7 E0 01 00 7F 1F 00 1F C7 FF FF 81 FF E0 01 00 7F 1F 00 1F C7 FF FF 81 FF E0 01 00 7F 1F 00 1F C7 FF FF 81 FF FF 0F FF C1 1E FF 1E 7F 1F 00 00 C0 FF 0F FF C1 1E FF 1E 7F 1F 00 00 C0 FF 0F FF C1 1E FF 1E 7F 1F 00 00 C0 FF 07 FF FF 1C FF FC 3F FF 38 8E FF 1F 00 1F FF 00 C0 F0 00 FF 38 8E FF 1F 00 1F FF 00 C0 F0 00 FF 38 8E FF FF 0F FF FF FE FF FE 00 FF 3F FF FF E3 0F E0 C1 FE 7F 1E 00 81 07 FF C7 E3 0F E0 C1 FE 7F 1E 00 81 07 FF C7 FF 0F E1 C7 FE FF 7F 38 8F FF FF FF 1C 0F 03 0F F0 CF FF 78 9F FF 0F FC 1C 0F 03 0F F0 CF FF 78 9F FF 0F FC 1C 0F 03 0F F0 CF FF 78 9F FF 0F FC 1F 0F FE C1 F0 78 FF 07 83 C7 00 C0 1F 0F FE C1 F0 78 FF 07 81 C7 00 C0 1F 0F FE C1 F0 78 FF 07 81 C7 00 C0 1F 0F FF C0 FF F0 FF FF F1 FF 8E FF 1F 0F 03 C0 1F C0 F0 F8 F1 3F 8E FF 1F 0F 03 C0 1F C0 F0 F8 F1 3F 8E FF 1F 0F E3 80 1F C7 FE F8 F1 3F FE FF 00 0F E0 01 03 0F 1E F8 01 00 FE C7 00 0F E0 01 03 0F 1E F8 01 00 FE C7 03 0F FF FF 1F FF FF F8 FF FF FF FF 03 0F 1F FF 1F F8 FF C0 FF FF 0F FC 03 0F 1F FF 1F F8 FF C0 FF FF 0F FC 03 0F 1F FF 1F F8 FF C0 FF FF 1F FC 03 0F E3 C1 1E FF FE 7F FF 07 FE FF 03 0F E3 C1 1E FF FE 7F FF 07 FE FF 03 0F E3 C1 1E FF FE 7F FF 07 FE FF 03 06 E3 80 0C FF FC 3F FF 3F FE FF 00 00 03 00 00 C0 00 00 80 3F 0E 3C 00 00 03 00 00 C0 00 00 80 3F 0E 3C FF 0F E3 FF FE F8 FE 07 8E 3F FE 3F FF 0F E3 FF FE F8 FE 07 9E 07 F0 3F FF 0F E3 FF FE F8 FE 07 9E 07 F0 3F FF 0F E3 FF FE FF FF 3F 9E FF FF 3F 00 0F E0 01 F0 7F FF 78 80 FF FF 3C 00 0F E0 01 F0 7F FF 78 80 FF FF 3C 00 0F E0 01 F0 7F FF 78 80 FF FF 3C 1F 0F E3 F9 F0 C0 FF 07 FF C7 FF FC 1F 0F E3 F9 F0 C0 FF 07 FF C7 FF FC 1F 0F E3 F9 F0 C0 FF 07 FF C7 FF FC 1F 0F E3 F9 1F FF 80 FF 83 07 00 E0 1F 0F E3 F9 1F FF 00 FF 81 07 00 C0 1F 0F E3 F9 1F FF 00 FF 81 07 00 C0 1F 0F E3 F9 1F FF 1E FF 01 3F FE C0 "
frame2 = "1F 0F E3 F9 03 C0 1E FF 01 38 FE C0 1F 0F E3 F9 03 C0 1E FF 01 38 FE C0 1F 0F E3 F9 1F C0 FF FF 01 F8 FF C7 00 0F E3 01 1F C0 F3 C0 00 C0 F1 07 00 0F E3 01 1F C0 F3 C0 00 C0 F1 07 00 0F E3 01 1F C0 F3 C0 00 C0 F1 07 FF 0F E3 FF 1E FF F0 7F FE 3F F0 3F FF 0F E3 FF 1E FF F0 7F FE 3F F0 3F FF 0F E3 FF 1E FF F0 7F FE 3F F0 3F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3F 00 00 FF 00 00 00 00 00 00 00 00 3F 00 00 FF 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 01 00 00 E0 00 E0 00 70 00 00 38 00 0F 00 07 F8 03 FE 00 FC 00 1C 38 00 0F 00 0F FC 07 FE 06 FE 00 7F 38 00 00 00 0E 0E 0F 04 06 0F 80 FF 38 00 00 00 0C 0E 0E 00 07 07 C0 83 3F 00 00 F8 0C 06 1C 00 87 03 C0 01 3F 00 00 F8 0E 06 1C 00 87 03 C0 01 38 00 00 00 0F 7E 1C 00 87 03 C0 01 38 00 07 00 07 FE 1C F0 87 03 C0 01 38 00 0F 00 03 C6 18 FC 87 01 C0 01 38 00 1C 00 00 06 1C 7E 87 03 C0 01 38 00 18 00 00 06 1C 0E 87 03 C0 01 38 00 38 00 00 06 1C 06 87 03 C0 01 38 00 38 00 00 06 1C 06 87 03 C0 01 38 00 38 00 00 0E 0E 06 07 07 C0 01 38 00 1C 00 0C 1E 0F 0E 07 0F C0 01 3F 00 1F FF 1F F6 07 FC 07 FE C0 01 3F 00 0F FF 0F E6 03 F8 07 FC C0 01 00 00 00 00 00 00 00 00 07 00 C0 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF 3F 80 FF 7F 1C 0F FC 7F FE FF FF FF 3F 80 FF 7F 1C 0F FE 7F FE FF FF FF 3F 80 FF 7F 1C 0F FE 7F FE FF FF FF 3F 8F FF 7F 1C FF FF 7F FC FF FF 00 3C 8F 03 0F 1C F8 FF 78 E0 07 00 00 3C 8F 03 0F 1C F8 FF 78 E0 07 00 7F 3C 8F C3 3F 1C FF FF 78 E0 C7 FF 7F 3C 80 E3 7F 00 7F 1E 78 E0 C7 FF 7F 3C 80 E3 7F 00 7F 1E 78 E0 C7 FF 7F 3C 80 E3 7F 00 7F 1E 78 F0 C7 FF 7F 3C 8F E3 79 FC 00 FE 78 1E C7 FF 7F 3C 8F E3 79 FC 00 FE 78 1E C7 FF 7F 3C 8F E3 79 FC 00 FE 78 1E C7 FF "
frame3 = "7F 3C 81 E3 00 E0 C0 3F 78 1E C7 FF 7F 3C 81 E3 00 E0 C0 1F 78 1E C7 FF 7F 3C 81 E3 00 E0 C0 1F 78 1E C7 FF 7F 3C 8F E3 F8 E3 F8 1F 78 FC C7 FF 00 3C 8F 03 F8 E3 78 00 78 E0 07 00 00 3C 8F 03 F8 E3 78 00 78 E0 07 00 FF 3F 8F FF F8 FF FF E3 7F FC FF FF FF 3F 8F FF 79 1C CF E3 7F 1E FF FF FF 3F 8F FF 79 1C CF E3 7F 1E FF FF FF 3F 8F FF 79 3E CF E3 7F 1E FF FF 00 00 01 00 C1 FF CF E3 00 FE 00 00 00 00 01 00 C1 FF CF E3 00 FE 00 00 00 00 01 00 C1 FF CF E3 00 FE 00 00 FF 3F FF E3 C0 E3 00 00 C7 1F 38 1E FF 3F FF E3 C0 E3 00 00 C7 1F 38 1E FF 3F FF E3 C0 E3 00 00 C7 1F 38 1E FF 3F FF FF FF FF 00 FC FF 1F 38 FE 7F 3C 78 3C 7F 1C 00 FE 7F 00 38 FE 7F 3C 78 3C 7F 1C 00 FE 7F 00 38 FE 7E 3C FF 1F 7F 1C 7F FE FF 03 F8 FF 70 00 FF 03 0F 1C 7F FE C7 03 C0 1F 70 00 FF 03 0F 1C 7F FE C7 03 C0 1F F0 00 FF 07 0F 1C 7F FE CF 03 C0 9F 81 07 00 FC 7F 00 7F 1E FF FF 07 FE 81 07 00 FC 7F 00 7F 1E FF FF 07 FE 81 07 00 FC 7F 00 7F 1E FF FF 07 FE FF 3F FF 07 79 FC 78 FE 7F 00 00 80 FF 3F FF 03 79 FC 78 FE 7F 00 00 00 FF 3F FF 03 79 FC 78 FE 7F 00 00 00 FF 1F FF FF 30 FC F0 FF FF E3 38 FE 7F 00 7F FC 00 00 C0 03 FF E3 38 FE 7F 00 7F FC 00 00 C0 03 FF E3 38 FE FF 3F FF FF F8 FF F8 03 FF FF FF FF 8F 3F 81 03 F8 FF 78 00 07 1E FF 1F 8F 3F 81 03 F8 FF 78 00 07 1E FF 1F FF 3F 87 1F F8 FF FF E1 3F FE FF FF 70 3C 0F 3C C1 1F FF E3 7F FE 3F F0 70 3C 0F 3C C1 1F FF E3 7F FE 3F F0 70 3C 0F 3C C1 1F FF E3 7F FE 3F F0 7F 3C F9 07 C0 E3 FF 1F 07 1E 00 80 7F 3C F9 03 C0 E3 FF 1F 07 1E 00 00 7F 3C F9 03 C0 E3 FF 1F 07 1E 00 00 7F 3C FF 03 FF C3 FF FF C7 FF 38 FE 7F 3C 0F 00 7F 00 C0 E3 C7 FF 38 FE 7F 3C 0F 00 7F 00 C0 E3 C7 FF 38 FE 7E 3C 8F 03 7F 1C F8 E3 C7 FF F8 FF 00 3C 80 03 0F 1C 78 E0 07 00 F8 1F 00 3C 80 03 0F 1C 78 E0 07 00 F8 1F 0F 3C FF FF 3F FC FF E3 FF FF FF FF 0F 3C 7F FC 7F E0 FF 03 FF FF 3F F0 0F 3C 7F FC 7F E0 FF 03 FF FF 3F F0 0F 3C 7F FC 7F E0 FF 03 FF FF 7F F0 0F 3C 8F 03 79 FC F8 FF FF 1F F8 FF 0F 3C 8F 03 79 FC F8 FF FF 1F F8 FF 0F 3C 8F 03 79 FC F8 FF FF 1F F8 FF 06 18 8F 03 30 FC F0 FF FF FF F8 FF 00 00 0F 00 00 00 00 00 00 FE 38 F0 00 00 0F 00 00 00 00 00 00 FE 38 F0 FF 3F 8F FF F8 E3 F8 1F 38 FE F8 FF FF 3F 8F FF F8 E3 F8 1F 78 1E C0 FF FF 3F 8F FF F8 E3 F8 1F 78 1E C0 FF FF 3F 8F FF F8 FF FF FF 78 FE FF FF 00 3C 81 03 C1 FF FF E3 00 FE FF F3 00 3C 81 03 C1 FF FF E3 00 FE FF F3 00 3C 81 03 C1 FF FF E3 00 FE FF F3 7F 3C 8F E3 C0 03 FF 1F FF 1F FF F3 7F 3C 8F E3 C0 03 FF 1F FF 1F FF F3 7F 3C 8F E3 C0 03 FF 1F FF 1F FF F3 7F 3C 8F E3 7F FE 00 FE 0F 1E 00 80 7F 3C 8F E3 7F FC 00 FE 07 1E 00 00 7F 3C 8F E3 7F FC 00 FE 07 1E 00 00 7F 3C 8F E3 7F FC 78 FE 07 FC F8 01 7F 3C 8F E3 0F 00 78 FE 07 E0 F8 03 7F 3C 8F E3 0F 00 78 FE 07 E0 F8 03 7F 3C 8F E3 7F 00 FF FF 07 E0 FF 1F 00 3C 8F 03 7F 00 CF 03 00 00 C7 1F 00 3C 8F 03 7F 00 CF 03 00 00 C7 1F 00 3C 8F 07 7F 00 CF 03 00 00 C7 1F FF 3F 8F FF 79 FC C0 FF F8 FF C0 FF FF 3F 8F FF 79 FC C0 FF F8 FF C0 FF FF 3F 8F FF 79 FC C0 FF F8 FF C0 FF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 "
frame4 = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3F 00 00 FE 00 00 00 00 00 00 00 00 3F 00 00 FE 00 00 00 00 00 00 00 00 3F 00 00 FE 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 07 00 03 F0 00 F8 00 00 00 00 38 00 0F 00 07 FC 01 FE 06 F8 00 7E 38 00 0C 00 0E 3C 07 1C 06 FC 00 FF 38 00 00 00 0C 0E 0F 00 07 1E 80 C7 3F 00 00 F8 0C 0E 0E 00 07 07 80 03 3F 00 00 F8 0C 0E 1C 00 06 07 80 01 3F 00 00 F8 0E 0E 1C 00 06 03 C0 01 38 00 03 00 0F FE 1C C0 86 03 C0 01 38 00 0F 00 07 FE 18 F8 86 03 C0 01 38 00 1E 00 00 0E 18 FC 86 03 C0 01 38 00 18 00 00 0E 18 1E 86 03 C0 01 38 00 38 00 00 0E 18 0E 86 03 C0 01 38 00 38 00 00 0E 1C 06 06 03 C0 01 38 00 38 00 00 0E 1C 0E 06 07 80 01 38 00 1C 00 0C 1E 0C 0E 06 07 80 01 3F 00 1F FF 1F F6 0E FC 06 0E 80 01 3F 00 0F FF 0F E6 07 F8 06 FE 80 01 00 00 00 00 00 00 03 00 06 F8 80 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 "
# Each column is 96 pixels tall (8 pixels per byte)
data = bytes.fromhex(
    frame1 + frame2 + frame3 + frame4
)
print(f"Input data: {len(data)} bytes.")
height = 96

# Compute width dynamically
total_bits = len(data) * 8
width = total_bits // height
print(f"Computed dimensions: {width} x {height}")

# Convert to bits
bits = np.unpackbits(np.frombuffer(data, dtype=np.uint8))

# Reshape column-major: each group of 96 bits = one column
bitmap = bits.reshape((width, height)).T  # transpose to make it image-like (rows x cols)

# Thermal printer: 1 = black, 0 = white → invert for visualization
bitmap = (1 - bitmap) * 255

# Create and show image
img = Image.fromarray(bitmap.astype(np.uint8), mode="L")
img.show()
img.save("thermal_reconstructed.png")

Something is not yet right with the scan order.

Ha! Got it.

A 16-bit byte swap was needed (AB CDCD AB). Also scanning it from bottom to top instead of top to bottom.

Now I can reconstruct the image from the decompressed data fully.

reconstructed_fixed

from PIL import Image
import numpy as np

# Simulated input: all black except last 12 bytes (rightmost column) = white
#data = b"\xFF" * (3468 - 12) + b"\x00" * 12
# decompressed from btsnoop_hci1
frame1 = "FF 0F E0 FF 1F 07 03 FF 9F FF FF FF FF 0F E0 FF 1F 0F 03 FF 9F FF FF FF FF 0F E0 FF 1F 0F 03 FF 9F FF FF FF FF 0F E3 FF 1F CF FF FF 1F FF FF FF 00 0F E3 01 03 CF FE FF 1E 38 01 00 00 0F E3 01 03 CF FE FF 1E 38 01 00 1F 0F E3 F1 1F C7 FF FF 1E F8 F1 3F 1F 0F E0 F9 1F 00 1F C7 1E F8 F1 3F 1F 0F E0 F9 1F 00 1F C7 1E F8 F1 3F 1F 0F E0 F9 1F 00 1F C7 1E FC F1 3F 1F 0F E3 F9 1E FF 00 7F 9E 07 F1 3F 1F 0F E3 F9 1E FF 00 7F 9E 07 F1 3F 1F 0F E3 F9 1E FF 00 7F 9E 07 F1 3F 1F 0F E0 F9 00 78 F0 0F 9E 07 F1 3F 1F 0F E0 F9 00 78 F0 07 9E 07 F1 3F 1F 0F E0 F9 00 78 F0 07 9E 07 F1 3F 1F 0F E3 F1 FE F8 FE 07 1E 3F F1 3F 00 0F E3 01 FE F8 1E 00 1E 38 01 00 00 0F E3 01 FE F8 1E 00 1E 38 01 00 FF 0F E3 FF FE FF FF 38 1F FF FF FF FF 0F E3 FF 1E CF F3 78 9F C7 FF FF FF 0F E3 FF 1E CF F3 78 9F C7 FF FF FF 0F E3 FF 1E CF F3 78 9F C7 FF FF 00 00 00 00 F0 7F F3 78 80 FF 00 00 00 00 00 00 F0 7F F3 78 80 FF 00 00 00 00 00 00 F0 7F F3 78 80 FF 00 00 FF 0F FF F9 F0 F8 00 00 F1 07 8E C7 FF 0F FF F9 F0 F8 00 00 F1 07 8E C7 FF 0F FF F9 F0 F8 00 00 F1 07 8E C7 FF 0F FF FF FF FF 00 FF FF 07 8E FF 1F 0F 1E CF 1F 0F 00 FF 1F 00 8E FF 1F 0F 1E CF 1F 0F 00 FF 1F 00 8E FF 1F 0F FF 8F 1F CF 1F FF FF C0 FE FF 1C 00 FF 01 03 CF 1F FF F1 C0 F0 C7 1C 00 FF 01 03 CF 1F FF F1 C0 F0 C7 3C 00 FF 01 03 C7 1F FF F3 C0 F0 E7 E0 01 00 7F 1F 00 1F C7 FF FF 81 FF E0 01 00 7F 1F 00 1F C7 FF FF 81 FF E0 01 00 7F 1F 00 1F C7 FF FF 81 FF FF 0F FF C1 1E FF 1E 7F 1F 00 00 C0 FF 0F FF C1 1E FF 1E 7F 1F 00 00 C0 FF 0F FF C1 1E FF 1E 7F 1F 00 00 C0 FF 07 FF FF 1C FF FC 3F FF 38 8E FF 1F 00 1F FF 00 C0 F0 00 FF 38 8E FF 1F 00 1F FF 00 C0 F0 00 FF 38 8E FF FF 0F FF FF FE FF FE 00 FF 3F FF FF E3 0F E0 C1 FE 7F 1E 00 81 07 FF C7 E3 0F E0 C1 FE 7F 1E 00 81 07 FF C7 FF 0F E1 C7 FE FF 7F 38 8F FF FF FF 1C 0F 03 0F F0 CF FF 78 9F FF 0F FC 1C 0F 03 0F F0 CF FF 78 9F FF 0F FC 1C 0F 03 0F F0 CF FF 78 9F FF 0F FC 1F 0F FE C1 F0 78 FF 07 83 C7 00 C0 1F 0F FE C1 F0 78 FF 07 81 C7 00 C0 1F 0F FE C1 F0 78 FF 07 81 C7 00 C0 1F 0F FF C0 FF F0 FF FF F1 FF 8E FF 1F 0F 03 C0 1F C0 F0 F8 F1 3F 8E FF 1F 0F 03 C0 1F C0 F0 F8 F1 3F 8E FF 1F 0F E3 80 1F C7 FE F8 F1 3F FE FF 00 0F E0 01 03 0F 1E F8 01 00 FE C7 00 0F E0 01 03 0F 1E F8 01 00 FE C7 03 0F FF FF 1F FF FF F8 FF FF FF FF 03 0F 1F FF 1F F8 FF C0 FF FF 0F FC 03 0F 1F FF 1F F8 FF C0 FF FF 0F FC 03 0F 1F FF 1F F8 FF C0 FF FF 1F FC 03 0F E3 C1 1E FF FE 7F FF 07 FE FF 03 0F E3 C1 1E FF FE 7F FF 07 FE FF 03 0F E3 C1 1E FF FE 7F FF 07 FE FF 03 06 E3 80 0C FF FC 3F FF 3F FE FF 00 00 03 00 00 C0 00 00 80 3F 0E 3C 00 00 03 00 00 C0 00 00 80 3F 0E 3C FF 0F E3 FF FE F8 FE 07 8E 3F FE 3F FF 0F E3 FF FE F8 FE 07 9E 07 F0 3F FF 0F E3 FF FE F8 FE 07 9E 07 F0 3F FF 0F E3 FF FE FF FF 3F 9E FF FF 3F 00 0F E0 01 F0 7F FF 78 80 FF FF 3C 00 0F E0 01 F0 7F FF 78 80 FF FF 3C 00 0F E0 01 F0 7F FF 78 80 FF FF 3C 1F 0F E3 F9 F0 C0 FF 07 FF C7 FF FC 1F 0F E3 F9 F0 C0 FF 07 FF C7 FF FC 1F 0F E3 F9 F0 C0 FF 07 FF C7 FF FC 1F 0F E3 F9 1F FF 80 FF 83 07 00 E0 1F 0F E3 F9 1F FF 00 FF 81 07 00 C0 1F 0F E3 F9 1F FF 00 FF 81 07 00 C0 1F 0F E3 F9 1F FF 1E FF 01 3F FE C0 "
frame2 = "1F 0F E3 F9 03 C0 1E FF 01 38 FE C0 1F 0F E3 F9 03 C0 1E FF 01 38 FE C0 1F 0F E3 F9 1F C0 FF FF 01 F8 FF C7 00 0F E3 01 1F C0 F3 C0 00 C0 F1 07 00 0F E3 01 1F C0 F3 C0 00 C0 F1 07 00 0F E3 01 1F C0 F3 C0 00 C0 F1 07 FF 0F E3 FF 1E FF F0 7F FE 3F F0 3F FF 0F E3 FF 1E FF F0 7F FE 3F F0 3F FF 0F E3 FF 1E FF F0 7F FE 3F F0 3F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3F 00 00 FF 00 00 00 00 00 00 00 00 3F 00 00 FF 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 01 00 00 E0 00 E0 00 70 00 00 38 00 0F 00 07 F8 03 FE 00 FC 00 1C 38 00 0F 00 0F FC 07 FE 06 FE 00 7F 38 00 00 00 0E 0E 0F 04 06 0F 80 FF 38 00 00 00 0C 0E 0E 00 07 07 C0 83 3F 00 00 F8 0C 06 1C 00 87 03 C0 01 3F 00 00 F8 0E 06 1C 00 87 03 C0 01 38 00 00 00 0F 7E 1C 00 87 03 C0 01 38 00 07 00 07 FE 1C F0 87 03 C0 01 38 00 0F 00 03 C6 18 FC 87 01 C0 01 38 00 1C 00 00 06 1C 7E 87 03 C0 01 38 00 18 00 00 06 1C 0E 87 03 C0 01 38 00 38 00 00 06 1C 06 87 03 C0 01 38 00 38 00 00 06 1C 06 87 03 C0 01 38 00 38 00 00 0E 0E 06 07 07 C0 01 38 00 1C 00 0C 1E 0F 0E 07 0F C0 01 3F 00 1F FF 1F F6 07 FC 07 FE C0 01 3F 00 0F FF 0F E6 03 F8 07 FC C0 01 00 00 00 00 00 00 00 00 07 00 C0 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF 3F 80 FF 7F 1C 0F FC 7F FE FF FF FF 3F 80 FF 7F 1C 0F FE 7F FE FF FF FF 3F 80 FF 7F 1C 0F FE 7F FE FF FF FF 3F 8F FF 7F 1C FF FF 7F FC FF FF 00 3C 8F 03 0F 1C F8 FF 78 E0 07 00 00 3C 8F 03 0F 1C F8 FF 78 E0 07 00 7F 3C 8F C3 3F 1C FF FF 78 E0 C7 FF 7F 3C 80 E3 7F 00 7F 1E 78 E0 C7 FF 7F 3C 80 E3 7F 00 7F 1E 78 E0 C7 FF 7F 3C 80 E3 7F 00 7F 1E 78 F0 C7 FF 7F 3C 8F E3 79 FC 00 FE 78 1E C7 FF 7F 3C 8F E3 79 FC 00 FE 78 1E C7 FF 7F 3C 8F E3 79 FC 00 FE 78 1E C7 FF "
frame3 = "7F 3C 81 E3 00 E0 C0 3F 78 1E C7 FF 7F 3C 81 E3 00 E0 C0 1F 78 1E C7 FF 7F 3C 81 E3 00 E0 C0 1F 78 1E C7 FF 7F 3C 8F E3 F8 E3 F8 1F 78 FC C7 FF 00 3C 8F 03 F8 E3 78 00 78 E0 07 00 00 3C 8F 03 F8 E3 78 00 78 E0 07 00 FF 3F 8F FF F8 FF FF E3 7F FC FF FF FF 3F 8F FF 79 1C CF E3 7F 1E FF FF FF 3F 8F FF 79 1C CF E3 7F 1E FF FF FF 3F 8F FF 79 3E CF E3 7F 1E FF FF 00 00 01 00 C1 FF CF E3 00 FE 00 00 00 00 01 00 C1 FF CF E3 00 FE 00 00 00 00 01 00 C1 FF CF E3 00 FE 00 00 FF 3F FF E3 C0 E3 00 00 C7 1F 38 1E FF 3F FF E3 C0 E3 00 00 C7 1F 38 1E FF 3F FF E3 C0 E3 00 00 C7 1F 38 1E FF 3F FF FF FF FF 00 FC FF 1F 38 FE 7F 3C 78 3C 7F 1C 00 FE 7F 00 38 FE 7F 3C 78 3C 7F 1C 00 FE 7F 00 38 FE 7E 3C FF 1F 7F 1C 7F FE FF 03 F8 FF 70 00 FF 03 0F 1C 7F FE C7 03 C0 1F 70 00 FF 03 0F 1C 7F FE C7 03 C0 1F F0 00 FF 07 0F 1C 7F FE CF 03 C0 9F 81 07 00 FC 7F 00 7F 1E FF FF 07 FE 81 07 00 FC 7F 00 7F 1E FF FF 07 FE 81 07 00 FC 7F 00 7F 1E FF FF 07 FE FF 3F FF 07 79 FC 78 FE 7F 00 00 80 FF 3F FF 03 79 FC 78 FE 7F 00 00 00 FF 3F FF 03 79 FC 78 FE 7F 00 00 00 FF 1F FF FF 30 FC F0 FF FF E3 38 FE 7F 00 7F FC 00 00 C0 03 FF E3 38 FE 7F 00 7F FC 00 00 C0 03 FF E3 38 FE FF 3F FF FF F8 FF F8 03 FF FF FF FF 8F 3F 81 03 F8 FF 78 00 07 1E FF 1F 8F 3F 81 03 F8 FF 78 00 07 1E FF 1F FF 3F 87 1F F8 FF FF E1 3F FE FF FF 70 3C 0F 3C C1 1F FF E3 7F FE 3F F0 70 3C 0F 3C C1 1F FF E3 7F FE 3F F0 70 3C 0F 3C C1 1F FF E3 7F FE 3F F0 7F 3C F9 07 C0 E3 FF 1F 07 1E 00 80 7F 3C F9 03 C0 E3 FF 1F 07 1E 00 00 7F 3C F9 03 C0 E3 FF 1F 07 1E 00 00 7F 3C FF 03 FF C3 FF FF C7 FF 38 FE 7F 3C 0F 00 7F 00 C0 E3 C7 FF 38 FE 7F 3C 0F 00 7F 00 C0 E3 C7 FF 38 FE 7E 3C 8F 03 7F 1C F8 E3 C7 FF F8 FF 00 3C 80 03 0F 1C 78 E0 07 00 F8 1F 00 3C 80 03 0F 1C 78 E0 07 00 F8 1F 0F 3C FF FF 3F FC FF E3 FF FF FF FF 0F 3C 7F FC 7F E0 FF 03 FF FF 3F F0 0F 3C 7F FC 7F E0 FF 03 FF FF 3F F0 0F 3C 7F FC 7F E0 FF 03 FF FF 7F F0 0F 3C 8F 03 79 FC F8 FF FF 1F F8 FF 0F 3C 8F 03 79 FC F8 FF FF 1F F8 FF 0F 3C 8F 03 79 FC F8 FF FF 1F F8 FF 06 18 8F 03 30 FC F0 FF FF FF F8 FF 00 00 0F 00 00 00 00 00 00 FE 38 F0 00 00 0F 00 00 00 00 00 00 FE 38 F0 FF 3F 8F FF F8 E3 F8 1F 38 FE F8 FF FF 3F 8F FF F8 E3 F8 1F 78 1E C0 FF FF 3F 8F FF F8 E3 F8 1F 78 1E C0 FF FF 3F 8F FF F8 FF FF FF 78 FE FF FF 00 3C 81 03 C1 FF FF E3 00 FE FF F3 00 3C 81 03 C1 FF FF E3 00 FE FF F3 00 3C 81 03 C1 FF FF E3 00 FE FF F3 7F 3C 8F E3 C0 03 FF 1F FF 1F FF F3 7F 3C 8F E3 C0 03 FF 1F FF 1F FF F3 7F 3C 8F E3 C0 03 FF 1F FF 1F FF F3 7F 3C 8F E3 7F FE 00 FE 0F 1E 00 80 7F 3C 8F E3 7F FC 00 FE 07 1E 00 00 7F 3C 8F E3 7F FC 00 FE 07 1E 00 00 7F 3C 8F E3 7F FC 78 FE 07 FC F8 01 7F 3C 8F E3 0F 00 78 FE 07 E0 F8 03 7F 3C 8F E3 0F 00 78 FE 07 E0 F8 03 7F 3C 8F E3 7F 00 FF FF 07 E0 FF 1F 00 3C 8F 03 7F 00 CF 03 00 00 C7 1F 00 3C 8F 03 7F 00 CF 03 00 00 C7 1F 00 3C 8F 07 7F 00 CF 03 00 00 C7 1F FF 3F 8F FF 79 FC C0 FF F8 FF C0 FF FF 3F 8F FF 79 FC C0 FF F8 FF C0 FF FF 3F 8F FF 79 FC C0 FF F8 FF C0 FF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 "
frame4 = "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3F 00 00 FE 00 00 00 00 00 00 00 00 3F 00 00 FE 00 00 00 00 00 00 00 00 3F 00 00 FE 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 00 00 00 00 38 00 07 00 03 F0 00 F8 00 00 00 00 38 00 0F 00 07 FC 01 FE 06 F8 00 7E 38 00 0C 00 0E 3C 07 1C 06 FC 00 FF 38 00 00 00 0C 0E 0F 00 07 1E 80 C7 3F 00 00 F8 0C 0E 0E 00 07 07 80 03 3F 00 00 F8 0C 0E 1C 00 06 07 80 01 3F 00 00 F8 0E 0E 1C 00 06 03 C0 01 38 00 03 00 0F FE 1C C0 86 03 C0 01 38 00 0F 00 07 FE 18 F8 86 03 C0 01 38 00 1E 00 00 0E 18 FC 86 03 C0 01 38 00 18 00 00 0E 18 1E 86 03 C0 01 38 00 38 00 00 0E 18 0E 86 03 C0 01 38 00 38 00 00 0E 1C 06 06 03 C0 01 38 00 38 00 00 0E 1C 0E 06 07 80 01 38 00 1C 00 0C 1E 0C 0E 06 07 80 01 3F 00 1F FF 1F F6 0E FC 06 0E 80 01 3F 00 0F FF 0F E6 07 F8 06 FE 80 01 00 00 00 00 00 00 03 00 06 F8 80 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 "
# Each column is 96 pixels tall (8 pixels per byte)
data = bytes.fromhex(
    frame1 + frame2 + frame3 + frame4
)
print(f"Input data: {len(data)} bytes.")

# byteswap 16-bit words
swapped = bytearray()
for i in range(0, len(data), 2):
    if i + 1 < len(data):
        swapped += data[i + 1:i + 2] + data[i:i + 1]
    else:
        swapped += data[i:i + 1]
data = swapped

# flip every bit (black <-> white inverted)
data = bytes([x ^ 0xff for x in data])

height = 96
total_bits = len(data) * 8
width = total_bits // height

print(f"Decoded dimensions: {width}x{height}")

# Convert to bit array
bits = np.unpackbits(np.frombuffer(data, dtype=np.uint8))

# Reshape as [width, height], scanning columns bottom-to-top
image_data = np.zeros((height, width), dtype=np.uint8)

for x in range(width):
    col_bits = bits[x * height:(x + 1) * height]
    image_data[:, x] = col_bits[::-1] * 255  # invert vertically (bottom→top)


# Create and save grayscale image
img = Image.fromarray(image_data, mode="L")

img.save("reconstructed_fixed.png")
img.show()
1 Like

Works nicely in full Python only from the BLE frames.

from PIL import Image
import numpy as np
import minilzo

# Raw payload bytes from frame with headers
# from btsnoop_hci3.log
# all_frames = [
#     "66 35 00 1b 2f 03 01 00 01 00 01 33 01 55 00 03 00 02 00 00 00 00 00 3d 03 00 ff 3f ff 28 00 00 35 2e 00 00 38 f3 08 03 00 00 20 00 00 00 89 2c 00 11 00 00 63 ",
#     "66 2f 00 1b 2f 03 01 00 01 00 01 33 01 55 00 02 00 02 00 38 00 00 00 80 00 02 03 00 00 38 00 c3 00 03 00 00 20 00 00 00 c5 2c 00 11 00 00 b1 ",
#     "66 2f 00 1b 2f 03 01 00 01 00 01 33 01 55 00 01 00 02 00 38 00 00 00 80 00 02 03 00 00 38 00 c3 00 03 00 00 20 00 00 00 c5 2c 00 11 00 00 b2 ",
#     "66 44 00 1b 2f 03 01 00 01 00 01 33 01 34 00 00 00 02 00 38 00 00 00 80 00 02 03 00 00 38 00 c3 00 03 00 00 20 00 00 08 2f 00 ff 3f ff 28 00 00 35 2c 00 09 00 00 00 00 00 00 00 00 00 00 00 00 11 00 00 aa "
# ]
all_frames = [
    "66 3d 02 1b 2f 03 01 00 01 00 01 2b 01 55 00 03 00 0e ff 0f e0 ff 1f 07 81 ff 8f ff ff ff ff 0f e0 ff 1f 33 2c 00 02 e3 ff 1f c7 ff 8c 04 09 00 0f e3 00 03 c7 fe ff 0e 3c 01 00 2a 2c 00 00 06 1f 0f e3 f0 0f c7 ff ff 0e fc f1 1f 1f 0f e0 f8 1f 00 9f c7 0e fc f1 3f 38 2c 00 09 e3 f8 1e ff 80 3f 8e 07 f1 3f 1f 0f 36 2c 00 03 e0 f8 00 78 f0 07 ac 04 36 2c 00 0f e3 f8 fe f8 fe 07 8e 3f f1 3f 00 0f e3 00 fe f8 1e 00 ec 13 ec 01 06 ff 0f e3 ff fe ff 7f 38 0f 8c 1c 09 e3 ff 1e c7 f1 38 8f c7 ff ff ff 0f 34 2c 00 08 00 00 00 00 f0 7f f1 38 80 ff 00 80 00 32 2c 00 09 ff 0f ff f8 f0 f8 00 00 f1 07 8f e7 39 2c 00 00 01 ff ff ff 00 ff ff 07 8f ff 1f 0f 1e cf 1f 07 80 ff 0f 00 2e 2c 00 00 04 ff 8f 1f 87 8f ff 7f c0 fe ff 1c 00 ff 00 03 c7 9f ff f1 c0 f0 e7 36 2c 00 09 e0 01 00 7f 1f 00 9f c7 ff ff 81 ff 36 2c 00 09 ff 0f ff c0 1e ff 9e 3f 0f 00 00 e0 39 2c 00 00 02 ff 1e ff fe 3f ff 38 8e ff 1f 00 1f ff 00 c0 f0 00 ff 3c 8f 2b 2c 00 00 06 ff 07 ff ff fc ff fe 00 ff 3f ff ff e3 0f e0 c0 fe 7f 1e 00 81 07 ff e7 36 2c 00 09 1c 0f 03 0f f0 c7 ff 38 8f ff 0f fc 36 2c 00 09 1f 0f fe c0 f0 78 ff 07 81 c7 00 e0 38 2c 00 00 03 ff c0 ff f8 ff ff f1 ff 8e ff 1f 0f 03 c0 1f c0 f0 f8 f1 3f 8f 2d 2c 00 00 04 e3 80 1f c7 fe f8 f1 3f ff ff 00 0f e0 00 03 07 1e f8 01 00 ff e7 36 2c 00 09 03 0f 1f ff 1f f8 ff c0 ff ff 0f fc 38 2c 00 09 e3 c0 1e ff fe 3f ff 07 ff ff 03 0f 37 2d 00 80 8c 04 0d 3f ff ff 00 00 03 00 00 c0 00 00 80 3f 0f 3c 00 29 2c 00 09 ff 07 e3 ff fc f8 fe 07 8e 3f fe 3f 8c 4f 03 f8 fe 07 8e 07 f0 37 2c 00 09 00 0f e0 00 f0 7f ff 38 80 ff 7f 3c 36 2c 00 09 1f 0f e3 f8 f0 c0 ff 07 ff c7 7f fc 3a 2c 00 00 02 ff ff ff ff ff c7 7f f8 1f 0f e3 f8 1f ff 80 ff 81 07 00 e0 30 2c 00 03 8e ff 81 3f 7e e0 11 00 00 43 ",
    "66 88 01 1b 2f 03 01 00 01 00 01 2b 01 55 00 02 00 0d 1f 0f e3 f8 03 c0 9e ff 01 3c 7f e0 1f 0f e3 f8 32 2c 00 09 00 0f e3 00 1f c0 f1 c0 00 c0 f1 07 36 2c 00 09 ff 0f e3 ff 1e ff f0 3f fe 3f f0 3f 3c 2d 00 e0 8d 04 00 20 3e 00 00 02 3f 00 00 ff 00 c0 00 2a 2d 00 38 cc 02 01 00 00 00 00 38 2c 00 00 39 01 00 00 e0 00 e0 00 70 00 00 38 00 0f 00 07 f8 03 fe 00 fc 00 1c 38 00 0f 00 0f fc 07 fe 06 fe 00 7f 38 00 00 00 0e 0e 0f 04 06 0f 80 ff 38 00 00 00 0c 0e 0e 00 07 07 c0 83 3f 00 00 f8 0c 06 1c 00 87 03 c0 01 3f 00 00 f8 0e cc 01 03 38 00 00 00 0f 7e bc 02 00 0e 38 00 07 00 07 fe 1c f0 87 03 c0 01 38 00 0f 00 03 c6 18 fc 87 01 c0 01 38 00 1c 00 00 06 1c 7e bc 02 03 18 00 00 06 1c 0e ac 01 03 38 00 00 06 1c 06 33 2c 00 03 0e 0e 06 07 07 c0 8c 07 00 0f 0c 1e 0f 0e 07 0f c0 01 3f 00 1f ff 1f f6 07 fc 07 fe c0 01 3f 00 0f ff 0f e6 03 f8 07 fc c0 01 00 c2 00 07 00 28 2c 00 c8 02 20 00 39 18 00 09 ff 3f 80 ff 7f 1c 07 fe 3f fe ff ff 38 2c 00 00 04 8f ff 7f 1c ff ff 3f fe ff ff 00 3c 8f 03 0f 1c f8 ff 38 f0 07 00 2a 2c 00 00 06 7f 3c 8f c3 3f 1c ff ff 38 f0 c7 7f 7f 3c 80 e3 7f 00 7f 1e 38 f0 c7 ff 38 2c 00 09 8f e3 78 fc 00 fe 38 1e c7 ff 7f 3c 34 2c 00 11 00 00 9a ",
    "66 14 02 1b 2f 03 01 00 01 00 01 2b 01 55 00 01 00 0d 7f 3c 81 e3 00 e0 c0 1f 38 1e c7 ff 7f 3c 81 e3 34 2c 00 00 04 8f e3 f8 e3 f8 1f 38 fe c7 ff 00 3c 8f 03 f8 e3 78 00 38 f0 07 00 2a 2c 00 00 04 ff 3f 8f ff f8 ff ff e1 3f fc ff ff ff 3f 8f ff 78 1c c7 e3 3f 1e 38 2c 00 09 00 00 01 00 c0 ff c7 e3 00 fe 00 00 36 2c 00 09 ff 3f ff e3 c0 e3 00 00 c7 1f 3c 9e 39 2c 00 00 01 ff ff ff 00 fe ff 1f 3c fe 7f 3c 78 3c 7f 1c 00 fe 3f 00 2e 2c 00 00 04 fe 1f 7f 1c 3f fe ff 01 f8 ff 70 00 ff 03 0f 1c 7f fe c7 03 c0 9f 36 2c 00 09 81 07 00 fc 7f 00 7f 1e ff ff 07 fe 36 2c 00 09 ff 3f ff 03 78 fc 78 fe 3f 00 00 80 39 2c 00 00 02 ff 78 fc f8 ff ff e3 38 fe 7f 00 7f fc 00 00 c0 03 ff f3 3c 2b 2c 00 00 06 ff 1f ff ff f0 ff f8 03 ff ff ff ff 8f 3f 81 03 f8 ff 78 00 07 1e ff 9f 36 2c 00 09 70 3c 0f 3c c0 1f ff e3 3f fe 3f f0 36 2c 00 09 7f 3c f9 03 c0 e3 ff 1f 07 1e 00 80 38 2c 00 00 03 ff 03 ff e3 ff ff c7 ff 38 fe 7f 3c 0f 00 7f 00 c0 e3 c7 ff 3c 2d 2c 00 00 04 8f 03 7f 1c f8 e3 c7 ff fc ff 00 3c 80 03 0f 1c 78 e0 07 00 fc 9f 36 2c 00 09 0f 3c 7f fc 7f e0 ff 03 ff ff 3f f0 38 2c 00 09 8f 03 78 fc f8 ff ff 1f fc ff 0f 3c 34 2d 00 0e fc 02 04 ff fc ff 00 00 0f 00 80 00 01 fe 3c f0 00 29 2c 00 02 ff 1f 8f ff f0 8d 52 f8 9c 4d 04 f8 e3 f8 1f 38 1e c0 37 2c 00 09 00 3c 81 03 c0 ff ff e3 00 fe ff f1 36 2c 00 09 7f 3c 8f e3 c0 03 ff 1f ff 1f ff f1 3a 2c 00 01 ff ff ff ff ec 04 01 7f fc 00 fe be 2c 8f e3 2c 2c 00 0e 38 fe 07 fe f8 81 7f 3c 8f e3 0f 00 78 fe 07 f0 fc 37 2c 00 09 00 3c 8f 03 7f 00 c7 03 00 00 c7 1f 36 2c 00 8c 6a 02 fc c0 ff f8 ff ae 1c 78 fc 3c 2d 00 00 20 02 00 00 11 00 00 34 ",
    "66 f6 00 1b 2f 03 01 00 01 00 01 2b 01 2c 00 00 00 02 00 00 00 00 00 20 22 00 00 02 3f 00 00 fe 00 c0 00 36 2d 00 38 dc 03 01 00 00 00 00 38 2c 00 03 07 00 03 f0 00 f8 ac 04 00 1c 0f 00 07 fc 01 fe 06 f8 00 7e 38 00 0c 00 0e 3c 07 1c 06 fc 00 ff 38 00 00 00 0c 0e 0f 00 07 1e 80 c7 3f 00 00 f8 0c 0e 0e 00 07 07 80 03 ac 01 00 09 1c 00 06 07 80 01 3f 00 00 f8 0e 0e 1c 00 06 03 c0 01 38 00 03 00 0f fe 1c c0 86 8c 01 03 0f 00 07 fe 18 f8 ac 01 03 1e 00 00 0e 18 fc ac 01 03 18 00 00 0e 18 1e ac 01 03 38 00 00 0e 18 0e 28 2e 00 1c 06 bc 08 0e 38 00 00 0e 1c 0e 06 07 80 01 38 00 1c 00 0c 1e 0c 8c 01 00 07 3f 00 1f ff 1f f6 0e fc 06 0e 80 01 3f 00 0f ff 0f e6 07 f8 06 fe 80 01 00 80 00 01 03 00 06 f8 ec 01 80 02 20 7c 10 00 11 00 00 23 "
]

def lzo_decompress_ble_frame(data: bytes) -> bytes | None:
    try:
        # convert hex bytes to actual bytes
        frame_bytes = bytes.fromhex(data)
        # skip 17 bytes (header), then discard last byte (checksum)
        compressed_data = frame_bytes[17:-1]
        dst_len = 9999 # weird API, max decompressed length has to be known beforehand or we fail.
        # TODO: find a better python API for LZO-1X decompress
        decompressed = minilzo.decompress(compressed_data, dst_len)
        print(f"[✓] Decompressed {len(compressed_data)} bytes to {len(decompressed)} bytes")
        return decompressed
    except Exception as e:
        print(f"[-] Failed to decompress raw LZO ({len(compressed_data)}B)", e)
        return None

def convert_decompressed_bytes_to_image(data: bytes, output_path: str):
    # byteswap 16-bit words
    swapped = bytearray()
    for i in range(0, len(data), 2):
        if i + 1 < len(data):
            swapped += data[i + 1:i + 2] + data[i:i + 1]
        else:
            swapped += data[i:i + 1]
    data = swapped
    # flip every bit (black <-> white inverted)
    data = bytes([x ^ 0xff for x in data])
    # assuem static height of 96 pixels and adjust width to match.
    height = 96
    total_bits = len(data) * 8
    width = total_bits // height

    print(f"Decoded dimensions: {width}x{height}")

    # Convert to bit array
    bits = np.unpackbits(np.frombuffer(data, dtype=np.uint8))

    # Reshape as [width, height], scanning columns bottom-to-top
    image_data = np.zeros((height, width), dtype=np.uint8)

    for x in range(width):
        col_bits = bits[x * height:(x + 1) * height]
        image_data[:, x] = col_bits[::-1] * 255  # invert vertically (bottom→top)


    # Create and save grayscale image
    img = Image.fromarray(image_data, mode="L")
    print(f"Saving to {output_path}")
    img.save(output_path)
    img.show()

def main():
    # get decompressed data from all frames
    all_data = b""
    for frame in all_frames:
        decompressed = lzo_decompress_ble_frame(frame)
        if decompressed is not None:
            all_data += decompressed
    convert_decompressed_bytes_to_image(all_data, "reconstructed.png")

if __name__ == '__main__':
    main()

I wish I knew about how minilzo works and how should I actually prepare the frames (right now I just only divide it by 4) from my programmatic bitmap functions, I have been working non-stop on this from 8am till 8pm from where I live. Even so, I have only able to get this code and its result.

// snippet from ble_printer_manager.cpp
bool prepareFramesFromBitmap(const Bitmap &userBitmap) {
  Serial.println("=== Preparing Print Job ===");

  // Clear previous frames
  printFrames.clear();

  // Transform to printer format (uses static buffer)
  Serial.println("Converting to printer format...");
  transformToPrinterFormatInPlace(userBitmap, printerFormatBuffer);

  // Compress with LZO
  Serial.println("Compressing with LZO...");
  lzo_uint compressedLen = 0;
  int result = lzo1x_1_compress(printerFormatBuffer.data, BITMAP_SIZE,
                                compressed, &compressedLen, lzoWorkMem);

  if (result != LZO_E_OK) {
    Serial.println("LZO compression failed!");
    return false;
  }

  Serial.printf("Compressed %lu → %lu bytes (%.1f%%)\n",
                (unsigned long)BITMAP_SIZE, (unsigned long)compressedLen,
                (compressedLen * 100.0) / BITMAP_SIZE);

  // Always split into 4 frames equally (last frame gets leftover bytes)
  const int NUM_FRAMES = 4;
  size_t baseChunkSize = compressedLen / NUM_FRAMES;
  size_t remainder = compressedLen % NUM_FRAMES;
  size_t offset = 0;

  for (int i = 0; i < NUM_FRAMES; i++) {
    size_t chunkSize = baseChunkSize;

    // Distribute remainder bytes evenly among first few chunks
    if (i < remainder) {
      chunkSize++;
    }

    // If compressedLen < NUM_FRAMES (edge case), assign 0-size safely
    if (offset >= compressedLen)
      chunkSize = 0;
    else if (offset + chunkSize > compressedLen)
      chunkSize = compressedLen - offset;

    bool isFinal = (i == NUM_FRAMES - 1);
    uint16_t framesRemaining = NUM_FRAMES - 1 - i;

    std::vector<uint8_t> frame = createFrame(
        compressed, offset, chunkSize, currentJobId, framesRemaining, isFinal);

    printFrames.push_back(frame);

    Serial.printf("Frame %d/%d: payload=%d bytes, remaining=%d\n", i + 1,
                  NUM_FRAMES, (int)chunkSize, framesRemaining);

    offset += chunkSize;
  }

  // Verify total coverage
  Serial.printf("Total frames: %d, total payload bytes used: %u / %u\n",
                (int)printFrames.size(), (unsigned int)offset,
                (unsigned int)compressedLen);

  // Increment Job ID for next print
  currentJobId++;

  return true;
}

/// ........................................... ///
/// second snippet from ble_printer_manager.cpp ///

// second, third, and fourth are only sent when received ACK from printer
void notifyCallback(NimBLERemoteCharacteristic *chr, uint8_t *data, size_t len,
                    bool isNotify) {
  // Store 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();

  // If printing, send next frame after ACK
  if (printingInProgress && chr == pNotifyChar) {
    delay(50); // Brief delay for printer buffer

    currentFrameIndex++;

    if (currentFrameIndex < printFrames.size()) {

      const auto &frame = printFrames[currentFrameIndex];
      pWriteChar->writeValue(frame.data(), frame.size(), false);

      Serial.printf("=== Frame %d (%d bytes) ===\n", currentFrameIndex + 1,
                    (int)frame.size());
      for (size_t j = 0; j < frame.size(); j++) {
        if (j % 16 == 0)
          Serial.println(); // break lines every 16 bytes
        Serial.printf("%02X ", frame[j]);
      }
      Serial.println("\n===========================");
    } else {
      Serial.println("All frames sent! Print job complete.");
      printingInProgress = false;
    }
  }
}


/// .......................................... ///
/// third snippet from ble_printer_manager.cpp ///

// Start printing the prepared frames
bool startPrintJob() {
  if (!pWriteChar) {
    Serial.println("No write characteristic available!");
    return false;
  }

  if (printFrames.empty()) {
    Serial.println("No frames prepared! Call prepareFramesFromBitmap() first.");
    return false;
  }

  currentFrameIndex = 0;
  printingInProgress = true;

  const auto &firstFrame = printFrames[0];
  pWriteChar->writeValue(firstFrame.data(), firstFrame.size(), false);

  Serial.printf("=== Frame 1 (%d bytes) ===\n", (int)firstFrame.size());
  for (size_t j = 0; j < firstFrame.size(); j++) {
    if (j % 16 == 0)
      Serial.println(); // break lines every 16 bytes
    Serial.printf("%02X ", firstFrame[j]);
  }
  Serial.println("\n===========================");

  return true;
}

// High-level: Prepare and print in one call
bool printBitmap(const Bitmap &userBitmap) {
  if (!prepareFramesFromBitmap(userBitmap)) {
    return false;
  }
  return startPrintJob();
}

// main.cpp
#include <Arduino.h>
#include <ble_printer_manager.h>
#include <image_compressor.h>

void setup() {
  Serial.begin(115200);

  Serial.println("\n=== MakeID L1 Thermal Printer Demo ===");
  Serial.printf("Free heap at start: %d bytes\n", ESP.getFreeHeap());

  beginBLESniffer();
  if (PRINTER_MAC[0] == '\0')
    return;

  Serial.println("\n=== Creating Custom Bitmap ===");
  Bitmap image = createEmptyBitmap();

  // Draw border and diagonals
  drawBorder(image, 5);
  drawDiagonals(image);

  // Add text
  drawString(image, "42", 150, 40);

  Serial.println("Bitmap created");
  Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap());

  // Print it!
  if (isPrinterConnected()) {
    Serial.println("\n=== Printing Custom Bitmap ===");
    if (printBitmap(image)) {
      // Wait for print to complete
      while (isPrinting()) {
        delay(100);
      }
      Serial.println("Print complete!");
    } else {
      Serial.println("Print failed!");
    }
  }
}

void loop() { delay(1000); }

for image_compressor.cpp, see this file in the github repo

=== MakeID L1 Thermal Printer Demo ===
Free heap at start: 175276 bytes
Starting BLE...
Connecting to printer: 58:8c:81:72:ab:0a
Connected!
Negotiated MTU: 255
Printer service found.
Write characteristic ABF1 found.
Notify characteristic ABF2 found.
Subscribed to ABF2 notifications.

=== Creating Custom Bitmap ===
Bitmap created
Free heap: 156156 bytes

=== Printing Custom Bitmap ===
=== Preparing Print Job ===
Converting to printer format...
Compressing with LZO...
Compressed 4608 → 793 bytes (17.2%)
Frame 1/4: payload=199 bytes, remaining=3
Frame 2/4: payload=198 bytes, remaining=2
Frame 3/4: payload=198 bytes, remaining=1
Frame 4/4: payload=198 bytes, remaining=0
Total frames: 4, total payload bytes used: 793 / 793
=== Frame 1 (219 bytes) ===

66 DB 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 03
02 00 00 00 00 00 20 16 13 00 FF E0 FF C2 00 03
FF 28 2D 00 05 29 2D 00 06 29 2E 00 07 7F 28 8E
00 07 BF 29 2D 00 DF 29 2D 00 EF 29 2D 00 F7 29
2D 00 FB 29 2D 00 FD 29 2D 00 FE EF 01 7F FF 07
27 AD 01 BF 29 2D 00 DF 29 2D 00 EF 29 2D 00 F7
29 2D 00 FB 29 2D 00 FD 29 2D 00 FE 29 2E 00 FF
7F 28 7E 01 FF BF 29 2D 00 DF 29 2D 00 EF 29 2D
00 F7 29 2D 00 FB 29 2D 00 FD 29 2D 00 FE EF 01
7F FF FF 27 AD 01 BF 29 2D 00 DF 29 2D 00 EF 29
2D 00 F7 29 2D 00 FB 29 2D 00 FD 29 2D 00 FE 29
2E 00 FF 7F 28 7E 01 FF BF 29 2D 00 DF 29 2D 00
EF 29 2D 00 F7 29 2D 00 FB 29 2D 00 FD 29 2D 00
FE EC 01 74 17 EC 01 11 00 00 40
===========================
Notification [0xabf2] : 23 23 01 01 66 25 00 10 40 49 18 0F 00 00 4C 31 43 00 00 01 C9 09 A0 0F 4C 43 2D 31 36 59 36 00 00 00 00 00 00 00 43 00 7E
=== Frame 2 (218 bytes) ===

66 DA 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 02
74 17 EC 01 74 17 EC 01 74 17 EC 01 74 17 EC 01
74 17 EC 01 74 17 EC 01 74 17 EC 01 74 17 27 2C
00 80 0C DC 26 80 0C CC 01 80 0C CC 01 80 0C CC
01 80 0C CC 01 80 0C CC 01 80 0C 6C 01 B4 17 BC
0B 90 0B DC 26 90 0B CC 01 90 0B CC 01 90 0B CC
01 90 0B CC 01 90 0B CC 01 90 0B CC 01 B4 17 BC
0B 6C 2F 68 66 7D 0E FF C0 0C 8C 01 C0 0C 8C 01
C0 0C 8C 01 C0 0C 8C 01 C0 0C 8C 01 C3 0C 07 FF
7F 27 0F 0E 07 FF BF 29 2D 00 DF 29 2D 00 EF 29
2D 00 F7 29 2D 00 FB 29 2D 00 FD 29 2D 00 FE 29
2E 00 FF 60 7C 15 6C 00 01 07 FF FF A0 7C 00 6C
00 01 07 FF FF C0 7C 00 6C 00 9C 11 60 01 FC 1D
20 00 00 9D 2D 00 11 00 00 75
===========================
Notification [0xabf2] : 23 23 01 01 66 25 00 10 40 49 18 0F 00 00 4C 31 43 00 00 01 C9 09 A0 0F 4C 43 2D 31 36 59 36 00 00 00 00 00 00 00 43 00 7E
=== Frame 3 (218 bytes) ===

66 DA 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 01
E7 DC 74 6D 58 D7 29 2D 00 B7 29 2D 00 01 29 2C
00 2A 9C 14 2A FD 0B BD 29 8D 00 79 29 2D 00 75
29 2D 00 6D 29 2D 00 9D 29 2C 00 2A 1C 01 20 00
00 00 00 00 00 03 2C 00 2A 0C 26 2A 6C 26 29 CC
26 2A 2C 27 2A 8C 27 2A EC 27 2A 4C 28 2A AC 28
2A 0C 29 2A 6C 29 2A CF 29 FF E0 FF 27 2F 2A FF
E0 FF 2A 8C 2A 2A EC 2A 2A 4C 2B 2A AC 2B 2A 0C
2C 25 F0 2D 23 3C 29 25 50 2E 6C 01 D8 0B 23 7C
34 D8 0B 8C 01 D8 0B 8C 01 D8 0B 8C 01 D8 0B 8C
01 D8 0B 8C 01 D8 0B 8C 01 D8 0B 8E 01 FF FF A4
18 6E 0D FF FF A4 18 CC 01 88 0C CC 01 88 0C CC
01 88 0C CC 01 88 0C CC 01 88 0C CC 01 88 0C AC
01 A4 18 AC 01 A4 11 00 00 56
===========================
Notification [0xabf2] : 23 23 01 01 66 25 00 10 40 49 18 0F 00 00 4C 31 43 00 00 01 C9 09 A0 0F 4C 43 2D 31 36 59 36 00 00 00 00 00 00 00 43 00 7E
=== Frame 4 (218 bytes) ===

66 DA 00 1B 2F 03 01 00 01 00 01 2B 01 2C 00 00
18 AC 01 98 0B CC 10 98 0B CC 01 98 0B CC 01 98
0B CC 01 98 0B CC 01 98 0B CC 01 A4 18 AC 0A A4
18 27 2C 00 2A EC 36 2A AC 22 2A AC 37 2A 0C 38
2A 6C 38 29 CC 38 64 18 EC 0A 64 18 EC 01 64 18
EC 01 64 18 EC 01 64 18 EC 01 64 18 EC 01 64 18
EC 01 64 18 EC 01 64 18 29 2C 00 2A 8C 3C 2A EC
3C 2A 4C 3D 2A AC 3D 2A 0C 3E 2A 6C 3E 29 CC 3E
2A 2C 3F 2A 8C 3F 2A EC 3F 2A 4C 40 2A AC 40 2A
0C 41 2A 6C 41 2A CF 41 FF FF 07 27 2F 42 FF FF
07 2A 8C 42 2A EC 42 2A 4C 43 2A AC 43 2A 0C 44
2A 6C 44 29 CC 44 2A 2C 45 2A 8E 45 03 FF 20 0B
F8 46 0D 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 11 00 00 11 00 00 0C
===========================
Notification [0xabf2] : 23 23 01 01 66 25 00 10 40 49 18 0F 00 00 4C 31 43 00 00 01 C9 09 A0 0F 4C 43 2D 31 36 59 36 00 00 00 00 00 00 00 43 00 7E
All frames sent! Print job complete.
Print complete!

This is a false positive, as it says successful when receiving fourth ACK, but in actuality, nothing comes out from the printer… maybe I should summarize the btsnoop_hci logs’ ACK with its respective payload?

piping the 4 frames into your decoder

# ble_frames_to_png.py
from PIL import Image
import numpy as np
import minilzo

all_frames = [
    "66 db 00 1b 2f 03 01 00 01 00 01 2b 01 55 00 03 02 00 00 00 00 00 20 16 13 00 ff e0 ff c2 00 03 ff 28 2d 00 05 29 2d 00 06 29 2e 00 07 7f 28 8e 00 07 bf 29 2d 00 df 29 2d 00 ef 29 2d 00 f7 29 2d 00 fb 29 2d 00 fd 29 2d 00 fe ef 01 7f ff 07 27 ad 01 bf 29 2d 00 df 29 2d 00 ef 29 2d 00 f7 29 2d 00 fb 29 2d 00 fd 29 2d 00 fe 29 2e 00 ff 7f 28 7e 01 ff bf 29 2d 00 df 29 2d 00 ef 29 2d 00 f7 29 2d 00 fb 29 2d 00 fd 29 2d 00 fe ef 01 7f ff ff 27 ad 01 bf 29 2d 00 df 29 2d 00 ef 29 2d 00 f7 29 2d 00 fb 29 2d 00 fd 29 2d 00 fe 29 2e 00 ff 7f 28 7e 01 ff bf 29 2d 00 df 29 2d 00 ef 29 2d 00 f7 29 2d 00 fb 29 2d 00 fd 29 2d 00 fe ec 01 74 17 ec 01 11 00 00 40",
    "66 da 00 1b 2f 03 01 00 01 00 01 2b 01 55 00 02 74 17 ec 01 74 17 ec 01 74 17 ec 01 74 17 ec 01 74 17 ec 01 74 17 ec 01 74 17 ec 01 74 17 27 2c 00 80 0c dc 26 80 0c cc 01 80 0c cc 01 80 0c cc 01 80 0c cc 01 80 0c cc 01 80 0c 6c 01 b4 17 bc 0b 90 0b dc 26 90 0b cc 01 90 0b cc 01 90 0b cc 01 90 0b cc 01 90 0b cc 01 90 0b cc 01 b4 17 bc 0b 6c 2f 68 66 7d 0e ff c0 0c 8c 01 c0 0c 8c 01 c0 0c 8c 01 c0 0c 8c 01 c0 0c 8c 01 c3 0c 07 ff 7f 27 0f 0e 07 ff bf 29 2d 00 df 29 2d 00 ef 29 2d 00 f7 29 2d 00 fb 29 2d 00 fd 29 2d 00 fe 29 2e 00 ff 60 7c 15 6c 00 01 07 ff ff a0 7c 00 6c 00 01 07 ff ff c0 7c 00 6c 00 9c 11 60 01 fc 1d 20 00 00 9d 2d 00",
    "66 da 00 1b 2f 03 01 00 01 00 01 2b 01 55 00 01 e7 dc 74 6d 58 d7 29 2d 00 b7 29 2d 00 01 29 2c 00 2a 9c 14 2a fd 0b bd 29 8d 00 79 29 2d 00 75 29 2d 00 6d 29 2d 00 9d 29 2c 00 2a 1c 01 20 00 00 00 00 00 00 03 2c 00 2a 0c 26 2a 6c 26 29 cc 26 2a 2c 27 2a 8c 27 2a ec 27 2a 4c 28 2a ac 28 2a 0c 29 2a 6c 29 2a cf 29 ff e0 ff 27 2f 2a ff e0 ff 2a 8c 2a 2a ec 2a 2a 4c 2b 2a ac 2b 2a 0c 2c 25 f0 2d 23 3c 29 25 50 2e 6c 01 d8 0b 23 7c 34 d8 0b 8c 01 d8 0b 8c 01 d8 0b 8c 01 d8 0b 8c 01 d8 0b 8c 01 d8 0b 8c 01 d8 0b 8e 01 ff ff a4 18 6e 0d ff ff a4 18 cc 01 88 0c cc 01 88 0c cc 01 88 0c cc 01 88 0c cc 01 88 0c cc 01 88 0c ac 01 a4 18 ac 01 a4 11 00 00 56",
    "66 da 00 1b 2f 03 01 00 01 00 01 2b 01 2c 00 00 18 ac 01 98 0b cc 10 98 0b cc 01 98 0b cc 01 98 0b cc 01 98 0b cc 01 98 0b cc 01 a4 18 ac 0a a4 18 27 2c 00 2a ec 36 2a ac 22 2a ac 37 2a 0c 38 2a 6c 38 29 cc 38 64 18 ec 0a 64 18 ec 01 64 18 ec 01 64 18 ec 01 64 18 ec 01 64 18 ec 01 64 18 ec 01 64 18 ec 01 64 18 29 2c 00 2a 8c 3c 2a ec 3c 2a 4c 3d 2a ac 3d 2a 0c 3e 2a 6c 3e 29 cc 3e 2a 2c 3f 2a 8c 3f 2a ec 3f 2a 4c 40 2a ac 40 2a 0c 41 2a 6c 41 2a cf 41 ff ff 07 27 2f 42 ff ff 07 2a 8c 42 2a ec 42 2a 4c 43 2a ac 43 2a 0c 44 2a 6c 44 29 cc 44 2a 2c 45 2a 8e 45 03 ff 20 0b f8 46 0d 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 11 00 00 11 00 00 0c"
]

def lzo_decompress_ble_frame(data: bytes) -> bytes | None:
    try:
        # convert hex bytes to actual bytes
        frame_bytes = bytes.fromhex(data)
        # skip 17 bytes (header), then discard last byte (checksum)
        compressed_data = frame_bytes[17:-1]
        dst_len = 9999 # weird API, max decompressed length has to be known beforehand or we fail.
        # TODO: find a better python API for LZO-1X decompress
        decompressed = minilzo.decompress(compressed_data, dst_len)
        print(f"[✓] Decompressed {len(compressed_data)} bytes to {len(decompressed)} bytes")
        return decompressed
    except Exception as e:
        print(f"[-] Failed to decompress raw LZO ({len(compressed_data)}B)", e)
        return None

def convert_decompressed_bytes_to_image(data: bytes, output_path: str):
    # byteswap 16-bit words
    swapped = bytearray()
    for i in range(0, len(data), 2):
        if i + 1 < len(data):
            swapped += data[i + 1:i + 2] + data[i:i + 1]
        else:
            swapped += data[i:i + 1]
    data = swapped
    # flip every bit (black <-> white inverted)
    data = bytes([x ^ 0xff for x in data])
    # assuem static height of 96 pixels and adjust width to match.
    height = 96
    total_bits = len(data) * 8
    width = total_bits // height

    print(f"Decoded dimensions: {width}x{height}")

    # Convert to bit array
    bits = np.unpackbits(np.frombuffer(data, dtype=np.uint8))

    # Reshape as [width, height], scanning columns bottom-to-top
    image_data = np.zeros((height, width), dtype=np.uint8)

    for x in range(width):
        col_bits = bits[x * height:(x + 1) * height]
        image_data[:, x] = col_bits[::-1] * 255  # invert vertically (bottom→top)


    # Create and save grayscale image
    img = Image.fromarray(image_data, mode="L")
    print(f"Saving to {output_path}")
    img.save(output_path)
    img.show()

def main():
    # get decompressed data from all frames
    all_data = b""
    for frame in all_frames:
        decompressed = lzo_decompress_ble_frame(frame)
        if decompressed is not None:
            all_data += decompressed
    convert_decompressed_bytes_to_image(all_data, "reconstructed.png")

if __name__ == '__main__':
    main()

running command python ble_frames_to_png.py results in

[-] Failed to decompress raw LZO (201B) Decompression failed
[-] Failed to decompress raw LZO (196B) Decompression failed
[-] Failed to decompress raw LZO (200B) Decompression failed
[-] Failed to decompress raw LZO (200B) Decompression failed
Decoded dimensions: 0x96
/home/deveaston06/experimental/python/L1Printer/ble_frames_to_png.py:57: DeprecationWarning: 'mode' parameter is deprecated and will be removed in Pillow 13 (2026-10-15)
  img = Image.fromarray(image_data, mode="L")
Saving to reconstructed.png
Traceback (most recent call last):
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/ImageFile.py", line 644, in _save
    fh = fp.fileno()
         ^^^^^^^^^
AttributeError: '_idat' object has no attribute 'fileno'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/deveaston06/experimental/python/L1Printer/ble_frames_to_png.py", line 72, in <module>
    main()
    ~~~~^^
  File "/home/deveaston06/experimental/python/L1Printer/ble_frames_to_png.py", line 69, in main
    convert_decompressed_bytes_to_image(all_data, "reconstructed.png")
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/deveaston06/experimental/python/L1Printer/ble_frames_to_png.py", line 59, in convert_decompressed_bytes_to_image
    img.save(output_path)
    ~~~~~~~~^^^^^^^^^^^^^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/Image.py", line 2588, in save
    save_handler(self, fp, filename)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 1495, in _save
    ImageFile._save(
    ~~~~~~~~~~~~~~~^
        single_im,
        ^^^^^^^^^^
        cast(IO[bytes], _idat(fp, chunk)),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)],
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/ImageFile.py", line 648, in _save
    _encode_tile(im, fp, tile, bufsize, None, exc)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/ImageFile.py", line 666, in _encode_tile
    encoder.setimage(im.im, extents)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
SystemError: tile cannot extend outside image

Or maybe I should just keep focusing on the prepareFramesFromBitmap function? any pointer would be helpful, thanks!

No I think the order of operations is wrong. You have to do everything in reverse:

  1. Take in the original bitmap
  2. Scale it down (or crop it) to have a height of 96 pixels and width w
  3. Get the 1 pixel per bit (or 8 pixels per byte) pixel data for it, scanning the original picture from left to right, bottom to top
  4. Invert every pixel (flipping white to black and vice versa, just XORing every byte with 0xFF)
  5. Do a 16-bit byteswap on the data
  6. You should now have a few thousand byte of raw bitmap data
  7. In chunks of at maximum 1020 bytes, iterate through the raw bitmap data
    7.1. LZO-1X compress the chunk
    7.2 Construct the BLE frame from it

You LZO-1X compress the whole bitmap data and then send it chunkwise, I don’t think that’s correct.

I think it would also be easier to first work on it in Python, until you can e.g. regenerate the BLE frames for the all-white image, and then for arbitrary image, so that the decode(encode(image)) returns the original (or scaled..) image, then just have the ESP sent the precalculated BLE frames that the Python script generated.

I will put out whatever I got today so far, I think I will keep trying tomorrow morning, but do help me with this if possible😥

Compression Algorithm (Second Attempt)

#!/usr/bin/env python3
import numpy as np
import minilzo

# === PRINTER SETTINGS ===
PRINTER_ID = bytes([0x1B, 0x2F, 0x03, 0x01, 0x00, 0x01, 0x00, 0x01])

def calculate_checksum(frame_bytes):
    checksum = 0
    for byte in frame_bytes[:-1]:
        checksum = (checksum - byte) & 0xFF
    return checksum

def create_test_bitmap(width, height, pattern="border"):
    """Create test bitmap with border pattern"""
    bitmap = np.zeros((height, width), dtype=np.uint8)
    if pattern == "border":
        border_size = min(5, height // 10, width // 10)
        bitmap[0:border_size, :] = 255
        bitmap[-border_size:, :] = 255
        bitmap[:, 0:border_size] = 255
        bitmap[:, -border_size:] = 255
    elif pattern == "all_white":
        bitmap[:] = 255
    elif pattern == "all_black":
        bitmap[:] = 0
    elif pattern == "checker":
        bitmap[:, ::2] = 255
        bitmap[::2, :] ^= 255
    return bitmap

def bitmap_to_bytes_optimized(bitmap):
    """Convert bitmap to 1bpp byte array"""
    height, width = bitmap.shape
    bytes_per_row = (width + 7) // 8
    out = bytearray()
    
    for y in range(height):
        for x in range(0, width, 8):
            byte_val = 0
            for bit in range(8):
                if x + bit < width and bitmap[y, x + bit] > 0:
                    byte_val |= (1 << (7 - bit))
            out.append(byte_val)
    
    return bytes(out)

def transform_to_printer_format_optimized(bitmap_bytes, width, height):
    """
    Transform bitmap to printer format
    """
    bytes_per_row = (width + 7) // 8
    total_bytes = height * bytes_per_row
    
    # Simple inversion (common in thermal printers)
    result = bytearray()
    for byte in bitmap_bytes:
        result.append(byte ^ 0xFF)  # Invert bits
    
    # Add the common header
    final_output = bytearray()
    final_output.extend(b'\x00\x02')  # Common header
    final_output.extend(result)
    
    return bytes(final_output)

def compress_and_generate_frames(bitmap, job_id, final_magic, image_name):
    """Compress bitmap and generate frames"""
    height, width = bitmap.shape
    print(f"\n=== Generating {image_name} ({width}x{height}) ===")
    
    # Convert to bytes
    bitmap_bytes = bitmap_to_bytes_optimized(bitmap)
    print(f"Original bitmap: {len(bitmap_bytes)} bytes")
    
    # Transform to printer format
    printer_format = transform_to_printer_format_optimized(bitmap_bytes, width, height)
    print(f"Transformed format: {len(printer_format)} bytes")
    
    # Compress using LZO
    try:
        compressed_data = minilzo.compress(printer_format)
        print(f"Compressed: {len(compressed_data)} bytes")
    except Exception as e:
        print(f"Compression failed: {e}")
        return None
    
    # Split into 4 chunks
    TARGET_FRAMES = 4
    chunks = []
    
    if len(compressed_data) <= TARGET_FRAMES:
        base_size = max(1, len(compressed_data) // TARGET_FRAMES)
        for i in range(TARGET_FRAMES):
            start = i * base_size
            end = start + base_size if i < TARGET_FRAMES - 1 else len(compressed_data)
            if start < len(compressed_data):
                chunks.append(compressed_data[start:end])
            else:
                chunks.append(b'')
    else:
        base_size = len(compressed_data) // TARGET_FRAMES
        for i in range(TARGET_FRAMES):
            start = i * base_size
            if i < TARGET_FRAMES - 1:
                end = start + base_size
            else:
                end = len(compressed_data)
            chunks.append(compressed_data[start:end])
    
    print(f"Split into {len(chunks)} chunks: {[len(c) for c in chunks]}")
    
    # Create frames
    frames = []
    for i, chunk in enumerate(chunks):
        is_final = (i == TARGET_FRAMES - 1)
        frames_remaining = TARGET_FRAMES - i - 1
        
        frame = bytearray()
        frame.append(0x66)  # Magic
        
        # Calculate length
        length = 16 + len(chunk) + 4
        frame.extend([length & 0xFF, (length >> 8) & 0xFF])
        
        # Printer ID
        frame.extend(PRINTER_ID)
        
        # Job ID (little-endian)
        frame.extend([job_id & 0xFF, (job_id >> 8) & 0xFF])
        
        # Frame magic
        frame_magic = final_magic if is_final else 0x55
        frame.append(frame_magic)
        
        # Remaining frames (big-endian)
        frame.extend([(frames_remaining >> 8) & 0xFF, frames_remaining & 0xFF])
        
        # Payload
        frame.extend(chunk)
        
        # End marker
        frame.extend([0x11, 0x00, 0x00])
        
        # Calculate checksum
        temp_frame = frame + b'\x00'
        checksum = calculate_checksum(temp_frame)
        frame.append(checksum)
        
        frames.append(bytes(frame))
        print(f"Frame {i+1}: {len(chunk)}B payload, {len(frame)}B total")
    
    return frames

def generate_esp32_code(frames, test_name):
    print(f"\n// ===========================================")
    print(f"// {test_name} - Generated with current algorithm")
    print(f"// ===========================================")
    print("std::vector<std::vector<uint8_t>> printFrames;")
    
    for i, frame in enumerate(frames):
        print(f"\n// Frame {i+1} - {len(frame)} bytes")
        print("printFrames.push_back({")
        
        # Format with proper indentation (16 bytes per line)
        hex_lines = []
        for j in range(0, len(frame), 16):
            line = frame[j:j+16]
            hex_str = ', '.join(f"0x{b:02X}" for b in line)
            if j + 16 >= len(frame):
                hex_lines.append(f"    {hex_str}")
            else:
                hex_lines.append(f"    {hex_str},")
        
        print('\n'.join(hex_lines))
        print("});")
    
    print(f"\n// Total: {len(frames)} frames")

def debug_frame_details(frames):
    print(f"\n=== Frame Details ===")
    for i, frame in enumerate(frames):
        payload = frame[16:-4]  # Extract payload
        print(f"Frame {i+1}:")
        print(f"  Total: {len(frame)} bytes")
        print(f"  Payload: {len(payload)} bytes")
        print(f"  Payload hex: {payload.hex()}")
        print(f"  Checksum: 0x{frame[-1]:02X}")

def generate_frames_string_format(frames, test_name):
    print(f"all_frames = [")
    
    frame_strings = []
    for i, frame in enumerate(frames):
        # Convert frame to space-separated hex string
        hex_string = ' '.join(f"{b:02X}" for b in frame)
        frame_strings.append(f'    "{hex_string}"')
    
    # Join with newlines and commas
    print(',\n'.join(frame_strings))
    print("]")

# Test configurations
test_configs = [
    (384, 96, "border", 0x012B, 0x2C, "Border_384x96"),
    # (384, 96, "all_white", 0x012B, 0x2C, "White_384x96"), 
    # (384, 96, "all_black", 0x012B, 0x2C, "Black_384x96"),
    # (384, 96, "checker", 0x012B, 0x2C, "Checker_384x96"),
]

if __name__ == "__main__":
    print("MakeID L1 Printer - ESP32 Code Generator")
    print("Using current compression algorithm")
    
    for width, height, pattern, job_id, final_magic, name in test_configs:
        # Create bitmap
        bitmap = create_test_bitmap(width, height, pattern)
        
        # Generate frames using current algorithm
        frames = compress_and_generate_frames(bitmap, job_id, final_magic, name)
        
        if frames:
            # Generate ESP32 code
            # generate_esp32_code(frames, f"{name} (JobID: 0x{job_id:04X})")
            generate_frames_string_format(frames, f"{name} (JobID: 0x{job_id:04X})")
            
            # Show frame details for debugging
            debug_frame_details(frames)
            
            print(f"\n{'='*60}")
My Output
MakeID L1 Printer - ESP32 Code Generator
Using current compression algorithm

=== Generating Border_384x96 (384x96) ===
Original bitmap: 4608 bytes
Transformed format: 4610 bytes
Compressed: 66 bytes
Split into 4 chunks: [16, 16, 16, 18]
Frame 1: 16B payload, 36B total
Frame 2: 16B payload, 36B total
Frame 3: 16B payload, 36B total
Frame 4: 18B payload, 38B total
all_frames = [
    "66 24 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 03 03 00 02 00 00 00 00 20 CB 02 00 07 FF 20 0C 01 11 00 00 6C",
    "66 24 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 02 00 E0 20 00 00 00 00 00 00 00 00 00 00 00 00 00 11 00 00 92",
    "66 24 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 01 00 00 DE BC 00 20 BB 2C 44 00 02 00 00 00 00 00 11 00 00 AC",
    "66 26 00 1B 2F 03 01 00 01 00 01 2B 01 2C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 11 00 00 11 00 00 AA"
]

=== Frame Details ===
Frame 1:
  Total: 36 bytes
  Payload: 16 bytes
  Payload hex: 0300020000000020cb020007ff200c01
  Checksum: 0x6C
Frame 2:
  Total: 36 bytes
  Payload: 16 bytes
  Payload hex: 00e02000000000000000000000000000
  Checksum: 0x92
Frame 3:
  Total: 36 bytes
  Payload: 16 bytes
  Payload hex: 0000debc0020bb2c4400020000000000
  Checksum: 0xAC
Frame 4:
  Total: 38 bytes
  Payload: 18 bytes
  Payload hex: 000000000000000000000000000000110000
  Checksum: 0xAA

Plug into Decoder

from PIL import Image
import numpy as np
import minilzo

all_frames = [
    "66 24 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 03 03 00 02 00 00 00 00 20 CB 02 00 07 FF 20 0C 01 11 00 00 6C",
    "66 24 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 02 00 E0 20 00 00 00 00 00 00 00 00 00 00 00 00 00 11 00 00 92",
    "66 24 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 01 00 00 DE BC 00 20 BB 2C 44 00 02 00 00 00 00 00 11 00 00 AC",
    "66 26 00 1B 2F 03 01 00 01 00 01 2B 01 2C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 11 00 00 11 00 00 AA"
]

def lzo_decompress_ble_frame(data: bytes) -> bytes | None:
    try:
        # convert hex bytes to actual bytes
        frame_bytes = bytes.fromhex(data)
        # skip 17 bytes (header), then discard last byte (checksum)
        compressed_data = frame_bytes[17:-1]
        dst_len = 9999 # weird API, max decompressed length has to be known beforehand or we fail.
        # TODO: find a better python API for LZO-1X decompress
        decompressed = minilzo.decompress(compressed_data, dst_len)
        print(f"[✓] Decompressed {len(compressed_data)} bytes to {len(decompressed)} bytes")
        return decompressed
    except Exception as e:
        print(f"[-] Failed to decompress raw LZO ({len(compressed_data)}B)", e)
        return None

def convert_decompressed_bytes_to_image(data: bytes, output_path: str):
    # byteswap 16-bit words
    swapped = bytearray()
    for i in range(0, len(data), 2):
        if i + 1 < len(data):
            swapped += data[i + 1:i + 2] + data[i:i + 1]
        else:
            swapped += data[i:i + 1]
    data = swapped
    # flip every bit (black <-> white inverted)
    data = bytes([x ^ 0xff for x in data])
    # assuem static height of 96 pixels and adjust width to match.
    height = 96
    total_bits = len(data) * 8
    width = total_bits // height

    print(f"Decoded dimensions: {width}x{height}")

    # Convert to bit array
    bits = np.unpackbits(np.frombuffer(data, dtype=np.uint8))

    # Reshape as [width, height], scanning columns bottom-to-top
    image_data = np.zeros((height, width), dtype=np.uint8)

    for x in range(width):
        col_bits = bits[x * height:(x + 1) * height]
        image_data[:, x] = col_bits[::-1] * 255  # invert vertically (bottom→top)


    # Create and save grayscale image
    img = Image.fromarray(image_data, mode="L")
    print(f"Saving to {output_path}")
    img.save(output_path)
    img.show()

def main():
    # get decompressed data from all frames
    all_data = b""
    for frame in all_frames:
        decompressed = lzo_decompress_ble_frame(frame)
        if decompressed is not None:
            all_data += decompressed
    convert_decompressed_bytes_to_image(all_data, "reconstructed.png")

if __name__ == '__main__':
    main()
Decoder Output
[-] Failed to decompress raw LZO (18B) Decompression failed
[-] Failed to decompress raw LZO (18B) Decompression failed
[-] Failed to decompress raw LZO (18B) Decompression failed
[-] Failed to decompress raw LZO (20B) Decompression failed
Decoded dimensions: 0x96
/home/deveaston06/experimental/python/L1Printer/./ble_frames_to_png.py:58: DeprecationWarning: 'mode' parameter is deprecated and will be removed in Pillow 13 (2026-10-15)
  img = Image.fromarray(image_data, mode="L")
Saving to reconstructed.png
Traceback (most recent call last):
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/ImageFile.py", line 644, in _save
    fh = fp.fileno()
         ^^^^^^^^^
AttributeError: '_idat' object has no attribute 'fileno'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/deveaston06/experimental/python/L1Printer/./ble_frames_to_png.py", line 73, in <module>
    main()
    ~~~~^^
  File "/home/deveaston06/experimental/python/L1Printer/./ble_frames_to_png.py", line 70, in main
    convert_decompressed_bytes_to_image(all_data, "reconstructed.png")
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/deveaston06/experimental/python/L1Printer/./ble_frames_to_png.py", line 60, in convert_decompressed_bytes_to_image
    img.save(output_path)
    ~~~~~~~~^^^^^^^^^^^^^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/Image.py", line 2588, in save
    save_handler(self, fp, filename)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 1495, in _save
    ImageFile._save(
    ~~~~~~~~~~~~~~~^
        single_im,
        ^^^^^^^^^^
        cast(IO[bytes], _idat(fp, chunk)),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)],
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/ImageFile.py", line 648, in _save
    _encode_tile(im, fp, tile, bufsize, None, exc)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/ImageFile.py", line 666, in _encode_tile
    encoder.setimage(im.im, extents)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
SystemError: tile cannot extend outside image

*P.S: I don’t have experience dealing with back and forth conversation in an online platform before, thank you so much for being patient with me, if I missing something or need to provide more in the topic, feel free to tell me so

There is no end marker. You’ve been misinterpreting the end of the compressed data (which seems to be the same among all frames, possibly a LZO-1X artefact) as this end marker. Remove it.

Thanks again, that is my bad, but something is still wrong

Removed this:

payload = frame[16:-4]  # Extract payload
# I changed above to below
payload = frame[16:-1]
length = 16 + len(chunk) + 4
# I changed the length from above to below
length = 16 + len(chunk) + 1

My Output (3rd attempt)

MakeID L1 Printer - ESP32 Code Generator
Using current compression algorithm

=== Generating Border_384x96 (384x96) ===
Original bitmap: 4608 bytes
Transformed format: 4610 bytes
Compressed: 66 bytes
Split into 4 chunks: [16, 16, 16, 18]
Frame 1: 16B payload, 33B total
Frame 2: 16B payload, 33B total
Frame 3: 16B payload, 33B total
Frame 4: 18B payload, 35B total

// ===========================================
// Border_384x96 (JobID: 0x012B) - Generated with current algorithm
// ===========================================
std::vector<std::vector<uint8_t>> printFrames;

// Frame 1 - 33 bytes
printFrames.push_back({
    0x66, 0x21, 0x00, 0x1B, 0x2F, 0x03, 0x01, 0x00, 0x01, 0x00, 0x01, 0x2B, 0x01, 0x55, 0x00, 0x03,
    0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCB, 0x02, 0x00, 0x07, 0xFF, 0x20, 0x0C, 0x01,
    0x80
});

// Frame 2 - 33 bytes
printFrames.push_back({
    0x66, 0x21, 0x00, 0x1B, 0x2F, 0x03, 0x01, 0x00, 0x01, 0x00, 0x01, 0x2B, 0x01, 0x55, 0x00, 0x02,
    0x00, 0xE0, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xA6
});

// Frame 3 - 33 bytes
printFrames.push_back({
    0x66, 0x21, 0x00, 0x1B, 0x2F, 0x03, 0x01, 0x00, 0x01, 0x00, 0x01, 0x2B, 0x01, 0x55, 0x00, 0x01,
    0x00, 0x00, 0xDE, 0xBC, 0x00, 0x20, 0xBB, 0x2C, 0x44, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xC0
});

// Frame 4 - 35 bytes
printFrames.push_back({
    0x66, 0x23, 0x00, 0x1B, 0x2F, 0x03, 0x01, 0x00, 0x01, 0x00, 0x01, 0x2B, 0x01, 0x2C, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11,
    0x00, 0x00, 0xBE
});

// Total: 4 frames
all_frames = [
    "66 21 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 03 03 00 02 00 00 00 00 20 CB 02 00 07 FF 20 0C 01 80",
    "66 21 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 02 00 E0 20 00 00 00 00 00 00 00 00 00 00 00 00 00 A6",
    "66 21 00 1B 2F 03 01 00 01 00 01 2B 01 55 00 01 00 00 DE BC 00 20 BB 2C 44 00 02 00 00 00 00 00 C0",
    "66 23 00 1B 2F 03 01 00 01 00 01 2B 01 2C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 11 00 00 BE"
]

=== Frame Details ===
Frame 1:
  Total: 33 bytes
  Payload: 16 bytes
  Payload hex: 0300020000000020cb020007ff200c01
  Checksum: 0x80
Frame 2:
  Total: 33 bytes
  Payload: 16 bytes
  Payload hex: 00e02000000000000000000000000000
  Checksum: 0xA6
Frame 3:
  Total: 33 bytes
  Payload: 16 bytes
  Payload hex: 0000debc0020bb2c4400020000000000
  Checksum: 0xC0
Frame 4:
  Total: 35 bytes
  Payload: 18 bytes
  Payload hex: 000000000000000000000000000000110000
  Checksum: 0xBE

Decoder Output (3rd attempt)

[-] Failed to decompress raw LZO (15B) Decompression failed
[-] Failed to decompress raw LZO (15B) Decompression failed
[-] Failed to decompress raw LZO (15B) Decompression failed
[-] Failed to decompress raw LZO (17B) Decompression failed
Decoded dimensions: 0x96
/home/deveaston06/experimental/python/L1Printer/./ble_frames_to_png.py:58: DeprecationWarning: 'mode' parameter is deprecated and will be removed in Pillow 13 (2026-10-15)
  img = Image.fromarray(image_data, mode="L")
Saving to reconstructed.png
Traceback (most recent call last):
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/ImageFile.py", line 644, in _save
    fh = fp.fileno()
         ^^^^^^^^^
AttributeError: '_idat' object has no attribute 'fileno'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/deveaston06/experimental/python/L1Printer/./ble_frames_to_png.py", line 73, in <module>
    main()
    ~~~~^^
  File "/home/deveaston06/experimental/python/L1Printer/./ble_frames_to_png.py", line 70, in main
    convert_decompressed_bytes_to_image(all_data, "reconstructed.png")
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/deveaston06/experimental/python/L1Printer/./ble_frames_to_png.py", line 60, in convert_decompressed_bytes_to_image
    img.save(output_path)
    ~~~~~~~~^^^^^^^^^^^^^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/Image.py", line 2588, in save
    save_handler(self, fp, filename)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/PngImagePlugin.py", line 1495, in _save
    ImageFile._save(
    ~~~~~~~~~~~~~~~^
        single_im,
        ^^^^^^^^^^
        cast(IO[bytes], _idat(fp, chunk)),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)],
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/ImageFile.py", line 648, in _save
    _encode_tile(im, fp, tile, bufsize, None, exc)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/deveaston06/.pyenv/versions/3.13.0/lib/python3.13/site-packages/PIL/ImageFile.py", line 666, in _encode_tile
    encoder.setimage(im.im, extents)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
SystemError: tile cannot extend outside image

No this is still not right, you’re compressing the whole bitmap data at once and only chunking afterwards in 4 chunks.

You have to create CEILING_DIVISION( len(printer_format), 1020) chunks of length 1020 (or less if there’s less bytes availalbe in the last chunk) from the printer_format data, then individually compress that.

Can you post your whole current encoder script?

Sorry, I need some sleep now, here is the code

#!/usr/bin/env python3
import numpy as np
import minilzo

# === PRINTER SETTINGS ===
PRINTER_ID = bytes([0x1B, 0x2F, 0x03, 0x01, 0x00, 0x01, 0x00, 0x01])

def calculate_checksum(frame_bytes):
    checksum = 0
    for byte in frame_bytes[:-1]:
        checksum = (checksum - byte) & 0xFF
    return checksum

def create_test_bitmap(width, height, pattern="border"):
    """Create test bitmap with border pattern"""
    bitmap = np.zeros((height, width), dtype=np.uint8)
    if pattern == "border":
        border_size = min(5, height // 10, width // 10)
        bitmap[0:border_size, :] = 255
        bitmap[-border_size:, :] = 255
        bitmap[:, 0:border_size] = 255
        bitmap[:, -border_size:] = 255
    elif pattern == "all_white":
        bitmap[:] = 255
    elif pattern == "all_black":
        bitmap[:] = 0
    elif pattern == "checker":
        bitmap[:, ::2] = 255
        bitmap[::2, :] ^= 255
    return bitmap

def bitmap_to_bytes_optimized(bitmap):
    """Convert bitmap to 1bpp byte array"""
    height, width = bitmap.shape
    bytes_per_row = (width + 7) // 8
    out = bytearray()
    
    for y in range(height):
        for x in range(0, width, 8):
            byte_val = 0
            for bit in range(8):
                if x + bit < width and bitmap[y, x + bit] > 0:
                    byte_val |= (1 << (7 - bit))
            out.append(byte_val)
    
    return bytes(out)

def transform_to_printer_format_optimized(bitmap_bytes, width, height):
    """
    Transform bitmap to printer format
    """
    bytes_per_row = (width + 7) // 8
    total_bytes = height * bytes_per_row
    
    # Simple inversion (common in thermal printers)
    result = bytearray()
    for byte in bitmap_bytes:
        result.append(byte ^ 0xFF)  # Invert bits
    
    # Add the common header
    final_output = bytearray()
    final_output.extend(b'\x00\x02')  # Common header
    final_output.extend(result)
    
    return bytes(final_output)

def compress_and_generate_frames(bitmap, job_id, final_magic, image_name):
    """Compress bitmap and generate frames"""
    height, width = bitmap.shape
    print(f"\n=== Generating {image_name} ({width}x{height}) ===")
    
    # Convert to bytes
    bitmap_bytes = bitmap_to_bytes_optimized(bitmap)
    print(f"Original bitmap: {len(bitmap_bytes)} bytes")
    
    # Transform to printer format
    printer_format = transform_to_printer_format_optimized(bitmap_bytes, width, height)
    print(f"Transformed format: {len(printer_format)} bytes")
    
    # Compress using LZO
    try:
        compressed_data = minilzo.compress(printer_format)
        print(f"Compressed: {len(compressed_data)} bytes")
    except Exception as e:
        print(f"Compression failed: {e}")
        return None
    
    # Split into 4 chunks
    TARGET_FRAMES = 4
    chunks = []
    
    if len(compressed_data) <= TARGET_FRAMES:
        base_size = max(1, len(compressed_data) // TARGET_FRAMES)
        for i in range(TARGET_FRAMES):
            start = i * base_size
            end = start + base_size if i < TARGET_FRAMES - 1 else len(compressed_data)
            if start < len(compressed_data):
                chunks.append(compressed_data[start:end])
            else:
                chunks.append(b'')
    else:
        base_size = len(compressed_data) // TARGET_FRAMES
        for i in range(TARGET_FRAMES):
            start = i * base_size
            if i < TARGET_FRAMES - 1:
                end = start + base_size
            else:
                end = len(compressed_data)
            chunks.append(compressed_data[start:end])
    
    print(f"Split into {len(chunks)} chunks: {[len(c) for c in chunks]}")
    
    # Create frames
    frames = []
    for i, chunk in enumerate(chunks):
        is_final = (i == TARGET_FRAMES - 1)
        frames_remaining = TARGET_FRAMES - i - 1
        
        frame = bytearray()
        frame.append(0x66)  # Magic
        
        # Calculate length
        length = 16 + len(chunk) + 1
        frame.extend([length & 0xFF, (length >> 8) & 0xFF])
        
        # Printer ID
        frame.extend(PRINTER_ID)
        
        # Job ID (little-endian)
        frame.extend([job_id & 0xFF, (job_id >> 8) & 0xFF])
        
        # Frame magic
        frame_magic = final_magic if is_final else 0x55
        frame.append(frame_magic)
        
        # Remaining frames (big-endian)
        frame.extend([(frames_remaining >> 8) & 0xFF, frames_remaining & 0xFF])
        
        # Payload
        frame.extend(chunk)
        
        # Calculate checksum
        temp_frame = frame + b'\x00'
        checksum = calculate_checksum(temp_frame)
        frame.append(checksum)
        
        frames.append(bytes(frame))
        print(f"Frame {i+1}: {len(chunk)}B payload, {len(frame)}B total")
    
    return frames

def generate_esp32_code(frames, test_name):
    print(f"\n// ===========================================")
    print(f"// {test_name} - Generated with current algorithm")
    print(f"// ===========================================")
    print("std::vector<std::vector<uint8_t>> printFrames;")
    
    for i, frame in enumerate(frames):
        print(f"\n// Frame {i+1} - {len(frame)} bytes")
        print("printFrames.push_back({")
        
        # Format with proper indentation (16 bytes per line)
        hex_lines = []
        for j in range(0, len(frame), 16):
            line = frame[j:j+16]
            hex_str = ', '.join(f"0x{b:02X}" for b in line)
            if j + 16 >= len(frame):
                hex_lines.append(f"    {hex_str}")
            else:
                hex_lines.append(f"    {hex_str},")
        
        print('\n'.join(hex_lines))
        print("});")
    
    print(f"\n// Total: {len(frames)} frames")

def debug_frame_details(frames):
    print(f"\n=== Frame Details ===")
    for i, frame in enumerate(frames):
        payload = frame[16:-1]  # Extract payload
        print(f"Frame {i+1}:")
        print(f"  Total: {len(frame)} bytes")
        print(f"  Payload: {len(payload)} bytes")
        print(f"  Payload hex: {payload.hex()}")
        print(f"  Checksum: 0x{frame[-1]:02X}")

def generate_frames_string_format(frames, test_name):
    print(f"all_frames = [")
    
    frame_strings = []
    for i, frame in enumerate(frames):
        # Convert frame to space-separated hex string
        hex_string = ' '.join(f"{b:02X}" for b in frame)
        frame_strings.append(f'    "{hex_string}"')
    
    # Join with newlines and commas
    print(',\n'.join(frame_strings))
    print("]")

# Test configurations
test_configs = [
    (384, 96, "border", 0x012B, 0x2C, "Border_384x96"),
    # (384, 96, "all_white", 0x012B, 0x2C, "White_384x96"), 
    # (384, 96, "all_black", 0x012B, 0x2C, "Black_384x96"),
    # (384, 96, "checker", 0x012B, 0x2C, "Checker_384x96"),
]

if __name__ == "__main__":
    print("MakeID L1 Printer - ESP32 Code Generator")
    print("Using current compression algorithm")
    
    for width, height, pattern, job_id, final_magic, name in test_configs:
        # Create bitmap
        bitmap = create_test_bitmap(width, height, pattern)
        
        # Generate frames using current algorithm
        frames = compress_and_generate_frames(bitmap, job_id, final_magic, name)
        
        if frames:
            # Generate ESP32 code
            # generate_esp32_code(frames, f"{name} (JobID: 0x{job_id:04X})")
            generate_frames_string_format(frames, f"{name} (JobID: 0x{job_id:04X})")
            
            # Show frame details for debugging
            debug_frame_details(frames)
            
            print(f"\n{'='*60}")

Max Gerhardt found the content/bitmap format: MiniLZO, and I was able to find the BLE header format that is partially functional.

(Solution will be given to Max, he deserves it for the helpful insights, nicee)