Skip to content

Instantly share code, notes, and snippets.

@stecman stecman/_readme.md
Last active Aug 20, 2019

Embed
What would you like to do?
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
@dogtopus

This comment has been minimized.

Copy link

commented May 20, 2018

Great job!
One thing that are probably worth to point out: the line width (16 bytes, 128px) actually has very little to do with the resolution of the print, but more or less a protocol thing (like, e.g. MTU) The printer actually has resolution of 180dpi instead of 128px/12mm, ~271dpi, also it does not really make sense when you think about different tape width. So modelling a printing job as a 128x<height>px image is not really ideal. (I found this out when I was playing with the CUPS driver for ptouch series printers and trying to add support for Cube)

@stecman

This comment has been minimized.

Copy link
Owner Author

commented May 20, 2018

@dogtopus thanks, that's useful to know! When sending completely filled 128px lines, only a 9mm image is actually printed on the 12mm tape though. If you account for the ~32 pixels from each side that it doesn't print, that almost exactly gives a resolution of 180dpi:

(128px - (32px * 2)) / 9mm = 180.62 dpi

So that sounds right that the 16 bytes (128px) is just a standard part of the protocol, and the device only uses what it actually needs from that.

@dogtopus

This comment has been minimized.

Copy link

commented Jun 15, 2018

Some update: While working on a plugin for inkscape that generates TZe labels, I need to know the exact position of the boundaries that each tape size have. I then found programming references for TZe label printers (PT-P700 and PT-P750W) which contains the dimensions for the tape (although it turns out not helpful because the printing area seems arbitrary and does not match what P300BT produces. Maybe it is unrelated to the tape, but a hardware limitation?) and also more documentation specific to TZe label printers (e.g. how to decode tape type, dimension, etc. But there's no documentation on battery indication (shows as reserved) although looks like P700 can also be powered by batteries)

@MackPI

This comment has been minimized.

Copy link

commented Nov 20, 2018

Thank You!
I need to print from my own custom Android app. This helped me a lot. The one point that I missed is the number of raster lines in the Set Media command. The printer will give a communication error if the number of lines sent and this number do not match.
In he command sequence 1B 69 7A C4 01 0C 00 08 01 00 00 00 00:
The 0C must match the tape size installed in the printer 0C = 12 mm (1/2 inch) 06 = 6 mm (1/4 inch)
The 08 01 is the number of raster lines 1*256 + 8 LSB first.
If your software needs to know the width of the tape installed it can be extracted from the get printer status command 1B 69 53. The response is 32 bytes long. The eleventh byte is the tape width in mm.

@henrik-muehe

This comment has been minimized.

Copy link

commented Jan 25, 2019

This is really cool, thanks! Mind putting it under a LICENSE so I can reuse it instead of writing my own? :-)

@stecman

This comment has been minimized.

Copy link
Owner Author

commented Jan 25, 2019

@henrik-muehe it's a proof of concept and is Creative Commons Zero (CC0) as far as I'm concerned. Use it in any way you like!

@henrik-muehe

This comment has been minimized.

Copy link

commented Jan 25, 2019

Thanks, I hacked a small image generator around it in python and might just upload the combo somewhere, it's pretty helpful when you want to print 100 labels or so.

@DavidMStraub

This comment has been minimized.

Copy link

commented May 19, 2019

Thanks for sharing this! Unfortunately I get

Traceback (most recent call last):
  File "labelmaker.py", line 76, in <module>
    data = read_png(sys.argv[1])
  File "/home/straub/Dokumente/Code/labels/labelmaker_encode.py", line 95, in read_png
    buf.append(unsigned_char.pack(byte))
TypeError: an integer is required

Any idea what I am doing wrong?

@stecman

This comment has been minimized.

Copy link
Owner Author

commented May 21, 2019

What version of Python are you running, @DavidMStraub? I can only get a similar message (though not the same) under Python 3 as the bytesarray built-in has changed a bit.

It looks like the manual conversion to bytes isn't necessary in either version, and removing this fixes the error for me. Can you give this a try?

Line 95 in labelmaker_encode.py
- buf.append(unsigned_char.pack(byte))
+ buf.append(byte)
@DavidMStraub

This comment has been minimized.

Copy link

commented May 25, 2019

Thanks for the hint! Indeed I run Python 3. Your suggested change solves this error, but then there are others and I wasn't quite able to solve them. Thanks anyway!

@probonopd

This comment has been minimized.

Copy link

commented Jun 9, 2019

Maybe you are interested in https://github.com/probonopd/ptouch-770, a GUI tool written in Python for the Brother P-touch P700 label printer for Linux.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.