Writing in the front

Main knowledge points used in this project: Mobile phone Bluetooth (dynamic permission application, Bluetooth opening, connection, pairing, communication based on 2.0 Bluetooth serial Socket), customized View SurfaceView (real-time drawing of pulse waveform collected). Oneself for one year work experience small white, hope everybody has good opinion and train of thought in reading process again, still hope many give directions. Tips: Reading this article should take about 5 to 10 minutes.

1. Bluetooth

1.1 Bluetooth Application

To obtain bluetooth permission, you need to add permission in the AndroidManifest file.


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

Mobile phones with operating system 6.0 or later need to be configured to add dynamic permission applications. After I refer to the document, bluetooth 6.0 use is to apply for a position permissions ActivityCompat. CheckSelfPermission (mainly by applying this method, in this article is not detailed explanation, others dynamic permissions apply for tools: www.jianshu.com/p/5f24c14ea…

1.2 Bluetooth Enable connection

Get the Bluetooth adapter. Bluetooth adapter is the main object for us to operate Bluetooth, from which we can get paired Bluetooth sets, bluetooth transmission objects and so on


 BluetoothAdapter _bluetooth =BluetoothAdapter.getDefaultAdapter();

        if (_bluetooth == null) {
            appUtils.e("This device does not support Bluetooth");
            return;
        }
        if(! _bluetooth.isEnabled()) {new Thread() {
                public void run(a) {
                    if(! _bluetooth.isEnabled()) {// Turn on bluetooth_bluetooth.enable(); } } }.start(); } Turn off Bluetoothif (mBtAdapter.isDiscovering()) {
            mBtAdapter.cancelDiscovery();
        }
Copy the code

Dynamically register bluetooth broadcast


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

Radio reception


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("Search complete.");
                if (mNewDevicesArrayAdapter.getCount() == 0) {
                    titleNewDevices.setText("Search complete."); }}}};Copy the code

By searching nearby devices through Bluetooth connection, we can get the address of the device, and then we can connect and communicate with Bluetooth Socket.

1.3 Bluetooth Socket Connection

The code I used in the program is given below. Here is a look at Android 2.0 serial communication access socket method. (It is very unstable to obtain device communication connection without reflection method)


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("Connection" + _device.getName() + "Failure");
                } catch (IOException ignored) {
                }
                return;
            } catch (IOException e) {
                return;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
Copy the code

1.4 the socket communication

The socket sends messages through the output stream and receives data through the input stream.


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) {
        }
Copy the code

Since the data bits sent in this project are in hexadecimal format, transmitting to the pulsar requires transmitting a byte binary array. So this posts a hexadecimal string into a byte array.


 /** * hexadecimal string is converted to byte array */
    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;
    }
Copy the code

1.5 Receiving Socket Data

It is best to send and receive socket messages in child threads, because socket is relatively time-consuming. Because there is a long amount of data returned in this project, so here we need to judge the is length, so as to set the array length according to the length.


int count = 0;
while (count == 0) {
    count = is.available();
}
 byte[] buffer = new byte[count];
 is.read(buffer);
 for (byte b : buffer) {
    // Add data to queue as required by individual project
    byteQueue.offer(b);
  }
Copy the code

Once you have the data, the next thing you need to do is draw the waveform in real time from the SurfaceView.

  1. SurfaceView draws the waveform

Because of the product needs, the display of pulse needs to be plotted in real time. So I chose the more familiar Surfaceview here, because of the Surfaceview double buffer mechanism processing, separate running in the view of the child thread, here is very suitable. Before drawing the View, let’s talk about some simple parameters of our company (our company is a small company next to BAT -, -) : sampling rate: 1K /s, paper walking speed: 25mm/s. Other command information is not convenient to disclose.

2.1 Surfaceview’s shallow

SurfaceView name meaning

Surface means Surface, SurfaceView is a View object on the Surface. The reason why it’s on the surface, is because it’s a little bit different than other views, which are drawn on top of the surface, but it acts as the surface itself. For example, if you want to draw on a ball, the surface of the ball will act as your canvas object. The object you draw will block the surface of the ball. By default, the SurfaceView is not used, so the surface of the ball will be blank. If we use SurfaceView, we can understand that the surface of the ball itself has a grain, you are drawing on the grain. The SDK documentation says: SurfaceView is a hole in the window, it is displayed in the hole, other views are displayed in the window, so the View can be explicitly on top of the SurfaceView, you can also add layers on top of the SurfaceView. “SurfaceView has other features as well. We mentioned above that SurfaceView can control the number of frames. What does it control? This requires an understanding of how it is used. In a lot of game design, what we do is we create a background thread that computs game-related data, and then we refresh the View object based on that new data. Because the View can only be drawn on the UI thread, when you’re done computing data on another thread, You need to call the view. invalidate method to tell the system to refresh the View object, so the game-related data also needs to be accessible to the UI thread. This is a complicated design architecture, and it would be nice if the background calculation thread could directly access the data and update the View object. We know that a View can only be updated in the UI thread, so you can’t do that with a custom View, but you can do that with a SurfaceView. One of the nice things about it is that it allows other threads (other than the UI thread) to draw (using the Canvas), so you can control the number of frames, and if you make that thread draw 50 times a second, you’ll end up with 50 frames.” But I have my doubts about the source of this quote. What do you think?)

