Android Kotlin ke Bluetooth Printer

Topik ini belum banyak bahasannya. Ane kesulitan sewaktu ingin membuat aplikasi yang perlu melakukan print dengan baik ke printer bluetooth thermal POS/receipt murah meriah, secara (ketika postingan ini dibuat) belum sampai 1 bulan yang lalu ane membuka developer.android.com dan mendownload Android Studio.

Karena itulah ane ingin membagi pengalaman, semoga bisa membantu seseorang di luar sana, atau semoga ada masukan-masukan yang dapat berguna untuk memperbaiki pemahaman ane atas android dan kotlin.

Dan juga sekalian nyoba fitur Post baru di WP ini 😀

Alur UI

Pada awalnya ane berniat agar pengguna dapat 100% mengatur printer (termasuk bluetooth discovery & pairing) dari dalam aplikasi, tapi sepertinya target ini terlalu tinggi karena selang 2 hari kepala ane sakit dan ane putuskan untuk menurunkan targetnya, yang penting:

  1. Pengguna bisa lihat textview yang berisi informasi-informasi soal printernya.
  2. Kalau bluetooth aktif dan sudah pernah melakukan print sebelumnya, akan langsung dicoba untuk konek jadi bisa langsung print.
  3. Kalau konek gagal, atau belum pernah print sebelumnya, akan ditampilkan pilihan printer yang ada di daftar pair.
  4. Kalau tidak ada printer di daftar pair, tampilkan informasi (suruh pair).

Jadi, aktivasi bluetooth dan pairing tetap dilakukan di luar, namun si aplikasi harus dapat menangani sisanya dengan baik.

Layout

Yang sedang ane kembangkan adalah sebuah aplikasi POS di mana Transaksi tetap bisa dilakukan tanpa harus melakukan print nota. Ane akan jabarkan seminimal mungkin di sini supaya nggak rumit & fleksibel.

Elemen layoutnya:

  • Switch untuk mengaktifkan fitur Print
  • ProgressBar
  • Textview untuk info Print
  • Tombol untuk melakukan transaksi (dan print)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity" android:orientation="vertical">
    <Switch
            android:text="Print"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" android:id="@+id/printSwitch"/>
    <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content" android:id="@+id/printInfo"
            tools:text="Print info"/>
    <ProgressBar
            android:layout_gravity="center"
            android:visibility="invisible"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:id="@+id/printLoading"/>
    <Button
            android:text="Button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" android:id="@+id/transactionButton"/>
</LinearLayout>

Activity

Pada dasarnya di Activity:

  1. Kita initialize class BtPrint.kt dengan memberitahukan View-view apa saja yang perlu diutak-atik olehnya. Dari layout di atas:
    • printSwitch: Switch utama untuk mematikan/menghidupkan fitur Print
    • printLoading: Progress bar
    • printInfo: TextView untuk info
    • transactionButton: Tombol untuk melakukan print.
  2. Meng-initialize class BtPrint juga akan otomatis melakukan test koneksi awal (dan lain-lain untuk persiapan/memastikan printer terkoneksi). Namun ketika akan melakukan print beneran, kita tetap lakukan test lagi dari Activity untuk memastikan printer masih dalam keadaan tersambung (nggak mati, sedang dipakai, atau keluar jangkauan).
  3. Setelah itu, baru beritahukan apa yang harus di-print.
package id.tigaer.gudang.cashier

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {


    // I'm putting the variable here in case it is needed outside onCreate() later

    private lateinit var btPrint: BtPrint


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        // Init and pass the Views

        btPrint = BtPrint( printSwitch, printLoading, printInfo, transactionButton )


        // Actual print

        transactionButton.setOnClickListener{


            // We do socket connect here so we can do some handling if something happen with the printer before
            // the actual printing.

            btPrint.socketConnect { result ->

                if ( result["success"] == false ){

                    this@MainActivity.runOnUiThread {

                        printInfo.text = result["text"].toString()
                        printSwitch.isChecked = false

                        Toast.makeText(this, "OOPS!!!", Toast.LENGTH_SHORT).show()

                        // TODO: Pooling?

                    }

                } else {

                    btPrint.doPrint( android.os.Build.MODEL + "\n", true )
                    btPrint.doPrint( android.os.Build.BRAND + "\n\n\n" )


                    // I'll share how I handle printing format for regular receipts next... :)

                }

            }

        }


    }

}

BtPrint.kt

Layout & Activity sudah, ini class BtPrint() nya

package id.tigaer.gudang.cashier


import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket
import android.content.Context
import android.support.v7.app.AlertDialog
import android.view.View
import android.widget.Button
import android.widget.ProgressBar
import android.widget.Switch
import android.widget.TextView
import java.io.IOException
import java.util.*
import kotlin.collections.HashMap
import kotlin.concurrent.thread


