Skip to content

Instantly share code, notes, and snippets.

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

@dogtopus dogtopus 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

@stecman stecman 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

@dogtopus dogtopus 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

@MackPI MackPI 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

@henrik-muehe henrik-muehe 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

@stecman stecman 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

@henrik-muehe henrik-muehe 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

@DavidMStraub DavidMStraub 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

@stecman stecman 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

@DavidMStraub DavidMStraub 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

@probonopd probonopd 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.

@alexose

This comment has been minimized.

Copy link

@alexose alexose commented Sep 29, 2019

Here's a quick example of how to generate a printable text label with ImageMagick:

convert -rotate 90 -pointsize 200 label:"Lorem Ipsum" -flop -bordercolor white -border 80x80 -resize 128x -monochrome label.png

@brzd

This comment has been minimized.

Copy link

@brzd brzd commented Jun 15, 2020

This is excellent! Are there any end user versions available by anyone yet? Application to print labels from file would be really appreciated.

Brzd

@stecman

This comment has been minimized.

Copy link
Owner Author

@stecman stecman commented Jun 16, 2020

@brzd do you mean a command-line program wrapping this up tidily or a GUI? Looks like there's a Qt GUI based on this already. Haven't seen a CLI version, but it'd be pretty straight forward to put together.

I don't print labels all that often, so haven't taken this beyond a script on top of this that uses imagemagick to make labels from text on stdin, similar to the comment above yours. If there's interest I can make this into a tidier Python CLI program though.

@puzrin

This comment has been minimized.

Copy link

@puzrin puzrin commented Dec 27, 2020

https://support.brother.com/g/b/manualtop.aspx?c=us&lang=en&prod=p710bteus - PT-P710BT has most close protocol.

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.

My one stucks on attempt to use > 40px of printing head. More narrow images are printed ok.

https://github.com/puzrin/lbprint - i created cli tool for Bother PT-P300BT & Dymo LabelManager PnP. Personally, Dymo is much better (stable) and cost the same.

@probonopd

This comment has been minimized.

Copy link

@probonopd probonopd commented Dec 27, 2020

@puzrin what annoys me about the Brother is how much tape is wasted before and after the printable area on each label. Is the Dymo better in this regard?

@puzrin

This comment has been minimized.

Copy link

@puzrin puzrin commented Dec 27, 2020

@probonopd Dymo also does some feed before print, but much less than Brother. With tapes from aliexpress price is not a big deal.

I can advise two things:

  1. Use D1 tapes for Dymo Rhino from ali - better choice of materials (vinyl, nylon, polyester, heat shrink tubes).
  2. Buy L-form connector short cable - more compact store.

Then dymo pnp "just works" and does exactly what you expect.

To be honest, i added P300BT driver only because did no wished to drop work at half of way. I would not recommend it for purchase. It's more big, heavy & with horrible fimware.

@probonopd

This comment has been minimized.

Copy link

@probonopd probonopd commented Dec 27, 2020

Are you saying dymo pnp can print Dymo Rhino shrink tubes? I'm sold...
Thank you for this great hint. Those materials (vinyl, nylon, polyester, heat shrink tubes) are awesome!

@puzrin

This comment has been minimized.

Copy link

@puzrin puzrin commented Dec 27, 2020

Are you saying dymo pnp can print Dymo Rhino shrink tubes? I'm sold...

YES! All 6-12mm D1 tapes from Dymo Rhino are ok with Dymo PNP. I purchased and tested all those - no problems. IMO different naming by printer is just a marketing.

@probonopd

This comment has been minimized.

Copy link

@probonopd probonopd commented Dec 27, 2020

Made my day. Ordering 2!

@ristomatti

This comment has been minimized.

Copy link

@ristomatti ristomatti commented Dec 27, 2020

Just to complement the discussion: the AliExpress heat shrink tube cartridges for Brother also work with the PT-P300BT. You just need to cover the identification holes in a certain manner. IIRC it works by just skipping the auto detection on the app and using a basic black on white tape setting.

@brzd

This comment has been minimized.

