begin documenting the project, README

This commit is contained in:
GiggioG
2025-09-07 03:32:22 +03:00
parent ca42b7bf20
commit 1bc7bd7565
11 changed files with 731 additions and 1 deletions

View File

@@ -0,0 +1,135 @@
MAGIC VALUE: 0x51 78
```js
function getHex(){return Array.from(new Uint8Array(arguments)).map(e=>e.toString(16).padStart(2, '0').toUpperCase()).join(' ');}
```
0. Inspo (cat printer) i motivaciq
1. Sniff s ESP32 i nrF connect
2. TestGen.py, Gledane samo na dannite
- 0x5178 100% e magic value kato header
- packetitie zavurshvat na 0xFF
- `ggVGgJ:%s/5178/^V^M5178/g^MggddG:s/0*$/^M:%s/\(\w\w\)\(\w\w\)/\1 \2/g^M:%&g^M` for data parsing
- 2ta scripta za visualizirane, ne bqh zabelqzal, che e B/W dithered, a ne grayscale, ochakvah da pop outne kartinkata
- dataToBin.py, da go gledam s hex editor => do nikude
3. zabivane v PrintPreviewActivity.java
- printBtn
- printData
- nedecompiliraniq handler, emptyMessage-5 => emptyMessage-4
4. Zabivane v activityType (LABEL, OFFICE, ...)
- predpolagam label, produljavam
5. PrintModelUtils i PrinterModel => x6.java, izvadq argumentite
- legendarno vim macro:
`0f(l"ddt,xxj0f(ldw"edt,xxjvi{:s/<C-R>e;/<C-R>d;<CR>?{k0`
6. naj-nakraq v pravilnata posoka: `BitmapToData()` v `PrintDataUtils.java`
7. status byte-ovete rediscovernati: 0x`51 78 A3 00 01 00 00 00 FF` obratno v PrintData (PrintPreviewActivity)
8. v BitmapToData se vika eachLinePixTo...B, no to ne e decomp, zatova gledam eachLine...Gray, sigurno e sushtoto
- zashto tf raw byte-ovete na dithera se zavirat direktno v packeta, bez packet (0x`51 78 ... FF`).
9. bluskam si glavata, no vednaga vijdam, che v `eachLineP...B` ne e sushtoto, oshte v purvite redove se getva
width / 8, ochevidno e razlichno.
10. buildvame packeta:
- enerAgy packet 0x`51 78 AF 00 02 00 <nrg_low> <nrg_high> <crc> FF`
- printType packet 0x`51 78 BE 00 01 00 <type> <crc> FF`
Type: `00` - image, `01` - text, `02` - tattoo, `03` - label
- feedPaper packet 0x`51 78 BD 00 01 00 <textSpeed> <crc> FF`
11. obratno v BitmapToData:
- blackeningPacket 0x`51 78 A4 00 01 00 33 99 FF` (quality 3 (51) (0x33) - vinagi za moq printer e tova quality)
te sa ot 1 do 5 (ot 49 do 53)
- feedPaper 0x`51 78 BD 00 01 00 0F <crc> FF` - feedPaper(15)
12. obratno v `eachLinePixToCmdB`: polzvam nqkolko regexa, ne pomagat osobeno mn
- zagrqvam, che DataTrim() vzima (runLenght, byte, arrayList), i byte-a legit e 0x00 ili 0x01
vseki byte, kojto pravi e `<1b: stojnost na byte-a, 0 ili 1><7b: duljina na run-length(do 127)>`
- run-length encoding, ako e po-zle ot bitpacking, togava bit-packvame
- L_0x02fb dataTrim-vame 0, ako izobshto nishto ne sme imali
- v L_0x0328: stranni kriterii dali sme kompresirali:
ne sme kompresirali, ako: 1. purviq byte e 0xFF, 2. byte-ovete ne sa width/8 + 1
- 0x`51 78` + -94(0x`A2`) ako ne sme kompresirali, -65(0x`BF`), ako sme kompresirali
- 0x`51 78 <A2 | BF> 0 <datalen_low> <datalen_high | 00> <dannite, run-lenghtnati ili bitpack-nati> <crc> FF`
- da se izfukam, che kompresiqta q nqma v originalniq writeup
12. naj-nakraq q svurshih, obratno v BitmapToData
- paper packet, ot BluetoothOrder 0x`51 78 A1 00 02 00 30 00 F9 FF`, sled vseki PrintBean, kojto ima .isAddWhite()
- maj nqma takiva
13. finalna naredba na komandite
14. decode.py, bachka s probite (primeri)
15. kakvo e energy-to?
- gledam v koda: (concentration-a - Code.DEFCONCENTRATION(4))\*0.15\*d + d, trqbva da e 0x7B2A(=10875), zashtoto tova poluchavam
- osuznavam, che concentration sigurno e print depth, zashtoto defaulta e 4 ot 1 do 7 (DEF _\[ault\]_ CONCENTRATION = 4)
- az polzvam 7, znachi (7-4)*0.15.d+d = 10875 => d = 7500, no moderateConcentration-a na X6 e 8000.....
- moqt printer e x6_n or PrinterModelUtils.java, vse pak ne obqsnqva zashto isCanPrintLabel qvno e false
- hipoteza: energy = (printDepth - 4)\*0.15\*7500 + 7500, shte testvam
- hipoteza 2: textEnergy=> energy da e =0 pri textov rejim
16. kakuv e print type-a
- probvam da send-na snimka kato text, image; text kato text,image; opitam se da dokaram label
17. textSpeed-a e model.textSpeed(10) za text(0x`01`), model.imgSpeed(30) za image(0x`00`) i label(0x`03`)
- probvajki prednoto, sledq feedPaper-a
18. Da probvam:
1. da dokaram print type: label
2. da dokaram print type: text
- vidq dali energy == 0
3. probvam pone 2 drugi energy-ta, da vidq dali scale-va pravilno
4. probvam da sendna snimka kato text, obratnoto, sledq feedPaper-a
19. Findings:
- kogato printiram document, type: img, printType-a se meni, no nikoga nqma energy i textSpeed = 10
- energyto naistina e po onazi formula
- tova s documenta e izkluchenie, sus image i text:
1. pri image ima energy po formulata, BD=00, textSpeed=30
2. pri text nqma energy, BD=01, textSpeed=10
- label-a se durji tochno kato chist image, samo BD=03; nezavisimo dali type-a e setnat na Text ili Image
- kato mu dam poveche kopiq apparently ne sa otdelni bean-ove, poneje ima blackening pred vsqko i dopulnitelen
feedPaper(25) sled vsqko => probvam s multiple beans
20. Recreate in script
- crc-to, deleted59 => gledam lipsvashtoto sus xor na vsichki ostanali, razbiram che e `0x3d`
- printImage.py -> uspeshno generira commandi
21. Make script actually send BLE packets
- reading what printer sends back, the 4th byte is 0x00 if phone->printer and 0x01 if printer->phone
21. status (0x`A3`) response (length 3)
1. status, ends with:
- =0: ok,
- 0b1: out of paper,
- 0b10: compartment open,
- 0b100: overheated,
- 0b1000: low battery,
- 0b10000: currently charging,
- 0b10000000: currently printing
2. labelNum, kakvoto i da e tova
3. battery level, po nqkakva skala?
22. Bachka
23. Kakvo ostana?
- kakvo pravi 0xBD FeedPaper?
24. Look back
- ako imah akul i bqh zabelqzal, che e cherno-bql, mojeshe samo ot protocol capturi-te
no nqmashe da imam compressiq, da znam za type-ove, za energy i t.n.
# Vim commands for easier decompiling
1. `:v/L_0x....:/norm A;^[`
2. `:%s/\(\d\+\)(0x.*)/\1/g`
3. `:%s/\(.* \)\?r\(\d\+\) = \(.*\);$\n\t\(.* \)r\2 = \(.*\);$/\=("\t".submatch(4)."r".submatch(2)." = ".substitute(submatch(5),"r".submatch(2),submatch(3),"")."; \/\/ rule 1")/g`
4. `:%s/\(.* \)\?r\(\d\+\) = \(.*\);$\n\tr\2 = \(.*\);$/\=("\t".submatch(1)."r".submatch(2)." = ".substitute(submatch(4),"r".submatch(2),submatch(3),"")."; \/\/ rule 2")/g`
5. `:%s/int r\(\d\+\) = r\1 \([+-\*\/]\) \(.\+\);/r\1 \2= \3; \/\/ rule 3/g`
6. `:v/r\(\d\+\) = \(\-\?\d\+\);\n.*r\1 \?=.*/ s/r\(\d\+\) = \(\-\?\d\+\);$\n\t\(.*[ (]\)r\1\(.*\)/r\1 = \2;^M\t\3\2\4 \/\/ rule 4/g`
7. `:%s/boolean r\(\d\+\) = \(.*\);\n\tif (r\1\(.*\)$/if ((r\1 = \2\)\3 \/\/ rule XXX/g`
8. `:%s/if (\(.*\)) goto \(L_0x....\);\n\tif (\(.*\)) goto \2;/if ((\1) || (\3)) goto \2; \/\/ rule 6/g`
- broken but still cool: `:%s/if \(.*\) goto \(L_0x....\);$\n\(^\t.*$\n\)\+\tgoto \(L_0x....\);\n\2:$\n\(^\t.*$\n\)\+\4:/if \1 { \/\/ rule 5^M\5\t} else {^M\3\t}/g`
# command order
1. blackening /// ne za vseki bean
2. vsichkite komandi ot EachLinePixToCmdB
1. enerAgy
2. printType
3. feedPaper(textSpeed)
4. draw
3. paper (ako bean-a ima addWhite)
4. feedPaper(25)
5. paper x2
bi trqbvalo da gi nqma, poneje X6 e isCanPrintLabel.
6. feedPaper(25) /// ne za vseki bean
7. ne poluchavam devInfo (0x`51 78 A8 00 01 00 00 00 FF`), nz zashto
i tuk i pri dvata `paper`-a se durji, sqkash X6 ne e isCanPrintLabel
# todo
- [ ] napisha property-ta na X6_n (ot file)
- [ ] writeup

