Android development: a Bluetooth label/receipt printing demo

BtPrint4

A Bluetooth Label printer demo

This is my first Android project and I had to learn how to get an Android activity working. I am a Windows Mobile C# developer and had to learn that you have to do many handwritten code in compare to what Visual Studio for SmartDevice development does automatically.

An Android activity consists of many, many files with references to each other. Fortunately an IDE helps you creating and finding all the files and references.

I started with Eclipse and ADT but went over to use Android Studio. Eclipse showed very strange behaving when designing the GUI of my activities.

functions

To print to a bluetooth printer we need several functions:

  • A reference to the printer, the BT MAC address
  • A socket to communicate with the printer
  • A set of demo data

Make the activity more user friendly

  • provide a list of Bluetooth devices nearby to be used as target
  • move the communication part to a background thread
  • a list with demo data

implementation

The UI has an EditText holding the BT MAC address and a TextView to hold the demo data reference. Buttons for BT discovery, Connect/Disconnect, Demo select and a Print button. There are two list activities (separate windows or forms): the BT device list and a demo data list. The BT MAC address is filled either manually or by selecting a BT device from the list.

btprint4_main

I always try to keep code re-useable and so I implemented some helper classes.

demo data

As BtPrint4 should provide a flexible set of demo data, this data is hold in separate files. I can add new demo data files by simply putting them in the assets dir of the project. To show the user better details about a demo file, I use a xml file with the description of each file.
The supported label and receipt printers use different print languages to print: ESC/P, IPL (Intermec Printer Language), ZSIM (a Zebra Simulation), CSIM, FP (Intermec Fingerprint) and others.
Further the data send to the label/receipt printer should match the width of the print media. There are printers with 2, 3, 4 and 5 inch media.

To manage all these different supported print languages and media sizes the file names I use reflect the target printer/media. The xml file with the descriptions provides more details about the demo files for the user.

getting started

Fortunately I did not have to start everything from scratch and found the Andorid SDK sample project BluetoothChat. It already comes with a BT device list and a background thread handling the communication. So, many thanks to Google to provide this sample.

thread and GUI communication

The background thread has to communicate to the main activity to announce state changes. And I need a function to write the demo data to the Bluetooth socket inside the thread. This was already implemented in the BluetoothChat thread code.
Within Compact Framework I can have several threads and have different handlers (delegates and events) for each thread.

On Android you only have one BroadcastReceiver that will handle all communication with different threads. Similar to WndProc (the main window message handler) on a windows system. AFAIK on Android you can also use Callbacks, but this will couple your thread code with the GUI. But message handling is asynchronous and so de-coupled. It may be a challenge to handle many different background threads within only one message handler. You have to provide identifiers for all possible sources of messages from background threads and the handler function in your main activity gets longer and longer. Not that nice.

states

The background thread sends its state to the main activity. So we know if has a connection to the BT device (printer). The thread will also send received data to the activity. To provide the received data a so called bundle is used. The states are using a bundle (al list of keys and values) and a simple arg of the message to provide state changes to the main code.