2.2 Surfaceview

First of all, we create a custom view, the surfaceView mainly contains a grid style background and a certain drawing frequency waveform (here to supplement the basic knowledge of custom view comparison, onMeasure method: View size measurement OnSizeChange method: Determine the size of the View, OnLayout method, eradicate ViewGroup locate the View If there is a basis of comparison to look bad here www.gcssloop.com/customview/…

The following is all the code for custom Surfaceview.


 /** * Sampling rate: 1s/ 1000 packet data, paper moving speed: 1s/25mm * Custom electrocardiogram * 

* 1. Solve the background grid drawing problem * 2. Real-time data padding *

* Author Bruce Young * August 7, 2017 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;// Wave speed: 25mm/s 25 private int sleepTime = 8; // Each screen lock interval is 8, in ms 8 private float lockWidth;// Every lock screen needs to be drawn private int ecgPerCount = 17;// Draw the number of ecg data at each time, 8 17 private static Queue<Float> ecg0Datas = new LinkedBlockingQueue<>(); private Paint mPaint;// Draw the waveform brush private int mWidth;// Control width private int mHeight;// Control height private float startY0; private Rect rect; public Thread RunThread = null; private boolean isInto = false; // Whether to enter the thread drawing point private float startX;// Start the line each time public static double ecgXOffset;// The pixel of each X offset private int blankLineWidth = 5;// The width of the blank spot on the right public static float widthStart = 0f; // Where the width begins (landscape) public static float highStart = 0f; // Where height starts (landscape) public static float ecgSensitivity = 2; // 1 represents 5 gigabytes and 2 represents 10 gigabytes public static float baseLine = 2f / 4f; // Background grid related properties / / brush protected Paint mbgPaint; // Grid color protected int mGridColor = Color.parseColor("#1b4200"); // Background color protected int mBackgroundColor = Color.BLACK; // The number of small cells protected int mGridWidths = 40; // The number of abscissa private int mGridHighs = 0; // Table width private int latticeWidth; // Table height 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(a) { mbgPaint = new Paint(); mbgPaint.setAntiAlias(true); mbgPaint.setStyle(Paint.Style.STROKE); // The joints are smoother mbgPaint.setStrokeJoin(Paint.Join.ROUND); mPaint = new Paint(); mPaint.setColor(Color.WHITE); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(4); // The joints are smoother 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;// Wave 1's initial Y coordinate is 1/2 the height of the control } /** * Calculate the pixels added to each X coordinate according to the wave speed *

* Calculate the px value that should be drawn on each lock screen */

private void converXOffset(a) { DisplayMetrics dm = getResources().getDisplayMetrics(); int width = dm.widthPixels; int height = dm.heightPixels; // Get the screen diagonal length in px double diagonalMm = Math.sqrt(width * width + height * height) / dm.densityDpi;// Unit: inch diagonalMm = diagonalMm * 2.54 * 10;// Convert to mm double diagonalPx = width * width + height * height; diagonalPx = Math.sqrt(diagonalPx); // How many px are there per millimeter double px1mm = diagonalPx / diagonalMm; // How many pixels per second double px1s = wave_speed * px1mm; // The width required for each screen lock 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:Temporary use of fixed 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(a) { isRunning = true; RunThread = new Thread(drawRunnable); // Each time you start to empty the canvas, redraw ClearDraw(); RunThread.start(); } public void stopThread(a) { if (isRunning) { isRunning = false; RunThread.interrupt(); startX = 0; startY0 = -1; } } Runnable drawRunnable = new Runnable() { @Override public void run(a) { 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(a) { // Lock the canvas modification position 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(a) { 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 { // Empty the canvas if (isRead) { if (startY0 == -1) { startX = 0; } Paint paint = new Paint(); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); mCanvas.drawPaint(paint); paint.setXfermode(newPorterDuffXfermode(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(a) { if (ecg0Datas.size() > 0) { ecg0Datas.clear(); }}// Draw the background grid private void initBackground(Canvas canvas) { canvas.drawColor(mBackgroundColor); // The size of the cell 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) {// Every 5 squares are displayed in bold 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); }}/ * * / width 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); }}}/** * Empty the canvas */ public void ClearDraw(a) { Canvas canvas = null; try { canvas = surfaceHolder.lockCanvas(null); canvas.drawColor(Color.WHITE); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.SRC); // Draw the grid initBackground(canvas); } catch (Exception e) { } finally { if(canvas ! =null) { surfaceHolder.unlockCanvasAndPost(canvas); }}}// Float is rounded to 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; returni; }}Copy the code

I am making the Demo

Up to here, the main drawing are basically completed, language ability organization is poor, I hope you can excuse me, MY QQ :745612618. Add a friend please note name and purpose. If you are the one, thank you. I also have an Android technology development group here (no blowing water, no money, Hanwang Meituan BAT leaders everywhere) answer the right question into the group (Kotlin question), can enter. Group no. : 195135516