View File

@@ -0,0 +1,18 @@
import os
import glob
os.makedirs("databin", exist_ok=True)
for data_file in glob.glob("data/data*.txt"):
with open(data_file, "r") as f:
hex_data = f.read().replace('\n', '').replace(' ', '')
# Convert hex string to bytes
byte_data = bytes.fromhex(hex_data)
# Generate output filename: databin/data#.bin
base = os.path.splitext(os.path.basename(data_file))[0]
out_file = f"databin/{base}.bin"
with open(out_file, "wb") as fout:
fout.write(byte_data)

View File

@@ -0,0 +1,86 @@
import os
from PIL import Image
import sys
import glob
os.makedirs("decodedImg", exist_ok=True)
for in_file in glob.glob("data/data*.txt"):
cmds = []
with open(in_file, "r") as file:
data = file.read().split('\n')
data = [d.strip() for d in data if len(d)>0]
binary = []
for d in data:
binary.extend(bytes.fromhex(d))
idx = -1
data = []
cmdCode = 0x00
dataLen = 0
crc = 0x00
for i, b in enumerate(binary):
idx += 1
#print(i, idx, hex(b))
if b == 0x51 and idx == 0:
pass
elif b == 0x78 and idx == 1:
pass
elif idx == 2:
cmdCode = b
elif b == 0x00 and idx == 3:
pass
elif idx == 4:
dataLen = b
elif idx == 5:
if b != 0:
dataLen = dataLen*0x100 + b
elif idx >= 6 and idx < 6+dataLen:
data.append(b)
elif idx == 6+dataLen:
crc = b
elif idx == 6+dataLen+1 and b == 0xFF:
o = {
"cmd": cmdCode,
"data": data,
"crc": crc
}
cmds.append(o)
idx = -1
crc = 0
data = []
cmdCode = 0
else:
idx = -1
crc = 0
data = []
cmdCode = 0
#print(cmds)
#for c in cmds:
# print(hex(c["cmd"]), hex(len(c["data"])), hex(c["crc"]))
rows = []
for c in cmds:
if c["cmd"] == 0xa2:
s = "".join([bin(b)[2:10].zfill(8)[::-1] for b in c["data"]])
row = [int(x)==1 for x in s]
rows.append(row)
elif c["cmd"] == 0xbf:
row = []
for b in c["data"]:
bit = (b & (1<<7)) == 128
times = b & ~(1<<7)
row.extend([bit] * times)
rows.append(row)
HEIGHT = len(rows)
WIDTH = max(len(r) for r in rows)
img = Image.new("1", (WIDTH, HEIGHT))
for y in range(HEIGHT):
for x in range(WIDTH):
img.putpixel((x,y), not rows[y][x])
base = in_file.split('\\')[-1].split('.')[0] # Get base name without extension
output_file = f"decodedImg/{base}.png"
img.save(output_file)

