android: ipPrint4 print label/receipts to ip printer

ipPrint4

An android label/receipt printing app for TCP/IP connected printers

This app is based on my btPrint4 app. In contrast to btPrint4 this time we print on TCP/IP connected printers to port 9100.

As with btPrint4 we have a main activity and one to list available printers and one to list available demo files.

ipprint4_main pb31

The challenge with ipPrint4 was a replacement for the bluetooth device discovery. This time we have to scan TCP/IP address range for port 9100. This port is also called HP direct printing port and supported by many printers. It behaves similar to telnet and you can just send print commands to the interface.

The second main change to btPrint4 was the printing code. This time we do not have to use a bluetooth socket but a network TCP/IP socket.

A TCP/IP portscanner

If you scan a range of IP addresses in sequence and try to open a port with a timeout of, let’s say 200ms, the scan will take (200msx254, scan from 1 to 254) 50 seconds.

Port scan code

    ...
    ScanResult portIsOpen1(String sIp, int p, int timeout){
        ScanResult scanResult=new ScanResult(sIp, p, false);
        try {
            Socket socket = new Socket();
            socket.connect(new InetSocketAddress(sIp, p), timeout);
            socket.close();
            scanResult = new ScanResult(sIp, p, true);// true;
        } catch (Exception ex) {
            doLog("Exception in portIsOpen1 for " + sIp + "/" + p);
            //return new ScanResult(ip, port, false);// false;
        }
        return scanResult;
    }
    ...
    //scanning 250 addresses with 200ms each will take 50 seconds if done one by one!
    for (int ip1=1; ip1<=254; ip1++){
        String sip=String.format(baseIP + ".%03d", ip1);
        scanResult = portIsOpen1(sip, port, timeout);
        if(scanResult.isOpen){
        Log.i(TAG, scanResult.sIP + " 9100 open");
        // Send the name of the connected device back to the UI Activity
        msg = mHandler.obtainMessage(msgTypes.addHost);
        bundle = new Bundle();
        bundle.putString(msgTypes.HOST_NAME, scanResult.sIP);
        msg.setData(bundle);
        mHandler.sendMessage(msg);
        doLog("added host msg for " + scanResult.sIP);
    }
    else{
        Log.i(TAG, scanResult.sIP + " 9100 unavailable");
    }
    if(backgroundThread.interrupted())
        break;
    }
    ...

That long time seems too much for me to let the user wait. Fortunately there is a post at stackoverflow showing how to use an executer service to run multiple port scans in parallel.

Start multiple port scans at once

    ...
    static Future<ScanResult> portIsOpen(final ExecutorService es, final String ip, final int port, final int timeout) {
        return es.submit(new Callable<ScanResult>() {
            @Override public ScanResult call() {
                try {
                    Socket socket = new Socket();
                    socket.connect(new InetSocketAddress(ip, port), timeout);
                    socket.close();
                    return new ScanResult(ip, port, true);// true;
                } catch (Exception ex) {
                    return new ScanResult(ip, port, false);// false;
                }
            }
        });
    }
    ...
    synchronized void startDiscovery1(){
        ...
        doLog("startDiscovery1: starting futures...");
        for (int ip1=1; ip1<=254; ip1++){
            String sip=String.format(baseIP + ".%03d", ip1);
            futures.add(portIsOpen(es, sip, port, timeout));
        }
        doLog("startDiscovery1: es.shutdown()");
        es.shutdown();
        int openPorts = 0;
        doLog("startDiscovery1 getting results...");
        for (final Future<ScanResult> f : futures) {
            try {
                if (f.get().isOpen) {
                    openPorts++;
                    Log.i("portScan:", f.get().sIP + " 9100 open");
                    // Send the name of the connected device back to the UI Activity
                    msg = mHandler.obtainMessage(msgTypes.addHost);
                    bundle = new Bundle();
                    bundle.putString(msgTypes.HOST_NAME, f.get().sIP);
                    msg.setData(bundle);
                    mHandler.sendMessage(msg);
                    doLog("added host msg for " + f.get().sIP);
                }
                else{
                    Log.i("portScan:", f.get().sIP + " 9100 closed");
                }
            }
            catch(ExecutionException e){
                doLog("ExecutionException: "+e.getMessage());
            }
            catch(InterruptedException e){
                doLog("InterruptedException: "+e.getMessage());
            }
        }
    ...
    }
    ...

