Adding ptcr2gif to generate animated GIF from the .dat / PTCR file.

Missing: drawing segment and circle.
Updated the PTCR documentation along thing found while making the tool.
This commit is contained in:
Godzil 2022-11-17 17:51:34 +00:00
parent f2fe97098e
commit d56b29162a
3 changed files with 312 additions and 7 deletions

16
PTCR.md
View File

@ -7,8 +7,8 @@ PTCR File:
struct PTCRFile: struct PTCRFile:
struct Header: struct Header:
char[4] = "PTCR" char[4] = "PTCR"
uint32_t version uint32_t version?
uint32_t numberOfCommands (only drawable commands?) uint32_t numberOfCommands? (only drawable commands?)
uint32_t fileSize - Header uint32_t fileSize - Header
char[12]: unknown char[12]: unknown
char[4] = 0xAA, 0x55, 0xAA, 0x55 char[4] = 0xAA, 0x55, 0xAA, 0x55
@ -88,6 +88,9 @@ The circle is drawn in the rectangle designed by [x0;y0] - [x1; y1]
|--------|-----------|-----------|-----------|----------------| |--------|-----------|-----------|-----------|----------------|
| F5 | Stroke #1 | Stroke #2 | New color | Replaced color | | F5 | Stroke #1 | Stroke #2 | New color | Replaced color |
Colors are stored with an offet of 28 (0x1C). Reason unknown.
So if you read 28, the actual color to use is 0
#### Examples: #### Examples:
- `F5 0A 5B 5B 5F` - `F5 0A 5B 5B 5F`
- `F5 0A 7B 2A 5B` - `F5 0A 7B 2A 5B`
@ -124,9 +127,12 @@ Code clear the canvas to all white whatever the value. Not sure what the byte 3
|--------|-----------|--------| |--------|-----------|--------|
| FA | new color | ??? | | FA | new color | ??? |
Colors are stored with an offet of 28 (0x1C). Reason unknown.
So if you read 28, the actual color to use is 0
#### examples: #### examples:
- `FA 67 6A` : now work with colour 0x67 - `FA 67 6A` : now work with colour 0x4B
- `FA 20 67` : now work with colour 0x20 - `FA 20 67` : now work with colour 0x04
### FB: setCanvasResolution ### FB: setCanvasResolution
@ -134,7 +140,7 @@ Code clear the canvas to all white whatever the value. Not sure what the byte 3
|--------|-----------------|---------------| |--------|-----------------|---------------|
| FB | horizontal size | vertical size | | FB | horizontal size | vertical size |
_If the value is not 16, 24 or 32 it will default back to 16_ _If the value is not 0 (16x16), 1 (24x24) or 2 (32x32) it will default back to 0_
### FC: Not used ### FC: Not used

View File

@ -26,10 +26,12 @@ the update file properly leading the board to crash in weird way.
### TODO ### TODO
- [ ] Documenting the update file format - [ ] Documenting the update file format
- [ ] Creating a sample tool to create animated an gif from a .dat file - [ ] Documenting the BLE protocol
- [ ] Finish the the pixb2img to crop image if wanted (it only show imaged in their full 32x32 format) - [X] Creating a sample tool to create animated an gif from a .dat file
- [ ] Finish the pixb2img to crop image if wanted (it only show imaged in their full 32x32 format)
- [ ] Hardware documentation, there is definitely a serial port and other port on the board. - [ ] Hardware documentation, there is definitely a serial port and other port on the board.
- [ ] Maybe add datasheet of the components used on the board. - [ ] Maybe add datasheet of the components used on the board.
- [ ] Find how they draw straigh lines and circle to have an exact match.
## Note ## Note
This repository is obviously not related in anyway with MinBay the creators of the MB701. This repository is obviously not related in anyway with MinBay the creators of the MB701.

297
sample_tools/ptcr2gif.py Normal file
View File

