前言
之前公司有个需求,想要打印学生的假条,但是所用纸张较小,宽度在100mm~150mm之间,打印如外卖小票、快递单据等的标签打印机,打印机基本上只用于横向打印,由于纸张太窄只能想办法实现竖向打印。因为佳博的SDK的比较完善,且有云打印功能,所以选用佳博的打印机。
本文将介绍使用佳博打印机实现云打印,竖向打印。
实现效果:
一、打印原理
先了解打印机的工作原理:
打印机只能接受符合一定标准的指令才能去执行打印,通过指令去控制打印格式,所以只需要控制好发送给打印机的指令信息就可以完成打印。无论使用佳博、TSC还是其他第三方SDK、DEMO,只要发送的打印指令标准一致,均可控制不同品牌的打印机。
打印指令:直接驱动打印机使用的指令,非常精简,通常需要专用的手册解释其含义(行业标准,如TSPL、CPCL、ESC、ZPLII指令,不同指令有不同的语法)
SDK:将抽象的打印机指令封装为可调用,易读的接口,不可单独运行
DEMO:演示SDK或打印机特定功能的例子,可单独运行
文档:用于阐述SDK、DEMO或指令的定义与使用方法
二、实现
打印机主要注意这几点:打印类型、打印宽度(纸张大小)、打印指令、打印机接口(是否有网口或者wifi去支持云打印)
打印的时候选择间隙纸会比连续纸多出大部分空白纸,一般都设置连续纸
佳博主要的打印机类型:
- 小票打印机:58mm或者80mm
- 标签打印机:40mm、50mm、70mm
- 面单打印机:76mm、100mm (不想用竖向打印,可以直接用这种)
打印资料下载:
进入佳博官网:https://cn.gainscha.com/
查看打印机是否支持:
这里我使用的是标签云打印机,ZPLII指令:
1. 本地打印
连接方式主要就是USB或蓝牙打印,厂家也提供了相应的demo,里面有详细代码,可以直接用。打印内容可以用指令,还可以用xml文件或者pdf文件(某些格式用指令不方便时),这里不详细展开。
demo在打印资料里面的佳博官方->Android->标签打印机安卓SDK-V3.3.1-20230327中:
这些打印方法需要导入jar包,jar包在标签打印机demo里面
implementation files('libs/SDKLib.jar')
2. 云打印
云打印就是网络远程由调用佳博接口,由他们的服务器替我们发送指令给打印机,需要支持网络的打印机,下面具体实现。
佳博云打印是软硬件深度整合的云打印服务商,拥有多类型打印设备的研发、制造能力、与各行业打印业务的云端服务能力。满足多终端、跨网络的异地远程打印需求。同时,佳博开放者中心提供丰富的Web API接口、详尽的文档手册及各种解决方案,例如,测试工具、API调试工具、服务监控告警、日志分析系统等。
佳博开发者中心:https://dev.poscom.cn/
- 去开发者中心注册账号:获取接口需要的商户编号和api密钥(开发中心->开发信息),管理打印机,查看接口文档
- 登录后添加打印机:终端编号就是打印机底部的条形码上的编号
3. 调用接口:查看接口文档,打印需要哪些参数,安全校验码最关键
/**
* 发送指令
*/
@FormUrlEncoded //必须要这个注解
@POST("/apisc/sendMsg")
fun sendPrintMsg(
@Field("reqTime") reqTime: String,
@Field("securityCode") securityCode: String,
@Field("memberCode") memberCode: String,
@Field("deviceID") deviceID: String,
@Field("mode") mode: String,
@Field("charset") charset: String,
@Field("msgDetail") msgDetail: String,
): Call<PrintResult>
reqTime:当前UNIX时间戳。13位,精确到毫秒
securityCode:安全校验码。memberCode(商户编号)+deviceID(打印机编号)+msgNo(如果存在,一般不用)+reqTime+apiKey(api密钥)。所有字符串合并后进行 MD5 运算,即 MD5(合并后字符串)。结果字符为小写。
memberCode:商户编号
deviceID:打印机编号
mode:打印信息的格式类型。取值范围[2-3]。这里传"2"
charset:编码格式,取值范围[1-10] 默认1,这里传"4",代表UTF-8
msgDetail:对应的打印指令,开发者中心模板管理也能自动生成指令
我的调用:
val reqTime = Calendar.getInstance().timeInMillis.toString() //时间戳
model.sendPrintMsg(
reqTime,
RxUtils.Md5(memberCode + deviceID + "" + reqTime + apiKey),
memberCode,
deviceID,
"2",
"4",
"",
msgDetail
)
生成md5的方法:
/**
* 生成MD5加密32位字符串
*
* @param MStr :需要加密的字符串
* @return
*/
public static String Md5(String MStr) {
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(MStr.getBytes());
return bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
return String.valueOf(MStr.hashCode());
}
}
// MD5内部算法
private static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
- 生成打印指令:
查看对应指令手册,熟悉语法
先模板中心生成一段基础的指令,^XA和 ^XZ是开始和结束
横向如何变成竖向打印,之前想过纸张整体旋转或者用xml旋转打印,但是效果不好,后面想到一个新的思路:一个字一个字地旋转,先摆好位置,一开始字是歪的,然后再统一旋转就可以完美解决。主要用^FWR文字旋转90度:
下面这两行一个是打印一行字,一个是一条直线,都是用绝对定位的x和y去控制位置。大概只用到这些了,具体语法查看文档。
^FO80,60^AA,24,24^FD测试^FS
^FO100,50^GB240,4,4^FS
下面是我的实现,代码用循环去生成指令:
private fun generateOrder(bean:PrintInfoBean?):String{
var str="^XA" +
"^MCY^MTD^MD8^LT0^MMT^MNN^MTD^PW880^LL1360^PR4^PMN^PON^JMA^LH0,4^LRN^CWA,Z:MSUNG.FNT^CI28"
str += "^FWR" //旋转,假条由横向变竖向
if(bean?.leaveInfoPrinerItems != null && bean.leaveInfoPrinerItems.size>0){
val contentList=bean.leaveInfoPrinerItems2
val otherList=bean.leaveInfoPrinerItems
val title=otherList[0].item?:""
val tag=otherList[1].item?:""
val pSignTitle=otherList[2].item?:""
val tSignTitle=otherList[3].item?:""
val tSign=otherList[4].item?:""
val dateTitle=otherList[5].item?:""
val date=otherList[6].item?:""
val notes1=otherList[7].item?:""
val notes2=otherList[8].item?:""
val notes3=otherList[9].item?:""
//宽:1360 高:880
val start=32*2 //宽起点
val end=1360-50-32*2 //宽终点
var h=880-100-48 //高起点,顶部边距100
val space=32+20 //行距
//标题起点
val tStart= if ((1360 / 2 - title.length * 48 / 2)<0) 0 else (1360 / 2 - title.length * 48 / 2)
for (i in title.indices){
str+="^FO$h,${
tStart+i*48}^AA,48,48^FD${
title[i]}^FS"
}
//加标题行距
h=h-48-40
for (i in tag.indices){
str+="^FO$h,${
start+i*32}^AA,32,32^FD${
tag[i]}^FS"
}
h -= space
var cStart=start+32*2
contentList?.forEachIndexed {
i, item ->
val content= item.item ?:""
for (i in content.indices){
//超过宽度换行
if(cStart>=end){
cStart=start
h -= space
}
str+="^FO$h,$cStart^AA,32,32^FD${
content[i]}^FS"
if(item.isValue==true){
str+="^FO$h,$cStart^GB1,${
if(content[i].isDigit()) 16 else 32},1^FS" //下划线
}
cStart += if(content[i].isDigit()) 16 else 32
}
}
h =h-space-30
val len =if(date.isEmpty()||date.length<11) 11 else date.length
val signMaxWidth= len*32 - 8*16 //签名最大宽度,日期11的长度
val signStart=end - signMaxWidth//可能有数字,数字只占16
for ((m, i) in (pSignTitle.length-1 downTo 0).withIndex()){
//m从1开始
str+="^FO$h,${
signStart-(m+1)*32}^AA,32,32^FD${
pSignTitle[i]}^FS"
}
str+="^FO$h,${
signStart}^GB1,${
signMaxWidth},1^FS" //下划线
h -= space
for ((m, i) in (tSignTitle.length-1 downTo 0).withIndex()){
str+="^FO$h,${
signStart-(m+1)*32}^AA,32,32^FD${
tSignTitle[i]}^FS"
}
str+="^FO$h,${
signStart}^GB1,${
signMaxWidth},1^FS" //下划线
var tSignStart= signStart + if ((signMaxWidth/2-tSign.length*32/2)<0) 0 else (signMaxWidth/2-tSign.length*32/2)
for (i in tSign.indices){
//超过宽度换行
if(tSignStart>=end){
tSignStart=signStart
h -= space
str+="^FO$h,${
signStart}^GB1,${
signMaxWidth},1^FS" //下划线
}
str+="^FO$h,${
tSignStart}^AA,32,32^FD${
tSign[i]}^FS"
tSignStart+=32
}
h -= space
for ((m, i) in (dateTitle.length-1 downTo 0).withIndex()){
str+="^FO$h,${
signStart-(m+1)*32}^AA,32,32^FD${
dateTitle[i]}^FS"
}
str+="^FO$h,${
signStart}^GB1,${
signMaxWidth},1^FS" //下划线
var dStart=signStart
for (i in date.indices){
str+="^FO$h,${
dStart}^AA,32,32^FD${
date[i]}^FS"
//str+="^FO$h,$dStart^GB1,${if(date[i].isDigit()) 16 else 32},1^FS"
dStart += if(date[i].isDigit()) 16 else 32
}
h -= space
for (i in notes1.indices){
str+="^FO$h,${
start+i*32}^AA,32,32^FD${
notes1[i]}^FS"
}
h -= space
for (i in notes2.indices){
str+="^FO$h,${
start+32*2+i*32}^AA,32,32^FD${
notes2[i]}^FS"
}
h -= space
for (i in notes3.indices){
str+="^FO$h,${
start+32*2+i*32}^AA,32,32^FD${
notes3[i]}^FS"
}
}else{
str+="^FO$0,0^AA,48,48^FD无^FS"
}
str += "^XZ"
return str
}
@Keep
data class PrintInfoBean (
/**
* 假条信息 分开的固定部分
*/
val leaveInfoPrinerItems: List<LeaveInfoPrinterItems>? = null,
/**
* 假条信息 分开的中间假条信息部分
*/
val leaveInfoPrinerItems2: List<LeaveInfoPrinterItems>? = null,
){
/**
* LeaveInfoPrinerItems
*/
@Keep
data class LeaveInfoPrinterItems (
val isValue: Boolean? = null, //是否有签字
val item: String? = null //内容
)
}
- 返回结果处理:
总结
这是灵活使用第三方接口或者库必经的流程,例如elementUI、低代码平台的使用等等。