The main activity message handler:

    ...
    // The Handler that gets information back from the btPrintService
    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case msgTypes.MESSAGE_STATE_CHANGE:
                    Bundle bundle = msg.getData();
                    int status = bundle.getInt("state");
                    if (D)
                        Log.i(TAG, "handleMessage: MESSAGE_STATE_CHANGE: " + msg.arg1);  //arg1 was not used! by btPrintFile
                    setConnectState(msg.arg1);
                    switch (msg.arg1) {
                        case btPrintFile.STATE_CONNECTED:
                            addLog("connected to: " + mConnectedDeviceName);
                            mConversationArrayAdapter.clear();
                            Log.i(TAG, "handleMessage: STATE_CONNECTED: " + mConnectedDeviceName);
                            break;
                            ...

The thread sending state changes in a message:

    ...
        void addText(String msgType, int state){
            // Give the new state to the Handler so the UI Activity can update
            msgTypes type;
            Message msg;
            Bundle bundle = new Bundle();
            if(msgType.equals(msgTypes.STATE)){
                msg = mHandler.obtainMessage(msgTypes.MESSAGE_STATE_CHANGE);// mHandler.obtainMessage(_Activity.MESSAGE_DEVICE_NAME);
            }
            else if(msgType.equals(msgTypes.DEVICE_NAME)){
                msg = mHandler.obtainMessage(msgTypes.MESSAGE_DEVICE_NAME);
            }
            ...
            bundle.putInt(msgType, state);
            msg.setData(bundle);
            msg.arg1=state;             //we can use arg1 or the bundle to provide additional information to the message handler
            mHandler.sendMessage(msg);
            Log.i(TAG, "addText: "+msgType+", state="+state);
        }
        ...

And the thread sending received data as a message:

            // Keep listening to the InputStream while connected
            while (true) {
                try {
                    // Read from the InputStream
                    bytes = mmInStream.read(buffer);

                    // Send the obtained bytes to the UI Activity
                    mHandler.obtainMessage(msgTypes.MESSAGE_READ, bytes, -1, buffer).sendToTarget();
                } catch (IOException e) {
                    Log.e(TAG, "disconnected", e);
                    connectionLost();
                    break;
                }
            }

select a Bluetooth device

You can enter a known BT MAC address directly of let the device show y ou a list of paired devices and scan for new devices. The BT device list activity is a separate activity and has to be added to the app Manifest. Such child activities communicate the selection back to the main activity using an onActivityResult handler funcion. Again, all child activities use the same handler function callback and so your list of switch/case codes increases. On windows (C#, .NET) you have one handler for one form or dialog.

In the original BluetoothChat code the Scan button was only visible on startup of the list activity and was hidden after a first press. I extended this to show the scan button all the time, so you can initiate another scan for devices without closing and opening the list again. During the scan (discovery) the button is changed to a cancel button which then stops the actula discovery.

connect to device

The connection is done async in a separate thread. We need a socket Bluetooth SPP connection:

        public ConnectThread(BluetoothDevice device) {
            mmDevice = device;
            BluetoothSocket tmp = null;

            // Get a BluetoothSocket for a connection with the
            // given BluetoothDevice
            try {
                addText("createInsecureRfcommSocketToServiceRecord");
                tmp = device.createInsecureRfcommSocketToServiceRecord(UUID_SPP);
            } catch (IOException e) {
                Log.e(TAG, "create() failed", e);
            }
            mmSocket = tmp;
        }

To identity a Bluetooth device we use the BT MAC address only.

select a demo file

The demo files are presented by another child activity. It lists the files ending in ‘.prn’ of the assets dir. When you select a file, the content of the printdemo files xml is shown. This xml is read and fills creates a set of classes with the data of the xml for each demo file. The parsing and demo file classes are spread over three class files:

  • PrintFileDetails.java
  • PrintFileXML.java
  • PrintLanguage.java

The details are the printer language, the media width, a short and a long description and the file name:

    public String shortname;
    public String description;
    public String help;
    public String filename;
    public PrintLanguage.ePrintLanguages printLanguage;
    public Integer printerWidth=2;

The demo files have speaking names but these give not all details and so the xml is used to describe the files further. Example file name: escp4prodlist.prn (print language=ESP/P, media width=4 inch, prints a product list?) and the xml for that file is:

	<fileentry>
		<shortname>PrintESCP4plistBT</shortname>
		<description>Intermec (BT,ESCP,4inch) Product List Print</description>
		<help>Print 4inch product list to an Intermec printer in ESCP</help>
		<filename>escp4prodlist.prn</filename>
	</fileentry>

printing is sending binary data

The demo data is within asset files. To write it to the printer (a Bluetooth socket) we need to obtain the bytes fo the file. We need to ensure the data is not changed and the data is send as is.

    void printFile() {
        String fileName = mTxtFilename.getText().toString(); //[1]
        if (!fileName.endsWith("prn")) {
            myToast("Not a prn file!", "Error");
            return; //does not match file pattern for a print file
        }
        if (btPrintService.getState() != btPrintFile.STATE_CONNECTED) { //[2]
            myToast("Please connect first!", "Error");
            //PROBLEM: this Toast does not work!
            //Toast.makeText(this, "please connect first",Toast.LENGTH_LONG);
            return; //does not match file pattern for a print file
        }
        //do a query if escp
        if (fileName.startsWith("escp")) { //[3]
            byte[] bufQuery = escpQuery();
            btPrintService.write(bufQuery);
        }
        if (mTxtFilename.length() > 0) {
            //TODO: add code
            InputStream inputStream = null;
            ByteArrayInputStream byteArrayInputStream; //[4]
            Integer totalWrite = 0;
            StringBuffer sb = new StringBuffer();
            try {
                inputStream = this.getAssets().open(fileName);  //[5]

                byte[] buf = new byte[2048];
                int readCount = 0;
                do {
                    readCount = inputStream.read(buf);
                    if (readCount > 0) {
                        totalWrite += readCount;
                        byte[] bufOut = new byte[readCount];
                        System.arraycopy(buf, 0, bufOut, 0, readCount);
                        btPrintService.write(bufOut);
                    }
                } while (readCount > 0); //[6]
                inputStream.close();
                addLog(String.format("printed " + totalWrite.toString() + " bytes"));
            } catch (IOException e) {
                Log.e(TAG, "Exception in printFile: " + e.getMessage());
                addLog("printing failed!");
                //Toast.makeText(this, "printing failed!", Toast.LENGTH_LONG);
                myToast("Printing failed","Error");
            }
        } else {
            addLog("no demo file");
            //Toast.makeText(this, "no demo file", Toast.LENGTH_LONG);
            myToast("No demo file selected!","Error");
        }
    }

Notes for the above code:

[1] mTxtFilename holds the name of the file the user has selected
[2] ensure we are connected
[3] we can also query the printer status asyncronously
[4] read the data as is means read it as ByteArrayInputStream
If the binary data is read as InputStream it is converted to unicode. But it has to remain as is and found I had to read it as ByteArrayInputStream to get unconverted data.
[5] open the stream using an asset file
[6] read and write the data in chunks

annoyances

Here is a short list of what drived me crazy and took a long trial and error phase.

  • EditText covered by soft keyboard if main layout is not a SrollView
  • TextView not directly updated from inside message handler
  • TextView not scrolling if inside XYZ layout
  • updating GUI from message handler works for some code and for other not

If you need an EditText to move automatically into view when the soft keyboard comes up, you need to wrap all your layout info a ScrollView.

First I did updates to the log TextView by TextView.append() inside the message handler. But the text was not updated all the time. May be if before or after a Toast, but I changed the code to call a function to update the log TextView.

Although a TextView shows scroll bars automatically, if in the right order of the layout and having a fixed height, scrolling stops working in some combinations and I finally added some hard code scrolling.

resume

Although Android offers a great programming environment, some behaviour is strange and does not behave like documented. Solving these problems took me 70% of the coding. There was a lot of try-and-check before BtPrint4 behaved like desired. For example TextView scrolling, automatically and manually is a place of problems and trial and error, just search the internet about this and you will find many different ‘solutions’ that may work for you or not. It totally depends on your layout and the order of containers and which attributes they have.

Forgive my coding style I am not a Java specialist.
Full code at github

APK download by https://github.com/hjgode/BtPrint4/blob/master/app/btPrint4.apk?raw=true

16 Comments

  1. […] app is based on my btPrint4 app. In contrast to btPrint4 this time we print on TCP/IP connected […]

  2. Zack says:

    Hi, I am a bit of a newbie to all this, but I’d really like to test out your bluetooth app on my android device. Is there anywhere I can download the APK for a direct install? Would really appreciate it! Thanks!

  3. Evgeny says:

    Hello, Author.
    Can You do solution for printing page on PB22 printer from Chrome from Android via BT?..

    Thank You.

  4. josef says:

    @EVENGY

    NO, Think about your question and then you know why this is not possible, at least not with 1-2 man year work.

    Android does not have a printing context and PB22 is not designed to print chrome content.

    The easiest way to print from chrome to a printer is to let a host load the chrome content (a web page) and let the host do the printing. The host can be any OS supporting printing by default, like Windows, Linux, Mac OS X. To print the web page you must render it and translate the ‘drawing’ into a language the printer understands.

    ~josef

  5. Evgeny says:

    Ok. Thank You.
    Josef, I found in Chrome last version print menu. And ready drivers for HP and Samsung printers.
    Not necessary to use Chrome, need print info from web page…

  6. MAx says:

    Thanks for sample, it help me, i apreciate it, regards.

  7. Paulo Moura says:

    Hi Josef,

    I’m having some difficulty on converting an image(jpeg/png/bmp) to GRF format to print using Zsim Simulator(Zebra Printing Language). The main problem is that I can’t convert programmatically using Java.
    Do you know how to convert programmatically an image to GRF using Java?

    Thank you.

  8. josef says:

    Hello Paulo

    as looking at other code that does image conversion to grf (see https://github.com/asharif/img2grf/blob/master/src/main/java/org/orphanware/App.java) I assume it is a simple black&white bit stream.
    Unfortunately doing a btPrint4 with GRF support is beyond my intention. But you will find lots of other posts in internet about “Zebra GRF”.

    sorry

    josef

  9. claudior2184 says:

    Hi!! i have a problem with an app i had to migrate from c++ (handheld) to java – android (smartphone), this app prints a lot and they are using intermec pr2 printers, so we migrated most of the application just fine, but recently they started to have a problem on most recent android versions, say 4.4.2 and above (on 4.3 it was fine) it happens that when we do a print everything is fine, but then when we print something else, it prints some part of the previos print, as if that string was held in the buffer, but we are closing all the communication with the printer after it finishes every print job, do you have any idea what could be the problem?

  10. josef says:

    Hello claudior2184

    the issue may be related to not ending the label code (printer command language) correctly.

    Ensure you end the label (doc) correctly and reset the printer when starting a new label.

    The print commands to use for end doc/reset depend on the printer command language. For example, AFAIR, the reset for ESC/P is [ESC]@.

    You should open a support ticket on the honeywell site.

    ~josef

  11. sferricha says:

    hello

    i need to use this in my application in the moment of print order,can you tell me how i can do it.

    thanks in advance.

    Regards

  12. josef says:

    Hello sferricha

    you need to build the printer command strings, convert that to a byte buffer and send the data via the BT socket.

    ~josef

  13. sferricha says:

    Hello Josef

    thank you for your response

    i builded my string,i used your code to print with intermec PR3 (its work fine) but now i need to create a class (Bluetooth manager),this class open connection with device Bluetooth on start app,with a public method print text to print any time from any activity and close Bluetooth connection on app close.

    its possible or not ?

    thanks in advance.

    Regards

  14. Rodnei says:

    Does you software controls the feed of labels, like put the label in the right position to be printed?

  15. josef says:

    Hello Rodnei

    I am sorry, but this application does simply only send print data in the printer’s language to the bluetooth connaction.
    I assume, you are talking about a special printer. If that supports positioning of labels, you can control this also from code. OTOS label printers normally position the label for example to enable rip-off, then drawing back the label stock before printing the next label.

    Hope that helped

    Josef

Leave a Reply