class BtPrint(

    private var printSwitch: Switch,
    private var printLoading: ProgressBar,
    private var printInfo: TextView,
    private var printButton: Button

) {


    // Define the caller context and activity (tips on optimization will be much appreciated :)

    private val context = printSwitch.context
    private val activity = context as Activity
    private val sharedPrefs = context.getSharedPreferences(context.packageName + ".META", Context.MODE_PRIVATE)


    // Other initializer

    private var bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()

    private var printers = ArrayList<BluetoothDevice>()

    private lateinit var printer: BluetoothDevice
    private lateinit var socket: BluetoothSocket


    init {


        // Set Views to initial value.

        preCheckStart()


        // Only proceed if there's a record in sharedPrefs...

        if (sharedPrefs.getString("lastPrinter", "") != "") {

            preCheck()

        } else {


            // ...to skip (second) auto connection attempt if no printers active...

            printInfo.text = "Not going to print"
            preCheckDone()

        }


        // ...but of course proceed if switched manually.

        printSwitch.setOnClickListener {

            if (printSwitch.isChecked) {

                preCheck()

            } else {

                printInfo.text = "Not going to print"

            }

        }

    }


    private fun preCheck() {

        preCheckStart()


        // No bluetooth

        if (bluetoothAdapter == null) {

            printInfo.text = "This thing has no bluetooth"
            printSwitch.isChecked = false
            preCheckDone()

        } else {


            // Bluetooth inactive

            if (!bluetoothAdapter.isEnabled) {

                printInfo.text = "Bluetooth inactive"
                printSwitch.isChecked = false
                preCheckDone()

            } else {


                // Bluetooth available and active, so refresh printers list.

                refreshPrinters()

                if (printers.size > 0) {


                    // Loop the printers to crosscheck with sharedPrefs, and also to prepare arrays for printer selection
                    // dialog if needed.

                    val pNames = Array(printers.size) { "" }
                    val pAddrs = Array(printers.size) { "" }

                    var deviceFound = false

                    for (i in 0 until printers.size) {

                        pNames[i] = printers[i].name
                        pAddrs[i] = printers[i].address // How to do this "correctly" in Kotlin? :D


                        // Printer available in sharedPrefs, attempt connection and break.

                        if (printers[i].address == sharedPrefs.getString("lastPrinter", "")) {

                            deviceFound = true
                            printer = printers[i]
                            testConnection()
                            break

                        }

                    }


                    // If it gets here

                    if (!deviceFound) {


                        // Show printer selection dialog

                        val builder = AlertDialog.Builder(context)
                        builder.setTitle("Select printer")
                            .setItems(
                                pNames
                            ) { _, which ->


                                // On selected, save and rerun preCheck()

                                sharedPrefs.edit().putString("lastPrinter", pAddrs[which]).apply()
                                preCheck()

                            }
                        builder.create()
                        builder.setCancelable(false)
                        builder.show()

                    }

                } else {


                    // No printers

                    printInfo.text = "Please pair a printer"
                    printSwitch.isChecked = false
                    preCheckDone()

                }

            }

        }

    }


    private fun refreshPrinters() {


        // Clean up first, but I found out that sometimes the device must be reset, and/or Clear Data in
        // Bluetooth Share app, to really refresh the paired devices list, after we Forget/Add a bluetooth device.
        // This probably a device issue (or just me lacking experience).

        printers.clear()


        // Filter the paired devices for printers

        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() // this is my attempt to "really refresh" the list

        val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter.bondedDevices

        if (pairedDevices != null) {

            for (i in pairedDevices) {

                if (i.bluetoothClass.deviceClass.toString() == "1664") { // Identifier (or something) for printers.


                    // Now we have a list of printers

                    printers.add(i)

                }

            }

        }

    }


    private fun testConnection() {


        // Make sure discovery isn't running

        bluetoothAdapter?.cancelDiscovery()


        // Socket connect will freeze the UI, so we're doing this in another thread and get the callback

        printInfo.text = printer.name
        printInfo.append("... ")

        socketConnect { result ->


            // Other threads can't touch UI without runOnUiThread()

            activity.runOnUiThread {

                printInfo.append(result["text"].toString())


                // Connection failed, delete from sharedPrefs

                if (result["success"] == false) {

                    sharedPrefs.edit().putString("lastPrinter", "").apply()
                    printSwitch.isChecked = false

                } else {

                    printSwitch.isChecked = true

                }

                preCheckDone()

            }


            // Done checking. From here we assume the printer is active, nearby, and is ready for real printing.
            // We close the socket to allow other devices to use the printer although it might require some more coding
            // to imitate some kind of pooling service (no such service in my two test/cheap printers here).

            socket.close()

        }

    }


    fun socketConnect(callback: (HashMap<String, Any>) -> Unit) {


        // Make sure socket is closed

        if (::socket.isInitialized) socket.close()


        // Google for explanation on this :D

        socket = printer.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))

        thread(start = true) {

            val result = HashMap<String, Any>()

            try {
                socket.connect()
                result["success"] = true
                result["text"] = "connected."
            } catch (e: IOException) {
                result["success"] = false
                result["text"] = e
            }

            callback(result)

        }

    }


    private fun preCheckStart() {

        printLoading.visibility = View.VISIBLE
        printButton.isClickable = false
        printSwitch.alpha = .25f

    }

    private fun preCheckDone() {

        printLoading.visibility = View.INVISIBLE
        printButton.isClickable = true
        printSwitch.alpha = 1f

    }


    fun doPrint(stringToPrint: String, keepSocket: Boolean = false) {


        // ESC/POS default format

        socket.outputStream.write(byteArrayOf(27, 33, 0))


        // Print your string

        socket.outputStream.write(stringToPrint.toByteArray())


        if (!keepSocket) {
            socket.close()
        }

    }


}

