写在前面

本项目用到的 主要知识点: 手机蓝牙 (动态权限申请,蓝牙打开,连接,配对,基于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() {@Overridepublic 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   25private int sleepTime = 8; //每次锁屏的时间间距 8,单位:ms   8private float lockWidth;//每次锁屏需要画的private int ecgPerCount = 17;//每次画心电数据的个数,8  17private 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 --> pxecgXOffset = 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;//获取屏幕对角线的长度,单位:pxdouble diagonalMm = Math.sqrt(width * width + height * height) / dm.densityDpi;//单位:英寸diagonalMm = diagonalMm * 2.54 * 10;//转换单位为:毫米double diagonalPx = width * width + height * height;diagonalPx = Math.sqrt(diagonalPx);//每毫米有多少pxdouble px1mm = diagonalPx / diagonalMm;//每秒画多少pxdouble px1s = wave_speed * px1mm;//每次锁屏所需画的宽度lockWidth = (float) (px1s * (sleepTime / 1000f));float widthSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, wave_speed, dm); // 25mm --> pxwidthStart = (width % widthSize) / 2;}@Overridepublic void surfaceCreated(SurfaceHolder holder) {Canvas canvas = holder.lockCanvas();canvas.drawColor(Color.parseColor(bgColor));initBackground(canvas);holder.unlockCanvasAndPost(canvas);}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}@Overrideprotected 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 --> pxwidthStart = (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);}@Overrideprotected 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);}@Overridepublic 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() {@Overridepublic 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

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

  1. wince下的蓝牙串口通信

    wince下的蓝牙串口通信(上) wince下的蓝牙串口通信(下)--客户端

  2. Android 蓝牙串口通信工具类 SeriaPortUtil 2.0.+

    原文地址:https://www.shanya.world/archives/2fd981ea.html SerialPortUtil 提示 最新版本 3.0.+ 已发布,其对比 2.0.+ 版本,A ...

  3. Vitis项目:基于 ZYNQ 的 IMX2221 摄像头实时视频流采集传输 (一)传感器配置

    项目:基于 ZYNQ 的 IMX2221 摄像头实时视频流采集传输 章节:传感器配置(一) 本章目的:使用 ZYNQ 芯片的 PS 端的 SPI 接口对 CMOS 图像传感器进行设置操作,保证 CMO ...

  4. 【嵌入式】蓝牙串口通信透传模块(HC-08)的使用

    一 使用蓝牙透传模块简介 HC-08 蓝牙串口通信模块是新一代的基于 Bluetooth Specification V4.0 BLE 蓝牙协议的数传模块.无线工作频段为 2.4GHz ISM,调制方 ...

  5. 关于Android蓝牙串口通信那点破事

    Android蓝牙串口通讯 闲着无聊玩起了Android蓝牙模块与单片机蓝牙模块的通信,简单思路就是要手机通过蓝牙发送控制指令给单片机,并作简单的控制应用.单片机的蓝牙模块连接与程序暂且略过,此文主要 ...

  6. simulink接收串口数据_基于Unity串口通信的解决方案

    思路有三种,等下我会详细介绍. 后面的博客详细介绍是我收录两年前写的博客,现在我已经没有往串口方向进行开发了,所以只能将一些思路分享给大家. ​ ​ 解决方式一:将Unity串口通信数据模块(接收与发 ...

  7. 蓝牙串口通信java_Java程序与串口通信的实现及通信原码-全网最详细,一步一步教会...

    RS-232(ANSI/EIA-232标准)是IBM-PC及其兼容机上的串行连接标准.RS-422(EIA RS-422-AStandard)是Apple的Macintosh计算机的串口连接标准.RS ...

  8. 基于STC串口通信和VC6.0MFC编程的电子琴设计

    电子实训课程实验项目 --电子琴 [前言] 为进一步激发学生对于硬件编程的兴趣而开展的课程"电子实训"课程到目前为止已经要告一段落了.将近四周的时间,从电路板印刷.贴片参观,到自己 ...

  9. python 串口测试,基于python串口通信简单实现物联网设备的自动化测试

    1.环境 python2.7 serial 库 2.AT command 什么是AT command: https://baike.baidu.com/item/AT命令/3441555?fr=ala ...

最新文章

  1. 专家解释即将到来的BCH网络升级
  2. 小程序商城选什么服务器,小程序商城到底用来干什么?
  3. c#图片base64去转义字符_C#实现字符串与图片的Base64编码转换操作示例|chu
  4. python requests发送websocket_Pywss - 用python实现WebSocket服务端
  5. Linux网络协议栈(四)——链路层(2)
  6. raid5需要几块硬盘_Raid5磁盘阵列数据恢复思路分析--附真实案例
  7. 平均年薪60.8万!拿下这个证书,算法岗直接起飞!
  8. iPhone 13 Pro系列被抢疯:官网已推迟36天发货
  9. list复制到另一个list_一文总结saltstack的十一个常用模块,附实例讲解
  10. EnableViewState和ViewStateMode差别详解
  11. PhotoShop 各历史版本
  12. 用友u8 无法安装服务器系统,用友u8安装【解决方案】
  13. 主机安全 服务器windows
  14. C语言关于排序的十一个函数
  15. PHP.ini配置文件[中文]
  16. 卡特兰数——Catalan数
  17. 两子公司犯单位行贿罪各罚100万 鹏博士收警示函高度重视
  18. [Leetcode] 买卖股票合集(动态规划)
  19. 服务器虚拟化发展现状_无服务器艺术的现状
  20. 【以太网硬件三】1000base-T和1000base-TX有什么区别?

热门文章

  1. 家谱管理系统性能要求_华北工控 | 嵌入式计算机在智慧社区管理系统中的大范畴应用...
  2. 【小沐学GIS】基于Cesium实现三维数字地球Earth(CesiumJS入门安装)
  3. 透过六家年中总结,看2021协同办公市场新趋势
  4. 为啥就业这么难——聊聊我在培训机构的所见所闻
  5. mysql远程登录失败,但是密码正确。
  6. [手游屌丝研究] 手游IP市场调查:授权金似北京房价【一】
  7. Win Server 08 R2前生今世
  8. Caffe Examples
  9. android 日倒计时计算器,Days Matter倒数日app下载安装
  10. sql语句——根据身份证号提取省份、出生日期、年龄、性别。