【Android】佳博打印机Gprinter实现打印功能(云打印、竖向打印)


前言

之前公司有个需求,想要打印学生的假条,但是所用纸张较小,宽度在100mm~150mm之间,打印如外卖小票、快递单据等的标签打印机,打印机基本上只用于横向打印,由于纸张太窄只能想办法实现竖向打印。因为佳博的SDK的比较完善,且有云打印功能,所以选用佳博的打印机。

本文将介绍使用佳博打印机实现云打印,竖向打印。

实现效果:

在这里插入图片描述


一、打印原理

先了解打印机的工作原理:

打印机只能接受符合一定标准的指令才能去执行打印,通过指令去控制打印格式,所以只需要控制好发送给打印机的指令信息就可以完成打印。无论使用佳博、TSC还是其他第三方SDK、DEMO,只要发送的打印指令标准一致,均可控制不同品牌的打印机

在这里插入图片描述

打印指令:直接驱动打印机使用的指令,非常精简,通常需要专用的手册解释其含义(行业标准,如TSPL、CPCL、ESC、ZPLII指令,不同指令有不同的语法)
SDK:将抽象的打印机指令封装为可调用,易读的接口,不可单独运行
DEMO:演示SDK或打印机特定功能的例子,可单独运行
文档:用于阐述SDK、DEMO或指令的定义与使用方法

二、实现

打印机主要注意这几点:打印类型、打印宽度(纸张大小)、打印指令、打印机接口(是否有网口或者wifi去支持云打印)

打印的时候选择间隙纸会比连续纸多出大部分空白纸,一般都设置连续纸
在这里插入图片描述
佳博主要的打印机类型:

  1. 小票打印机:58mm或者80mm
    在这里插入图片描述
  2. 标签打印机:40mm、50mm、70mm
    在这里插入图片描述
  3. 面单打印机: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/

  1. 去开发者中心注册账号:获取接口需要的商户编号和api密钥(开发中心->开发信息),管理打印机,查看接口文档
  2. 登录后添加打印机:终端编号就是打印机底部的条形码上的编号
    在这里插入图片描述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();
    }
  1. 生成打印指令
    查看对应指令手册,熟悉语法
    在这里插入图片描述
    先模板中心生成一段基础的指令,^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  //内容
    )
}

  1. 返回结果处理
    在这里插入图片描述

总结

这是灵活使用第三方接口或者库必经的流程,例如elementUI、低代码平台的使用等等。

猜你喜欢

转载自blog.csdn.net/T01151018/article/details/139435404