文章目錄
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 客戶端即可安裝測試。