Skip to content

Instantly share code, notes, and snippets.

@stecman
Last active October 6, 2024 17:31
Show Gist options
  • Save stecman/ee1fd9a8b1b6f0fdd170ee87ba2ddafd to your computer and use it in GitHub Desktop.
Save stecman/ee1fd9a8b1b6f0fdd170ee87ba2ddafd to your computer and use it in GitHub Desktop.
Brother P-Touch PT-P300BT bluetooth driver python

Controlling the Brother P-Touch Cube label maker from a computer

The Brother PTP300BT label maker is intended to be controlled using the official Brother P-Touch Design & Print iOS/Android app. The app has arbitrary limits on what you can print (1 text object and up to 3 preset icons), so I thought it would be a fun challenge to reverse engineer the protocol to print whatever I wanted.

Python code at the bottom if you want to skip the fine details.

Process

Intitially I had a quick peek at the Android APK to see if there was any useful information inside. The code that handles the communication with the printer in Print&Design turned out to be a native library, but the app clearly prepares a bitmap image and passes it to this native library for printing. Bitmaps are definitely something we can work with.

Next I used the bluetooth sniffing capability of stock Android to capture a few label prints from the official app. Inspecting these packet captures in Wireshark, it was apparent that all of the communication used bluetooth's serial port profile (SPP). Interestingly, the the printer shows up as "Fujitsu" in packet captures, and since Brother has a lot of label maker products I figured there was a good chance they were using some existing label maker hardware and firmware with a bluetooth to serial adapter bolted on.

After a little Googling, this hunch paid off - a bunch of developer documentation for some of Brother's higher-end/business label maker products matched up with the bytes being sent over the bluetooth serial connection. Mainly:

At first I found similarities in a manual for Brother's ESC/P protocol, which has the same command format, initialisation command and 32 byte status format, but the P-Touch Cube doesn't appear to support this (based on trying the ESC/P commands on the device).

Serial protocol

From Brother's developer docs for a different device, the packet captures could be broken down as:

// 64 bytes of 0x0 (to clear print buffer?)
...

// Initialize/reset settings registers
1B 40

// Enter raster mode (aka. PTCBP)
1B 69 61 01

// Set media and quality (most importantly, the number of 128px lines to expect)
// Found docs for this last by searching for the first three bytes (command)
// See http://www.undocprint.org/formats/page_description_languages/brother_p-touch
1B 69 7A C4 01 0C 00 08 01 00 00 00 00

// Set expanded mode bits (print chaining: off)
1B 69 4B 08

// Set mode bits (mirror print: no, auto tape cut: no)
1B 69 4D 00

// Set margin (feed) size
1B 69 64 1C 00

// Set compression mode: TIFF (packbits)
4D 02

// Transfer n1 + n2*256 bytes of raster data
// The official app transfers one line of data at a time (16 bytes = 128 pixels)
47 n1 n2 [data]
...

// Print and feed
1A

Image data

Image data is sent to the printer as a 1-bit-per-pixel bitmap. The Brother app sends a 128 pixel wide image (regardless of tape width), oriented as lines across the print head. For a horizontal label (printing along the length of tape), the input image needs to be rotated by 90 degrees before sending.

Once in the correct orientation, image data needs to be mirrored horizontally (with the settings above at least). It looks like the command 1B 69 4D can be used to enable mirroring by the printer, but I haven't tested this.

The outer edges of a 12mm label do not appear to be printable (print head too narrow?). The outer 30 pixels of each side (length-wise) are not printed. I haven't tested with narrower labels.

Python code

