• ANDROID集成佳博热敏打印机打印小票功能


    一、说明

    最近公司项目需要做打印机打印小票功能,首先公司买了一个佳博小票打印机作为测试用机。然后在开发的过程中也遇到一些坑,在此记录一下。

    二、集成过程

    1. 下载开发文档

    首先需要去其官网下载SDK可开发文档:http://www.gainscha.cn/download/24

    2. 在MANIFEST中添加必要的声明

    // 权限声明
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.hardware.usb.accessory" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    <uses-permission android:name="android.permission.GET_TASKS" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-feature android:name="android.hardware.usb.host" />
    
    // service声明
    <service
        android:name="com.gprinter.service.GpPrintService" 
        android:enabled="true"
        android:exported="true" 
        android:label="GpPrintService" >
        <intent-filter>
            <action android:name="com.gprinter.aidl.GpPrintService" />
            </intent-filter>
    </service>
    <service android:name="com.gprinter.service.AllService" ></service>

    3. AIDL

    在main文件下添加aidl文件夹,然后在内部建com.gprinter.aidl文件夹。切记:创建文件夹的时候不要直接把文件夹名称写成com.gprinter.aidl,这样只会创建一个文件夹,要分别创建com,在com内部伊娃gprinter,在gprinter内部创建aidl,最后在aidl内部创建GpService.aidl,其代码如下:

    package com.gprinter.aidl;
    interface GpService{  
        int openPort(int PrinterId,int PortType,String DeviceName,int PortNumber);
        void closePort(int PrinterId);
        int getPrinterConnectStatus(int PrinterId);
        int printeTestPage(int PrinterId);   
        void queryPrinterStatus(int PrinterId,int Timesout,int requestCode);
        int getPrinterCommandType(int PrinterId);
        int sendEscCommand(int PrinterId, String b64);
        int sendLabelCommand(int PrinterId, String  b64);
        void isUserExperience(boolean userExperience);
        String getClientID();
        int setServerIP(String ip, int port);
    } 

    4. 启动并绑定服务

    private PrinterServiceConnection conn = null;
    
    class PrinterServiceConnection implements ServiceConnection {
            @Override
            public void onServiceDisconnected(ComponentName name) {
                Log.i("ServiceConnection", "onServiceDisconnected() called");
                mGpService = null;
            }
    
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                mGpService = GpService.Stub.asInterface(service);
                connectToPrinter();
            }
        }
    
    /**
     * 绑定打印服务
     */
    public void bindPrintService() {
        conn = new PrinterServiceConnection();
        Intent intent = new Intent(mActivity, GpPrintService.class);
        mActivity.bindService(intent, conn, Context.BIND_AUTO_CREATE); // bindService
    
        // 注册实时状态查询广播
        mActivity.registerReceiver(mBroadcastReceiver, new IntentFilter(GpCom.ACTION_DEVICE_REAL_STATUS));
        mActivity.registerReceiver(mBroadcastReceiver, new IntentFilter(GpCom.ACTION_RECEIPT_RESPONSE));
        mActivity.registerReceiver(mBroadcastReceiver, new IntentFilter(GpCom.ACTION_LABEL_RESPONSE));
    }
    
    /**
     * 解绑打印服务
     */
    public void unBindPrintService() {
         mActivity.unregisterReceiver(mBroadcastReceiver);
         disConnectToPrinter();
    }
    
    /**
     * 断开与打印机的连接
     */
     public void disConnectToPrinter() {
         try {
            mGpService.closePort(DEFAULT_PRINTER_ID);
         } catch (RemoteException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
         }
     }

    5. 连接打印机

    首先你需要获取到已经配对的蓝牙信息,然后找到是你打印机的蓝牙名称,用这个蓝牙名称进行配对。

        /**
         * 获取已配对的设备
         */
        public void initPairedDevice() {
            BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    
            Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
            // If there are paired devices, add each one to the ArrayAdapter
            if (pairedDevices.size() > 0) {
                for (BluetoothDevice device : pairedDevices) {
                    if (device.getName().startsWith("Printer_")) {
                        connectedDeviceList.add(device.getName() + "," + device.getAddress());
                    }
                }
            }
        }
    
        /**
         * 连接到打印机
         */
        public void connectToPrinter() {
            LogUtils.d(">>>>>>>>> 连接打印机");
            int rel = 0;
    
            if (connectedDeviceList != null && connectedDeviceList.size() > 0) {
                String device = connectedDeviceList.get(0);
                int subIndex = device.indexOf(",");
                String address = connectedDeviceList.get(0).substring(subIndex + 1);
                if (!TextUtils.isEmpty(address)) {
                    try {
                        rel = mGpService.openPort(DEFAULT_PRINTER_ID, 4, address, 0);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
    
            GpCom.ERROR_CODE r = GpCom.ERROR_CODE.values()[rel];
    
            if (r == GpCom.ERROR_CODE.SUCCESS) {
                LogUtils.d("打印机连接成功");
            } else {
                ToastBox.showBottom(mActivity, GpCom.getErrorText(r));
            }
            LogUtils.d("打印机连接状态:   " + GpCom.getErrorText(r));
        }

    到此为止,配置工作就已经完成了。首先你需要打开手机蓝牙,寻找以Gprinter开头的蓝牙名称,输入密码0000连接好打印机。然后用上面的代码连接到打印机,再用以下代码就可以打印出信息。

    EscCommand esc = new EscCommand();
    esc.addInitializePrinter();
    esc.addPrintAndFeedLines((byte) 3);
    esc.addSelectJustification(JUSTIFICATION.CENTER);// 设置打印居中 esc.addSelectPrintModes(FONT.FONTA, ENABLE.OFF, ENABLE.ON, ENABLE.ON, ENABLE.OFF);// 设置为倍高倍宽 esc.addText("Sample\n"); // 打印文字
    esc.addPrintAndLineFeed();
    /* 打印文字 */
    esc.addSelectPrintModes(FONT.FONTA, ENABLE.OFF, ENABLE.OFF, ENABLE.OFF, ENABLE.OFF);// 取消倍高倍宽 esc.addSelectJustification(JUSTIFICATION.LEFT);// 设置打印左对齐
    esc.addText("Print text\n"); // 打印文字
    esc.addText("Welcome to use SMARNET printer!\n"); // 打印文字
    /* 打印繁体中文 需要打印机支持繁体字库 */ String message = "佳博智匯票據打印機\n"; // esc.addText(message,"BIG5"); esc.addText(message, "GB2312"); esc.addPrintAndLineFeed();
    /* 绝对位置 具体详细信息请查看GP58编程手册 */ esc.addText("智汇"); esc.addSetHorAndVerMotionUnits((byte) 7, (byte) 0); esc.addSetAbsolutePrintPosition((short) 6); esc.addText("网络"); esc.addSetAbsolutePrintPosition((short) 10); esc.addText("设备");
    esc.addPrintAndLineFeed();
    /* 打印图片 */
    esc.addText("Print bitmap!\n"); // 打印文字 Bitmap b = BitmapFactory
    .decodeResource(getResources(), R.drawable.gprinter); esc.addRastBitImage(b, 384, 0); // 打印图片
    /* 打印一维条码 */
    esc.addText("Print code128\n"); // 打印文字 esc.addSelectPrintingPositionForHRICharacters(HRI_POSITION.BELOW);// // 设置条码可识别字符位置在条码下方
    esc.addSetBarcodeHeight((byte) 60); // 设置条码高度为60点 esc.addSetBarcodeWidth((byte) 1); // 设置条码单元宽度为1 esc.addCODE128(esc.genCodeB("SMARNET")); // 打印Code128码 esc.addPrintAndLineFeed();
    /*
    * QRCode命令打印 此命令只在支持QRCode命令打印的机型才能使用。 在不支持二维码指令打印的机型上,则需要发送二维条码图片 */
    esc.addText("Print QRcode\n"); // 打印文字 esc.addSelectErrorCorrectionLevelForQRCode((byte) 0x31); // 设置纠错等级 esc.addSelectSizeOfModuleForQRCode((byte) 3);// 设置qrcode模块大小 esc.addStoreQRCodeData("www.smarnet.cc");// 设置qrcode内容 esc.addPrintQRCode();// 打印QRCode
    esc.addPrintAndLineFeed();
    /* 打印文字 */
    esc.addSelectJustification(JUSTIFICATION.CENTER);// 设置打印左对齐 esc.addText("Completed!\r\n"); // 打印结束
    // 开钱箱
    esc.addGeneratePlus(LabelCommand.FOOT.F5, (byte) 255, (byte) 255); esc.addPrintAndFeedLines((byte) 8);
    Vector<Byte> datas = esc.getCommand(); // 发送数据
    byte[] bytes = GpUtils.ByteTo_byte(datas);
    String sss = Base64.encodeToString(bytes, Base64.DEFAULT); int rs;
    try {
    rs = mGpService.sendEscCommand(mPrinterIndex, sss); GpCom.ERROR_CODE r = GpCom.ERROR_CODE.values()[rs]; if (r != GpCom.ERROR_CODE.SUCCESS) {
    Toast.makeText(getApplicationContext(), GpCom.getErrorText(r), Toast.LENGTH_SHORT).show(); }
    } catch (RemoteException e) {
    // TODO Auto-generated catch block e.printStackTrace();
    }

    三、问题

    以上的过程可以让你打印小票了,但是打印的时候肯定还会遇到一些问题,比如文字的对齐问题,文字的换行问题等等。

    问题1:文字大小

    很遗憾,并没有可以设置具体大小的API,我只找到了倍高、倍宽、倍高+倍宽这3种字体大小模式。

    问题2:怎样进行对齐

    比如:商品、单价、数量、金额,它们的排列需要像表格一样对齐。利用以下2个API可以进行对齐设置:

    // 设置单位距离
    esc.addSetHorAndVerMotionUnits((byte) 7, (byte) 0);
    // 移动的距离(距离 = 单位 * position设定值)
    esc.addSetAbsolutePrintPosition(20);

    但这个对齐其实是有问题的,它的值的计算可以参考《佳博热敏票据打印机编程手册》,文档给出的是你将移动单位长度设置为7,这个长度大约等于1个字的长度,但是不够精准,其实际长度略有偏差,比较坑的一点就是这个距离并不能设定为小数,只能精确到整数位,我的做法是将单位长度设定为整个字的1/3,这样就可以更加精准一些。

    public static final short PRINT_POSITION_0 = 0;
    public static final short PRINT_POSITION_1 = 26 * 3;
    public static final short PRINT_POSITION_2 = 32 * 3;
    public static final short PRINT_POSITION_3 = 42 * 3;
    public static final int MAX_GOODS_NAME_LENGTH = 22 * 3;
    // 将unit设置为这个单位值,其实际距离大约是一个字的1/3
    public static final short PRINT_UNIT = 43;
    // 商品头信息
    esc.addSetHorAndVerMotionUnits((byte) PRINT_UNIT, (byte) 0);
    esc.addText("商品名");
    
    esc.addSetAbsolutePrintPosition(PRINT_POSITION_1);
    esc.addText("单价");
    
    esc.addSetAbsolutePrintPosition(PRINT_POSITION_2);
    esc.addText("数量");
    
    esc.addSetAbsolutePrintPosition(PRINT_POSITION_3);
    esc.addText("金额");

    在这样的设置下,基本可以对齐并占满整个小票打印纸,而具体的商品信息你则可以以这些位置为基准进行具体放置。

    问题3:商品名称过长换行问题

    有的时候商品名称比较长,一行是放不下的,这个时候你就需要对商品名称切割成若干行。但是切割的时候又会有个问题,商品信息里面有汉字,有字母,还会有数字和特殊符号,如果切割不好,很有可能会切出乱码来。

    一般一个汉字占2个字节,一个英文字母是占1个字节;平常在占位上一个汉字是占2个英文字母的位置,平常开发的时候我们一般是以UTF-8格式的,如果想计算好宽度又需要将其转为gbk 或 gb2312,总体来说需要考虑的面还是比较多的,废话不多说,直接贴出代码:

    /**
     * 按字节截取字符串
     */
    public class SubByteString {
    
        public static String subStr(String str, int subSLength) throws UnsupportedEncodingException{
            if (str == null)
                return "";
            else{
                int tempSubLength = subSLength;//截取字节数
                String subStr = str.substring(0, str.length()<subSLength ? str.length() : subSLength);//截取的子串
                int subStrByetsL = subStr.getBytes("GBK").length;//截取子串的字节长度
                //int subStrByetsL = subStr.getBytes().length;//截取子串的字节长度
                // 说明截取的字符串中包含有汉字
                while (subStrByetsL > tempSubLength){
                    int subSLengthTemp = --subSLength;
                    subStr = str.substring(0, subSLengthTemp>str.length() ? str.length() : subSLengthTemp);
                    subStrByetsL = subStr.getBytes("GBK").length;
                    //subStrByetsL = subStr.getBytes().length;
                }
                return subStr;
            }
        }
    
        public static String[] getSubedStrings(String string, int unitLength) {
            if (TextUtils.isEmpty(string)) {
                return null;
            }
    
            String str = new String(string);
    
            int arraySize = 0;
            try {
                arraySize = str.getBytes("GBK").length / unitLength;
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
    
            if (str.getBytes().length % unitLength > 0) {
                arraySize++;
            }
    
            String[] result = new String[arraySize];
    
            for (int i = 0; i < arraySize; i++) {
                try {
                    result[i] = subStr(str, unitLength);
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                str = str.replace(result[i], "");
    //            LogUtils.d(">>>>>>  " + result[i]);
            }
    
            return result;
        }
    }

    用以上代码可以将一个字符串切割成一个字符数组,这样将第一行显示不下的再多打印几行即可完成商品名称换行问题。

    最后,我将格式化整个商品信息小票打印的代码贴出来,以供参考:

    public class PrintSplitUtil {
    
        private static final String PRINT_LINE = "------------------------------------------------\n";
    
        public static final int PRINT_TOTAL_LENGTH = 48 * 3;
        public static final short PRINT_POSITION_0 = 0;
        public static final short PRINT_POSITION_1 = 26 * 3;
        public static final short PRINT_POSITION_2 = 32 * 3;
        public static final short PRINT_POSITION_3 = 42 * 3;
        public static final int MAX_GOODS_NAME_LENGTH = 22 * 3;
    
        public static final short PRINT_UNIT = 43;
    
        public static PrinterSplitInfo getPrintText(Context context, GoodsListInfo goodsInfo, String store, String userMobile, String qrCode) {
    
            PrinterSplitInfo printerSplitInfo = new PrinterSplitInfo();
    
            EscCommand esc = new EscCommand();
            esc.addInitializePrinter();
    
            // 顶部图片
            esc.addSelectJustification(JUSTIFICATION.CENTER);
            Bitmap b = BitmapFactory.decodeResource(context.getResources(), R.mipmap.printer_logo);
            esc.addRastBitImage(b, 200, 0); // 打印图片
    
            esc.addPrintAndLineFeed();
    
            esc.addText(PRINT_LINE);
    
            // 订单信息
            if (!TextUtils.isEmpty(store)) {
                esc.addSelectJustification(JUSTIFICATION.LEFT);
                esc.addSelectPrintModes(FONT.FONTA, ENABLE.ON, ENABLE.OFF, ENABLE.OFF, ENABLE.OFF);
                esc.addText(store + "\n"); // 打印文字
            }
    
            esc.addSelectJustification(JUSTIFICATION.LEFT);
            esc.addSelectPrintModes(FONT.FONTA, ENABLE.OFF, ENABLE.OFF, ENABLE.OFF, ENABLE.OFF);
    
            // 头部信息
            esc.addText("打印编号:" + goodsInfo.express_sn);
            esc.addPrintAndLineFeed();
            esc.addText("操作时间:" + DateTimeUtil.getCurrentDateTime());
            esc.addPrintAndLineFeed();
            esc.addText("操作员:" + userMobile);
            esc.addPrintAndLineFeed();
    
            esc.addText(PRINT_LINE);
    
            // 商品头信息
            esc.addSetHorAndVerMotionUnits((byte) PRINT_UNIT, (byte) 0);
            esc.addText("商品名");
    
            esc.addSetAbsolutePrintPosition(PRINT_POSITION_1);
            esc.addText("单价");
    
            esc.addSetAbsolutePrintPosition(PRINT_POSITION_2);
            esc.addText("数量");
    
            esc.addSetAbsolutePrintPosition(PRINT_POSITION_3);
            esc.addText("金额");
    
            esc.addPrintAndLineFeed();
    
            // 商品信息
            if (goodsInfo.goods_list != null && goodsInfo.goods_list.size() > 0) {
                esc.addSelectPrintModes(FONT.FONTA, ENABLE.OFF, ENABLE.ON, ENABLE.OFF, ENABLE.OFF);
                for (int i = 0; i < goodsInfo.goods_list.size(); i++) {
                    GoodsListInfo.GoodsListBean goods = goodsInfo.goods_list.get(i);
    
                    String[] goodsNames = SubByteString.getSubedStrings(goods.goods_name, 20);
                    printerSplitInfo.dataRow += goodsNames.length;
    
                    // 商品名称
                    if (goodsNames != null && goodsNames.length > 0) {
                        esc.addText((i + 1) + "." + goodsNames[0]);
                    } else {
                        esc.addText((i + 1) + "." + goods.goods_name);
                    }
    
                    esc.addSetHorAndVerMotionUnits((byte) PRINT_UNIT, (byte) 0);
    
                    // 单价
                    short priceLength = (short) goods.goods_price.length();
                    short pricePosition = (short) (PRINT_POSITION_1 + 12 - priceLength * 3);
                    esc.addSetAbsolutePrintPosition(pricePosition);
                    esc.addText(goods.goods_price);      // 单价还未获取
    
                    // 数量
                    short numLength = (short) (goods.goods_num + goods.goods_unit).getBytes().length;
                    short numPosition = (short) (PRINT_POSITION_2 + 14 - numLength * 3);
                    esc.addSetAbsolutePrintPosition(numPosition);
                    esc.addText(goods.goods_num + goods.goods_unit);
    
                    // 金额
                    short amountLength = (short) goods.goods_amount.replace(" ", "").getBytes().length;
                    short amountPosition = (short) (PRINT_POSITION_3 + 11 - amountLength * 3);
                    esc.addSetAbsolutePrintPosition(amountPosition);
                    esc.addText(goods.goods_amount);
    
                    if (goodsNames == null || goodsNames.length == 0) {
                        esc.addPrintAndLineFeed();
                    } else if (goodsNames != null && goodsNames.length > 1) {
                        for (int j = 1; j < goodsNames.length; j++) {
                            esc.addText("" + goodsNames[j]);
                            esc.addPrintAndLineFeed();
                        }
                    }
                }
    
                esc.addSelectPrintModes(FONT.FONTA, ENABLE.OFF, ENABLE.OFF, ENABLE.OFF, ENABLE.OFF);
                esc.addText(PRINT_LINE);
            }
    
            // 总计信息
            esc.addSelectJustification(JUSTIFICATION.RIGHT);// 设置打印居右
    
            if (!TextUtils.isEmpty(goodsInfo.subsidy)) {
                esc.addText("优惠补贴:" + goodsInfo.subsidy + "元\n");
            }
    
            if (!TextUtils.isEmpty(goodsInfo.goods_amount)) {
                esc.addText("金额总计:" + goodsInfo.goods_amount + "元\n");
            }
    
            if (!TextUtils.isEmpty(goodsInfo.order_amount)) {
                esc.addText("还需支付:" + goodsInfo.order_amount + "元\n");
            }
    
            esc.addText(PRINT_LINE);
    
    //        打印二维码
            if (!TextUtils.isEmpty(qrCode)) {
                esc.addPrintAndLineFeed();
                esc.addSelectJustification(JUSTIFICATION.CENTER);// 设置打印居中
                esc.addText("请打开微信,扫码付款\n");
                esc.addPrintAndLineFeed();
                // 48  49  50  51
                esc.addSelectErrorCorrectionLevelForQRCode((byte) 0x31); // 设置纠错等级
                esc.addSelectSizeOfModuleForQRCode((byte) 7);// 设置qrcode模块大小
                esc.addStoreQRCodeData(qrCode);// 设置qrcode内容
    
                esc.addPrintQRCode();// 打印QRCode
    
                esc.addPrintAndLineFeed();
                esc.addText("请将二维码放平整后再扫码\n");
            }
            esc.addPrintAndFeedLines((byte) 3);
    
            // 加入查询打印机状态,打印完成后,此时会接收到GpCom.ACTION_DEVICE_STATUS广播
            esc.addQueryPrinterStatus();
    
            // 最终数据
            Vector<Byte> datas = esc.getCommand();
            byte[] bytes = GpUtils.ByteTo_byte(datas);
            String result = Base64.encodeToString(bytes, Base64.DEFAULT);
    
            printerSplitInfo.data = result;
    
            return printerSplitInfo;
        }
    
    }

    问题4:打印状态兼听

    在一些情况下我们需要监听打印状态,比如:正在打印、打印成功、打印异常、打印机缺纸,然后在客户端以对话框的形式展示打印状态,类似微信聊天中正在语音、取消语音。

    • 正在打印:当打印命令发出去后,你就可以显示正在打印。
    • 打印异常:这个基本上也是实时的,如果有异常会立刻返回异常值。
    • 打印成功:这个需要在打印命令最后加一个查询指令,如果打印成功后会以广播的方式传递回结果。
    • 打印机缺纸:这个是需要实时查询的,在打印期间你需要每隔几秒就查询一下打印机的状态,当查询到缺纸的时候就你就可以显示缺纸对话框。

    补充:
    有的时候你加了查询打印成功的指令,却并没有接收到打印成功的广播,而App中的“正在打印”还傻乎乎的在那儿等着你告诉他打印成功了。 这个就需要你计算打印速度,当程序估计在一定的时间内打印应该已经成功了,打印机还没有给你成功的提示,你就自动隐藏掉“正在打印”的对话框。

    四、总结

    其实在开发打印机的过程中还遇到了很多问题,时间有限就不一一列举了。其官方的SDK虽然封装了不少,但感觉并不完善,好多时候还是需要开发者自己去写一大堆代码去实现,用起来很不友好。最后附上开发完成的小票打印样式,以供参考。

    这里写图片描述

     
    版权声明:本文为haha223545原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
    本文链接:https://blog.csdn.net/haha223545/article/details/80569597
  • 相关阅读:
    12.16省选模拟t2 消防
    12.17省选模拟t3 围豆豆
    12.17省选模拟t1 生日礼物
    CF1322D Reality Show
    winform拖动无边框窗体
    关于ToolTip控件在XP系统中问题
    JDK源代码里面的一个for循环
    IIS5.1 无法运行asp.net网站但可访问静态页的解决方案
    winform窗体去掉标题头部的两种方式
    C# 语法之泛型
  • 原文地址:https://www.cnblogs.com/Alex80/p/16539583.html
Copyright © 2020-2023  润新知