@ -0,0 +1,297 @@
import argparse
import struct
from PIL import Image, ImageDraw
COLOUR_PALETTE = [
0xFFFFFF, 0xE9BFA8, 0xECC7AC, 0xEFCFB0, 0xF4DDB7, 0xF7E4BB, 0xF7E4BB, 0xFAEBBE, 0xFDF2C1, 0xFFF9C4,
0xF2F2C3, 0xE3EBC5, 0xD4E3C0, 0xC5DBBE, 0xC5DDCC, 0xC4DEDA, 0xC4E0E8, 0xC4E1F5, 0xC8D9EE, 0xCBD0E6,
0xCEC5DE, 0xD0BAD5, 0xD6BBCB, 0xDDBDC0, 0xE3BEB4, 0xD1D1D2, 0xD37859, 0xD88A5F, 0xDE9B64, 0xE3AB69,
0xE9BA6E, 0xEFC972, 0xF4D777, 0xFAE57A, 0xFFF387, 0xE4E47E, 0xC6D57D, 0xA5C57C, 0x82B47B, 0x81B798,
0x7FBAB5, 0x7DBDD0, 0x7BBFE9, 0x8AB4DD, 0x96A3CE, 0x9F8EBB, 0xA572A6, 0xB17495, 0xBD7683, 0xC8776F,
0x7D7E7F, 0xC1001F, 0xC63F1F, 0xCC5E1E, 0xD4791D, 0xDC931A, 0xE5AB14, 0xEEC200, 0xF6D800, 0xFFEC00,
0xD4D51D, 0xA2BD30, 0x6BA53A, 0x118F40, 0x009266, 0x00958E, 0x0099B7, 0x009CDD, 0x348ECB, 0x5A76B2,
0x6F5496, 0x7E177A, 0x901366, 0xA20E50, 0xB20039, 0x1F1E21, 0x551415, 0x5F2517, 0x693618, 0x754819,
0x825B1A, 0x8F701A, 0x9E8519, 0xAD9B16, 0xBAAC12, 0x98981F, 0x6F8127, 0x456C2B, 0x005A2C, 0x006145,
0x006864, 0x007086, 0x0077A4, 0x266992, 0x3F537A, 0x4A3762, 0x500D4D, 0x560C3E, 0x58102E, 0x591320
]
def BGRtoRGB(val):
return (val & 0xFF0000) >> 16 | (val & 0x00FF00) | ((val & 0x0000FF) << 16)
class Position:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"[{self.x};{self.y}]"
class Canvas:
def __init__(self):
self._pixel_data = [0] * 32 * 32
self._current_resolution = 32
self._erase_color = 0
self._current_color = 0
def set(self, x, y):
self._pixel_data[y * 32 + x] = self._current_color
def get(self, x, y):
return self._pixel_data[y*32 + x]
def erase(self, x, y):
self._pixel_data[y * 32 + x] = self._erase_color
def clear(self, colour):
for x in range(32):
for y in range(32):
self.erase(x, y)
def set_colour(self, colour):
self._current_color = colour
def set_resolution(self, resolution):
self._current_resolution = resolution
def replace_colour(self, old_color, new_colour):
tmp = self._current_color
self._current_color = new_colour
for x in range(32):
for y in range(32):
if self.get(x, y) == old_color:
self.set(x, y)
self._current_color = tmp
def render(self, pixel_size):
img = Image.new("RGB", (32 * pixel_size, 32 * pixel_size))
draw = ImageDraw.Draw(img)
for y in range(32):
for x in range(32):
x0 = x * pixel_size
y0 = y * pixel_size
pos = [(x0, y0), (x0 + pixel_size, y0 + pixel_size)]
pixel_color_index = self.get(x, y) if self.get(x, y) < 100 else 0
pixel_color = BGRtoRGB(COLOUR_PALETTE[pixel_color_index])
draw.rectangle(pos, fill=pixel_color, outline=pixel_color)
return img
class CommandHandler:
def __init__(self, file, canvas: Canvas):
self._file = file
self._canvas = canvas
self._stroke_number = 0
self._was_a_stroke = False
self._command_list = {
b'\xF0': self.draw_pixels,
b'\xF1': self.draw_line,
b'\xF2': self.draw_circle,
#
b'\xF5': self.replace_color,
b'\xF6': self.erase_pixel,
b'\xF7': self.clear_canvas,
#
b'\xFA': self.set_color,
b'\xFB': self.set_canvas_resolution,
}
self._movement_list = {
1: Position(-1, -1),
2: Position( 0, -1),
3: Position( 1, -1),
4: Position(-1, 0),
6: Position( 1, 0),
7: Position(-1, 1),
8: Position( 0, 1),
9: Position( 1, 1),
}
def execute(self):
command = self._file.read(1)
if command not in self._command_list:
raise IndexError(f"Command {command} is not supported")
ret = 1
return ret + self._command_list[command]()
def was_command_a_stroke(self):
return self._was_a_stroke
def _get_stroke_number(self):
data = self._file.read(2)
data = struct.unpack("BB", data)
self._stroke_number = (data[1] - 0x0A) + (data[0] - 0x0A) * 0x9F
def _get_pixel_list(self):
data = self._file.read(3)
data = struct.unpack("BBB", data)
byte_read = 3
current_pixel = Position(data[1] >> 2, data[2] >> 2)
ret = [ current_pixel ]
pixel_count = data[0]
for i in range(1, pixel_count, 2):
data = self._file.read(1)
data = struct.unpack("B", data)
byte_read = byte_read + 1
pixel_couple = data[0]
for p in range(2):
val = pixel_couple & 0x0F
if val in self._movement_list:
move = self._movement_list[val]
current_pixel = Position(current_pixel.x + move.x, current_pixel.y + move.y)
ret.append(current_pixel)
pixel_couple = pixel_couple >> 4
return byte_read, ret
def draw_pixels(self):
self._was_a_stroke = True
self._get_stroke_number()
byte_count, pixel_list = self._get_pixel_list()
for pixel in pixel_list:
self._canvas.set(pixel.x, pixel.y)
return byte_count + 2
def draw_line(self):
self._was_a_stroke = True
self._get_stroke_number()
data = self._file.read(5)
data = struct.unpack("5B", data)
# TODO: Need to look what method they use to draw lines to be accurate
print("Draw Line...")
# Always read 7 bytes (2 for stroke, 5 for arguments)
return 7
def draw_circle(self):
self._was_a_stroke = True
self._get_stroke_number()
data = self._file.read(5)
data = struct.unpack("5B", data)
# TODO: Need to look what method they use to draw circles to be accurate
print("Draw Circle...")
# Always read 7 bytes (2 for stroke, 5 for arguments)
return 7
def replace_color(self):
self._was_a_stroke = True
self._get_stroke_number()
data = self._file.read(2)
data = struct.unpack("BB", data)
self._canvas.replace_colour(data[1] - 28, data[0] - 28)
# Always read 4 bytes (2 for stroke, 2 for arguments)
return 4
def erase_pixel(self):
self._was_a_stroke = True
self._get_stroke_number()
byte_count, pixel_list = self._get_pixel_list()
for pixel in pixel_list:
self._canvas.erase(pixel.x, pixel.y)
return byte_count + 2
def clear_canvas(self):
self._was_a_stroke = True
self._get_stroke_number()
data = self._file.read(1)
data = struct.unpack("B", data)
# Not sure what to do with data here.
self._canvas.clear(data[0])
# Always read 3 bytes (2 for stroke, 1 for ?)
return 3
def set_color(self):
self._was_a_stroke = False
data = self._file.read(2)
data = struct.unpack("BB", data)
self._canvas.set_colour(data[0] - 28)
# Always read 2 bytes
return 2
def set_canvas_resolution(self):
self._was_a_stroke = False
data = self._file.read(2)
data = struct.unpack("BB", data)
self._canvas.set_resolution(data[0] - 28)
# Always read 2 bytes
return 2
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", "-i", required=True)
parser.add_argument("--output", "-o", default="output.gif")
parser.add_argument("--pixelsize", "-p", default=10, type=int)
args = parser.parse_args()
pixel_size = args.pixelsize
with open(args.input, "rb") as f:
header = f.read(32)
header_data = struct.unpack("<4cLLL12x4c", header)
canvas = Canvas()
cmd = CommandHandler(f, canvas)
frames = []
if header_data[0] == b"P" and \
header_data[1] == b"T" and \
header_data[2] == b"C" and \
header_data[3] == b"R" and \
header_data[7] == b"\xAA" and \
header_data[8] == b"\x55" and \
header_data[9] == b"\xAA" and \
header_data[10] == b"\x55":
version = header_data[4]
number_of_strokes = header_data[5]
data_size = header_data[6]
pos = 0
while pos < data_size:
pos = pos + cmd.execute()
if cmd.was_command_a_stroke():
img = canvas.render(args.pixelsize)
img.save("frame.png")
frames.append(img)
print(f"NoS: {number_of_strokes}, last read: {cmd._stroke_number}")
frames[0].save(args.output,
save_all=True,
append_images=frames[1:],
optimize=False,
duration=100,
loop=1)
else:
print(f"File {args.input} is not a PTCR file")
if __name__ == "__main__":
main()