NF5501扫描与打印二次开发记要
2020-02-03
关键字:热敏打印设备二次开发
NF5501 是一款便携式的打印扫描一体式设备。
这篇博文记录一下笔者针对这款设备的条码扫描与热敏打印功能的二次开发。
首先这款设备官方是有给出扫码与热敏打印的示例程序及源码的,它的下载地址为:
http://www.nanfang001.com/Support/473918/
不过官方提供的示例源码稍稍有些零乱,需要花点时间才能明白控制扫描与打印的流程。笔者这边已经大致梳理过一次了,将关键步骤在此记录下来,以期能帮助到有需要的同学。
NF5501 应用层与扫描打印硬件层通信都是通过串口节点来实现的,并且扫描与打印使用的是同一个串口节点。设备开放了 JNI 接口供 APP 调用。
集成 NF5501 的扫描打印功能需要用到以下关键文件:
1、libzyapi_common.so
2、CommonApi.java
3、beep.ogg
这几个文件在官方示例源码中都有,当然笔者也将它们打包了一份在博文里公开:
链接:https://pan.baidu.com/s/17oGkZFcwqbcOHQYTOF9o8w 提取码:608b
其中,第 1 个库文件是 JNI 实现程序,这个库文件必须正确预置进我们的 APK 中,否则在调用相关 JNI 接口时会报找不到实现函数的错误。以 Android Studio 工程为例,将这 libzyapi_common.so 预置在 ./app/src/main/jniLibs/armeabi/libzyapi_common.so 中即可。
第 2 个文件则是封装的 JNI 接口,也是必须预置。当然你也可以只保留其中几个你会用到的接口方法,把它们直接嵌入到你自己的代码中也是可以的。只要保证方法名与加载的库名称正确即可。对了,似乎这个设备的 JNI 是静态封装的,它要求我们在封装 JNI native 方法时必须以包名 android.zyapi 来命名。即如果你直接预置示例源码中的 CommonApi.java 源码,那你必须将它以 ./android/zyapi/CommonApi.java 的路径来放置。
第 3 个则是扫描成功时设备发出的提示音音频文件。这个非刚性需求,并且你完全可以根据自己的实际需求预置其它音频文件。直接将这个文件预置在 ./res/raw/beep.ogg 下即可。
文件预置好以后即可开始撸代码了。
首先我们要知道设备对应的扫描引脚号。这个引脚具体的作用是什么笔者不太清楚,只知道它对于功能初始化很重要。
这个引脚号是根据设备的型号来确定的。即 Android 代码中的 Build.MODEL 的值,如果该值是 5501H 则引脚号就是 64,如果 Build.MODEL 的值是 5501L 则引脚号就是 84。Build.MODEL 的值也可以在 adb shell 中直接通过查看系统属性来获得,对应命令为:
getprop | grep model
笔者手里的设备是 5501L 型号的,因此对应的引脚号就是 84。
private static final int PIN = 84;
接下来可以再创建多几个关键全局变量:
private final int comFd; private CommonApi commonApi; private MediaPlayer beeper;
然后就可以开始初始化了。关于初始化的时机,同学可以根据实际需要来决定。笔者这边是在应用进入到主界面以后才做初始化:
commonApi = new CommonApi(); commonApi.setGpioDir(PIN, 0); commonApi.getGpioIn(PIN); new CountDownTimer(1000, 1000){ @Override public void onTick(long millisUntilFinished) { // do nothing. } @Override public void onFinish() { Logger.i(TAG, "1s count down onFinish()"); commonApi.setGpioDir(PIN, 1); commonApi.setGpioOut(PIN, 1); } }.start(); comFd = commonApi.openCom("/dev/ttyMT1", 115200, 8, 'N', 1); Logger.i(TAG, "scanner serial port fd:" + comFd); new CountDownTimer(2000, 1000){ @Override public void onTick(long millisUntilFinished) { // do nothing. } @Override public void onFinish() { Logger.i(TAG, "2s scanner serial port onFinish()"); if (comFd > 0) { canRead = true; write(new byte[] {0x1B, 0x23, 0x23, 0x35, 0x36, 0x55, 0x50}); readCom.start(); // 默认开启黑标 write(new byte[] {0x1F, 0x1B, 0x1F, (byte) 0x80, 0x04, 0x05, 0x06, 0x66}); }else{ canRead = false; } notifyScannerReady(); } // onFinish() -- end }.start();
上面这个初始化过程其实没有什么好说的,照着写就是了。这块代码正确执行完成以后就可以发现设备右上方的绿色小指标灯已经亮起来了。
其次是创建一个子线程专门用于读取扫描打印串口的数据:
private Thread readCom = new Thread(){ private static final int MAX_RECV_BUF_SIZE = 1023; private int ret; private String code; @Override public void run() { byte[] buf = new byte[MAX_RECV_BUF_SIZE + 1]; while (canRead) { ret = commonApi.readComEx(comFd, buf, MAX_RECV_BUF_SIZE, 0, 0); if (ret <= 0) { Logger.w(TAG, "read failed!!!! ret:" + ret); if(callback != null) { callback.onScanResult(null); } try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } continue; } byte[] recv = new byte[ret]; System.arraycopy(buf, 0, recv, 0, ret); code = new String(recv, 0, ret).trim(); Logger.i(TAG, "code scan:" + code); if (!code.contains("") && !code.contains("##55") && !code.contains("##56") && !code.equals("start") && code.length() > 0) { beeper.start(); }else{ code = null; } if(callback != null) { callback.onScanResult(code); } }// while -- end }// run() -- end
上面代码在中下方有一个字符显示不出来 !code.contains("") 这里,那个字符在 ASCII 表中是第 19 号:
以上就是 NF5501 扫描打印功能的初始化过程。这个过程由于有延时的存在,因此需要 1 秒钟左右。当然,你也可以在初始化过程中创建好扫描成功音频播放器实例:
beeper = MediaPlayer.create(activity.getApplicationContext(), R.raw.beep);
当我们需要扫描条形码与二维码时,则可以调用以下代码:
write(new byte[] {0x1B, 0x23, 0x23, 0x35, 0x35, 0x44, 0x4E}); commonApi.setGpioDir(74, 1); commonApi.setGpioOut(74, 0); commonApi.setGpioDir(75, 1); commonApi.setGpioOut(75, 0); new CountDownTimer(50, 50){ @Override public void onTick(long millisUntilFinished) { // do nothing. } @Override public void onFinish() { commonApi.setGpioDir(74, 1); commonApi.setGpioOut(74, 1); commonApi.setGpioDir(75, 1); commonApi.setGpioOut(75, 1); } }.start();
当我们需要打印时,则直接将要打印的数据以字节数组的形式往串口写即可:
if (comFd > 0) { commonApi.writeCom(comFd, data, data.length); }
当然,其实在写打印数据之前需要先写相应的指令数据以让设备做好打印准备。
这些打印指令都有哪些呢?
其实打印可以归结两种:
1、文字打印;
2、图像打印。
打印文字的话需要先发送两条指令:
1、设置对齐方式;
2、设置字号。
然后才是发送文字的字节数组数据。这里需要提一点,这个设备打印文字时是“按行打印”的,简单说就是你必须在你要打印的文字的末尾至少加一个换行符。下面是官方源码中打印文字的代码:
public static void printText(int size, int align, String text) { switch (align) { case 0: send(new byte[] { 0x1b, 0x61, 0x00 }); break; case 1: send(new byte[] { 0x1b, 0x61, 0x01 }); break; case 2: send(new byte[] { 0x1b, 0x61, 0x02 }); break; default: break; } switch (size) { case 1: send(new byte[] { 0x1D, 0x21, 0x00 }); break; case 2: send(new byte[] { 0x1D, 0x12, 0x11 }); break; default: break; } // 打印 try { send((text + "\n").getBytes("GBK")); send(new byte[] { 0x1D, 0x0c }); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } }
而打印图片,则是需要我们先准备好 Bitmap 格式的图片,再将它转成字节数组形式。将Bitmap 图片转成字节数组需要用到专用方法,具体请参阅下列代码,也是取自官方示例源码:
public void print(PrintInfo pi) { Bitmap bitmap = makePrintImage(pi); if(bitmap != null) { write(new byte[] {0x1b, 0x61, 0x00}); byte[] bm = draw2PxPoint(bitmap); write(bm); }else{ Logger.e(TAG, "Cannot print cause 'bitmap' is null."); } } private byte[] draw2PxPoint(Bitmap bmp) { int size = bmp.getWidth() * bmp.getHeight() / 8 + 1200; Logger.i(TAG, "size1:" + size + ",bitmap size:" + bmp.getByteCount()); byte[] data = new byte[size]; int k = 0; // 设置行距为0的指令 data[k++] = 0x1B; data[k++] = 0x33; data[k++] = 0x00; // 逐行打印 for (int j = 0; j < bmp.getHeight() / 24f; j++) { // 打印图片的指令 data[k++] = 0x1B; data[k++] = 0x2A; data[k++] = 33; data[k++] = (byte) (bmp.getWidth() % 256); // nL data[k++] = (byte) (bmp.getWidth() / 256); // nH // 对于每一行,逐列打印 for (int i = 0; i < bmp.getWidth(); i++) { // 每一列24个像素点,分为3个字节存储 for (int m = 0; m < 3; m++) { // 每个字节表示8个像素点,0表示白色,1表示黑色 for (int n = 0; n < 8; n++) { byte b = px2Byte(i, j * 24 + m * 8 + n, bmp); if (k < size) { data[k] += data[k] + b; } // data[k] = (byte) (data[k]+ data[k] + b); } k++; } } if (k < size) { data[k++] = 10;// 换行 } } return data; } private byte px2Byte(int x, int y, Bitmap bit) { if (x < bit.getWidth() && y < bit.getHeight()) { byte b; int pixel = bit.getPixel(x, y); int red = (pixel & 0x00ff0000) >> 16; // 取高两位 int green = (pixel & 0x0000ff00) >> 8; // 取中两位 int blue = pixel & 0x000000ff; // 取低两位 int gray = RGB2Gray(red, green, blue); if (gray < 128) { b = 1; } else { b = 0; } return b; } return 0; } private int RGB2Gray(int r, int g, int b) { return (int) (0.29900 * r + 0.58700 * g + 0.11400 * b); // 灰度转化公式 }
在使用完毕后,应用退出时最好将扫描打印功能去初始化操作一下。代码比较简单,如下:
@Override public void destroy(Activity activity) { if (comFd > 0) { commonApi.setGpioMode(84, 0); commonApi.setGpioDir(84, 0); commonApi.setGpioOut(84, 0); commonApi.closeCom(comFd); } }
上面的代码在正确执行完以后可以发现设备右上方的绿色指示灯会熄灭。
以下贴出笔者使用的完整扫描打印功能封装代码:
package com.jarwen.scanner.scanner; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.media.MediaPlayer; import android.os.CountDownTimer; import android.zyapi.CommonApi; import com.jarwen.scanner.MainActivity; import com.jarwen.scanner.R; import com.jarwen.scanner.data.model.PrintInfo; import com.jarwen.scanner.util.Logger; import com.jarwen.scanner.util.ToastManager; public class NF5501 extends Scanner implements MainActivity.OnScanKeyListener { private static final String TAG = "NF5501"; private static final int PIN = 84; // no why. private final int comFd; private boolean canRead; private OnScanResultCallback callback; private CommonApi commonApi; private MediaPlayer beeper; NF5501(Context context, ToastManager tm){ super(context, tm); Logger.i(TAG, "new NF5501()"); commonApi = new CommonApi(); commonApi.setGpioDir(PIN, 0); commonApi.getGpioIn(PIN); new CountDownTimer(1000, 1000){ @Override public void onTick(long millisUntilFinished) { // do nothing. } @Override public void onFinish() { Logger.i(TAG, "1s count down onFinish()"); commonApi.setGpioDir(PIN, 1); commonApi.setGpioOut(PIN, 1); } }.start(); comFd = commonApi.openCom("/dev/ttyMT1", 115200, 8, 'N', 1); Logger.i(TAG, "scanner serial port fd:" + comFd); new CountDownTimer(2000, 1000){ @Override public void onTick(long millisUntilFinished) { // do nothing. } @Override public void onFinish() { Logger.i(TAG, "2s scanner serial port onFinish()"); if (comFd > 0) { canRead = true; write(new byte[] {0x1B, 0x23, 0x23, 0x35, 0x36, 0x55, 0x50}); readCom.start(); // 默认开启黑标 write(new byte[] {0x1F, 0x1B, 0x1F, (byte) 0x80, 0x04, 0x05, 0x06, 0x66}); }else{ canRead = false; } notifyScannerReady(); } // onFinish() -- end }.start(); }// NF5501() -- end @Override public void prepare(Activity activity) { beeper = MediaPlayer.create(activity.getApplicationContext(), R.raw.beep); ((MainActivity)activity).setOnScanKeyListener(this); } @Override public void resume(OnScanResultCallback callback) { Logger.v(TAG, "resume()"); this.callback = callback; } @Override public void pause() { Logger.v(TAG, "pause()"); callback = null; } @Override public void destroy(Activity activity) { Logger.v(TAG, "destroy(), comFd:" + comFd); canRead = false; if (comFd > 0) { commonApi.setGpioMode(84, 0); commonApi.setGpioDir(84, 0); commonApi.setGpioOut(84, 0); commonApi.closeCom(comFd); } } @Override public void scan() { Logger.v(TAG, "scan()"); write(new byte[] {0x1B, 0x23, 0x23, 0x35, 0x35, 0x44, 0x4E}); commonApi.setGpioDir(74, 1); commonApi.setGpioOut(74, 0); commonApi.setGpioDir(75, 1); commonApi.setGpioOut(75, 0); new CountDownTimer(50, 50){ @Override public void onTick(long millisUntilFinished) { // do nothing. } @Override public void onFinish() { commonApi.setGpioDir(74, 1); commonApi.setGpioOut(74, 1); commonApi.setGpioDir(75, 1); commonApi.setGpioOut(75, 1); } }.start(); } private void write(byte[] data){ if (comFd > 0) { commonApi.writeCom(comFd, data, data.length); } } @Override public void onScanKeyClicked() { Logger.v(TAG, "onScanKeyClicked(),canRead:" + canRead + ",callback null?" + (callback == null)); if(canRead && callback != null) { scan(); } } @Override public void print(PrintInfo pi) { Logger.v(TAG, "print(),order number:" + pi.getOrderNo()); Bitmap bitmap = makePrintImage(pi); if(bitmap != null) { write(new byte[] {0x1b, 0x61, 0x00}); byte[] bm = draw2PxPoint(bitmap); write(bm); }else{ Logger.e(TAG, "Cannot print cause 'bitmap' is null."); } Logger.i(TAG, "print end."); } // print() -- end. /************************************************************************* * 假设一个240*240的图片,分辨率设为24, 共分10行打印 每一行,是一个 240*24 的点阵, 每一列有24个点,存储在3个byte里面。 * 每个byte存储8个像素点信息。因为只有黑白两色,所以对应为1的位是黑色,对应为0的位是白色。 * ------ * 把一张Bitmap图片转化为打印机可以打印的字节流 */ private byte[] draw2PxPoint(Bitmap bmp) { // 用来存储转换后的 bitmap 数据。为什么要再加1000,这是为了应对当图片高度无法 // 整除24时的情况。比如bitmap 分辨率为 240 * 250,占用 7500 byte, // 但是实际上要存储11行数据,每一行需要 24 * 240 / 8 =720byte 的空间。再加上一些指令存储的开销, // 所以多申请 1000byte 的空间是稳妥的,不然运行时会抛出数组访问越界的异常。 int size = bmp.getWidth() * bmp.getHeight() / 8 + 1200; Logger.i(TAG, "size1:" + size + ",bitmap size:" + bmp.getByteCount()); byte[] data = new byte[size]; int k = 0; // 设置行距为0的指令 data[k++] = 0x1B; data[k++] = 0x33; data[k++] = 0x00; // 逐行打印 for (int j = 0; j < bmp.getHeight() / 24f; j++) { // 打印图片的指令 data[k++] = 0x1B; data[k++] = 0x2A; data[k++] = 33; data[k++] = (byte) (bmp.getWidth() % 256); // nL data[k++] = (byte) (bmp.getWidth() / 256); // nH // 对于每一行,逐列打印 for (int i = 0; i < bmp.getWidth(); i++) { // 每一列24个像素点,分为3个字节存储 for (int m = 0; m < 3; m++) { // 每个字节表示8个像素点,0表示白色,1表示黑色 for (int n = 0; n < 8; n++) { byte b = px2Byte(i, j * 24 + m * 8 + n, bmp); if (k < size) { data[k] += data[k] + b; } // data[k] = (byte) (data[k]+ data[k] + b); } k++; } } if (k < size) { data[k++] = 10;// 换行 } } return data; } /** * 灰度图片黑白化,黑色是1,白色是0 * * @param x * 横坐标 * @param y * 纵坐标 * @param bit * 位图 */ private byte px2Byte(int x, int y, Bitmap bit) { if (x < bit.getWidth() && y < bit.getHeight()) { byte b; int pixel = bit.getPixel(x, y); int red = (pixel & 0x00ff0000) >> 16; // 取高两位 int green = (pixel & 0x0000ff00) >> 8; // 取中两位 int blue = pixel & 0x000000ff; // 取低两位 int gray = RGB2Gray(red, green, blue); if (gray < 128) { b = 1; } else { b = 0; } return b; } return 0; } /** * 图片灰度的转化 */ private int RGB2Gray(int r, int g, int b) { return (int) (0.29900 * r + 0.58700 * g + 0.11400 * b); // 灰度转化公式 } private Thread readCom = new Thread(){ private static final int MAX_RECV_BUF_SIZE = 1023; private int ret; private String code; @Override public void run() { byte[] buf = new byte[MAX_RECV_BUF_SIZE + 1]; while (canRead) { ret = commonApi.readComEx(comFd, buf, MAX_RECV_BUF_SIZE, 0, 0); if (ret <= 0) { Logger.w(TAG, "read failed!!!! ret:" + ret); if(callback != null) { callback.onScanResult(null); } try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } continue; } byte[] recv = new byte[ret]; System.arraycopy(buf, 0, recv, 0, ret); code = new String(recv, 0, ret).trim(); Logger.i(TAG, "code scan:" + code); if (!code.contains("") && !code.contains("##55") && !code.contains("##56") && !code.equals("start") && code.length() > 0) { beeper.start(); }else{ code = null; } if(callback != null) { callback.onScanResult(code); } // StringBuilder sb = new StringBuilder(); // for (int i = 0; i < recv.length; i++) { // if (recv[i] == 0x0D) { // sb.append("\n"); // } else { // sb.append((char) recv[i]); // } // } // Logger.d(TAG, "sb:" + sb.toString()); String str = byteToString(buf, ret); Logger.i(TAG, "hex data:" + str); if (str.contains("1C 00 0C 0F")) { // Logger.i(TAG, "no paper"); // Intent mIntent = new Intent("NOPAPER"); // instance.sendBroadcast(mIntent); // isCanprint = false; return; } else { // Logger.i(TAG, "can print"); } }// while -- end }// run() -- end private String byteToString(byte[] b, int size) { byte high, low; byte maskHigh = (byte) 0xf0; byte maskLow = 0x0f; StringBuilder buf = new StringBuilder(); for (int i = 0; i < size; i++) { high = (byte) ((b[i] & maskHigh) >> 4); low = (byte) (b[i] & maskLow); buf.append(findHex(high)); buf.append(findHex(low)); buf.append(" "); } return buf.toString(); } private char findHex(byte b) { int t = Byte.valueOf(b).intValue(); t = t < 0 ? t + 16 : t; if ((0 <= t) && (t <= 9)) { return (char) (t + '0'); } return (char) (t - 10 + 'A'); } }; }
最后,关于如何通过代码自定义生成打印小票的 Bitmap 图片,可以参阅笔者的另一篇博文:
https://www.cnblogs.com/chorm590/p/12240407.html