访客机或消费机等设备有连接打印机打印小票的需求,本篇记录一下设备连接USB打印机打印小票内容的功能实现。
其中涉及的工具类包括Other.java、Command.java、PrinterCommand.java和PrinterController.java
连接USB打印机
1. Other.java类
该工具类中只包含一个方法,用于将byteArrays数据转换为Bytes数据。
public class Other {
public static byte[] byteArraysToBytes(byte[][] data) {
int length = 0;
for (byte[] datum : data) {
length += datum.length;
}
byte[] send = new byte[length];
int k = 0;
for (byte[] datum : data) {
for (byte b : datum) {
send[k++] = b;
}
}
return send;
}
}
2. Command.java类
该类主要定义打印机所需的指令。
/**
* 打印机指令
*/
public class Command {
//打印机初始化
public static byte[] ESC_Init = new byte[] {0x1b, 0x40 };
//实时状态传送指令
public static byte[] DLE_eot = new byte[] {0x10, 0x04, 0x04 };
//实时状态传送指令 查询1 打印机
public static final byte[] DLE_eot1 = new byte[] {0x10, 0x04, 0x01};
//实时状态传送指令 查询2 脱机
public static final byte[] DLE_eot2 = new byte[] {0x10, 0x04, 0x02};
//实时状态传送指令 查询3 错误
public static final byte[] DLE_eot3 = new byte[] {0x10, 0x04, 0x03};
//实时状态传送指令 查询4 卷纸传感器
public static final byte[] DLE_eot4 = new byte[] {0x10, 0x04, 0x04};
//获取打印机打印状态 测试
public static final byte[] ESC_STATUS = new byte[] {0x1B, 0x76};
//设置对齐模式
public static byte[] ESC_Align = new byte[] {0x1b, 0x61, 0x00 };
//设置字体加粗
public static final byte[] ESC_BOLD = new byte[] {0x1b, 0x45,0x01};
//横向放大
public static final byte[] ESC_GS_H = new byte[] {0x1b, 0x55, 0x00};
//纵向放大
public static final byte[] ESC_GS_O = new byte[] {0x1b, 0x56, 0x00};
//横向纵向放大
public static final byte[] ESC_GS_O_H = new byte[] {0x1b, 0x57, 0x00};
//换行
public static final byte[] ESC_WRAP= new byte[] {0x0A};
//回车
public static final byte[] ESC_ENTER= new byte[] {0x0D};
//走纸N行, 最后一位是行数,默认一行
public static final byte[] ESC_Paper = new byte[] {0x1B, 0x64, 0x01};
//简体中文字符集
public static final byte[] ESC_Chinese = new byte[] {0x1c, 0x26, 0x1b, 0x74, 0x00};
}
3. PrinterCommand.java类
该类提供打印功能,包括打印机初始化、设置打印格式等功能。
public class PrinterCommand {
/**
* 打印机初始化
*/
public byte[] POS_Set_PrtInit() {
return Other.byteArraysToBytes(new byte[][]{Command.ESC_Init});
}
/**
* 设置对齐模式
*
* @param align 0 左对齐, 1 居中, 2 右对齐
* @return 打印机字节
*/
public byte[] POS_S_Align(int align) {
if (align < 0 || align > 2) {
return null;
}
byte[] data = Command.ESC_Align;
data[2] = (byte) align;
return data;
}
/**
* 换行
* @param count 换行行数1-10
* @return
*/
public byte[] POS_S_PrintLine(int count){
if(count <= 0) count = 1;
if(count >= 10) count = 10;
byte[] data = Command.ESC_Paper;
data[2] = (byte) count;
return data;
}
/**
* 横向放大
* @param font_size 放大尺寸
* @return
*/
public byte[] POS_S_GS_H(int font_size) {
if (font_size <= 0) font_size = 0;
if (font_size >= 7) font_size = 7;
byte[] data = Command.ESC_GS_H;
data[2] = (byte) (font_size );
return data;
}
/**
* 纵向放大
* @param font_size 放大尺寸
* @return
*/
public byte[] POS_S_GS_O(int font_size) {
if (font_size <= 0) font_size = 0;
if (font_size >= 7) font_size = 7;
byte[] data = Command.ESC_GS_O;
data[2] = (byte) (font_size );
return data;
}
/**
* 横向纵向同时放大
* @param font_size 放大尺寸
* @return
*/
public byte[] POS_S_GS_O_H(int font_size) {
if (font_size <= 0) font_size = 0;
if (font_size >= 7) font_size = 7;
byte[] data = Command.ESC_GS_O_H;
data[2] = (byte) ( font_size );
return data;
}
/**
* 加粗
* @return
*/
public byte[] Set_Bold(){
byte[] data = Command.ESC_BOLD;
data[2] = (byte) (1);
return data;
}
/**
* 换行
* @return
*/
public byte[] POS_Set_Enter(){
return Command.ESC_ENTER;
}
}
4. PrinterController.java类
该类为打印控制类,提供了打印机连接、打印内容、监听usb打印机连接状态等接口。
public class PrinterController extends BroadcastReceiver {
private static final String TAG = "PrinterController";
private Context appContext;
private UsbManager mUsbManager;
// 我们的发送端、打印机的接收端
private UsbEndpoint epOut;
// 我们的读取端、打印机的发送端
private UsbEndpoint epIn;
private UsbInterface usbIf;
private UsbDeviceConnection conn;
private UsbDevice dev;
private static final int LINE_BYTE_SIZE = 48;
public static final String PRINTER = "printer";
public static final String MICRO_PRINTER = "micro printer";
public static final String FMT_YYYY_MM_DD_HH_MM2 = "yyyy-MM-dd HH:mm";
public static final String ACTION_USB_PERMISSION = "com.zj.usbconn.USB";
private static final PrinterController INSTANCE = new PrinterController();
public PrinterController() {}
public static PrinterController getInstance() {
return INSTANCE;
}
public void getPrinterContext(Context context) {
appContext = context;
mUsbManager = (UsbManager) appContext.getSystemService(Context.USB_SERVICE);
}
public final synchronized void connect(UsbDevice dev) {
close();
PrinterController.getInstance().dev = dev;
if (!this.isHasPermission(dev)) {
this.getPermission(dev);
Toast.makeText(appContext, "打印机无权限", Toast.LENGTH_SHORT).show();
Log.i(TAG, "打印机无权限getPermission");
} else {
Toast.makeText(appContext, "打印机连接成功", Toast.LENGTH_SHORT).show();
Log.i(TAG, "打印机连接成功connect");
}
}
public final synchronized void tryConnect() {
close();
boolean hasPrinters = false;
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
String name = device.getProductName();
if (name == null || name.isEmpty()) {
continue;
}
name = name.toLowerCase(Locale.CHINA);
if (name.contains(PRINTER) || name.contains(MICRO_PRINTER)) {
hasPrinters = true;
connect(device);
break;
}
}
if (!hasPrinters){
Log.d(TAG, "未搜索到USB打印机!");
Toast.makeText(appContext, "未搜索到USB打印机!", Toast.LENGTH_SHORT).show();
}
}
public final synchronized boolean isHasPermission(UsbDevice dev) {
return mUsbManager.hasPermission(dev);
}
public final synchronized void getPermission(UsbDevice dev) {
if (!isHasPermission(dev)) {
PendingIntent pi = PendingIntent.getBroadcast(appContext, 0, new Intent(ACTION_USB_PERMISSION), 0);
mUsbManager.requestPermission(dev, pi);
Toast.makeText(appContext, "打印机请求权限!", Toast.LENGTH_SHORT).show();
Log.i(TAG, "打印机请求权限requestPermission");
} else {
Toast.makeText(appContext, "打印机连接成功!", Toast.LENGTH_SHORT).show();
Log.i(TAG, "打印机连接成功");
}
}
public final synchronized void sendByte(byte[] bits) {
if (bits == null) {
return;
}
// 打印机正常
if (epOut != null && usbIf != null && conn != null) {
conn.bulkTransfer(epOut, bits, bits.length, 0);
return;
}
Log.d(TAG, "PrinterController sendByte: initDeviceUsb()");
UsbDeviceConnection tmpConn = initDeviceUsb();
if (tmpConn != null && tmpConn.claimInterface(usbIf, true)) {
tmpConn.bulkTransfer(epOut, bits, bits.length, 0);
}
}
public UsbDeviceConnection initDeviceUsb() {
if (conn == null) {
conn = mUsbManager.openDevice(dev);
}
if (dev == null || dev.getInterfaceCount() == 0) {
return null;
}
usbIf = dev.getInterface(0);
if (usbIf == null) {
return null;
}
if (usbIf.getEndpointCount() == 0) {
return null;
}
Log.d(TAG, "PrinterController sendByte: i in 0 until it.endpointCount");
for (int i = 0; i < usbIf.getEndpointCount(); i++) {
UsbEndpoint endpoint = usbIf.getEndpoint(i);
if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK && endpoint.getDirection() == UsbConstants.USB_DIR_OUT) {
epOut = endpoint;
break;
}
}
return conn;
}
private final synchronized void sendByteDirectly(byte[] bits) {
UsbDeviceConnection tmpConn = initDeviceUsb();
if (tmpConn == null) {
return;
}
if (tmpConn.claimInterface(usbIf, true)) { // 获取USB设备接口的控制权
tmpConn.bulkTransfer(epOut, bits, bits.length, 0); // 批量数据传输
}
}
// 读打印机状态, 对海外打印机无用,只对国内打印机有用
// 不能频繁查询,会导致打印机无响应
public int readStatus(byte[] command) {
sendByteDirectly(command);
int count = (usbIf != null ? usbIf.getEndpointCount() : -1);
Log.d(TAG, "PrinterController readStatus: " + count);
if (epIn == null) {
for (int i = 0; i < count; i++) {
UsbEndpoint ep = usbIf != null ? usbIf.getEndpoint(i) : null;
if (ep != null && ep.getType() != UsbConstants.USB_ENDPOINT_XFER_BULK) return -1;
if (ep != null && ep.getDirection()!= UsbConstants.USB_DIR_IN) continue; //不是读端就跳过,一共2个端口,一个读一个写
//读端初始化
if (epIn == null) {
epIn = ep;
Log.d(TAG, "PrinterController read:epIn init");
}
break;
}
}
UsbEndpoint tmpEpIn = epIn;
UsbDeviceConnection tmpConn = conn;
if (tmpEpIn == null || tmpConn == null) return -1;
byte[] retData = new byte[64];
int readLen = tmpConn.bulkTransfer(tmpEpIn, retData, retData.length, 3000);
Log.d(TAG, "PrinterController read:readLen " + readLen);
if (readLen == -1) return -1;
Log.d(TAG, "PrinterController read:retData != -1");
byte[] realData = new byte[65];
System.arraycopy(retData, 0, realData, 0, retData.length);
Log.d(TAG, "PrinterController read:readLen " + realData[0]);
return realData[0];
}
// 一行打印2段内容
public String printTwoData(String leftText, String rightText) {
int leftTextLength = leftText.getBytes().length;
int rightTextLength = rightText.getBytes().length;
StringBuilder sb = new StringBuilder();
sb.append(leftText);
// 计算两侧文字中间的空格
int marginBetweenMiddleAndRight = LINE_BYTE_SIZE - leftTextLength - rightTextLength;
for (int i = 0; i < marginBetweenMiddleAndRight; i++) {
sb.append(" ");
}
sb.append(rightText);
// 处理信息过长问题
int outCome = (leftTextLength + rightTextLength) / LINE_BYTE_SIZE; // 结果
int remainder = (leftTextLength + rightTextLength) % LINE_BYTE_SIZE;// 余数
// 结果不为0并且还有余数,表明换行了,此时需要在后面加空白
// 计算出离右边的距离
int marginRightBlank = (outCome > 0 && remainder > 0) ? (((outCome + 1) * LINE_BYTE_SIZE) - leftTextLength - rightTextLength) : 0;
for (int i = 0; i < marginRightBlank; i++) {
sb.append(" ");
}
return sb.toString();
}
// 一行打印3段内容
public String printThreeData(String leftText, String centerText, String rightText) {
int leftTextLength = leftText.getBytes().length;
int centerTextLength = centerText.getBytes().length;
int rightTextLength = rightText.getBytes().length;
StringBuilder sb = new StringBuilder();
sb.append(leftText);
// 计算两侧文字中间的空格
int marginBetweenMiddleAndRight = LINE_BYTE_SIZE - leftTextLength - centerTextLength - rightTextLength;
for (int i = 0; i < marginBetweenMiddleAndRight/2; i++) {
sb.append(" ");
}
sb.append(centerText);
for (int i = 0; i < marginBetweenMiddleAndRight/2; i++) {
sb.append(" ");
}
sb.append(rightText);
// 处理信息过长问题
int outCome = (leftTextLength + centerTextLength + rightTextLength) / LINE_BYTE_SIZE; // 结果
int remainder = (leftTextLength + centerTextLength + rightTextLength) % LINE_BYTE_SIZE;// 余数
// 结果不为0并且还有余数,表明换行了,此时需要在后面加空白
// 计算出离右边的距离
int marginRightBlank = (outCome > 0 && remainder > 0) ? (((outCome + 1) * LINE_BYTE_SIZE) - leftTextLength - centerTextLength - rightTextLength) : 0;
for (int i = 0; i < marginRightBlank; i++) {
sb.append(" ");
}
return sb.toString();
}
public static String date2String(Date date, String format) {
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.format(date);
}
public final synchronized void close() {
if (conn != null) {
conn.close();
epOut = null;
epIn = null;
usbIf = null;
conn = null;
dev = null;
}
}
// 监听usb设备连接状态广播,
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive intent = " + intent);
// USB设备断开广播
if (intent.getAction() == UsbManager.ACTION_USB_DEVICE_DETACHED){
Toast.makeText(appContext, "打印机连接断开", Toast.LENGTH_SHORT).show();
}
}
}
5. 打印机使用
添加好打印工具类后就可以再MainActivity中连接、使用打印机了。MainActivity中添加了两个按钮,分别用于实现连接打印机和开始打印2个功能。
<MainActivity.java>
public class MainActivity extends AppCompatActivity {
private static final String TAG = "PrintingActivity";
private PrinterController printer;
private final PrinterCommand commands = new PrinterCommand();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化打印工具类
printer = PrinterController.getInstance();
printer.getPrinterContext(getApplicationContext());
// 动态注册 usb设备断开 广播
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
registerReceiver(printer, intentFilter);
initView();
}
private void initView(){
// 连接打印机
findViewById(R.id.connect_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
initPrint();
}
});
// 开始打印
findViewById(R.id.print_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startPrinting();
}
});
}
// 连接打印机
private void initPrint(){
printer.close();
printer.tryConnect();
}
// 开始打印
private void startPrinting(){
// 1. 打印之前先建立连接
printer.tryConnect();
// 2. 检查状态是否正常
int status = printer.readStatus(Command.DLE_eot4);
if (status != 18){
Toast.makeText(this, "打印机异常,请检查打印机!", Toast.LENGTH_SHORT).show();
Log.e(TAG,"printer error : status = " + status);
return;
}else {
Toast.makeText(this, "打印机正常!", Toast.LENGTH_SHORT).show();
Log.e(TAG,"printer over : status = " + status);
}
// 3. 自定义打印内容
customPrint();
// 4. 关闭打印机
printer.close();
}
/**
* 自定义打印格式和打印内容
*/
private void customPrint(){
printOneData("北京稻香村", 1, false, true, "all", 2, true, 2);
printTwoData("打印时间:", printer.date2String(new Date(), printer.FMT_YYYY_MM_DD_HH_MM2), 1, false, true, "all", 1, true, 1);
printOneData("-----------------------------------------------", 1, false, false, "", 0, true, 1);
printThreeData("单价", "数量", "金额", 1, false, false, "", 0, true, 1);
printOneData("-----------------------------------------------", 1, false, false, "", 0, true, 1);
printThreeData("蛋黄酥", "1", "¥50.00", 1, false, false, "", 0, true, 1);
printOneData("-----------------------------------------------", 1, false, false, "", 0, true, 1);
printTwoData("实收金额", "¥50.00", 1, true, false, "", 0, true, 4);
}
/**
* 设置打印内容:一行只有1个打印内容
* @param text 打印的文本
* @param align 对齐模式 0#左对齐, 1#居中, 2#右对齐
* @param isBold 是否加粗
* @param isEnlarge 是否放大
* @param direction 放大方向 horizontal#水平放大, vertical#垂直放大, all#整体放大
* @param multiple 放大倍数 [0,7]
* @param isEnter 是否回车
* @param enterLine 换行数 [1.10]
*/
private void printOneData(String text, int align, boolean isBold, boolean isEnlarge, String direction, int multiple, boolean isEnter, int enterLine){
printer.sendByte(commands.POS_Set_PrtInit()); // 初始化格式
printer.sendByte(commands.POS_S_Align(align)); // 对齐模式
if (isBold){
printer.sendByte(commands.Set_Bold()); //加粗
}
if (isEnlarge){
if ("horizontal".equals(direction)){
printer.sendByte(commands.POS_S_GS_H(multiple)); // 横向放大
}else if ("vertical".equals(direction)){
printer.sendByte(commands.POS_S_GS_O(multiple)); // 纵向放大
}else if ("all".equals(direction)){
printer.sendByte(commands.POS_S_GS_O_H(multiple)); // 放大字体
}
}
printer.sendByte(text.getBytes(Charset.forName("GBK"))); // 设置打印内容
// printer.sendByte(commands.POS_Set_Enter()); // 回车
if (isEnter) {
printer.sendByte(commands.POS_S_PrintLine(enterLine)); // 换行
}
}
/**
* 设置打印内容:一行有2个打印内容
* @param leftText 左侧打印的文本
* @param rightText 右侧打印的文本
* @param align 对齐模式 0#左对齐, 1#居中, 2#右对齐
* @param isBold 是否加粗
* @param isEnlarge 是否放大
* @param direction 放大方向 horizontal#水平放大, vertical#垂直放大, all#整体放大
* @param multiple 放大倍数 [0,7]
* @param isEnter 是否回车
* @param enterLine 换行数 [1.10]
*/
private void printTwoData(String leftText, String rightText, int align, boolean isBold, boolean isEnlarge, String direction, int multiple, boolean isEnter, int enterLine){
printer.sendByte(commands.POS_Set_PrtInit()); // 初始化格式
printer.sendByte(commands.POS_S_Align(align)); // 对齐模式
if (isBold){
printer.sendByte(commands.Set_Bold()); //加粗
}
if (isEnlarge){
if ("horizontal".equals(direction)){
printer.sendByte(commands.POS_S_GS_H(multiple)); // 横向放大
}else if ("vertical".equals(direction)){
printer.sendByte(commands.POS_S_GS_O(multiple)); // 纵向放大
}else if ("all".equals(direction)){
printer.sendByte(commands.POS_S_GS_O_H(multiple)); // 放大字体
}
}
printer.sendByte(printer.printTwoData(leftText, rightText).getBytes(Charset.forName("GBK"))); // 设置打印内容
if (isEnter) {
printer.sendByte(commands.POS_S_PrintLine(enterLine)); // 换行
}
}
/**
* 设置打印内容:一行有3个打印内容
* @param leftText 左侧打印的文本
* @param centerText 中间打印的文本
* @param rightText 右侧打印的文本
* @param align 对齐模式 0#左对齐, 1#居中, 2#右对齐
* @param isBold 是否加粗
* @param isEnlarge 是否放大
* @param direction 放大方向 horizontal#水平放大, vertical#垂直放大, all#整体放大
* @param multiple 放大倍数 [0,7]
* @param isEnter 是否回车
* @param enterLine 换行数 [1.10]
*/
private void printThreeData(String leftText, String centerText, String rightText, int align, boolean isBold, boolean isEnlarge, String direction, int multiple, boolean isEnter, int enterLine){
printer.sendByte(commands.POS_Set_PrtInit()); // 初始化格式
printer.sendByte(commands.POS_S_Align(align)); // 对齐模式
if (isBold){
printer.sendByte(commands.Set_Bold()); //加粗
}
if (isEnlarge){
if ("horizontal".equals(direction)){
printer.sendByte(commands.POS_S_GS_H(multiple)); // 横向放大
}else if ("vertical".equals(direction)){
printer.sendByte(commands.POS_S_GS_O(multiple)); // 纵向放大
}else if ("all".equals(direction)){
printer.sendByte(commands.POS_S_GS_O_H(multiple)); // 放大字体
}
}
printer.sendByte(printer.printThreeData(leftText, centerText, rightText).getBytes(Charset.forName("GBK"))); // 设置打印内容
if (isEnter) {
printer.sendByte(commands.POS_S_PrintLine(enterLine)); // 换行
}
}
}