From 4a7f7b316f07c8c71e850606c122b72a86b45e58 Mon Sep 17 00:00:00 2001 From: czm <2192718639@qq.com> Date: Mon, 18 Apr 2022 18:47:22 +0800 Subject: [PATCH] =?UTF-8?q?=E9=AB=98=E7=BA=A7=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.tsx | 31 +- src/common/bluetooth/color/colorDiff.js | 86 ++++ src/common/bluetooth/color/colorSpace.js | 13 + src/common/bluetooth/color/lab.js | 54 ++ src/common/bluetooth/color/rgb.js | 9 + src/common/bluetooth/color/xyz.js | 138 +++++ src/common/bluetooth/command.js | 146 ++++++ src/common/bluetooth/utils.js | 70 +++ src/components/bluetooth/LinkBlueTooth.tsx | 90 ++++ src/components/bluetooth/Popup.tsx | 73 +++ .../bluetooth/css/linkBlueTooth.module.scss | 30 ++ .../bluetooth/css/popup.module.scss | 90 ++++ src/components/search/index.module.scss | 3 +- src/components/searchInput/index.module.scss | 22 +- src/components/searchInput/index.tsx | 11 +- src/components/tabs/index.module.scss | 24 +- src/components/tabs/index.tsx | 9 +- src/pages/classList/index.module.scss | 15 +- src/pages/classList/index.tsx | 25 +- .../components/selectData/index.module.scss | 49 ++ .../components/selectData/index.tsx | 57 +++ .../searchList/hightSearchList.module.scss | 36 +- src/pages/searchList/hightSearchList.tsx | 57 ++- src/pages/searchList/searchList.module.scss | 18 + src/pages/searchList/searchList.tsx | 35 +- src/use/contextBlueTooth.tsx | 477 ++++++++++++++++++ 26 files changed, 1584 insertions(+), 84 deletions(-) create mode 100644 src/common/bluetooth/color/colorDiff.js create mode 100644 src/common/bluetooth/color/colorSpace.js create mode 100644 src/common/bluetooth/color/lab.js create mode 100644 src/common/bluetooth/color/rgb.js create mode 100644 src/common/bluetooth/color/xyz.js create mode 100644 src/common/bluetooth/command.js create mode 100644 src/common/bluetooth/utils.js create mode 100644 src/components/bluetooth/LinkBlueTooth.tsx create mode 100644 src/components/bluetooth/Popup.tsx create mode 100644 src/components/bluetooth/css/linkBlueTooth.module.scss create mode 100644 src/components/bluetooth/css/popup.module.scss create mode 100644 src/pages/searchList/components/selectData/index.module.scss create mode 100644 src/pages/searchList/components/selectData/index.tsx create mode 100644 src/use/contextBlueTooth.tsx diff --git a/src/app.tsx b/src/app.tsx index c9fd660..dc241a4 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,13 +1,26 @@ -import { MovableArea, View } from '@tarojs/components' +import { Component } from 'react' +import ContextBlueTooth from "@/use/contextBlueTooth" + import './app.scss' +class App extends Component { -const App = ({ children }) => { - return ( - <> - {children} - - ) -} + componentDidMount () {} -export default App + componentDidShow () {} + + componentDidHide () {} + + componentDidCatchError () {} + + // this.props.children 是将要会渲染的页面 + render () { + return ( + + {this.props.children} + + ) + } +} + +export default App \ No newline at end of file diff --git a/src/common/bluetooth/color/colorDiff.js b/src/common/bluetooth/color/colorDiff.js new file mode 100644 index 0000000..73e380d --- /dev/null +++ b/src/common/bluetooth/color/colorDiff.js @@ -0,0 +1,86 @@ +module.exports = function(lab1, lab2){ + + var rgb2labArray1 = lab1; + var rgb2labArray2 = lab2; + + var l1 = rgb2labArray1[0]; + var a1 = rgb2labArray1[1]; + var b1 = rgb2labArray1[2]; + + var l2 = rgb2labArray2[0]; + var a2 = rgb2labArray2[1]; + var b2 = rgb2labArray2[2]; + + + var avg_lp = (l1 + l2) / 2; + var c1 = Math.sqrt(Math.pow(a1, 2) + Math.pow(b1, 2)); + var c2 = Math.sqrt(Math.pow(a2, 2) + Math.pow(b2, 2)); + var avg_c = (c1 + c2) / 2; + var g = (1- Math.sqrt(Math.pow(avg_c, 7) / (Math.pow(avg_c, 7) + Math.pow(25, 7)))) / 2; + + var a1p = a1 * (1 + g); + var a2p = a2 * (1 + g); + + var c1p = Math.sqrt(Math.pow(a1p, 2) + Math.pow(b1, 2)); + var c2p = Math.sqrt(Math.pow(a2p, 2) + Math.pow(b2, 2)); + + var avg_cp = (c1p + c2p) / 2; + + var h1p = rad2deg(Math.atan2(b1, a1p)); + if(h1p < 0){ + + h1p = h1p + 360; + } + + var h2p = rad2deg(Math.atan2(b2, a2p)); + if(h2p < 0){ + + h2p = h2p + 360; + } + + var avg_hp = Math.abs(h1p - h2p) > 180 ? (h1p + h2p + 360) / 2 : (h1p + h1p) / 2; + + var t = 1 - 0.17 * Math.cos(deg2rad(avg_hp - 30)) + 0.24 * Math.cos(deg2rad(2 * avg_hp)) + 0.32 * Math.cos(deg2rad(3 * avg_hp + 6)) - 0.2 * Math.cos(deg2rad(4 * avg_hp - 63)) + + var delta_hp = h2p - h1p; + if(Math.abs(delta_hp) > 180){ + if (h2p <= h1p) { + delta_hp += 360; + } + else { + delta_hp -= 360; + } + } + + var delta_lp = l2 - l1; + var delta_cp = c2p - c1p; + + delta_hp = 2 * Math.sqrt(c1p * c2p) * Math.sin(deg2rad(delta_hp) / 2); + + var s_l = 1 + ((0.015 * Math.pow(avg_lp - 50, 2)) / Math.sqrt(20 + Math.pow(avg_lp - 50, 2))); + var s_c = 1 + 0.045 * avg_cp + var s_h = 1 + 0.015 * avg_cp * t; + + var delta_ro = 30 * Math.exp( - (Math.pow((avg_hp - 275) / 25, 2))); + var r_c = 2 * Math.sqrt(Math.pow(avg_cp, 7) / (Math.pow(avg_cp, 7) + Math.pow(25, 7))); + var r_t = -r_c * Math.sin(2 * deg2rad(delta_ro)); + + var kl = 1, kc =1, kh = 1; + + var delta_e = Math.sqrt(Math.pow(delta_lp / (kl * s_l), 2) + Math.pow(delta_cp / (kc * s_c), 2) + Math.pow(delta_hp / (kh * s_h), 2) + r_t * (delta_cp / (kc * s_c)) * (delta_hp / (kh * s_h))) + + return delta_e + + + function rad2deg(rad){ + + return 360 * rad / (2 * Math.PI); + } + function deg2rad(deg){ + + return (2 * Math.PI * deg) / 360; + } +} + + + diff --git a/src/common/bluetooth/color/colorSpace.js b/src/common/bluetooth/color/colorSpace.js new file mode 100644 index 0000000..8d05344 --- /dev/null +++ b/src/common/bluetooth/color/colorSpace.js @@ -0,0 +1,13 @@ + +import LabCom from './lab' +import XyzCom from './xyz' +import ColorDiff from './colorDiff' + +export const toRgb = (lab) => { + let xyz = LabCom.xyz(lab) + return XyzCom.rgb(xyz) +} + +export const Ediff = (lab1, lab2) => { + return ColorDiff(lab1, lab2) +} \ No newline at end of file diff --git a/src/common/bluetooth/color/lab.js b/src/common/bluetooth/color/lab.js new file mode 100644 index 0000000..b4c0eab --- /dev/null +++ b/src/common/bluetooth/color/lab.js @@ -0,0 +1,54 @@ + +var xyz = require('./xyz'); + +module.exports = { + name: 'lab', + min: [0,-100,-100], + max: [100,100,100], + channel: ['lightness', 'a', 'b'], + alias: ['LAB', 'cielab'], + + xyz: function(lab) { + var l = lab[0], + a = lab[1], + b = lab[2], + x, y, z, y2; + + if (l <= 8) { + y = (l * 100) / 903.3; + y2 = (7.787 * (y / 100)) + (16 / 116); + } else { + y = 100 * Math.pow((l + 16) / 116, 3); + y2 = Math.pow(y / 100, 1/3); + } + + x = x / 95.047 <= 0.008856 ? x = (95.047 * ((a / 500) + y2 - (16 / 116))) / 7.787 : 95.047 * Math.pow((a / 500) + y2, 3); + + z = z / 108.883 <= 0.008859 ? z = (108.883 * (y2 - (b / 200) - (16 / 116))) / 7.787 : 108.883 * Math.pow(y2 - (b / 200), 3); + + return [x, y, z]; + } +}; + + +//extend xyz +xyz.lab = function(xyz){ + var x = xyz[0], + y = xyz[1], + z = xyz[2], + l, a, b; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116); + + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); + + return [l, a, b]; +}; diff --git a/src/common/bluetooth/color/rgb.js b/src/common/bluetooth/color/rgb.js new file mode 100644 index 0000000..60ff577 --- /dev/null +++ b/src/common/bluetooth/color/rgb.js @@ -0,0 +1,9 @@ + + +module.exports = { + name: 'rgb', + min: [0,0,0], + max: [255,255,255], + channel: ['red', 'green', 'blue'], + alias: ['RGB'] +}; diff --git a/src/common/bluetooth/color/xyz.js b/src/common/bluetooth/color/xyz.js new file mode 100644 index 0000000..c6e2bee --- /dev/null +++ b/src/common/bluetooth/color/xyz.js @@ -0,0 +1,138 @@ + +var rgb = require('./rgb'); + +var xyz = { + name: 'xyz', + min: [0,0,0], + channel: ['X','Y','Z'], + alias: ['XYZ', 'ciexyz', 'cie1931'] +}; + + +/** + * Whitepoint reference values with observer/illuminant + * + * http://en.wikipedia.org/wiki/Standard_illuminant + */ +xyz.whitepoint = { + //1931 2° + 2: { + //incadescent + A:[109.85, 100, 35.585], + // B:[], + C: [98.074, 100, 118.232], + D50: [96.422, 100, 82.521], + D55: [95.682, 100, 92.149], + //daylight + D65: [95.045592705167, 100, 108.9057750759878], + D75: [94.972, 100, 122.638], + //flourescent + // F1: [], + F2: [99.187, 100, 67.395], + // F3: [], + // F4: [], + // F5: [], + // F6:[], + F7: [95.044, 100, 108.755], + // F8: [], + // F9: [], + // F10: [], + F11: [100.966, 100, 64.370], + // F12: [], + E: [100,100,100] + }, + + //1964 10° + 10: { + //incadescent + A:[111.144, 100, 35.200], + C: [97.285, 100, 116.145], + D50: [96.720, 100, 81.427], + D55: [95.799, 100, 90.926], + //daylight + D65: [94.811, 100, 107.304], + D75: [94.416, 100, 120.641], + //flourescent + F2: [103.280, 100, 69.026], + F7: [95.792, 100, 107.687], + F11: [103.866, 100, 65.627], + E: [100,100,100] + } +}; + + +/** + * Top values are the whitepoint’s top values, default are D65 + */ +xyz.max = xyz.whitepoint[2].D65; + + +/** + * Transform xyz to rgb + * + * @param {Array} xyz Array of xyz values + * + * @return {Array} RGB values + */ +xyz.rgb = function (_xyz, white) { + //FIXME: make sure we have to divide like this. Probably we have to replace matrix as well then + white = white || xyz.whitepoint[2].E; + + var x = _xyz[0] / white[0], + y = _xyz[1] / white[1], + z = _xyz[2] / white[2], + r, g, b; + + // assume sRGB + // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + r = (x * 3.240969941904521) + (y * -1.537383177570093) + (z * -0.498610760293); + g = (x * -0.96924363628087) + (y * 1.87596750150772) + (z * 0.041555057407175); + b = (x * 0.055630079696993) + (y * -0.20397695888897) + (z * 1.056971514242878); + + r = r > 0.0031308 ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055) + : r = (r * 12.92); + + g = g > 0.0031308 ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055) + : g = (g * 12.92); + + b = b > 0.0031308 ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055) + : b = (b * 12.92); + + r = Math.min(Math.max(0, r), 1); + g = Math.min(Math.max(0, g), 1); + b = Math.min(Math.max(0, b), 1); + + return [r * 255, g * 255, b * 255]; +} + + + +/** + * RGB to XYZ + * + * @param {Array} rgb RGB channels + * + * @return {Array} XYZ channels + */ +rgb.xyz = function(rgb, white) { + var r = rgb[0] / 255, + g = rgb[1] / 255, + b = rgb[2] / 255; + + // assume sRGB + r = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92); + g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92); + b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92); + + var x = (r * 0.41239079926595) + (g * 0.35758433938387) + (b * 0.18048078840183); + var y = (r * 0.21263900587151) + (g * 0.71516867876775) + (b * 0.072192315360733); + var z = (r * 0.019330818715591) + (g * 0.11919477979462) + (b * 0.95053215224966); + + white = white || xyz.whitepoint[2].E; + + return [x * white[0], y * white[1], z * white[2]]; +}; + + + +module.exports = xyz; diff --git a/src/common/bluetooth/command.js b/src/common/bluetooth/command.js new file mode 100644 index 0000000..97292d0 --- /dev/null +++ b/src/common/bluetooth/command.js @@ -0,0 +1,146 @@ +import { uint32ToUint8Array, uint8ArrayToHex } from "./utils"; + +export class Command { + // 测量序号 + static measureId = 1; + + // 命令完整响应的长度 + responseSize = 0; + // 命令发送的数据 + content = new Uint8Array(0); + // 命令响应的数据 + response = new Uint8Array(0); + // 等待响应的超时时间 + timeout = 3000; + // 发送的数据是否需要生成和校验值 + needSign = true; + + + /** + * @param {Uint8Array|ArrayBuffer|number[]} content + * @param {number} responseSize + * @param {number} timeout + * @param {boolean} needSign + */ + constructor(content, responseSize, timeout = 3000, needSign = true) { + if (content instanceof Uint8Array) { + this.content = content; + } else { + this.content = new Uint8Array(content); + } + this.responseSize = responseSize; + if (typeof timeout === 'number' && timeout >= 0) { + this.timeout = timeout; + } + this.needSign = needSign; + } + + /** + * 返回一个 ArrayBuffer 数组, 用于发送 + * @returns {ArrayBuffer[]} + */ + get data() { + if (this.content.length === 0) throw new Error('正文内容不能为空'); + const data = []; + const b = new Uint8Array(this.content.buffer); + if (this.needSign) { + b[b.length - 1] = Command.getSign(b); + } + for (let i = 0; i < b.length; i += 20) { + data.push(b.slice(i, i + 20).buffer); + } + return data; + } + + /** 是否接收完成 */ + get isComplete() { + return this.response.length >= this.responseSize; + } + + /** 是否有效 */ + get isValid() { + return Command.getSign(this.response) === this.response[this.response.length - 1]; + } + + /** + * 填充响应数组 + * @param {ArrayBuffer} buffer + */ + fillResponse(buffer) { + this.response = new Uint8Array([...this.response, ...(new Uint8Array(buffer))]); + } + + + /** + * 获取和校验值 + * @param {ArrayBuffer|Uint8Array} buffer + */ + static getSign(buffer) { + const _b = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); + let sum = 0; + _b.slice(0, _b.length - 1).forEach(i => sum += i); + return new Uint8Array([sum])[0]; + } + + // 唤醒命令 + static WakeUp = new Command([0xf0], 0, 0, false); + + /** + * 获取测量命令 + * @param {number} mode + */ + static measure(mode = 0) { + Command.measureId += 1; + const measureId = uint32ToUint8Array(Command.measureId); + return new Command([0xbb, 1, mode, ...measureId, 0, 0xff, 0], 10, 1500); + } + + /** + * 获取测量数据 (Lab) + * @param {number} mode + */ + static getLab(mode = 0) { + return new Command([0xbb, 3, mode, 0, 0, 0, 0, 0, 0xff, 0], 20, 1500); + } + + /** + * 获取测量数据 (RGB) + * @param {number} mode + */ + static getRGB(mode = 0) { + return new Command([0xbb, 4, mode, 0, 0, 0, 0, 0, 0xff, 0], 20, 1500); + } + + /** + * 获取测量的光谱数据 + * @param {number} mode + */ + static getSpectral(mode = 0) { + return new Command([0xbb, 2, 0x10 + mode, 0, 0, 0 ,0 ,0, 0xff, 0], 200, 5000); + } + + /** + * 白校准 + * @param {number} check 是否判断校准成功 1 判断 0 不判断 + */ + static whiteCalibrate(check = 1) { + return new Command([0xbb, 0x11, check, 0, 0, 0, 0, 0, 0xff, 0], 10, 1500); + } + + + /** + * 黑校准 + * @param {number} check 是否判断校准成功 + */ + static blackCalibrate(check = 1) { + return new Command([0xbb, 0x10, check, 0, 0, 0, 0, 0, 0xff, 0], 10, 1500); + } + + + /** 获取校准状态 */ + static GetCalibrationInf = new Command([0xbb, 0x1e, 0, 0, 0, 0, 0, 0, 0xff, 0], 20, 1500); + + + + static GetDeviceInf = new Command([0xbb, 0x12, 0x01, 0, 0, 0, 0, 0, 0xff, 0], 200, 5000); +} \ No newline at end of file diff --git a/src/common/bluetooth/utils.js b/src/common/bluetooth/utils.js new file mode 100644 index 0000000..9c4b018 --- /dev/null +++ b/src/common/bluetooth/utils.js @@ -0,0 +1,70 @@ +/** + * Uint32 转 Uint8 数组 + * @param {number} n + */ +export function uint32ToUint8Array(n) { + return new Uint8Array(new Uint32Array([n]).buffer); +} + +/** + * Uint8 数组 转 Float32 + * @param {Uint8Array} raw + */ +export function uint8ArrayToFloat32(raw) { + return new Float32Array(raw.buffer)[0]; +} + + +/** + * Uint8 数组 转 Uint16 + * @param {Uint8Array} raw + */ +export function uint8ArrayToUint16(raw) { + return new Uint16Array(raw.buffer)[0]; +} + + +/** + * Uint8 数组转 Uint32 + * @param {Uint8Array} raw + * @returns + */ +export function uint8ArrayToUnit32(raw) { + return new Uint32Array(raw.buffer)[0]; +} + + +/** + * 等待指定时长 + * @param {number} duration + */ +export function waitFor(duration) { + return new Promise(resolve => { + setTimeout(resolve, duration); + }); +} + + +/** + * uint8 数组转 hex 字符串 + * @param {Uint8Array} raw + */ +export function uint8ArrayToHex(raw) { + const s = []; + raw.forEach(i => { + const b = i.toString(16); + s.push(b.length > 1 ? b : `0${b}`); + }); + return s.join(' '); +} + + +// 二进制转字符串(ascii) +export function bufferToString(buffer) { + let str = ""; + for (let code of buffer) { + if (code === 0) break; + str += utf82string(code); + } + return str; +} \ No newline at end of file diff --git a/src/components/bluetooth/LinkBlueTooth.tsx b/src/components/bluetooth/LinkBlueTooth.tsx new file mode 100644 index 0000000..66f01a7 --- /dev/null +++ b/src/components/bluetooth/LinkBlueTooth.tsx @@ -0,0 +1,90 @@ +import { View } from "@tarojs/components"; +import { memo, useEffect, useMemo, useState } from "react"; +import Taro from "@tarojs/taro"; +import {useBluetooth} from "@/use/contextBlueTooth" +import SearchInput from "@/components/searchInput"; +import Popup from "@/components/bluetooth/Popup" +import classnames from "classnames"; +import styles from "./css/linkBlueTooth.module.scss" + +export default memo(() => { + const {state, init, startScan, connect, disconnect} = useBluetooth() + + useEffect(() => { + init() + }, []) + + const [linkStatus, setLinkStatus] = useState(1) + useEffect(() => { + if(!state.available) { + setLinkStatus(1) + } else if(state.available&&state.connected?.name) { + setLinkStatus(3) + } else { + setLinkStatus(2) + } + console.log('aaa:::',state.connected) + }, [state.available, state.connected]) + + const linkName = useMemo(() => { + return state.connected?.localName||'' + }, [state.connected]) + + //链接设备 + const onLinkListen = (item) => { + if(!state.connected&&!state.connecting) + connect(item) + } + + const [popupShow, setPopupShow] = useState(false) + //显示设备列表 + const onFindDevice = () => { + if(linkStatus == 1) { + Taro.showToast({ + title:'请打开蓝牙', + icon:'none' + }) + } else { + setPopupShow(true) + onFindEven() + } + + } + const onFindEven = () => { + if(!state.discovering&&!state.connected&&!state.connecting) + startScan() + } + + //断开链接 + const onDisconnect = () => { + disconnect() + setPopupShow(false) + } + + return ( + <> + + + + + { + linkStatus == 1&&请开启蓝牙|| + linkStatus == 2&&未连接设备|| + linkStatus == 3&&{linkName} + } + + + setPopupShow(false)} + onLink={item => onLinkListen(item)} + onOff={onDisconnect} + onFind={onFindEven} + /> + + + + ); + +}) diff --git a/src/components/bluetooth/Popup.tsx b/src/components/bluetooth/Popup.tsx new file mode 100644 index 0000000..8a5070a --- /dev/null +++ b/src/components/bluetooth/Popup.tsx @@ -0,0 +1,73 @@ +import { ScrollView, View } from "@tarojs/components" +import { memo, useEffect, useState } from "react" +import Loading from "@/components/loading" +import style from "./css/popup.module.scss" + +interface params { + state: any, + show: Boolean, + onClose: (Boolean) => void, + onLink: (any) => void, + children?: React.ReactNode + onOff: () => void, + onFind: () => void, +} + +export default memo(({state, show=false, onClose, onLink, onOff, onFind}:params) => { + const [popupShow, setPopupShow] = useState(show) + useEffect(() => { + setPopupShow(show) + }, [show]) + const onCloseListener = () => { + onClose(false) + } + + return ( + <> + { + popupShow&& + + 搜索设备 + + + { + (state.devices&&state.devices.length > 0)&&state?.devices.map(item => { + return ( + onLink(item)}> + {item.name} + { + (!state.connecting&&!state.connected)&&链接|| + (state.connecting&&item.deviceId == state.connecting.deviceId)&&正在链接...|| + (state.connected&&item.deviceId == state.connected.deviceId)&&链接成功 + } + + ) + })|| + + { + (!state.discovering)&& <> + 暂无设备,请按以下条件检查 + 1.请确保取色仪处于激活状态 + 2.请确保取色仪没有链接其他设备 + 3.请打开手机定位 + || + 设备搜索中 + } + + + + } + + + { + state.connected&&断开链接|| + (!state.connected&&state.discovering)&&搜索中|| + 重新搜索 + } + + + + } + + ) +}) \ No newline at end of file diff --git a/src/components/bluetooth/css/linkBlueTooth.module.scss b/src/components/bluetooth/css/linkBlueTooth.module.scss new file mode 100644 index 0000000..4156811 --- /dev/null +++ b/src/components/bluetooth/css/linkBlueTooth.module.scss @@ -0,0 +1,30 @@ + +.main{ + .bluetooth_link{ + display: flex; + align-items: center; + .link_status{ + width: 12px; + height: 12px; + background: #f02409; + border-radius: 50%; + } + .link_statused{ + background: #07C160; + } + .link_statused_no{ + background: #f0ec09; + } + .link_name{ + font-size: $font_size; + margin-left: 20px; + + } + .link_name_no{ + color: #f02409; + } + .link_name_no_link{ + color: #f0ec09; + } + } +} \ No newline at end of file diff --git a/src/components/bluetooth/css/popup.module.scss b/src/components/bluetooth/css/popup.module.scss new file mode 100644 index 0000000..04eecb7 --- /dev/null +++ b/src/components/bluetooth/css/popup.module.scss @@ -0,0 +1,90 @@ +.popup{ + width: 100vw; + height: 100vh; + position: absolute; + top: 0; + left: 0; + .mask{ + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); + position: fixed; + top:0; + left:0; + z-index: 9; + } + .content{ + z-index: 99; + background-color: #fff; + width: 75vw; + height: 600px; + position: fixed; + top: 50%; + left: 50%; + border-radius: 20px; + transform: translateX(-50%) translateY(-50%); + display: flex; + flex-direction: column; + font-size: 28px; + .title{ + text-align: center; + margin: 20px; + } + .list{ + height: 480px; + padding: 0 20px; + .scroll{ + height: 100%; + } + .item{ + margin-bottom: 20px; + display: flex; + justify-content: space-between; + border-bottom: 1px dashed #ccc; + padding: 15px 0; + color: #3b3b3b; + @mixin link{ + font-size: 25px; + } + .link_success{ + @include link; + color: green; + } + .link_ing { + color: orange; + } + } + .noDevice{ + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #a8a8a8; + .n_item{ + width: 100%; + text-align: left; + margin-top: 20px; + padding: 0 30px; + box-sizing: border-box; + } + } + + } + .footer{ + text-align: center; + padding: 20px 0; + background-color: #f1f1f1; + border-radius: 0 0 10px 10px; + display: flex; + justify-content: center; + align-items: center; + } + .finding{ + color: orange; + } + .footer_off{ + color: red; + } + } +} diff --git a/src/components/search/index.module.scss b/src/components/search/index.module.scss index 2121879..46a8c27 100644 --- a/src/components/search/index.module.scss +++ b/src/components/search/index.module.scss @@ -20,6 +20,7 @@ border-radius: 50px; padding: 0 60px; box-sizing: border-box; + z-index:0; &::-webkit-input-placeholder { /* WebKit browsers */ color: #999; font-size: 16px; @@ -55,7 +56,7 @@ position: absolute; left: 10px; margin-right: 0; - + z-index: 10; } .icon_out{ margin-right: 10px; diff --git a/src/components/searchInput/index.module.scss b/src/components/searchInput/index.module.scss index c77b507..c381191 100644 --- a/src/components/searchInput/index.module.scss +++ b/src/components/searchInput/index.module.scss @@ -4,20 +4,24 @@ align-items: center; min-height: 62px; padding: 10px 0; - border-bottom: 1px solid #f3f3f3; + border-bottom: 1px solid #F0F0F0; .searchInput_title { width: 150px; + min-height: 50px; font-size: 28px; - border-right: 1px solid #f3f3f3; - color: $color_font_on; + border-right: 1px solid #F0F0F0; + color: $color_font_two; margin-right: 20px; padding-left: 20px; - &::before{ - content: ""; - height: 50px; - width: 1px; - background-color: #f3f3f3; - } + display: flex; + align-items: center; + + // &::before{ + // content: ""; + // height: 50px; + // width: 1px; + // background-color: #F0F0F0; + // } } .searchInput_con{ flex:1; diff --git a/src/components/searchInput/index.tsx b/src/components/searchInput/index.tsx index 2d826fd..35d507c 100644 --- a/src/components/searchInput/index.tsx +++ b/src/components/searchInput/index.tsx @@ -1,5 +1,5 @@ import { Input, View } from "@tarojs/components"; -import { memo, useDebugValue, useMemo } from "react"; +import { memo, ReactHTMLElement, ReactNode, useDebugValue, useMemo } from "react"; import styles from './index.module.scss'; type Params = { @@ -10,7 +10,8 @@ type Params = { showTitle?: false|true, showBorder?: false|true, changeOnInput?: (string) => void, - clickOnInput?: () => void + clickOnInput?: () => void, + children?: ReactNode } @@ -37,9 +38,11 @@ export default memo((props: Params) => { {showTitle&&{title}} - clickOnInput?.()} onInput={(e) => changeOnInput?.(e.detail.value)}/> + {!props.children&& clickOnInput?.()} onInput={(e) => changeOnInput?.(e.detail.value)}/> + ||{props.children} + } - {showIcon&&} + {showIcon&&} ) }) \ No newline at end of file diff --git a/src/components/tabs/index.module.scss b/src/components/tabs/index.module.scss index c04b80f..2f0bc2d 100644 --- a/src/components/tabs/index.module.scss +++ b/src/components/tabs/index.module.scss @@ -1,20 +1,28 @@ .tabs_main{ display: flex; + width: 100%; .tabs_scroll{ width: 100%; display: flex; white-space: nowrap; - border-bottom: 1px solid $color_font_two; - border-top: 1px solid $color_font_two; - height: 102px; - + ::-webkit-scrollbar { + display:none; + width:0; + height:0; + color:transparent; + } .tabs_item{ flex:1; display: inline-block; - padding: 10px 20px; - height: 100%; - box-sizing: border-box; - position: relative; + font-size: 24rpx; + background-color: #F0F0F0; + border-radius: 24rpx; + min-width: 126rpx; + height: 46.93rpx; + text-align: center; + line-height: 46.93rpx; + color: #707070; + margin-right: 20px; .tabs_item_con{ height: 100%; display: flex; diff --git a/src/components/tabs/index.tsx b/src/components/tabs/index.tsx index 71a1bc5..3c7492e 100644 --- a/src/components/tabs/index.tsx +++ b/src/components/tabs/index.tsx @@ -13,10 +13,12 @@ type Params = { list?: ListProps[], defaultValue?: number|string, children?: ReactNode, - tabsOnClick?: (ListProps) => void + tabsOnClick?: (ListProps) => void, + style?:Object, + } -export default memo(({list = [], defaultValue = 0, tabsOnClick}: Params) => { +export default memo(({list = [], defaultValue = 0, tabsOnClick, style={}}: Params) => { const [selected, setSelected] = useState(defaultValue) const [tabId, setTabId] = useState('') @@ -46,8 +48,7 @@ export default memo(({list = [], defaultValue = 0, tabsOnClick}: Params) => { list.map((item, index) => { return ( clickEvent({item,index})}> - {item.title} - {(selected == item.value) && } + {item.title} ) }) diff --git a/src/pages/classList/index.module.scss b/src/pages/classList/index.module.scss index b8028e4..7b62533 100644 --- a/src/pages/classList/index.module.scss +++ b/src/pages/classList/index.module.scss @@ -15,6 +15,8 @@ color: $color_font_three; .text_one{ color: $color_main; + display: flex; + align-items: center; } .text_two{ position: relative; @@ -33,20 +35,11 @@ } } } - .filter_btn{ + .filter_btns{ display: flex; justify-content: space-between; padding: 20px; - view{ - font-size: $font_size_medium; - background-color: #F0F0F0; - border-radius: 24px; - width: 126px; - height: 46.93px; - text-align: center; - line-height: 46.93px; - color: $color_font_three; - } + .selected{ background-color: #ecf5ff; border: 2px solid #cde5ff; diff --git a/src/pages/classList/index.tsx b/src/pages/classList/index.tsx index f9a60b7..bb497cc 100644 --- a/src/pages/classList/index.tsx +++ b/src/pages/classList/index.tsx @@ -7,9 +7,22 @@ import Popup from "@/components/popup"; import styles from './index.module.scss' import { useState } from "react"; import Filter from "./components/filter"; +import Tabs from "@/components/tabs"; +import SortBtn from "@/components/sortBtn"; export default () => { const [showPopup, setShowPopup] = useState(false) + const [selectList, setSelectList] = useState([ + {title: '系列', value:1}, + {title: '系列', value:2}, + {title: '系列', value:3}, + {title: '系列', value:4}, + {title: '系列', value:6}, + {title: '系列', value:7}, + {title: '系列', value:8}, + {title: '系列', value:9}, + {title: '系列', value:10}, + ]) return ( @@ -17,17 +30,17 @@ export default () => { - 综合 + + 综合 + + setShowPopup(true)}> 筛选 - - 系列 - 幅宽 - 克重 - 成分 + + diff --git a/src/pages/searchList/components/selectData/index.module.scss b/src/pages/searchList/components/selectData/index.module.scss new file mode 100644 index 0000000..2f0bc2d --- /dev/null +++ b/src/pages/searchList/components/selectData/index.module.scss @@ -0,0 +1,49 @@ +.tabs_main{ + display: flex; + width: 100%; + .tabs_scroll{ + width: 100%; + display: flex; + white-space: nowrap; + ::-webkit-scrollbar { + display:none; + width:0; + height:0; + color:transparent; + } + .tabs_item{ + flex:1; + display: inline-block; + font-size: 24rpx; + background-color: #F0F0F0; + border-radius: 24rpx; + min-width: 126rpx; + height: 46.93rpx; + text-align: center; + line-height: 46.93rpx; + color: #707070; + margin-right: 20px; + .tabs_item_con{ + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: $font_size_medium; + } + .tabs_index{ + height: 5px; + width: 100%; + background-color:$color_main; + position:absolute; + bottom: 0; + left:0; + border-radius: 50px; + } + .tabs_item_select{ + color: $color_main; + } + } + + + } +} \ No newline at end of file diff --git a/src/pages/searchList/components/selectData/index.tsx b/src/pages/searchList/components/selectData/index.tsx new file mode 100644 index 0000000..60db6ee --- /dev/null +++ b/src/pages/searchList/components/selectData/index.tsx @@ -0,0 +1,57 @@ +import { ScrollView, View } from "@tarojs/components"; +import { memo, useState, ReactNode, useEffect } from "react"; +import classnames from "classnames"; +import styles from './index.module.scss' + + +type ListProps = { + title: string, + value: number +} + +type Params = { + list?: ListProps[], + defaultValue?: number|string, + children?: ReactNode, + tabsOnClick?: (ListProps) => void, + +} + +export default memo(({list = [], defaultValue = 0, tabsOnClick}: Params) => { + + const [tabId, setTabId] = useState('') + + useEffect(() => { + const index = list?.findIndex(item => { + return item.value == defaultValue + }) + if(index !== -1) { + const num = index > 0?( index - 1) : 0 + setTabId(list[num].value.toString()) + } + }, []) + + const clickEvent = ({item, index}: {item:ListProps, index:number}) => { + tabsOnClick?.(item) + setTabId(index.toString()) + } + return ( + <> + + + + { + list.map((item, index) => { + return ( + clickEvent({item,index})}> + {item.title} + + ) + }) + } + + + + + ) +}) \ No newline at end of file diff --git a/src/pages/searchList/hightSearchList.module.scss b/src/pages/searchList/hightSearchList.module.scss index fe0e4de..b40fa10 100644 --- a/src/pages/searchList/hightSearchList.module.scss +++ b/src/pages/searchList/hightSearchList.module.scss @@ -5,13 +5,30 @@ background-color: $color_bg_one; .search{ padding: 20px; + .SearchInput{ + background-color: #fff; + padding: 10px 20px; + box-sizing: border-box; + border-radius: 10px; + } + + .bluetooth_color{ + .color_bock{ + width: 100px; + height: 46px; + } + .color_bock_no{ + font-size: $font_size_medium; + color: $color_font_three; + } + } } .filter{ .filter_all { display: flex; justify-content: space-between; align-items: center; - padding: 20px 50px; + padding: 20px 130px; font-size: $font_size_medium; color: $color_font_three; .text_zh, .text_sc{ @@ -149,6 +166,23 @@ height: 224px; background: #e5ad3a; border-radius: 20px 20px 0px 0px; + position: relative; + image{ + width: 100%; + height: 100%; + border-radius: 20px 20px 0px 0px; + } + .color_num { + background: rgba(0,0,0, 0.5); + border-radius: 0px 50px 0px 0px; + font-size: $font_size_min; + color: #fff; + position: absolute; + left:0; + bottom:0; + padding: 5px 20px; + box-sizing: border-box; + } } } .product_info{ diff --git a/src/pages/searchList/hightSearchList.tsx b/src/pages/searchList/hightSearchList.tsx index 8f77271..a11ec20 100644 --- a/src/pages/searchList/hightSearchList.tsx +++ b/src/pages/searchList/hightSearchList.tsx @@ -1,12 +1,17 @@ -import { ScrollView, Text, View } from "@tarojs/components" +import { Image, ScrollView, Text, View } from "@tarojs/components" import classnames from "classnames"; import Search from '@/components/search' import Filter from "@/components/filter"; import InfiniteScroll from '@/components/infiniteScroll' import SortBtn from "@/components/sortBtn"; +import SearchInput from "@/components/searchInput"; +import LinkBlueTooth from "@/components/bluetooth/LinkBlueTooth"; +import {useBluetooth} from "@/use/contextBlueTooth" +import {toRgb} from '@/common/bluetooth/color/colorSpace' import Tabs from "@/components/tabs"; import styles from './hightSearchList.module.scss' -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; +import Taro, { useReady } from "@tarojs/taro"; export default () => { const [showFilter, setShowFilter] = useState(false) @@ -29,10 +34,40 @@ export default () => { setScrollStatus(false) } },[]) + + const {state, measureAndGetLab} = useBluetooth() + const getLab = () => { + if(state.connected) { + measureAndGetLab() + } else { + Taro.showToast({ + title: '请链接设备', + icon: 'none' + }) + } + } + + const [blueToothColor, setBlueToothColor] = useState('') + useEffect(() => { + if(state.deviceLab) { + console.log('颜色:',state.deviceLab) + const rgb = toRgb([state.deviceLab.L, state.deviceLab.a, state.deviceLab.b]) + setBlueToothColor(`rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`) + } + }, [state.deviceLab]) return ( - + + + + getLab()}> + {blueToothColor&&|| + 请取色 + } + + + @@ -44,10 +79,7 @@ export default () => { 收藏 - - 高级搜索 - - + @@ -76,14 +108,13 @@ export default () => { {new Array(9).fill(' ').map(item => { return - + + + 25色 + 0770#21S精棉平纹 - - 160cm - 110g - - 67.6%棉24%涤纶6.4%氨纶 + 平纹系列 })} diff --git a/src/pages/searchList/searchList.module.scss b/src/pages/searchList/searchList.module.scss index fe0e4de..11f55be 100644 --- a/src/pages/searchList/searchList.module.scss +++ b/src/pages/searchList/searchList.module.scss @@ -57,6 +57,7 @@ .filter_scroll{ flex:1; width: 0; + padding-left: 20px; ::-webkit-scrollbar { display:none; width:0; @@ -149,6 +150,23 @@ height: 224px; background: #e5ad3a; border-radius: 20px 20px 0px 0px; + position: relative; + image{ + width: 100%; + height: 100%; + border-radius: 20px 20px 0px 0px; + } + .color_num { + background: rgba(0,0,0, 0.5); + border-radius: 50px 0px 0px 0px; + font-size: $font_size_min; + color: #fff; + position: absolute; + right:0; + bottom:0; + padding: 5px 20px; + box-sizing: border-box; + } } } .product_info{ diff --git a/src/pages/searchList/searchList.tsx b/src/pages/searchList/searchList.tsx index 2379d9c..21a5757 100644 --- a/src/pages/searchList/searchList.tsx +++ b/src/pages/searchList/searchList.tsx @@ -1,10 +1,12 @@ -import { ScrollView, Text, View } from "@tarojs/components" +import { Image, ScrollView, Text, View } from "@tarojs/components" import classnames from "classnames"; import Search from '@/components/search' import Filter from "@/components/filter"; import InfiniteScroll from '@/components/infiniteScroll' import SortBtn from "@/components/sortBtn"; +import SelectData from "./components/selectData"; import Tabs from "@/components/tabs"; +import { goLink } from "@/common/common"; import styles from './searchList.module.scss' import { useCallback, useState } from "react"; @@ -15,11 +17,11 @@ export default () => { {title: '系列', value:2}, {title: '系列', value:3}, {title: '系列', value:4}, - {title: '系列', value:5}, - {title: '系列', value:5}, - {title: '系列', value:5}, - {title: '系列', value:5}, - {title: '系列', value:5}, + {title: '系列', value:6}, + {title: '系列', value:7}, + {title: '系列', value:8}, + {title: '系列', value:9}, + {title: '系列', value:10}, ]) const [scrollStatus, setScrollStatus] = useState(false) const onscroll = useCallback((e) => { @@ -44,22 +46,16 @@ export default () => { 收藏 - + goLink('/pages/searchList/hightSearchList')}> 高级搜索 + - - - 系列 - 幅宽 - 克重 - 克重 - 克重 - 成分 - - + + + setShowFilter(true)}> 筛选 @@ -76,7 +72,10 @@ export default () => { {new Array(9).fill(' ').map(item => { return - + + + 25色 + 0770#21S精棉平纹 diff --git a/src/use/contextBlueTooth.tsx b/src/use/contextBlueTooth.tsx new file mode 100644 index 0000000..518c5cc --- /dev/null +++ b/src/use/contextBlueTooth.tsx @@ -0,0 +1,477 @@ +import React, {useRef, useState } from "react" +import Taro from "@tarojs/taro"; +import { Command } from "@/common/bluetooth/command"; +import { uint8ArrayToFloat32, uint8ArrayToHex, waitFor } from "@/common/bluetooth/utils"; + + +interface params { + init: () => void + state: Object, + startScan: () => void, + measureAndGetLab: () => any, + getAdapterState: () => void, + connect: (any) => void, + disconnect: () => void +} +const Context = React.createContext(null) + +interface stateStype { + listeners: any, + discovering: boolean, + available: boolean, + connected: any, + connecting: any, + serviceRule: any, + serviceId: any, + characteristicRule: any, + characteristicId: any, + + /** 正在执行的命令 */ + command: any, + responseResolve: any, + responseReject: any, + responseTimer: any, + + /** 是否显示蓝牙调试信息 */ + debug: any, + //搜索到的设备 + devices: any, + //取色仪主动返回的数据 + deviceLab: any +} + +let stateObj: stateStype = { + /** 事件监听器 */ + listeners: new Set(), + /** 正在扫描设备 */ + discovering: false, + /** 蓝牙是否可用 */ + available: true, + /** 当前连接的设备 */ + connected: null, + /** 正在连接的设备 */ + connecting: null, + + serviceRule: /^0000FFE0/, + serviceId: null, + characteristicRule: /^0000FFE1/, + characteristicId: null, + + /** 正在执行的命令 */ + command: null, + responseResolve: null, + responseReject: null, + responseTimer: null, + + /** 是否显示蓝牙调试信息 */ + debug: true, + //搜索到的设备 + devices: [], + //取色仪主动返回的数据 + deviceLab: null +} + +export default (props) => { + let refStatus = useRef(stateObj) + let [state, setState] = useState(refStatus.current) + + const changeStatus = (obj:Object): void => { + refStatus.current = {...refStatus.current, ...obj} + setState({...refStatus.current}) + } + + const init = async () => { + try{ + await openAdapter(); + }catch(e) { + changeStatus({available:false}) + } + + // 绑定事件通知 + Taro.onBluetoothAdapterStateChange(res => { + emit({ type: 'stateUpdate', detail: res }); + }); + Taro.onBLEConnectionStateChange(res => { + emit({ type: res.connected ? 'connected' : 'disconnect', detail: res }); + }); + Taro.onBLECharacteristicValueChange(({ value }) => notifySubscriber(value)); + subscribe(async ev => { + if (ev.type === 'stateUpdate') { + // 蓝牙状态发生的变化 + changeStatus({discovering:ev.detail.discovering, available:ev.detail.available}) + } else if (ev.type === 'disconnect' && refStatus.current.connected && refStatus.current.connected.deviceId === ev.detail.deviceId) { + // 断开连接 + changeStatus({ + connected:null, + serviceId:null, + characteristicId:null, + deviceLab:null, + devices:[] + }) + Taro.showToast({ icon: 'none', title: '蓝牙连接已断开' }); + } else if (ev.type === 'connected' && refStatus.current.connecting) { + // 连接成功 + changeStatus({connected: refStatus.current.connecting, connecting: null}) + Taro.showToast({ title: '蓝牙已连接' }); + } else if (ev.type === 'measure') { + //监听取色仪主动推送lab + await measureAndGetLab() + } + + }); + } + + /** 打开蓝牙适配器 */ + const openAdapter = () => { + return new Promise((resolve, reject) => { + Taro.openBluetoothAdapter({ + success: resolve, + fail: reject + }); + }); + } + + /** + * 推送事件 + * @param {{type: string; data: any}} event + */ + const emit = (event) => { + refStatus.current.listeners.forEach(cb => { + cb && cb(event); + }); + } + + const subscribe = (cb) => { + if (cb) { + changeStatus({ + listeners: refStatus.current.listeners.add(cb) + }) + } + } + + /** + * 获取蓝牙适配器状态 + * @returns {Promise<{discovering: boolean; available: boolean}>} + */ + const getAdapterState = () => { + return new Promise((resolve, reject) => { + Taro.getBluetoothAdapterState({ + success: resolve, + fail: reject + }) + }) + } + + /** + * 启动设备扫描 + * @param {(res: { devices: { name: string, deviceId: string, RSSI: number }[] }) => void} cb + * @param {number} duration + */ + const startScan = (duration = 30000) => { + console.log('开始寻找') + changeStatus({devices:[]}) + Taro.onBluetoothDeviceFound(getDevices); + return new Promise((resolve, reject) => { + Taro.startBluetoothDevicesDiscovery({ + allowDuplicatesKey: true, + success: resolve, + fail: reject + }); + + if (duration > 0) { + setTimeout(() => { + Taro.offBluetoothDeviceFound(getDevices); + Taro.stopBluetoothDevicesDiscovery(); + console.log("停止搜索") + }, duration); + } + }); + } + + //获取搜索到的设备 + const getDevices = (res) => { + res.devices.forEach(device => { + // 排除掉已搜索到的设备和名称不合法的设备, 将新发现的设备添加到列表中 + if (/^CM/.test(device.name) && !refStatus.current.devices.find(i => i.deviceId === device.deviceId)) { + changeStatus({devices: [ ...refStatus.current.devices, device ]}) + } + }); + } + + /** + * 连接设备 + * @param {{ name: string, deviceId: string, RSSI: number }} device + */ + const connect = async (device) => { + try { + changeStatus({connecting: device}) + console.log('connecting::', device) + await createConnection(device.deviceId); + await discoverService(device.deviceId); + await discoverCharacteristic(device.deviceId); + await notifyCharacteristicValueChange(device.deviceId); + } catch (e) { + changeStatus({connecting: null}) + Taro.showToast({ icon: 'none', title: '蓝牙连接失败' }); + throw e; + } + } + + /** 断开当前连接的设备 */ + const disconnect = async () => { + if (!refStatus.current.connected && !refStatus.current.connecting) return; + if (refStatus.current.connected) { + await closeConnection(refStatus.current.connected.deviceId); + resetCommand(); + changeStatus({ + connected: null, + serviceId: null, + characteristicId: null, + devices: [], + deviceLab: null + }) + } + + if (refStatus.current.connecting) { + await closeConnection(refStatus.current.connecting.deviceId); + changeStatus({connecting:null}) + } + } + + /** 创建 BLE 连接 */ + function createConnection(deviceId) { + return new Promise((resolve, reject) => { + Taro.createBLEConnection({ + deviceId, + timeout: 2000, + success: resolve, + fail: reject + }); + }); + } + + /** 关闭 BLE 连接 */ + function closeConnection(deviceId) { + return new Promise((resolve, reject) => { + Taro.closeBLEConnection({ + deviceId, + success: resolve, + fail: reject + }); + }); + } + + /** 搜索服务 */ + function discoverService(deviceId) { + return new Promise((resolve, reject) => { + Taro.getBLEDeviceServices({ + deviceId, + success: ({ services }) => { + const service = services.find(i => refStatus.current.serviceRule.test(i.uuid)); + if (!service) { + reject(new Error('服务不可用')); + } else { + changeStatus({serviceId: service.uuid}) + resolve(service); + } + }, + fail: reject + }); + }); + } + + /** 搜索特征 */ + function discoverCharacteristic(deviceId) { + return new Promise((resolve, reject) => { + Taro.getBLEDeviceCharacteristics({ + deviceId, + serviceId: refStatus.current.serviceId, + success: ({ characteristics }) => { + const characteristic = characteristics.find(i => refStatus.current.characteristicRule.test(i.uuid)); + if (!characteristic) { + reject(new Error('特征不可用')); + } else { + changeStatus({characteristicId: characteristic.uuid}) + resolve(characteristic); + } + }, + fail: reject + }) + }); + } + + /** 启动特征通知 */ + function notifyCharacteristicValueChange(deviceId, stateParm = true) { + return new Promise((resolve, reject) => { + Taro.notifyBLECharacteristicValueChange({ + deviceId, + serviceId: refStatus.current.serviceId, + characteristicId: refStatus.current.characteristicId, + state:stateParm, + success: resolve, + fail: reject + }); + }); + } + + /** + * 通知订阅器 + * @param {ArrayBuffer} buffer + */ + function notifySubscriber(buffer) { + if (refStatus.current.command) { + if (refStatus.current.debug) { + console.log(`[BLE RESP] ${uint8ArrayToHex(new Uint8Array(buffer))}`); + } + refStatus.current.command.fillResponse(buffer); + if (refStatus.current.command.isComplete) { + if (refStatus.current.command.isValid && refStatus.current.responseResolve) { + refStatus.current.responseResolve(refStatus.current.command.response); + } else if (!refStatus.current.command.isValid) { + refStatus.current.responseReject(new Error('无效数据')); + } + resetCommand(); + } + } else { + const uint8Array = new Uint8Array(buffer); + if (uint8Array[0] === 0xbb && uint8Array[1] === 1 && uint8Array[3] === 0) { + const ev = { type: 'measure', detail: { mode: uint8Array[2] } }; + emit(ev); + } + } + } + + /** + * 发送命令 + * @param {Command}} command + * @returns {Promise} + */ + function exec(command) { + return new Promise(async (resolve, reject) => { + if (refStatus.current.command) { + reject(new Error('正在执行其他命令')); + } else { + try { + refStatus.current.command = command; + const data = command.data; + for (let i = 0; i < data.length; i++) { + await sendData(data[i]); + } + + if (command.responseSize <= 0) { + resolve(true); + resetCommand(); + } else { + refStatus.current.responseReject = reject; + refStatus.current.responseResolve = resolve; + refStatus.current.responseTimer = setTimeout(() => { + reject(new Error('命令响应超时')); + resetCommand(); + }, command.timeout); + } + } catch (e) { + reject(e); + } + } + + }); + } + + /** + * 发送命令 + * @param {ArrayBuffer} buffer + */ + function sendData(buffer) { + if (refStatus.current.debug) { + console.log(`[BLE SEND] ${uint8ArrayToHex(new Uint8Array(buffer))}`); + } + return new Promise((resolve, reject) => { + console.log('current:::',refStatus.current) + Taro.writeBLECharacteristicValue({ + deviceId: refStatus.current.connected.deviceId, + serviceId: refStatus.current.serviceId, + characteristicId: refStatus.current.characteristicId, + value: buffer, + success: resolve, + fail: reject + }) + }); + } + + function resetCommand() { + if (refStatus.current.responseTimer) { + clearTimeout(refStatus.current.responseTimer); + } + changeStatus({ + command: null, + responseResolve: null, + responseReject: null, + responseTimer: null + }) + } + + /** + * 测量 + * @param {number} mode + * @returns {Promise} + */ + async function measure (mode = 0) { + console.log('current1:::',Command.WakeUp) + await exec(Command.WakeUp); + console.log('current2:::',Command.WakeUp) + await waitFor(50); + console.log('current3:::',Command.WakeUp) + return await exec(Command.measure(mode)); + } + + /** + * 获取测量的 lab 值 + * @param {number} mode + * @returns {Promise<{ L: number, a: number, b: number }>} + */ + async function getLab(mode = 0) { + await exec(Command.WakeUp); + await waitFor(50); + const data: any = await exec(Command.getLab(mode)); + return { + L: uint8ArrayToFloat32(data.slice(5, 9)), + a: uint8ArrayToFloat32(data.slice(9, 13)), + b: uint8ArrayToFloat32(data.slice(13, 17)), + }; + } + + /** + * 测量并获取 lab 值 + * @param {number} mode + * @returns {Promise<{L: number, a: number, b: number}>} + */ + async function measureAndGetLab(mode = 0) { + await measure(mode); + await waitFor(50); + const lab = await getLab(mode); + console.log('lab2::',lab) + changeStatus({deviceLab:lab}) + return lab + } + + return +} + + +export const useBluetooth = () => { + const res = React.useContext(Context) + if(res) { + return {...res} + } else { + return {} + } + +} \ No newline at end of file