基于蓝牙串口通信,实现实时脉象采集(项目总结与思路梳理)
来源:互联网 发布: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
- 基于蓝牙串口通信,实现实时脉象采集(项目总结与思路梳理)
- 实现 光感采集 -串口通信 要点总结
- PDA蓝牙串口与PC蓝牙串口通信
- 天气实时显示系统--基于python网络爬虫的树莓派与Arduino蓝牙通信
- 手机通过蓝牙串口与arduino通信
- Android与蓝牙串口模块通信
- Arduino与蓝牙的串口通信
- 基于STM8的ADC0832采集及蓝牙通信系统
- 实时时钟与串口通信模块整合
- 基于蓝牙的家居网关数据采集处理系统的设计与实现
- labwindows cvi串口采集数据与实时显示
- android蓝牙和网络通信项目总结(一)
- Qt实现串口通信总结
- Qt实现串口通信总结
- Qt实现串口通信总结
- S60串口通信方法(USB,蓝牙)
- S60串口通信方法(USB,蓝牙)
- 车载蓝牙串口通信
- 同时安装py3和py2,为py3的django项目连接mysql
- mt2503[PPP]PPP拨号速度慢如何解决?
- Redis pub/sub机制在实际运用场景的缺陷&&模拟JMS消息发布订阅的持久化特性
- <每日一题>动态规划入门:求最小硬币个数
- Java面试题全集(中)
- 基于蓝牙串口通信,实现实时脉象采集(项目总结与思路梳理)
- 变邻域搜索算法(Variable Neighborhood Search,VNS)
- Light-Head R-CNN解读
- 了解 CMS 垃圾回收日志
- python迭代器:next( ),__next__( ), iter()
- 推荐8款Vue.js管理台模板
- Java中Transient关键字解释
- 微信小程序:2个月收入超千万!揭秘自媒体为何都进驻小程序电商?
- ps和grep 的用法