View File

@@ -0,0 +1,113 @@
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
// https://www.uuidgenerator.net/
class ServerCallbacks : public BLEServerCallbacks{
void onConnect(BLEServer *pServer){
Serial.println("Connected.");
}
void onDisconnect(BLEServer *pServer){
pServer->startAdvertising();
Serial.println("Disconnected, restarting advertising.");
}
};
const int MAX_LINE = 2000;
int lineCol = 0;
void myPrint(String s){
int len = s.length();
if(lineCol + len >= MAX_LINE){
Serial.println("");
lineCol = 0;
}
Serial.print(s);
lineCol += len;
}
void printReceived(BLECharacteristic *pRxCharacteristic){
String uuid = pRxCharacteristic->getUUID().toString();
String rxValue = pRxCharacteristic->getValue();
// Serial.print(uuid);
// Serial.print(" > ");
String hexByte = "##";
for(int i = 0; i < rxValue.length(); i++){
uint8_t byte = rxValue[i];
hexByte[0] = byte/16;
hexByte[1] = byte%16;
for(int i = 0; i < 2; i++){
hexByte[i] += (hexByte[i] >= 10 ? ('A'-10) : '0');
}
myPrint(hexByte);
}
}
BLECharacteristic *pRxChar = nullptr, *pTxChar = nullptr;
class WriteCharacteristicCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pRxCharacteristic){
printReceived(pRxCharacteristic);
if(pRxCharacteristic == pRxChar){
pTxChar->setValue("\x51\x78\xA3\x01\x03\x00\x00\x0F\x24\x3F\xFF");
pTxChar->notify();
}
}
};
void setup() {
Serial.begin(115200);
/// SETUP BLE
BLEDevice::init("X6");
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks());
{
BLEService *pService_AE30 = pServer->createService("AE30");
BLECharacteristic *pChar_AE01 = pService_AE30->createCharacteristic("AE01", BLECharacteristic::PROPERTY_WRITE_NR);
pChar_AE01->setCallbacks(new WriteCharacteristicCallbacks());
pRxChar = pChar_AE01;
BLECharacteristic *pChar_AE02 = pService_AE30->createCharacteristic("AE02", BLECharacteristic::PROPERTY_NOTIFY);
pChar_AE02->addDescriptor(new BLE2902());
pTxChar = pChar_AE02;
BLECharacteristic *pChar_AE03 = pService_AE30->createCharacteristic("AE03", BLECharacteristic::PROPERTY_WRITE_NR);
pChar_AE03->setCallbacks(new WriteCharacteristicCallbacks());
BLECharacteristic *pChar_AE04 = pService_AE30->createCharacteristic("AE04", BLECharacteristic::PROPERTY_NOTIFY);
pChar_AE04->addDescriptor(new BLE2902());
BLECharacteristic *pChar_AE05 = pService_AE30->createCharacteristic("AE05", BLECharacteristic::PROPERTY_INDICATE);
pChar_AE05->addDescriptor(new BLE2902());
BLECharacteristic *pChar_AE10 = pService_AE30->createCharacteristic("AE10", BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE);
pChar_AE10->setCallbacks(new WriteCharacteristicCallbacks());
pService_AE30->start();
}
{
BLEService *pService_AE3A = pServer->createService("AE3A");
BLECharacteristic *pChar_AE3B = pService_AE3A->createCharacteristic("AE3B", BLECharacteristic::PROPERTY_WRITE_NR);
pChar_AE3B->addDescriptor(new BLE2902());
BLECharacteristic *pChar_AE3C = pService_AE3A->createCharacteristic("AE3C", BLECharacteristic::PROPERTY_NOTIFY);
pChar_AE3C->addDescriptor(new BLE2902());
pService_AE3A->start();
}
BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->start();
Serial.println("Setup done!");
}
void loop(){}

