文章目录
Zebra 打印机 Android 端驱动接口开发及调用
Android 设备驱动 Zebra ZQ520 移动式打印机打印条码信息案例
GitHub 源码
开发步骤
环境配置
- 硬件环境
- Zebra ZQ520 移动打印机 1 台
- Android 设备 1 台
- 打印纸(50mm * 40mm)
- 软件环境
- Android Studio 3.2
- gradle:3.2.0
- Android Studio 3.2
Zebra SDK 资源下载
-
方法1:官网下载(需要各种注册信息)
由于我是 windows 系统,所以点击 windows Installer,下载完成后,在 windows 系统中直接双击安装,安装完成后在目录:\link_os_sdk\android\v2.14.5198\lib 目录下可以找到所需要的 jar 包。 -
方法2:CSDN 资源下载(会需要少量积分)
-
方法3:百度网盘下载(链接:https://pan.baidu.com/s/1tVg6lW52SSeh3j6kW7ih8Q 提取码: qfc8 )
Android 开发环境搭建
正常来讲,如果是开发完整的 app 引用,则直接引入上一步中的 jar 包就可以调用 Zebra 的驱动接口了。
但是由于业务需求,我这边只负责开发一个处理拆分数据、生成ZPL命令、驱动列印功能的接口。
其他同事负责开发 Android 客户端并调用我提供的接口
所以,本案例采用的开发模式是:
- 开发 Android Library ,打包成 arr 文件;
- 新建空 App 应用,调用打包的 arr 接口测试。
- 测试成功后,将测试案例与 arr 接口发送给调用者使用。
新建Android项目
-
输入项目名称、包名,选择项目目录。
-
选择支持的驱动环境。选择 Phone,最低 Android 4.0
-
创建一个空模板,方便后续测试。
-
Activity 的 name 和 layout name,默认即可,点击 finish
新建完成后, Gradle 会自动同步,此时需要保持网络畅通(需要一段时间,耐心等待即可。如果没有网络,需要保证 gradle 的离线版本支持)
如果遇到如下错误:
Unable to resolve dependency for ‘:app@debug/compileClasspath’: Could not find any version that matches com.android.support:appcompat-v7:29.+.
Open File
Show Details
这是因为没有找到符合正则验证: com.android.support:appcompat-v7:29.+. 版本的文件
解决方案:
简单暴力:放宽该包的正则验证规则。
步骤:
- 点击报错信息中的 Open File 超链接打开对应的 build gradle 文件;
- 找到 com.android.support:appcompat-v7:29.+ 这一行;
- 删除 29. 几个字符,保存
- 点击上方的 “try agine”,或者 “File --> Sync Project With Gradle Files” 重新同步 gradle 文件
至此,一个空的 Android 项目就新建完成了。
新建 Android Library
- 在项目文件上右键,或者 File --> new module
- 选择 Android Library
- 输入 library name,选择 minmum SDK 版本(建议和 Android 项目相同),点击 finish。
如果遇到如下错误:
Unable to resolve dependency for ‘:app@debug/compileClasspath’: Could not find any version that matches com.android.support:appcompat-v7:29.+.
Open File
Show Details
解决方案和上面相同(注意Library有自己对应的 build.gradle 文件,建议 点击报错信息中 open file 超链接跳转)
导入 Zebra 驱动包
- 点击左上角资源总览视图中的 Android 部分,切换为 project 显示方式
- 引入 Zebra 驱动文件到 Android Library 模块的 libs 目录下(第一步中下载的 jar 包)
- 选中所有 jar 包,右键,Add as Library,选择 add to module 为 刚才新建的 Android Library
Add 成功后,在 Android Library 对应的 build.gradle 文件下,会自动生成如下一段代码:
dependencies {
...
implementation files('libs/commons-io-2.2.jar')
implementation files('libs/commons-net-3.1.jar')
implementation files('libs/commons-validator-1.4.0.jar')
implementation files('libs/httpcore-4.3.1.jar')
implementation files('libs/httpmime-4.3.2.jar')
implementation files('libs/jackson-annotations-2.2.3.jar')
implementation files('libs/jackson-core-2.2.3.jar')
implementation files('libs/jackson-databind-2.2.3.jar')
implementation files('libs/opencsv-2.2.jar')
implementation files('libs/snmp6_1.jar')
implementation files('libs/ZSDK_ANDROID_API.jar')
}
如果上一步中右键没有找到 “Add as Library” 选项,也可以直接手动在对应的 build.gradle 文件中添加如上代码(注意路径)
Android Library 权限配置
由于本案例采用 BlueTooth 连接,所以,需要配置 BlueTooth 赋予蓝牙的访问权限。
-
在 Android Library 模块(zebraprinter)下依次展开 src --> main --> AndroidMainfest.xml 文件。加入如下两行代码:
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
如果需要其他需求,也可以在这里继续配置权限(如:后续可能需要获取主机mac地址,所以我再加入WIFI_STATE权限)
ZPL II 语法在线测试
推荐一个在线测试 ZPL II 的网站。点击跳转
在这里,你可以在线编写 ZPL 代码,并随时点击 redraw 按钮预览结果,非常方便!
主体功能开发
本案例的主体需求为:收到一份信息,将该信息进行拆分,打印出两份信息。
Android Library 的代码基本结构如下:
其中:
- Entity 包中存放了两个实体:
- ResultObj:返回信息实体类。包含一个 boolean 类型的执行状态,和一个 String 类型的执行信息
- ZeroSymbolBill:存储案例中收到与拆分的信息。为了方便操作。
- Utils 包中存放了一个工具类:
- Utils:工具类。包含一个生成唯一码 GUID 的静态方法。
- ZebraPrinter 包中存放了打印机相关代码
- ZQ520 Printer:存放了调用 ZQ520 打印机的主体代码
具体代码如下:
package com.amborsecdmeng.demo.entity;
/**
* Result Object
*/
public class ResultObj {
/**
* 状态
*/
private boolean status;
/**
* 信息
*/
private String message;
public ResultObj() {
this.status = false;
}
public ResultObj(boolean status) {
this.status = status;
}
public ResultObj(boolean status, String message) {
this.status = status;
this.message = message;
}
public boolean isStatus() {
return status;
}
public void setStatus(boolean status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
package com.amborsecdmeng.demo.entity;
/**
* Model of ZeroSymbolBill
*
* Example:
*
* W,NEW00182631190729C0001,P2a-J60102,2T459M000-000-G5,20190729,WmL-J76036,2200,PCS,VCN00182631190729C0001
* W,NEW00182631190729C0002,P2a-J60102,2T459M000-000-G5,20190729,WmL-J76036,5000,PCS,VCN00182631190729C0001
*/
public class ZeroSymbolBill {
/**
* 类型:W
*/
private String type;
/**
* 料号:2T459M000-000-G5
*/
private String pn;
/**
* 数量:7200
*/
private Integer qty;
/**
* 拆分:5000
*/
private Integer splitQty;
/**
* 单位:PCS
*/
private String unit;
/**
* 旧 GUID:VCN00182631190729C0001
*/
private String oldGuid;
/**
* 新 GUID:NEW00182631190729C0001
*/
private String newGuid;
/**
* 日期:20190729
*/
private String date;
/**
* 预留字段 1:P2a-J60102
*/
private String var1;
/**
* 预留字段 2:WmL-J76036
*/
private String var2;
/**
* Constructor without params
*/
public ZeroSymbolBill() {
}
/**
* Constructor with all params
*
* @param type
* @param newGuid
* @param var1
* @param pn
* @param date
* @param var2
* @param qty
* @param unit
* @param oldGuid
* @param splitQty
*/
public ZeroSymbolBill(String type, String newGuid, String var1, String pn, String date, String var2, Integer qty, String unit, String oldGuid, Integer splitQty) {
this.type = type;
this.newGuid = newGuid;
this.var1 = var1;
this.pn = pn;
this.date = date;
this.var2 = var2;
this.qty = qty;
this.unit = unit;
this.oldGuid = oldGuid;
this.splitQty = splitQty;
}
/**
* Getter & Setter
*/
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getPn() {
return pn;
}
public void setPn(String pn) {
this.pn = pn;
}
public Integer getQty() {
return qty;
}
public void setQty(Integer qty) {
this.qty = qty;
}
public Integer getSplitQty() {
return splitQty;
}
public void setSplitQty(Integer splitQty) {
this.splitQty = splitQty;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public String getOldGuid() {
return oldGuid;
}
public void setOldGuid(String oldGuid) {
this.oldGuid = oldGuid;
}
public String getNewGuid() {
return newGuid;
}
public void setNewGuid(String newGuid) {
this.newGuid = newGuid;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getVar1() {
return var1;
}
public void setVar1(String var1) {
this.var1 = var1;
}
public String getVar2() {
return var2;
}
public void setVar2(String var2) {
this.var2 = var2;
}
}
package com.amborsecdmeng.demo.utils;
import java.util.UUID;
public class Utils {
public static String GUID() {
UUID uuid = UUID.randomUUID();
String str = uuid.toString();
String uuidStr = str.replace("-", "");
return uuidStr;
}
}
package com.amborsecdmeng.demo.zerbraprinter;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.net.wifi.WifiManager;
import com.amborsecdmeng.demo.entity.ResultObj;
import com.amborsecdmeng.demo.entity.ZeroSymbolBill;
import com.amborsecdmeng.demo.utils.Utils;
import com.zebra.sdk.comm.BluetoothConnection;
import com.zebra.sdk.comm.Connection;
import com.zebra.sdk.comm.ConnectionException;
import com.zebra.sdk.comm.TcpConnection;
import com.zebra.sdk.printer.PrinterLanguage;
import com.zebra.sdk.printer.PrinterStatus;
import com.zebra.sdk.printer.ZebraPrinter;
import com.zebra.sdk.printer.ZebraPrinterFactory;
import com.zebra.sdk.printer.ZebraPrinterLinkOs;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Set;
/**
* ZQ520 Printer
*/
public class ZQ520Printer {
private Context context;
public ZQ520Printer(Context context) {
this.context = context;
}
private Connection connection = null;
private boolean isConnByBluetooth = true;
private String bluetoothMacAddress = "";// Test Machine mac address: AC:3F:A4:E4:D6:3F
private String tcpAddress = "127.0.0.1";
private Integer tcpPortNumber = 0;
private String printerStatusMsg = "";
/**
* ZeroSymbolBill Printer
*
* @param codeMsg BQ code message.
* Example:
* W,NEW00182631190729C0001,P2a-J60102,2T459M000-000-G5,20190729,WmL-J76036,2000,PCS,VCN00182631190729C0001
* @param splitQty split quantity
* @return
*/
public ResultObj zeroSymbolBill(String codeMsg, Integer splitQty) {
String[] codeMsgArr = codeMsg.split(",");
ArrayList<ZeroSymbolBill> zsbs = new ArrayList<>();
/* BQ code message before split. include split quantity */
ZeroSymbolBill zsb = new ZeroSymbolBill();
try {
zsb.setType(codeMsgArr[0].trim()); // type.
zsb.setNewGuid(codeMsgArr[1].trim()); // current guid. -- after split. It will become the old guid
zsb.setVar1(codeMsgArr[2].trim()); //
zsb.setPn(codeMsgArr[3].trim()); // p/n
zsb.setDate(codeMsgArr[4].trim()); // date
zsb.setVar2(codeMsgArr[5].trim()); //
zsb.setQty(Integer.valueOf(codeMsgArr[6].trim())); // quantity
zsb.setUnit(codeMsgArr[7].trim()); // unit
zsb.setSplitQty(splitQty);
} catch (NumberFormatException e) {
return new ResultObj(false, "Exc-03: "+e.getMessage());
}
if (zsb.getSplitQty() >= zsb.getQty())
return new ResultObj(false, "the split quantity must be less than quantity");
/* BQ code message after split. return an arrayList */
zsbs.add(new ZeroSymbolBill(
zsb.getType(),
Utils.GUID(), // set new guid for BQ code after split
zsb.getVar1(),
zsb.getPn(),
zsb.getDate(),
zsb.getVar2(),
zsb.getSplitQty(),// first split. quantity = split quantity
zsb.getUnit(),
zsb.getNewGuid(), // become the old guid after split
0
));
zsbs.add(new ZeroSymbolBill(
zsb.getType(),
Utils.GUID(), // set new guid for BQ code after split
zsb.getVar1(),
zsb.getPn(),
zsb.getDate(),
zsb.getVar2(),
zsb.getQty() - zsb.getSplitQty(),// second split. quantity = quantity - split quantity
zsb.getUnit(),
zsb.getNewGuid(), // become the old guid after split
0
));
/* BQ code split completion */
if (isConnByBluetooth) {
/* get first bonded mac address */
bluetoothMacAddress = findBluetoothMacAddress().get(0);
connection = new BluetoothConnection(bluetoothMacAddress);
} else {
try {
connection = new TcpConnection(tcpAddress, tcpPortNumber);
} catch (NumberFormatException e) {
return new ResultObj(false, "Exc-01: Tcp connection open failed");
}
}
try {
connection.open();
ZebraPrinter printer = ZebraPrinterFactory.getInstance(connection);
ZebraPrinterLinkOs linkOsPrinter = ZebraPrinterFactory.createLinkOsPrinter(printer);
PrinterStatus printerStatus = (linkOsPrinter != null) ? linkOsPrinter.getCurrentStatus() : printer.getCurrentStatus();
if (printerStatus.isReadyToPrint) {
return sendToPrint(printer, zsbs);
} else if (printerStatus.isHeadOpen)
printerStatusMsg = "Err-01: Head Open! \n Please close Printer Head to print. ";
else if (printerStatus.isHeadCold)
printerStatusMsg = "Err-02: Head Cold! \n Please try again. ";
else if (printerStatus.isHeadTooHot)
printerStatusMsg = "Err-03: Head too hot! \n Please do it later. ";
else if (printerStatus.isPaperOut)
printerStatusMsg = "Err-04: Media Out! \n Please load Media to Print. ";
else if (printerStatus.isPartialFormatInProgress)
printerStatusMsg = "Err-05: Head Open! \n Please try again later. ";
else if (printerStatus.isPaused)
printerStatusMsg = "Err-06: Printer Paused. ";
else if (printerStatus.isReceiveBufferFull)
printerStatusMsg = "Err-07: Buffer full! \n Please do it later. ";
else if (printerStatus.isRibbonOut)
printerStatusMsg = "Err-08: Ribbon Out! \n Please retry after adjustment. ";
connection.close();
return new ResultObj("".equals(printerStatusMsg), printerStatusMsg);
} catch (Exception e) {
return new ResultObj(false, "Exc-02: " + e.getMessage());
} finally {
}
}
private ResultObj sendToPrint(ZebraPrinter printer, ArrayList<ZeroSymbolBill> zsbs) {
String filename = "TEMP.LBL";
try {
File file = context.getFileStreamPath(filename);
if (!file.exists())
file.createNewFile();
createZPLFile(printer, filename, zsbs);
printer.sendFileContents(file.getAbsolutePath());
return new ResultObj(true, "filepath: " + file.getAbsolutePath());
} catch (IOException e) {
return new ResultObj(false, e.getMessage());
} catch (ConnectionException e) {
return new ResultObj(false, e.getMessage());
}
}
private void createZPLFile(ZebraPrinter printer, String filename, ArrayList<ZeroSymbolBill> zsbs) throws IOException {
FileOutputStream os = context.openFileOutput(filename, Context.MODE_PRIVATE);
byte[] zplByte = null;
PrinterLanguage printerLanguage = printer.getPrinterControlLanguage();
if (printerLanguage == printerLanguage.ZPL) {
StringBuilder sb = new StringBuilder();
for (ZeroSymbolBill zsb : zsbs) {
sb.append(buildZPLTemplate(zsb));
}
zplByte = sb.toString().getBytes();
}
os.write(zplByte);
os.flush();
os.close();
}
private String buildZPLTemplate(ZeroSymbolBill zsb) {
final String formName = "Zero Symbol Bill";
zsb.setNewGuid(Utils.GUID());
StringBuilder sb = new StringBuilder();
sb.append("^XA\n")
.append("^LL320\n")
.append("^PW400\n")
.append("^LH0,0\n")
.append("^CI26\n")
.append("^SEE:GB18030.DAT\n")
.append("^FO48,20^AEN,10,10^FD" + formName + "^FS\n")//Zero Symbol Bill
.append("^FO24,62^AEN,10,10^FDP/N:^FS\n")//P/N
.append("^FO54,102^AEN,10,10^FD" + zsb.getPn() + "^FS\n")//2T459M000-000-G5
.append("^FO24,142^AEN,10,10^FDQTY:^FS\n")//QTY
.append("^FO54,182^AEN,10,10^FD" + zsb.getQty() + " PCS^FS\n")//200000
.append("^FO24,222^AEN,10,10^FDDATE:^FS\n")//DATE
.append("^FO54,264^AEN,10,10^FD" + zsb.getDate() + "^FS\n")//20191107
.append("^FO280,172^BQN,2,2^FD\n")
.append(" " + zsb.getType()
+ "," + zsb.getNewGuid()
+ "," + zsb.getVar1()
+ "," + zsb.getPn()
+ "," + zsb.getDate()
+ "," + zsb.getVar2() + "," + zsb.getQty() + ",PCS," + zsb.getOldGuid() + "^FS\n")
.append("^XZ");
return sb.toString();
}
/**
* find bluetooth mac address list
*
* @return
*/
private ArrayList<String> findBluetoothMacAddress() {
ArrayList<String> macAddressList = new ArrayList<>();
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
Set<BluetoothDevice> devices = mBluetoothAdapter.getBondedDevices();
for (BluetoothDevice device : devices)
macAddressList.add(device.getAddress());
return macAddressList;
}
/**
* get Local Mac Address
*
* @return
*/
private String getLocalMacAddress(){
WifiManager wifi = (WifiManager)context.getSystemService(Context.WIFI_SERVICE);
return wifi.getConnectionInfo().getMacAddress();
}
}
打包 arr 文件
arr 文件和 jar 文件类似都可以理解为一个类库文件。不同的是,arr 文件包含了class以及res资源文件,而 jar 文件只包含了 class 文件。
至于这两种文件详细的异同,请自行查阅资料,本文不做详细介绍。
-
在 Android Studio 代码界面最右侧 “gradle” 选项,选中开始创建的 Android Library 目录,依次展开:Tasks --> build --> assemble。 双击执行。
-
打包 arr 文件。(输出在 /build/outputs/arr 目录下)
其中,zebraprinter-release.aar 文件就是打包好的发布版本(才发现,模块名字中我将 zebra 误写成 zerbra 了)
调用 arr 接口
以上,arr 接口开发以及打包已经完成,按理来说,这里只需要将 arr 文件打包发送 Android 客户端的开发人员即可。
但是,我们这里先自行测试一下。
-
将 arr 文件 copy 到 项目 app 模块(最开始创建的 Android 应用空模板)的 libs 目录下并重命名(记得切换到 project 显示,否则不显示 libs 包)。
-
修改 build.gradle 配置文件(如不修改,在使用时 Android Studio 也会提示修改,建议手动修改)
在 app 模块的 build.gradle 文件中的 dependencies 代码块中,加入如下代码:implementation files('libs/zebraprinter.aar')
-
在 app 模块的 activity_main.xml 界面设计文件中,增加一个按钮用于测试。
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/btnPrintTest" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="67dp" android:text="PrintTest" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout>
-
在 app 模块的 MainActivity 文件中为 printtest 按钮增加点击监听事件,调用 arr 接口中的 zeroSymbolBill 方法发送打印命令
package com.foxconn.mac1.zebraprintdemo; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.Toast; import com.foxconn.mac1.zebraprinter.Entity.ResultObj; import com.foxconn.mac1.zebraprinter.ZebraPrinter.ZQ520Printer; public class MainActivity extends AppCompatActivity { protected Button btnPrint; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnPrint = this.findViewById(R.id.btnPrintTest); btnPrint.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View view){ printTest(); } }); } public void printTest(){ try { /* Example for Test */ String BQcode = "W,VCN00182631190729C0001,P2a-J60102,2T459M000-000-G5,20190729,WmL-J76036,7200,PCS"; Integer split = 2000; ZQ520Printer zq520Printer = new ZQ520Printer(this); ResultObj resultObj = zq520Printer.zeroSymbolBill(BQcode, split); if (resultObj.isStatus()){ Toast.makeText(MainActivity.this, "print test success", Toast.LENGTH_SHORT).show(); }else{ Toast.makeText(MainActivity.this, resultObj.getMessage(), Toast.LENGTH_SHORT).show(); } /* Example end */ } catch (Exception e) { e.printStackTrace(); Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show(); } } }
至此,代码开发及测试已经完成。
附加:APP 打包成 APK 文件
-
选中 app 模块,依次选择 Build --> Generate Signed Bundle / APK
-
选择打包类型。
-
选择 Android APP Bundle 会生成 aab 格式文件;
-
选择 APK 会生成 apk 格式文件;
-
-
配置 app 开发证书信息
-
选择最终生成 apk 文件的目录
-
打包成功
打包完成,Copy 到 Android 客户端即可安装测试。