Unfortunately the start of the Future objects is blocking and so I had to wrap that in another thread, so the caller (the GUI) is not blocked:

Thread to start port scans

    ...
    @Override
    public void run() {
        doLog("thread: run()...");
        if(state!=eState.idle) {
            doLog("thread: run() ended as state!=idle");
            return;
        }
        state=eState.running;
        ScanResult scanResult;
        try {
            doLog("thread: starting...");
            msg=mHandler.obtainMessage(msgTypes.started);
            mHandler.sendMessage(msg);
            if( true) { 
                doLog("thread: starting discovery...");
                startDiscovery1();
            ...
       ...
    ...

With the above solution the port scan finishes with 3 to 10 seconds.Much better than 50 seconds waiting.

ipprint4_scan

Every time a new open port is found the IP is added to the list. The process shows a progress cycle in the title of the list view activity. All status changes are send via a message handler from the portscanner code to the calling list activity.

When a TCP/IP address with port 9100 is found and selected you can print a demo file to it. You can also enter an IP manually and then directly print a demo file to it.

Startup checks

On startup the app checks for TCP/IP connectivity and ends, if there is no newtwork connection. It makes no sense to run the app without a working connection.

check online status

    ...
    public static boolean isNetworkOnline(Context context) {
        boolean status=false;
        try{
            ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo netInfo = cm.getNetworkInfo(0);
            if (netInfo != null && netInfo.getState()==NetworkInfo.State.CONNECTED) {
                status= true;
            }else {
                netInfo = cm.getNetworkInfo(1);
                if(netInfo!=null && netInfo.getState()==NetworkInfo.State.CONNECTED)
                    status= true;
            }
        }catch(Exception e){
            e.printStackTrace();
            return false;
        }
        return status;
    }
    ...

The local IP is automatically inserted as ‘printer’ IP address (remote device). So you just have to change part of the IP for the printer’s address.

IP address range

The portscanner code is initialized with the device IP and then uses a class C conversion to define start and end address. This will not work if you have a class B or class A network. So there are possible improvements of the code.

class c network

    ...
    //constructor
    PortScanner(Handler handler, String startIP){
        mHandler=handler;
        m_sStartIP=startIP;
        //test(startIP);
        String[] ss = m_sStartIP.split("\\.");
        if(ss.length!=4){ //no regular IP
            state=eState.finished;
            msg = mHandler.obtainMessage(hgo.ipprint4.msgTypes.MESSAGE_TOAST);
            bundle = new Bundle();
            bundle.putString(hgo.ipprint4.msgTypes.TOAST, "inavlid IP");
            msg.setData(bundle);
            mHandler.sendMessage(msg);
            return;
        }
        bValidIP=true;
        baseIP=ss[0]+"."+ss[1]+"."+ss[2];
        state=eState.idle;
    }
    ...

As we want to print to a socket, we needed to change the bluetooth connection code to connet to a network socket.

    ...
    /**
    * This thread runs while attempting to make an outgoing connection
    * with a device. It runs straight through; the connection either
    * succeeds or fails.
    */
    private class ConnectThread extends Thread {
        private Socket mmSocket=null;
        private InetAddress serverIP;
        private SocketAddress socketAddress=null;

        //private final BluetoothDevice mmDevice;

        @TargetApi(Build.VERSION_CODES.GINGERBREAD_MR1)
        public ConnectThread(String sIPremote) {
            _IPaddr=sIPremote;
            try {
                addText("get host IP");
                serverIP=InetAddress.getByName(_IPaddr);
                socketAddress=new InetSocketAddress(serverIP, socketPort);
                //tmp = device.createRfcommSocketToServiceRecord(UUID_SPP);
            }catch (UnknownHostException e){
                Log.e(TAG, "ConnectThread create() failed", e);
            }
            catch (IOException e) {
                Log.e(TAG, "ConnectThread create() failed", e);
            }
        }
        @Override
        public void run() {
            Log.i(TAG, "ConnectThread::run()");
            setName("ConnectThread");
            Socket tmp = null;

            // Make a connection to the Socket
            try {

                addText("new Socket()...");
                // This is a blocking call and will only return on a
                // successful connection or an exception
                //tmp=new Socket(serverIP, socketPort);
                tmp=new Socket();
                tmp.connect(socketAddress, iTimeOut);
                addText("new socket() done");
                mmSocket=tmp;
            }
            catch(IllegalArgumentException e){
                addText("IllegalArgumentException: " + e.getMessage());
                //if new Socket() failed
                connectionFailed();
                addText("Connect failed");
                if(mmSocket!=null) {
                    // Close the socket
                    try {
                        mmSocket.close();
                        tmp = null;
                    } catch (IOException e2) {
                        Log.e(TAG, "unable to close() socket during connection failure", e2);
                    }
                }
                return;
            }
            catch (IOException e){
                addText("IOException: " + e.getMessage());
                //if new Socket() failed
                connectionFailed();
                addText("Connect failed");
                if(mmSocket!=null) {
                    // Close the socket
                    try {
                        mmSocket.close();
                        tmp = null;
                    } catch (IOException e2) {
                        Log.e(TAG, "unable to close() socket during connection failure", e2);
                    }
                }
                // Start the service over to restart listening mode
                return;
            }
            catch (Exception e) {
                //if new Socket() failed
                connectionFailed();
                addText("Connect failed");
                if(mmSocket!=null) {
                    // Close the socket
                    try {
                        mmSocket.close();
                        tmp = null;
                    } catch (IOException e2) {
                        Log.e(TAG, "unable to close() socket during connection failure", e2);
                    }
                }
                return;
            }//catch
    ...

Then we need a stream to write and one to listen. Same as in btPrint4

    ...
    private class ConnectedThread extends Thread {
        private final Socket mmSocket;
        private final InputStream mmInStream;
        private final OutputStream mmOutStream;

        public ConnectedThread(Socket socket) {
            Log.d(TAG, "create ConnectedThread");
            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            // Get the BluetoothSocket input and output streams
            try {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "temp sockets not created", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }
    ...

And then we can write to the socket.

    ...
        public void write(byte[] buffer) {
            addText("write...");
            try {
                mmOutStream.write(buffer);

                // Share the sent message back to the UI Activity
                mHandler.obtainMessage(msgTypes.MESSAGE_WRITE, -1, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                Log.e(TAG, "Exception during write", e);
            }
            addText("write done");
        }
    ...

The remaining code of ipPrintFile is similar to the one in btPrint4 and used to manage all the threads used.

Full Source Code at github

 

4 Comments

  1. Rajesh says:

    Hi, i am working on pos in which i want to print html format, is there any possibility to get HTML print using your app….

  2. josef says:

    Hi Rajesh

    no, there is no HTML formatting or conversion included in the code.

    The code just shows how to connect to the printer directly and use the printer’s command language to print some demo files.

    If you are not familar with the printer’s command language, you are unable to print anything.

    sorry

    Josef

  3. Akhtar says:

    Hi Josef,

    Thank you so much for your code. It works like a charm with a pos wifi thermal printer also 🙂
    However, i know its almost impossible to know if the printer has printer the text sent to it but i would like to know if there is a way of establishing a two-way communication with the printer.

    In fact, i am working on a project where there is a lot of traffic and proxy on the internal wifi and i would like to know if the printer(in the kitchen) has printed the order which the waiter has placed.

    thanks.

    Best Regards,
    Akhtar

  4. josef says:

    Hello Akhtar

    first, the code uses standard TCP/IP connection and no UDP. So, if there is an error in the transmission, this would give an error (exception). The proxy or traffic does not count, TCP/IP will resend packets if not achnowledged etc…

    To be sure that the job printed you may query the printer for number of printed pages before and after the job and, of course, query the printer status for errors like ‘paper out’.
    If you can query the printer depends on the printer software and imepmented printer command language.

    regards

    Josef

Leave a Reply