View File

@@ -0,0 +1,43 @@
from math import ceil
import os
from PIL import Image
import glob
WIDTH = 256
OFFSET = 9
os.makedirs("dataImg", exist_ok=True)
for data_file in glob.glob("data/data*.txt"):
with open(data_file, "r") as f:
data = f.read().replace('\n', '')
bytes_array = [int(data[i:i+2], 16) for i in range(0, len(data), 2)]
HEIGHT = 1 + ceil((len(bytes_array)-OFFSET)/WIDTH)
IMG_WIDTH = max(WIDTH, OFFSET)
def getColor(value):
return (value, value, value)
img = Image.new("RGB", (IMG_WIDTH, HEIGHT))
for i in range(0, IMG_WIDTH):
if i < OFFSET:
img.putpixel((i, 0), getColor(bytes_array[i]))
else:
img.putpixel((i, 0), (255, 0, 255))
for i in range(1, HEIGHT):
for j in range(0, IMG_WIDTH):
if j >= WIDTH:
img.putpixel((j, i), (255, 0, 255))
continue
idx = OFFSET + (i - 1) * WIDTH + j
if idx < len(bytes_array):
img.putpixel((j, i), getColor(bytes_array[idx]))
else:
img.putpixel((j, i), (255, 0, 255))
base = data_file.split('\\')[-1].split('.')[0] # Get base name without extension
output_file = f"dataImg/{base}.png"
img.save(output_file)