Screenshots

Discovery & pairing dilakukan di luar, tapi app harus sebisa mungkin membantu kasir dalam melakukan koneksi ke printer.

Walaupun, dari yang saya coba-coba, ada kalanya (test koneksi) tidak berhasil saat; bluetooth baru saja dinyalakan, pairing baru dilakukan, dan semacamnya, yang saya simpulkan disebabkan oleh HP-nya.

Kadang kan Bluetooth memang suka aneh, harus di-forget dulu, di-reset dulu, dkk.

Walau demikian, app tetap akan memberitahukan bahwa test koneksi tidak berhasil, dan menon-aktifkan fitur print. Kasir bisa memilih untuk lanjut Transaksi tanpa print, atau benerin koneksi dulu.

Di sebagian besar kasus (bluetooth lagi ga abis diapa2in) sih aman-aman saja, print lancar.

Ini 2 printer yang saya test btw:

Harga sekitar 500 ribu di Tokped, mending yang kanan 😀

Demikianlah, semoga bermanfaat, masukan sangat dihargai 😀

Update

Beberapa bulan lalu kita mengganti penggunaan HP2 standar + bluetooth printer dengan benda ini:

Sunmi V1
Kisaran 3 juta di Tokopedia, masih lebih murah dari 1 HP Android + 1 barcode scanner + 1 bluetooth printer.

… yang secara efektif menghilangkan masalah ketidak-stabilan pada koneksi bluetooth.

Benda tersebut memiliki printer built-in yang tetap menggunakan koneksi bluetooth dengan kestabilan yang tak tertandingi.

Benda-benda sejenis yang lebih murah ada, tapi untuk urusan “murah berkualitas” saya merekomendasikan Xiaomi.

Dan btw, kodingan di atas ada yang perlu diubah sedikit, di bagian”pengenalan kode bluetooth untuk printer”.

Printer built-in Sunmi ini walaupun menggunakan bluetooth namun tidak dikenali sebagai printer.

Kalau gak salah saya cuma hapus aja bagian utk pengenalan tsb, beres. Jadi deteksi bluetooth device nya jangan cuma dikhususkan untuk printer.


Comments

8 responses to “Android Kotlin ke Bluetooth Printer”

  1. Mantap.. boleh dong share source lengkap ke github..

    1. Maaf gan belum sempat 🙁

      Lagipula ane gak tuliskan ini untuk tinggal pakai, bisa jadi malah bikin rumit kalau langsung dipakai apa adanya. Sebaiknya agan coba buat sendiri dan lihat kode2 di atas untuk referensi aja.

  2. diubah sedikit, di bagian”pengenalan kode bluetooth untuk printer”.

    Boleh tanya diubah nya gimana pak

    1. Maaf kalau salah tapi setelah dibaca di atas kemungkinan besar di bagian:

      if (i.bluetoothClass.deviceClass.toString() == "1664")

      Kondisional ini dihilangkan saja supaya tidak hanya deviceClass 1664 yang dimasukkan ke daftar, karena printer Sunmi tidak masuk deviceClass ini.

  3. Thudos Avatar
    Thudos

    Makasih banyak gan sharingnya, terbantu banget!

  4. bang mind Avatar
    bang mind

    mau nanya mas
    pernah dapat error kayak gini gk mas?
    java.io.IOException:read failed, soket closed or timeout, read ret:-1

    caranya gmn ya?

    1. Iyah gan itu error kalau gagal koneksi, bisa karna printer sudah dimatikan, atau terlalu jauh. Printernya sebelumnya bisa terdeteksi?

      1. Bang Mind Avatar
        Bang Mind

        oke makasih gan
        udh bisa connect gan
        skrng masalahnya pas gw klik button print gk mau ngeprint padahal udh connect ke printer.
        ini tipe printer saya gan
        https://tokopedia.link/6Fown1iSZ8
        saya udh coba juga beberapa library ttp aja gk bisa ngeprint
        kira2 masalahnya kenapa ya gan?

Leave a Reply

Your email address will not be published. Required fields are marked *