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:
- Pengguna bisa lihat textview yang berisi informasi-informasi soal printernya.
- Kalau bluetooth aktif dan sudah pernah melakukan print sebelumnya, akan langsung dicoba untuk konek jadi bisa langsung print.
- Kalau konek gagal, atau belum pernah print sebelumnya, akan ditampilkan pilihan printer yang ada di daftar pair.
- 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:
- 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.
- 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).
- 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:
Demikianlah, semoga bermanfaat, masukan sangat dihargai 😀
Update
Beberapa bulan lalu kita mengganti penggunaan HP2 standar + bluetooth printer dengan benda ini:
… 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.
Leave a Reply