The code here is what I had at the point I got this working - it's a bit hacked together. It prints images, but the status messages printed aren't complete and the main script needs some tidying up. The printer sometimes goes to an error state after printing (haven't figured out why yet), which can be cleared by pressing the power button once.

This needs a few modules installed to run:

pyserial
pypng
packbits

Then it can be used as:

# Existing image formated to spec above
./labelmaker.py monochrome-128px-wide-image.png

# Using imagemagick to get a usable input image from any horizontal oriented image
# -resize 128x can be used instead of -crop 128x as needed
# -rotate 90 can be removed if the image is portrait already
convert inputimage.png -monochrome -gravity center -crop 128x -rotate 90 -flop out.png

I was working on Linux, so the serial device is currently hard-coded as /dev/rfcomm0. On OSX, a /dev/tty.* device will show up once the printer is paired.

To pair the printer with my Linux machine, I used:

# Pair device
$ bluetoothctl
> scan on
... (turn printer on and wait for it to show up: PT-P300BT8894)
> pair [address]

# Setup serial port
$ sudo modprobe rfcomm
$ sudo rfcomm bind rfcomm0 [address of printer]
#!/usr/bin/env python
from labelmaker_encode import encode_raster_transfer, read_png, unsigned_char
import binascii
import packbits
import serial
import sys
import time
STATUS_OFFSET_BATTERY = 6
STATUS_OFFSET_EXTENDED_ERROR = 7
STATUS_OFFSET_ERROR_INFO_1 = 8
STATUS_OFFSET_ERROR_INFO_2 = 9
STATUS_OFFSET_STATUS_TYPE = 18
STATUS_OFFSET_PHASE_TYPE = 19
STATUS_OFFSET_NOTIFICATION = 22
STATUS_TYPE = [
"Reply to status request",
"Printing completed",
"Error occured",
"IF mode finished",
"Power off",
"Notification",
"Phase change",
]
STATUS_BATTERY = [
"Full",
"Half",
"Low",
"Change batteries",
"AC adapter in use"
]
def print_status(raw):
if len(raw) != 32:
print "Error: status must be 32 bytes. Got " + len(raw)
return
if raw[STATUS_OFFSET_STATUS_TYPE] < len(STATUS_TYPE):
print "Status: " + STATUS_TYPE[raw[STATUS_OFFSET_STATUS_TYPE]]
else:
print "Status: 0x" + binascii.hexlify(raw[STATUS_OFFSET_STATUS_TYPE])
if raw[STATUS_OFFSET_BATTERY] < len(STATUS_BATTERY):
print "Battery: " + STATUS_BATTERY[raw[STATUS_OFFSET_BATTERY]]
else:
print "Battery: 0x" + binascii.hexlify(raw[STATUS_OFFSET_BATTERY])
print "Error info 1: 0x" + binascii.hexlify(raw[STATUS_OFFSET_ERROR_INFO_1])
print "Error info 2: 0x" + binascii.hexlify(raw[STATUS_OFFSET_ERROR_INFO_2])
print "Extended error: 0x" + binascii.hexlify(raw[STATUS_OFFSET_EXTENDED_ERROR])
print
# Check for input image
if len(sys.argv) < 2:
print "Usage: %s <path-to-image>" % sys.argv[0]
sys.exit(1)
# Get serial device
ser = serial.Serial(
'/dev/rfcomm0',
baudrate=9600,
stopbits=serial.STOPBITS_ONE,
parity=serial.PARITY_NONE,
bytesize=8,
dsrdtr=True
)
print(ser.name)
# Read input image into memory
data = read_png(sys.argv[1])
# Enter raster graphics (PTCBP) mode
ser.write(b"\x1b\x69\x61\x01")
# Initialize
ser.write(b"\x1b\x40")
# Dump status
ser.write(b"\x1b\x69\x53")
print_status( ser.read(size=32) )
# Flush print buffer
for i in range(64):
ser.write(b"\x00")
# Initialize
ser.write(b"\x1b\x40")
# Enter raster graphics (PTCBP) mode
ser.write(b"\x1b\x69\x61\x01")
# Found docs on http://www.undocprint.org/formats/page_description_languages/brother_p-touch
ser.write(b"\x1B\x69\x7A") # Set media & quality
ser.write(b"\xC4\x01") # print quality, continuous roll
ser.write(b"\x0C") # Tape width in mm
ser.write(b"\x00") # Label height in mm (0 for continuous roll)
# Number of raster lines in image data
raster_lines = len(data) / 16
print raster_lines, raster_lines % 256, int(raster_lines / 256)
ser.write( unsigned_char.pack( raster_lines % 256 ) )
ser.write( unsigned_char.pack( int(raster_lines / 256) ) )
# Unused data bytes in the "set media and quality" command
ser.write(b"\x00\x00\x00\x00")
# Set print chaining off (0x8) or on (0x0)
ser.write(b"\x1B\x69\x4B\x08")
# Set no mirror, no auto tape cut
ser.write(b"\x1B\x69\x4D\x00")
# Set margin amount (feed amount)
ser.write(b"\x1B\x69\x64\x00\x00")
# Set compression mode: TIFF
ser.write(b"\x4D\x02")
# Send image data
print("Sending image data")
ser.write( encode_raster_transfer(data) )
print "Done"
# Print and feed
ser.write(b"\x1A")
# Dump status that the printer returns
print_status( ser.read(size=32) )
# Initialize
ser.write(b"\x1b\x40")
ser.close()
import packbits
import png
import struct
# "Raster graphics transfer" serial command
TRANSFER_COMMAND = b"\x47"
unsigned_char = struct.Struct('B');
def as_unsigned_char(byte):
""" Interpret a byte as an unsigned int """
return unsigned_char.unpack(byte)[0]
def encode_raster_transfer(data):
""" Encode 1 bit per pixel image data for transfer over serial to the printer """
buf = bytearray()
# Send in chunks of 1 line (128px @ 1bpp = 16 bytes)
# This mirrors the official app from Brother. Other values haven't been tested.
chunk_size = 16
for i in xrange(0, len(data), chunk_size):
chunk = data[i : i + chunk_size]
# Encode as tiff
packed_chunk = packbits.encode(chunk)
# Write header
buf.append(TRANSFER_COMMAND)
# Write number of bytes to transfer (n1 + n2*256)
length = len(packed_chunk)
buf.append(unsigned_char.pack( int(length % 256) ))
buf.append(unsigned_char.pack( int(length / 256) ))
# Write data
buf.extend(packed_chunk)
return buf
def decode_raster_transfer(data):
""" Read data encoded as T encoded as TIFF with transfer headers """
buf = bytearray()
i = 0
while i < len(data):
if data[i] == TRANSFER_COMMAND:
# Decode number of bytes to transfer
n1 = as_unsigned_char(data[i+1])
n2 = as_unsigned_char(data[i+2])
num_bytes = n1 + n2*256
# Copy contents of transfer to output buffer
transferedData = data[i + 3 : i + 3 + num_bytes]
buf.extend(transferedData)
# Confirm
if len(transferedData) != num_bytes:
raise Exception("Failed to read %d bytes at index %s: end of input data reached." % (num_bytes, i))
# Shift to the next position after these command and data bytes
i = i + 3 + num_bytes
else:
raise Exception("Unexpected byte %s" % data[i])
return buf
def read_png(path):
""" Read a (monochrome) PNG image and convert to 1bpp raw data
This should work with any 8 bit PNG. To ensure compatibility, the image can
be processed with Imagemagick first using the -monochrome flag.
"""
buf = bytearray()
# State for bit packing
bit_cursor = 8
byte = 0
# Read the PNG image
reader = png.Reader(filename=path)
width, height, rows, metadata = reader.asRGB()
# Loop over image and pack into 1bpp buffer
for row in rows:
for pixel in xrange(0, len(row), 3):
bit_cursor -= 1
if row[pixel] == 0:
byte |= (1 << bit_cursor)
if bit_cursor == 0:
buf.append(unsigned_char.pack(byte))
byte = 0
bit_cursor = 8
return buf
@probonopd
Copy link

Thank you very much @vsigler, great to have a python3 version. :+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment