最近公司接了一个税控项目,要通过Andorid机串口链接打印机把发票打出来,那么串口通信就是大头了。这里记录一下过程。。。
这里主要有几个坑:
- 串口没有读写权限:
- 找到Andorid的sdk中platform-tools目录下的adb给添加到环境变量中,这样就能方便使用
- 直接adb devices 是否能查看到当前连接的设备
- 满足第2步的情况下,adb shell 进入控制台
- root过的机子可以直接su 获取到权限
- chmod 777 /dev/ttyHSL1
- 如何还不行,那就可能是防火墙的关系 setenforce 0
- 这只是应急处理,每次重启后都要经过这个操作
- 后来就是直接和硬件沟通,把这个串口的权限给我开放了出来,谁都可以调用
- 串口开放失败:这应该就是找对应打印机的那个串口(ttyHSL1这个是项目中链接打印机的串口,关于哪个串口是链接的,这我也是一个个试过来的,可以有工具类找到所有对应的串口,然后再一个个尝试一下)
这里贴一点代码
还有要说明一下,当中的 boolean chmod777 = chmod777(device) 对于我而言根本就没有用到,我是串口权限直接就获取到了,当没有权限的时候才执行chmod777(device),首先会通过 Process su = Runtime.getRuntime().exec("su"),获取root,关于这一步在硬件那边还没有开放的时候,我执行到这一步,但是却始终执行这句话就报错,手机也root过了,我很奇怪,以至于通过root来开放权限,各种方法也都尝试过,放在system/app下,也依旧不管用,这个问题有待解决。。。
SerialPortManager mSerialPortManager = new SerialPortManager();
// 打开串口
boolean openSerialPort = mSerialPortManager.setOnOpenSerialPortListener(this)
.openSerialPort(new File("/dev/ttyHSL1"), 9600);
public class SerialPortManager extends SerialPort {
private static final String TAG = SerialPortManager.class.getSimpleName();
private FileInputStream mFileInputStream;
private FileOutputStream mFileOutputStream;
private FileDescriptor mFd;
private OnOpenSerialPortListener mOnOpenSerialPortListener;
private OnSerialPortDataListener mOnSerialPortDataListener;
private HandlerThread mSendingHandlerThread;
private Handler mSendingHandler;
private SerialPortReadThread mSerialPortReadThread;
/**
* 打开串口
*
* @param device 串口设备
* @param baudRate 波特率
* @return 打开是否成功
*/
public boolean openSerialPort(File device, int baudRate) {
Log.i(TAG, "openSerialPort: " + String.format("打开串口 %s 波特率 %s", device.getPath(), baudRate));
// 校验串口权限
if (!device.canRead() || !device.canWrite()) {
boolean chmod777 = chmod777(device);
if (!chmod777) {
Log.i(TAG, "openSerialPort: 没有读写权限");
if (null != mOnOpenSerialPortListener) {
mOnOpenSerialPortListener.onFail(device, OnOpenSerialPortListener.Status.NO_READ_WRITE_PERMISSION);
}
return false;
}
}
try {
mFd = open(device.getAbsolutePath(), baudRate, 0);
mFileInputStream = new FileInputStream(mFd);
mFileOutputStream = new FileOutputStream(mFd);
Log.i(TAG, "openSerialPort: 串口已经打开 " + mFd);
if (null != mOnOpenSerialPortListener) {
mOnOpenSerialPortListener.onSuccess(device);
}
// 开启发送消息的线程
startSendThread();
// 开启接收消息的线程
startReadThread();
return true;
} catch (Exception e) {
e.printStackTrace();
if (null != mOnOpenSerialPortListener) {
mOnOpenSerialPortListener.onFail(device, OnOpenSerialPortListener.Status.OPEN_FAIL);
}
}
return false;
}
/**
* 关闭串口
*/
public void closeSerialPort() {
if (null != mFd) {
close();
mFd = null;
}
// 停止发送消息的线程
stopSendThread();
// 停止接收消息的线程
stopReadThread();
if (null != mFileInputStream) {
try {
mFileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
mFileInputStream = null;
}
if (null != mFileOutputStream) {
try {
mFileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
mFileOutputStream = null;
}
}
/**
* 添加打开串口监听
*
* @param listener listener
* @return SerialPortManager
*/
public SerialPortManager setOnOpenSerialPortListener(OnOpenSerialPortListener listener) {
mOnOpenSerialPortListener = listener;
return this;
}
/**
* 开启发送消息的线程
*/
private void startSendThread() {
// 开启发送消息的线程
mSendingHandlerThread = new HandlerThread("mSendingHandlerThread");
mSendingHandlerThread.start();
// Handler
mSendingHandler = new Handler(mSendingHandlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
byte[] sendBytes = (byte[]) msg.obj;
if (null != mFileOutputStream && null != sendBytes && 0 < sendBytes.length) {
try {
mFileOutputStream.write(sendBytes);
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
}
/**
* 停止发送消息线程
*/
private void stopSendThread() {
mSendingHandler = null;
if (null != mSendingHandlerThread) {
mSendingHandlerThread.interrupt();
mSendingHandlerThread.quit();
mSendingHandlerThread = null;
}
}
/**
* 开启接收消息的线程
*/
private void startReadThread() {
mSerialPortReadThread = new SerialPortReadThread(mFileInputStream);
mSerialPortReadThread.start();
}
/**
* 停止接收消息的线程
*/
private void stopReadThread() {
if (null != mSerialPortReadThread) {
mSerialPortReadThread.release();
}
}
/**
* 发送数据
*
* @param sendBytes 发送数据
* @return 发送是否成功
*/
public boolean sendBytes(byte[] sendBytes) {
if (null != mFd && null != mFileInputStream && null != mFileOutputStream) {
if (null != mSendingHandler) {
Message message = Message.obtain();
message.obj = sendBytes;
return mSendingHandler.sendMessage(message);
}
}
return false;
}
}
public class SerialPort {
static {
System.loadLibrary("SerialPort");
}
private static final String TAG = SerialPort.class.getSimpleName();
/**
* 文件设置最高权限 777 可读 可写 可执行
*
* @param file 文件
* @return 权限修改是否成功
*/
boolean chmod777(File file) {
if (null == file || !file.exists()) {
// 文件不存在
return false;
}
try {
// 获取ROOT权限
Process su = Runtime.getRuntime().exec("su");
//Process su = new ProcessBuilder().command("su").redirectErrorStream(true).start();
// 修改文件属性为 [可读 可写 可执行]
String cmd = "chmod 777 " + file.getAbsolutePath() + "\n" + "exit\n";
su.getOutputStream().write(cmd.getBytes());
int flag = su.waitFor();
if (0 == flag) {
if (file.canRead() && file.canWrite() && file.canExecute()){
return true;
}
}
} catch (IOException | InterruptedException e) {
// 没有ROOT权限
e.printStackTrace();
}
return false;
}
// 打开串口
protected native FileDescriptor open(String path, int baudRate, int flags);
// 关闭串口
protected native void close();
}
/**
* 发送字符串
*/
public void sendMsg(String args) throws UnsupportedEncodingException {
byte[] sendContentBytes = args.getBytes("GBK");
boolean sendBytes = mSerialPortManager.sendBytes(sendContentBytes);
}
/**
* 发送指令
*/
public void sendOrder(byte[] bytes) throws UnsupportedEncodingException {
boolean sendBytes = mSerialPortManager.sendBytes(bytes);
}
当中还需要打印二维码,通过实验,以下代码可行:
/**
* 生成二维码Bitmap
*
*/
public static Bitmap createQRImage(String url,int width,int height){
try{
if (url == null || "".equals(url) || url.length() < 1){//判断URL合法性
return null;
}
Hashtable<EncodeHintType, String> hints = new Hashtable<>();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
//图像数据转换,使用了矩阵转换
BitMatrix bitMatrix = new QRCodeWriter().encode(url, BarcodeFormat.QR_CODE, width, height, hints);
int[] pixels = new int[width * height];
//下面这里按照二维码的算法,逐个生成二维码的图片,
//两个for循环是图片横列扫描的结果
for (int y = 0; y < height; y++){
for (int x = 0; x < width; x++){
if (bitMatrix.get(x, y)){
pixels[y * width + x] = 0xff000000;
}
else{
pixels[y * width + x] = 0xffffffff;
}
}
}
//生成二维码图片的格式,使用ARGB_8888
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
/* *************************************************************************
* 假设一个240*240的图片,分辨率设为24, 共分10行打印
* 每一行,是一个 240*24 的点阵, 每一列有24个点,存储在3个byte里面。
* 每个byte存储8个像素点信息。因为只有黑白两色,所以对应为1的位是黑色,对应为0的位是白色
**************************************************************************/
/**
* 把一张Bitmap图片转化为打印机可以打印的字节流
*
* @param bmp
* @return
*/
public static byte[] draw2PxPoint(Bitmap bmp) {
//用来存储转换后的 bitmap 数据。为什么要再加1000,这是为了应对当图片高度无法
//整除24时的情况。比如bitmap 分辨率为 240 * 250,占用 7500 byte,5:5455,3,5447,4,5427
//但是实际上要存储11行数据,每一行需要 24 * 240 / 8 =720byte 的空间。再加上一些指令存储的开销,
//所以多申请 1000byte 的空间是稳妥的,不然运行时会抛出数组访问越界的异常。
int size = bmp.getWidth() * bmp.getHeight() / 8 + 1000;
byte[] data = new byte[size];
int k = 0;
//设置行距为0的指令
data[k++] = 0x1B;
data[k++] = 0x33;
data[k++] = 0x00;
// 逐行打印
for (int j = 0; j < bmp.getHeight() / 24f; j++) {
//打印图片的指令
data[k++] = 0x1B;
data[k++] = 0x2A;
data[k++] = 33;
data[k++] = (byte) (bmp.getWidth() % 256); //nL
data[k++] = (byte) (bmp.getWidth() / 256); //nH
//对于每一行,逐列打印
for (int i = 0; i < bmp.getWidth(); i++) {
//每一列24个像素点,分为3个字节存储
for (int m = 0; m < 3; m++) {
//每个字节表示8个像素点,0表示白色,1表示黑色
for (int n = 0; n < 8; n++) {
byte b = px2Byte(i, j * 24 + m * 8 + n, bmp);
data[k] += data[k] + b;
}
k++;
}
}
data[k++] = 10;//换行
}
// long a=System.currentTimeMillis();
byte[] data1 = new byte[k];
System.arraycopy(data, 0, data1, 0, k);
// long b=System.currentTimeMillis();
// System.out.println("结束字节:"+k+"---"+data.length+"耗时:"+(b-a));
return data1;
}
/**
* 灰度图片黑白化,黑色是1,白色是0
*
* @param x 横坐标
* @param y 纵坐标
* @param bit 位图
* @return
*/
public static byte px2Byte(int x, int y, Bitmap bit) {
if (x < bit.getWidth() && y < bit.getHeight()) {
byte b;
int pixel = bit.getPixel(x, y);
int red = (pixel & 0x00ff0000) >> 16; // 取高两位
int green = (pixel & 0x0000ff00) >> 8; // 取中两位
int blue = pixel & 0x000000ff; // 取低两位
int gray = RGB2Gray(red, green, blue);
if (gray < 128) {
b = 1;
} else {
b = 0;
}
return b;
}
return 0;
}
/**
* 图片灰度的转化
*/
private static int RGB2Gray(int r, int g, int b) {
int gray = (int) (0.29900 * r + 0.58700 * g + 0.11400 * b); //灰度转化公式
return gray;
}
其中找了不少的资料,在这里推荐 :
- https://github.com/cepr/android-serialport-api 串口开发api
- https://blog.youkuaiyun.com/itdo_just/article/details/80514116 关于如何将api运用到自己项目中
- https://blog.youkuaiyun.com/qq_25817651/article/details/53135685 CMAKE的使用
- https://github.com/GrassQing/CommonPrintProvider 工具类
- https://blog.youkuaiyun.com/akunainiannian/article/details/8740007 串口操作