diff --git a/README.md b/README.md new file mode 100644 index 0000000..e652e00 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# thermal-printer +A project to reverse engineer the protocol of my thermal printer - Vyzio B15 (X6) and [implement it myself](#Usage), so I can print from my laptop. + +This project was inspired by [WerWolv's Cat printer blogpost](https://werwolv.net/blog/cat_printer) +, but I avoided looking at their code and peaking at the blogpost as much as i could. + +## Protocol +### Command structure +Consists of commands. Each command has the following structure: +`0x51` - Magic value +`0x78` - Magic value +`0x..` - Command byte +`0x00` or `0x01` - Direction (0 for Phone->Printer, 1 for reverse) +`0x..` - Data length, low byte +`0x00` - Data length, high byte (always zero) +`data` * N - Data +`crc8` - crc8 of only the data +`0xFF` - End magic + +### Printing an image +And the app sends the following commands to print an image (refer to the [commands](#getcommandcodecmdname)): +1. Blackening (quality) - always 51 (3) for my printer. It is from 1 to 5 (49 to 53). +2. Energy - only if the print type isn't TEXT: corresponds to the _Print depth_ set in the app, with the formula `energy = 7500 + (PD - 4)*0.15*7500`. The Print depth is from 1 to 7. +3. PrintType - Image(`0x00`), Text(`0x01`) of Label(`0x03`). Still can't find the differences between them, but I think that Image allows for darker images, but I'm not sure. +4. FeedPaper(speed) - I don't know what it does, but the speed is 10 for text and 30 for images and labels. +5. Drawing - commands with either run-length encoded or bit-packed lines +6. FeedPaper(25), Paper(0x30), Paper(0x30), FeedPaper(25) +I don't know what the FeedPaper calls do, but the Paper command feeds the paper so the printed image emerges. + +## Usage +In the `./src` directory you can find python files with functions to use the protocol and example usage in `./src/main.py`. + +### commands.py +Here you can find functions to create the bytes to command the printer. + +#### checkImage(imgPath) +Checks if the image at this path is usable for the printer (384 px wide and 1bpp) + +#### calcCrc8(data) +Calculates the crc8 checksum + +#### commandBytes(cmd, data) +Generates the byte array for a command + +#### getCommandCode(cmdName) +Returns the byte corresponding to the command. Accepts: +| Name(s) | Description | Byte | +| - | - | - | +| BLACKENING _(QUALITY)_ | Sets the quality level for the printer. I recommend keeping at default value 51. | `0xA4` | +| ENER(A)GY | Sets the energy (print depth) - the strength of heating of the black pixels. Use the `getEnergy()` function. Default value is 7500 for my model. | `0xAF` | +| PRINT_TYPE | Sets the print type. | `0xBE` | +| FEED_PAPER | Idk what, **DOES NOT** feed the paper. | `0xBD` | +| DRAW_COMPRESSED | Draws a row of pixels, encoded with run-length encoding. The highest bit of each byte is the value and the other 7 encode the run length. | `0xBF` | +| DRAW_PACKED | Draws a row of pixels where each pixel corresponds to one bit in the command's data. | `0xA2` | +| PAPER | Actually feeds the paper. Takes number of lines to move, with data size 2 (potentially 16 bite, little endian). | `0xA1` | + +#### runLenght(line) +Creates a byte array with the line compresses as explained above (see DRAW_COMPRESSED). + +#### bitPack(line) +Creates a byte array with each pixel corresponding to a bit. + +#### toArrayLE(num) +Converts 16-bit number to little-endian + +#### getEnergy(printDepth) +Calculates the energy corresponding to a certain value of "Print depth" in the app + +#### getTypeByte(typeStr) +Returns byte code for the print type. Supports +| Type | Byte | +| - | - | +| IMAGE | `0x00` | +| TEXT | `0x01` | +| LABEL | `0x03` | + +#### getSpeed(typeStr) +Returns the app's default "printSpeed" for types. Supports "IMAGE" (30) and "TEXT" (10). This is used with FEED_PAPER, idk how. + +#### readImage(imgPath) +Reads image from the path specified. + +#### compressImageToCmds(img) +For each line applies both run-length encoding and bit packing and chooses the shorter. Packages each line's compressed data into the appropriate command. + +#### saveCommandsToFile(commands, saveToFile=None, saveToFileHuman=None) +Saves a list of commands to a binary file and/or a human-readable hex file. + +#### genMoveUp(lines=0x60) +Generates the paper feeding commands at the end of the app's routine for printing an image. + +#### genCommands( img, ... _(see source code)_ ) +Generates the commands for printing an image with the specified parameters. + +### connection.py and class Connection +#### async connect() +Connects to the device and awaits OKAY status. + +#### async disconnect() +Disconnects from the device. + +#### \_\_init\_\_(macAddress) +Sets up the Connection for the .connect method. + +#### send(commands, delay=0) +Writes each command individually to the appropriate characteristic, waiting _delay_ seconds between each one. In the end asks device for status and awaits OKAY. + +#### Context managers +The Connection class supports the context manager protocol. It automatically connects and disconnects to the device. +```python +async with Connection("AA:BB:CC:DD:EE:FF") as conn: + ... +``` + +#### FakeConnection +This class mirrors the methods of Connection, but its constructor takes paths to log files, where the sent data gets dumped. + +### decode.py +#### readHumanFile(fileName) +Reads a human-readable file of hex values into an array of bytes. + +#### readFile(fileName) +Reads a binary file into an array of bytes. + +#### decodeCommands(cmdBytes, saveFilename=None) +Decodes the bytes given into the image they convey and returns it. If _saveFilename_ is set, then it saves the image there. + +### textToImage.py +#### textToImage(text, ... _(see source code)_ ) +Generates a PIL image from the given text. + +## Write up +You can read about my process of reverse engineering the protocol [here](WRITE_UP.md). \ No newline at end of file diff --git a/WRITE_UP.md b/WRITE_UP.md new file mode 100644 index 0000000..f87f5c1 --- /dev/null +++ b/WRITE_UP.md @@ -0,0 +1 @@ +# TODO \ No newline at end of file diff --git a/reverse_engineering/NOTES.md b/reverse_engineering/NOTES.md new file mode 100644 index 0000000..60be816 --- /dev/null +++ b/reverse_engineering/NOTES.md @@ -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/e;/d;?{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 FF` + - printType packet 0x`51 78 BE 00 01 00 FF` + Type: `00` - image, `01` - text, `02` - tattoo, `03` - label + - feedPaper packet 0x`51 78 BD 00 01 00 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 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 0 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 \ No newline at end of file diff --git a/reverse_engineering/dataToBin.py b/reverse_engineering/dataToBin.py new file mode 100644 index 0000000..1aa00bd --- /dev/null +++ b/reverse_engineering/dataToBin.py @@ -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) \ No newline at end of file diff --git a/reverse_engineering/decode.py b/reverse_engineering/decode.py new file mode 100644 index 0000000..54edaa5 --- /dev/null +++ b/reverse_engineering/decode.py @@ -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) diff --git a/reverse_engineering/esp32_emulate_printer.ino b/reverse_engineering/esp32_emulate_printer.ino new file mode 100644 index 0000000..b1c4446 --- /dev/null +++ b/reverse_engineering/esp32_emulate_printer.ino @@ -0,0 +1,113 @@ +#include +#include +#include +#include + +// 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(){} \ No newline at end of file diff --git a/reverse_engineering/img_v1.py b/reverse_engineering/img_v1.py new file mode 100644 index 0000000..bca637b --- /dev/null +++ b/reverse_engineering/img_v1.py @@ -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) \ No newline at end of file diff --git a/reverse_engineering/img_v2.py b/reverse_engineering/img_v2.py new file mode 100644 index 0000000..e8e0f7c --- /dev/null +++ b/reverse_engineering/img_v2.py @@ -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) \ No newline at end of file diff --git a/reverse_engineering/testGen.py b/reverse_engineering/testGen.py new file mode 100644 index 0000000..b904d11 --- /dev/null +++ b/reverse_engineering/testGen.py @@ -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") \ No newline at end of file diff --git a/reverse_engineering/x6_n.java b/reverse_engineering/x6_n.java new file mode 100644 index 0000000..0b8730d --- /dev/null +++ b/reverse_engineering/x6_n.java @@ -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 +} diff --git a/src/main.py b/src/main.py index ac4d214..5e25f0e 100644 --- a/src/main.py +++ b/src/main.py @@ -1,5 +1,5 @@ import asyncio -from import datetime import datetime +from datetime import datetime from commands import * from connection import Connection, FakeConnection;