Copy link

@brzd brzd commented Dec 28, 2020

Already shortly discussed the issue @puzrin on an attempt to use his js app with PT300BT. Used the narrow tape and less pixels, but there seems to be no difference:

  • Could the error be related to communications with bt? Device seems to be receiving the data, but then turns on the blinking red led.

Any suggestions to getting the PT300BT to work? Or other solutions to be able to print from laptop on this fine device ;)

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.

My one stucks on attempt to use > 40px of printing head. More narrow images are printed ok.

@puzrin

This comment has been minimized.

Copy link

@puzrin puzrin commented Dec 28, 2020

@brzd Try to manually setup connection before run:

# replace MAC with your one
sudo rfcomm connect 0 A8:B2:DA:7B:6E:6D 1
@brzd

This comment has been minimized.

Copy link

@brzd brzd commented Dec 28, 2020

Connection seems otherwise ok, but something seems to go awry at the end of the transmission.

@puzrin

This comment has been minimized.

Copy link

@puzrin puzrin commented Dec 28, 2020

Please describe in issue how to reproduce

@brzd

This comment has been minimized.

Copy link

@brzd brzd commented Dec 28, 2020

If there's interest I can make this into a tidier Python CLI program though.

@stecman Haven't really found any easy to use solutions yet. Any plans for the gui for this device? Would be most appreciated!

@brzd

This comment has been minimized.

Copy link

@brzd brzd commented Dec 28, 2020

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.

@henrik-muehe Did you ever package the generator with this code? It would be useful to be able to print a few tens of different labels more easily. As I have the PT300BT would really love to utilize it as well.

I am not able to get this working for me. puzrin's code is also good and got it working for single prints but not able to use it for bulk printing.

@probonopd

This comment has been minimized.

Copy link

@probonopd probonopd commented Dec 28, 2020

@brzd what kind of GUI do you need? Would something like this work for you?

I am working to combine my GUI with the code here.

@puzrin

This comment has been minimized.

Copy link

@puzrin puzrin commented Dec 28, 2020

@probonopd can't say anything concrete about p-700 without examples. What exactly is "not quite right"?

To be honest, i have more high priority projects. I can share all i know, if you have concrete questions, but i'd like to avoid active participation.

@probonopd

This comment has been minimized.

Copy link

@probonopd probonopd commented Dec 28, 2020

@puzrin I have something working here... What license is your code under? Would MIT or BSD be OK? I want to upload it to a "real" repo.

@brzd

This comment has been minimized.

Copy link

@brzd brzd commented Dec 28, 2020

@probonopd That would be great to start with. I was thinking possibility to use cli for bulk printing from i.e. csv If there would be gui to have this possibility then even better. But to start with very simple solution to key in or copy would be fine.

. @brzd what kind of GUI do you need? Would something like this work for you?

@puzrin

This comment has been minimized.

Copy link

@puzrin puzrin commented Dec 28, 2020

@probonopd https://github.com/puzrin/lbprint/blob/master/LICENSE

MIT, + something for embedded fonts. If you copy without fonts => pure MIT.

@probonopd

This comment has been minimized.

Copy link

@probonopd probonopd commented Dec 28, 2020

I realize only now that https://github.com/puzrin/lbprint/ exists. I have used the snippets in this thread so far...

https://github.com/probonopd/ptouch-770/tree/patch-1/pure-python is a version of the GUI that works for me - still very rough around the edges (still Python 2, no proper error handling etc.) but it may be a start for anyone who is interested.

Trying to convert it to Python 3, but stuck with TypeError: 'bytes' object cannot be interpreted as an integer here:

            if bit_cursor == 0:
                buf.append(unsigned_char.pack(byte))
                # TypeError: 'bytes' object cannot be interpreted as an integer
                byte = 0
                bit_cursor = 8
@puzrin

This comment has been minimized.

Copy link

@puzrin puzrin commented Dec 29, 2020

UPD. Issues with image size caused by old batteries. After replace print max height as expected.

The only issue left - P300BT falls into error state several seconds after printing.

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