基于蓝牙串口通信,实现实时脉象采集(项目总结与思路梳理)

来源:互联网 发布:python 端口扫描 编辑:程序博客网 时间:2024/05/22 01:40

写在前面

本项目用到的 主要知识点: 手机蓝牙 (动态权限申请,蓝牙打开,连接,配对,基于2.0蓝牙串口 Socket 通信),自定义View SurfaceView(实时绘制采集到的脉象波形)。本人为 一年工作经验小白,希望大家再阅读过程中有好的见解和思路,还望多多指点。 温馨提示: 阅读完 本文 大约需要 5 到十分钟。

1.蓝牙相关

1.1蓝牙申请

需要获取蓝牙权限,都是要在 AndroidManifest 清单文件中 添加权限。

<uses-permission android:name="android.permission.BLUETOOTH"/>    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

需要配置6.0 及以上系统手机,添加动态权限申请。在本人查阅文档后,6.0蓝牙使用也是需要申请 位置权限ActivityCompat.checkSelfPermission (主要通过该方法申请,在本文中不详细做解释,其他人动态权限申请工具类:http://www.jianshu.com/p/5f24c14eae5a)

1.2 蓝牙打开连接

获取 蓝牙适配器。蓝牙适配器是我们操作蓝牙的主要对象,可以从中获得配对过的蓝牙集合,可以获得蓝牙传输对象等等

 BluetoothAdapter _bluetooth =BluetoothAdapter.getDefaultAdapter();        if (_bluetooth == null) {            appUtils.e("该设备不支持蓝牙");            return;        }        if (!_bluetooth.isEnabled()) {            new Thread() {                public void run() {                    if (!_bluetooth.isEnabled()) { // 打开蓝牙                        _bluetooth.enable();                    }                }            }.start();        }      关闭蓝牙       if (mBtAdapter.isDiscovering()) {            mBtAdapter.cancelDiscovery();        }

动态 注册蓝牙广播

filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);        this.registerReceiver(mReceiver, filter);

广播接收

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {        @Override        public void onReceive(Context context, Intent intent) {            String action = intent.getAction();            if (BluetoothDevice.ACTION_FOUND.equals(action)) {                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);                if (device.getBondState() != BluetoothDevice.BOND_BONDED) {                    String str = device.getName() + "\n" + device.getAddress();                    if (mNewDevicesArrayAdapter.getPosition(str) == -1)                        mNewDevicesArrayAdapter.add(str);                }            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {                setProgressBarIndeterminateVisibility(false);                titleNewDevices.setText("查找完毕");                if (mNewDevicesArrayAdapter.getCount() == 0) {                    titleNewDevices.setText("查找完毕");                }            }        }    };

通过蓝牙的连接 搜索附近设备,我们可以获得到 设备的 地址,此时我们就可以进行蓝牙的Socket 连接和通信了。

1.3 蓝牙 Socket连接

以下给出程序中本人使用的代码。 这里着重看一下 Android 2.0串口通信 获取socket 办法。(不用反射方法获取设备 通信连接很不稳定)

Method m = _device.getClass().getMethod("createRfcommSocket", int.class);        _socket = (BluetoothSocket) m.invoke(_device, 1);       try {                Method m = _device.getClass().getMethod("createRfcommSocket", int.class);                _socket = (BluetoothSocket) m.invoke(_device, 1);            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {                e.printStackTrace();            }            try {                _socket.connect();  etResources().getString(R.string.delete), handler);                IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED);                MainActivity.this.registerReceiver(mReceiver, filter);            } catch (IOException e) {                try {                    bRun = false;                    _socket.close();                    _socket = null;                    appUtils.e("连接" + _device.getName() + "失败");                } catch (IOException ignored) {                }                return;            } catch (IOException e) {                return;            } catch (InterruptedException e) {                e.printStackTrace();            }

1.4 socket 通信

socket 通过 发送消息 通过 输出流 接收数据 通过输入流。

try {            OutputStream os = _socket.getOutputStream();            if (hex) {                byte[] bos_hex = appUtils.hexStringToBytes(str);                os.write(bos_hex);            } else {                byte[] bos = str.getBytes("GB2312");                os.write(bos);            }        } catch (IOException e) {        }

由于本项目 发送的数据位16进制,传送给 脉象仪 需要传送 byte 二进制数组。所以这个贴出一个 十六进制 字符串 转换为 byte 数组的办法。

 /**     * 16进制 字符串转换为 byte数组     */    public byte[] hexStringToBytes(String hexString) {        hexString = hexString.replaceAll(" ", "");        if ((hexString == null) || (hexString.equals(""))) {            return null;        }        hexString = hexString.toUpperCase();        int length = hexString.length() / 2;        char[] hexChars = hexString.toCharArray();        byte[] d = new byte[length];        for (int i = 0; i < length; ++i) {            int pos = i * 2;            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[(pos + 1)]));        }        return d;    }

1.5 接收Socket 数据

这里说明一下 进行socket 发送消息和 接收消息,最好再 子线程中 进行,因为socket相对来说比较耗时。子线程中 接收消息的方法,因为本项目中 有较长的数据量返回,所以这里需要对 is 长度进行判断,好根据长度 设置 数组长度。

int count = 0;while (count == 0) {    count = is.available();} byte[] buffer = new byte[count]; is.read(buffer); for (byte b : buffer) {    // 个人项目需要,讲数据添加到 队列中    byteQueue.offer(b);  }

获得到 数据后 接下来的事情就 是 我们 开头提到的 SurfaceView实时绘制波形了。

2. SurfaceView 绘制波形

因为产品需要,对脉搏的展示 需要进行 实时的绘制。所以 这里选择了比较熟悉的 Surfaceview,由于SurfaceView的双缓冲机制处理,单独运行在view的子线程,在这里非常的适合。在进行view 的绘制之前,先说下 我公司的 部分简单的参数(公司是 BAT 旁边的 一家小公司 -,-): 采样率 : 1K /s ,走纸速度:25mm/s 。其他的 命令信息,暂时不方便透漏。

2.1 Surfaceview 浅见

SurfaceView的名称含义

Surface意为表层、表面,顾名思义SurfaceView就是指一个在表层的View对象。为什么说是在表层呢,这是因为它有点特殊跟其他View不一样,其他View是绘制在“表层”的上面,而它就是充当“表层”本身。举个形象的例子,假设要在一个球上画画,那么球的表层就当做你的画布对象,你画的东西会挡住它的表层,默认没使用SurfaceView,那么球的表层就是空白的。如果使用了SurfaceView,我 们可以理解为我们拿来的球本身表面就具有纹路,你是画在纹路之上。SDK的文档 说到:SurfaceView就是在窗口上挖一个洞,它就是显示在这个洞里,其他的View是显示在窗口上,所以View可以显式在 SurfaceView之上,你也可以添加一些层在SurfaceView之上。(Android中SurfaceView的使用详解原文中有一整段是这么介绍sufaceview控制帧数的原理:“ SurfaceView还有其他的特性,上面我们讲了它可以控制帧数,那它是什么控制的呢?这就需要了解它的使用机制。一般在很多游戏设计中,我们都是开辟一个后台线程计算游戏相关的数据,然后根据这些计算完的新数据再刷新视图对象,由于对View执行绘制操作只能在UI线程上, 所以当你在另外一个线程计算完数据后,你需要调用View.invalidate方法通知系统刷新View对象,所以游戏相关的数据也需要让UI线程能访 问到,这样的设计架构比较复杂,要是能让后台计算的线程能直接访问数据,然后更新View对象那该多好。我们知道View的更新只能在UI线程中,所以使用自定义View没办法这么做,但是SurfaceView就可以了。它一个很好用的地方就是允许其他线程(不是UI线程)绘制图形(使用Canvas),根据它这个特性,你就可以控制它的帧数,你如果让这个线程1秒执行50次绘制,那么最后显示的就是50帧。”但我对这段话的来源存疑,各位看官怎么看呢?)

2.2 Surfaceview

首先我们创建 自定义 view ,surfaceview 中主要 包含 网格样式的背景和 一定的绘制频率的波形(这里补充一下 自定义View 比较基础的知识,onMeasure 方法 :view大小的测量OnSizeChange方法: 确定View 的大小,OnLayout 方法,根绝ViewGroup确定view位置 如果有基础比较不好的 看这里 http://www.gcssloop.com/customview/CustomViewIndex/)

以下为 自定义Surfaceview 全部代码。

 /** * 采样率 : 1s/ 1000包数据  ,  走纸速度:1s/25mm * Custom electrocardiogram * <p> * 1. Solve the background grid drawing problem * 2. Real-time data padding * <p> * author Bruce Young * 2017年8月7日10:54:01 */public class EcgView extends SurfaceView implements SurfaceHolder.Callback {    private Context mContext;    private SurfaceHolder surfaceHolder;    public static boolean isRunning = false;    public static boolean isRead = false;    private Canvas mCanvas;    private String bgColor = "#00000000";    public static int wave_speed = 25;//波速: 25mm/s   25    private int sleepTime = 8; //每次锁屏的时间间距 8,单位:ms   8    private float lockWidth;//每次锁屏需要画的    private int ecgPerCount = 17;//每次画心电数据的个数,8  17    private static Queue<Float> ecg0Datas = new LinkedBlockingQueue<>();    private Paint mPaint;//画波形图的画笔    private int mWidth;//控件宽度    private int mHeight;//控件高度    private float startY0;    private Rect rect;    public Thread RunThread = null;    private boolean isInto = false;  // 是否进入线程绘制点    private float startX;//每次画线的X坐标起点    public static double ecgXOffset;//每次X坐标偏移的像素    private int blankLineWidth = 5;//右侧空白点的宽度    public static float widthStart = 0f;  // 宽度开始的地方(横屏)    public static float highStart = 0f;  // 高度开始的地方(横屏)    public static float ecgSensitivity = 2;  // 1 的时候代表 5g 一大格  2 的时候 10g 一大格    public static float baseLine = 2f / 4f;    // 背景 网格 相关属性    //画笔    protected Paint mbgPaint;    //网格颜色    protected int mGridColor = Color.parseColor("#1b4200");    //背景颜色    protected int mBackgroundColor = Color.BLACK;    // 小格子 个数    protected int mGridWidths = 40;    // 横坐标个数    private int mGridHighs = 0;    // 表格宽度    private int latticeWidth;    // 表格高度    private int latticeHigh;    public EcgView(Context context, AttributeSet attrs) {        super(context, attrs);        this.mContext = context;        this.surfaceHolder = getHolder();        this.surfaceHolder.addCallback(this);        rect = new Rect();        converXOffset();    }    private void init() {        mbgPaint = new Paint();        mbgPaint.setAntiAlias(true);        mbgPaint.setStyle(Paint.Style.STROKE);        //连接处更加平滑        mbgPaint.setStrokeJoin(Paint.Join.ROUND);        mPaint = new Paint();        mPaint.setColor(Color.WHITE);        mPaint.setAntiAlias(true);        mPaint.setStyle(Paint.Style.STROKE);        mPaint.setStrokeWidth(4);        //连接处更加平滑        mPaint.setStrokeJoin(Paint.Join.ROUND);        DisplayMetrics dm = getResources().getDisplayMetrics();        float size = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, wave_speed, dm); // 25mm --> px        ecgXOffset = size / 1000f;        startY0 = -1;//波1初始Y坐标是控件高度的1/2    }    /**     * 根据波速计算每次X坐标增加的像素     * <p>     * 计算出每次锁屏应该画的px值     */    private void converXOffset() {        DisplayMetrics dm = getResources().getDisplayMetrics();        int width = dm.widthPixels;        int height = dm.heightPixels;        //获取屏幕对角线的长度,单位:px        double diagonalMm = Math.sqrt(width * width + height * height) / dm.densityDpi;//单位:英寸        diagonalMm = diagonalMm * 2.54 * 10;//转换单位为:毫米        double diagonalPx = width * width + height * height;        diagonalPx = Math.sqrt(diagonalPx);        //每毫米有多少px        double px1mm = diagonalPx / diagonalMm;        //每秒画多少px        double px1s = wave_speed * px1mm;        //每次锁屏所需画的宽度        lockWidth = (float) (px1s * (sleepTime / 1000f));        float widthSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, wave_speed, dm); // 25mm --> px        widthStart = (width % widthSize) / 2;    }    @Override    public void surfaceCreated(SurfaceHolder holder) {        Canvas canvas = holder.lockCanvas();        canvas.drawColor(Color.parseColor(bgColor));        initBackground(canvas);        holder.unlockCanvasAndPost(canvas);    }    @Override    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        DisplayMetrics dm = getResources().getDisplayMetrics();        int width = dm.widthPixels;        int high = dm.heightPixels;        float widthSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, wave_speed, dm); // 25mm --> px        widthStart = (width % widthSize) / 2;        w = floatToInt(w - widthStart);        // TODO: 2017/11/21 暂时使用固定的 25mm/s         mGridWidths = (floatToInt(w / TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 25, dm)) * 5);        mWidth = w;        float highSize = 0f;        if (high / widthSize >= 3) {            highSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, wave_speed, dm);            mGridHighs = (floatToInt(high / highSize) * 5);            highStart = (high % highSize) / 2;            h = floatToInt(h - highStart);        } else {            highStart = high % 3;            high = (int) (high - highStart);            highSize = high / 15;            mGridHighs = 15;            h = floatToInt(h - highStart);        }        mHeight = h;        isRunning = false;        init();        super.onSizeChanged(w, h, oldw, oldh);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        int width = MeasureSpec.getSize(widthMeasureSpec);        int high = MeasureSpec.getSize(heightMeasureSpec);        Log.e("ecgview:", "width:" + width + " height:" + high);    }    @Override    public void surfaceDestroyed(SurfaceHolder holder) {        stopThread();    }    public void startThread() {        isRunning = true;        RunThread = new Thread(drawRunnable);        // 每次开始清空画布,重新画        ClearDraw();        RunThread.start();    }    public void stopThread() {        if (isRunning) {            isRunning = false;            RunThread.interrupt();            startX = 0;            startY0 = -1;        }    }    Runnable drawRunnable = new Runnable() {        @Override        public void run() {            while (isRunning) {                long startTime = System.currentTimeMillis();                startDrawWave();                long endTime = System.currentTimeMillis();                if (endTime - startTime < sleepTime) {                    try {                        Thread.sleep(sleepTime - (endTime - startTime));                    } catch (InterruptedException e) {                        e.printStackTrace();                        break;                    }                }            }        }    };    private void startDrawWave() {        //锁定画布修改 位置        rect.set((int) (startX), 0, (int) (startX + lockWidth + blankLineWidth), mHeight);        mCanvas = surfaceHolder.lockCanvas(rect);        if (mCanvas == null) return;        mCanvas.drawColor(Color.parseColor(bgColor));        drawWave0();        if (isInto) {            startX = (float) (startX + ecgXOffset * ecgPerCount);        }        if (startX > mWidth) {            startX = 0;        }        surfaceHolder.unlockCanvasAndPost(mCanvas);    }    /**     * 画 脉象     */    private void drawWave0() {        try {            float mStartX = startX;            isInto = false;            initBackground(mCanvas);            if (ecg0Datas.size() > ecgPerCount) {                isInto = true;                for (int i = 0; i < ecgPerCount; i++) {                    float newX = (float) (mStartX + ecgXOffset);                    float newY = (mHeight * baseLine) - (ecg0Datas.poll() * (mHeight / mGridHighs) / ecgSensitivity);                    if (startY0 != -1) {                        mCanvas.drawLine(mStartX, startY0, newX, newY, mPaint);                    }                    mStartX = newX;                    startY0 = newY;                }            } else {                // 清空画布                if (isRead) {                    if (startY0 == -1) {                        startX = 0;                    }                    Paint paint = new Paint();                    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));                    mCanvas.drawPaint(paint);                    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));                    initBackground(mCanvas);                    stopThread();                }            }        } catch (NoSuchElementException e) {            e.printStackTrace();        }    }    public static boolean addEcgData0(Float data) {        return ecg0Datas.offer(data);    }    public static void clearEcgData0() {        if (ecg0Datas.size() > 0) {            ecg0Datas.clear();        }    }    //绘制背景 网格    private void initBackground(Canvas canvas) {        canvas.drawColor(mBackgroundColor);        //小格子的尺寸        latticeWidth = mWidth / mGridWidths;        latticeHigh = mHeight / mGridHighs;//        Log.e("lattice", "initBackground---latticeWidth:" + latticeWidth + "  latticeHigh:" + latticeHigh);        mbgPaint.setColor(mGridColor);        for (int k = 0; k <= mWidth / latticeWidth; k++) {            if (k % 5 == 0) {//每隔5个格子粗体显示                mbgPaint.setStrokeWidth(2);                canvas.drawLine(k * latticeWidth, 0, k * latticeWidth, mHeight, mbgPaint);            } else {                mbgPaint.setStrokeWidth(1);                canvas.drawLine(k * latticeWidth, 0, k * latticeWidth, mHeight, mbgPaint);            }        }            /* 宽度 */        for (int g = 0; g <= mHeight / latticeHigh; g++) {            if (g % 5 == 0) {                mbgPaint.setStrokeWidth(2);                canvas.drawLine(0, g * latticeHigh, mWidth, g * latticeHigh, mbgPaint);            } else {                mbgPaint.setStrokeWidth(1);                canvas.drawLine(0, g * latticeHigh, mWidth, g * latticeHigh, mbgPaint);            }        }    }    /**     * 清空 画布     */    public void ClearDraw() {        Canvas canvas = null;        try {            canvas = surfaceHolder.lockCanvas(null);            canvas.drawColor(Color.WHITE);            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.SRC);            // 绘制网格            initBackground(canvas);        } catch (Exception e) {        } finally {            if (canvas != null) {                surfaceHolder.unlockCanvasAndPost(canvas);            }        }    }    //  float 四舍五入 转换为 int 类型    public static int floatToInt(float f) {        int i = 0;        if (f > 0) {            i = (int) ((f * 10 + 5) / 10);        } else if (f < 0) {            i = (int) ((f * 10 - 5) / 10);        } else i = 0;        return i;    }}

本人GitHub Demo

到这里,主要的绘制 都基本完成,语言能力组织比较差,希望大家多多担待,本人QQ :745612618。加好友 请备注 名称 目的。 非诚勿扰 谢谢了。 本人这里也有 一个 android 技术开发群 (不吹水,不要钱,汉王 美团 bat 大佬 比比皆是) 回答对 入群问题 (Kotlin 问题),方可进入。群号:195135516

阅读全文
2 0
原创粉丝点击