View File

@@ -0,0 +1,47 @@
from math import ceil
from PIL import Image
import glob
import os
os.makedirs("dataImg", exist_ok=True)
def getColor(value):
return (value, value, value)
# Find all files matching data*.txt
for data_file in glob.glob("data/data*.txt"):
with open(data_file, "r") as f:
data = f.read().replace('\n', '')
bytes_array = [int(data[i:i+2], 16) for i in range(0, len(data), 2)]
runs_array = []
run = []
runSum = 0
for b in bytes_array:
run.append(b)
runSum += b
if b == 255:
runs_array.append(run)
run = []
if len(run) > 0 and runSum > 0:
runs_array.append(run)
if not runs_array:
continue
HEIGHT = len(runs_array)
WIDTH = max(len(run) for run in runs_array)
img = Image.new("RGB", (WIDTH, HEIGHT))
for i in range(HEIGHT):
for j in range(WIDTH):
if j < len(runs_array[i]):
img.putpixel((j, i), getColor(runs_array[i][j]))
else:
img.putpixel((j, i), (255, 0, 255))
# Generate output filename: image#.png
base = data_file.split('\\')[-1].split('.')[0] # Get base name without extension
output_file = f"dataImg/{base}.png"
img.save(output_file)

View File

@@ -0,0 +1,104 @@
from math import ceil
import random
from PIL import Image
import zipfile
import os
def getColor(value):
return (value, value, value)
WIDTH = 384
def makeTestImage(testGen, name):
rows = testGen()
HEIGHT = len(rows)
print("Test image " + name + " is " + str(WIDTH) + "x" + str(HEIGHT))
img = Image.new("RGB", (WIDTH, HEIGHT))
for i in range(HEIGHT):
for j in range(WIDTH):
img.putpixel((j, i), getColor(rows[i][j]))
img.save("testImages/" + name + ".png")
def genTest1():
rows = []
i = 0
while 2**i <= WIDTH:
row = [255]
power = 2**i
for j in range(1, WIDTH):
color = row[j-1]
if j % power == 0:
color = 255-color
row.append(color)
rows.append(row)
i += 1
return rows
def genTest2():
rows = []
i = 0
while 2**i <= WIDTH:
row = []
power = 2**i
for j in range(0, WIDTH):
color = (j % power) * (255 // max(power-1, 1))
row.append(color)
rows.append(row)
i += 1
return rows
def genTest3():
rows = []
for _ in range(9):
row = []
for j in range(WIDTH):
color = (j%2) * 255
row.append(color)
rows.append(row)
return rows
def genTest4():
blackRow = [0 for _ in range(WIDTH)]
whiteRow = [255 for _ in range(WIDTH)]
rows = []
for i in range(9):
rows.append(blackRow if i % 2 == 0 else whiteRow)
return rows
def genTest5():
blackRow = [0 for _ in range(WIDTH)]
whiteRow = [255 for _ in range(WIDTH)]
rows = []
for i in range(9):
rows.append(whiteRow if i % 2 == 0 else blackRow)
return rows
def genTest6():
rows = []
for _ in range(9):
row = []
for _ in range(WIDTH):
color = random.randint(0, 255)
row.append(color)
rows.append(row)
return rows
tests = [
(genTest1, "test1"),
(genTest2, "test2"),
(genTest3, "test3"),
(genTest4, "test4"),
(genTest5, "test5"),
(genTest6, "test6")
]
os.makedirs("testImages", exist_ok=True)
for testGen, name in tests:
makeTestImage(testGen, name)
with zipfile.ZipFile("testImages/test_images.zip", "w") as zf:
for _, name in tests:
zf.write("testImages/" + name + ".png")

View File

@@ -0,0 +1,50 @@
public static PrinterModel.DataBean x6_n = new PrinterModel.DataBean();
public DataBean() {
this.grayPrint = false;
this.grayThinEneragy = 0;
this.grayModerationEneragy = 0;
this.grayDeepenEneragy = 0;
this.slowInterval = 0;
this.grayImageSpeed = 40;
this.grayScale = 1.0d;
this.showElectricityModel = 0;
this.addMorPix = true;
this.A4XII = false;
this.tattooPaper = false;
this.tattooSpeed = 0;
this.tattooEnergy = 0;
this.addMorePixNum = -1;
this.d1key = "";
this.useNewActivation = false;
this.lzoVersion = "";
this.labelDevice = false;
this.labelDeviceSize = 0.0f;
this.autoLabelCheck = false;
this.corePrint = false;
this.modelNo = "X6"; //FROM FUNCTION
this.model = 0; //FROM FUNCTION
this.size = 2; //FROM FUNCTION
this.paperSize = 384; //FROM FUNCTION
this.printSize = 384; //FROM FUNCTION
this.oneLength = 8; //FROM FUNCTION
this.headName = "X6-"; //FROM FUNCTION
this.canChangeMTU = true; //FROM FUNCTION
this.devdpi = 200; //FROM FUNCTION
this.imgPrintSpeed = 30; //FROM FUNCTION
this.textPrintSpeed = 10; //FROM FUNCTION
this.imgMTU = 123; //FROM FUNCTION
this.newCompress = true; //FROM FUNCTION
this.paperNum = 2; //FROM FUNCTION
this.interval = 6; //FROM FUNCTION
this.thinEneragy = 7500; //FROM FUNCTION
this.moderationEneragy = 7500; //FROM FUNCTION
this.deepenEneragy = 7500; //FROM FUNCTION
this.textEneragy = 0; //FROM FUNCTION
this.hasId = true; //FROM FUNCTION
this.useSPP = false; //FROM FUNCTION
this.newFormat = false; //FROM FUNCTION
this.canPrintLabel = true; //FROM FUNCTION
this.labelValue = "1"; //FROM FUNCTION
this.backPaperNum = 40; //FROM FUNCTION
this.detectionValue = 1; //FROM FUNCTION
}