ble-transport.js 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866
  1. const {
  2. buildReadFrame,
  3. buildWriteMultipleRegistersFrame,
  4. buildWriteSingleCoilFrame,
  5. buildWriteSingleRegisterFrame,
  6. formatHex,
  7. getReadResponseByteLength,
  8. MODBUS_CRC_OPTIONS,
  9. MAX_MODBUS_DMA_BYTES,
  10. hasValidCrc16Modbus
  11. } = require('./modbus-rtu')
  12. const {
  13. BYTE_ORDER_HIGH,
  14. hasValidCrc16Ccitt
  15. } = require('./crc')
  16. const {
  17. getBootloaderResponseLength,
  18. isBootloaderFrame
  19. } = require('./bootloader-frame')
  20. const {
  21. notifyPageToast
  22. } = require('./page-toast')
  23. const {
  24. padHex
  25. } = require('./base-utils')
  26. const {
  27. bytesToWords
  28. } = require('./binary-utils')
  29. const SCAN_TIMEOUT = 15000
  30. const CONNECT_TIMEOUT = 10000
  31. const RSSI_REFRESH_INTERVAL = 2000
  32. const DEFAULT_PACKET_SIZE = 20
  33. const MODULE_PACKET_SIZES = [
  34. {
  35. packetSize: 0,
  36. patterns: [/HC[-_ ]?05/i]
  37. },
  38. {
  39. packetSize: 320,
  40. patterns: [/BT[-_ ]?24/i, /\bBT24\b/i]
  41. }
  42. ]
  43. const RESPONSE_TIMEOUT = 1000
  44. const MAX_RESPONSE_BUFFER_BYTES = 128
  45. const MAX_LOG_COUNT = 100
  46. const TARGET_BLE_UUIDS = ['FFE0', 'FFE1']
  47. const MODBUS_EXCEPTION_MESSAGES = {
  48. 0x01: '非法功能',
  49. 0x02: '非法数据地址',
  50. 0x03: '非法数据值',
  51. 0x04: '从站设备故障',
  52. 0x05: '确认',
  53. 0x06: '从站设备忙',
  54. 0x08: '存储奇偶性错误',
  55. 0x0A: '网关路径不可用',
  56. 0x0B: '网关目标设备响应失败'
  57. }
  58. const MODBUS_COMMANDS = [
  59. { key: 'readCoils', label: '01 读取线圈', functionCode: 0x01, inputMode: 'quantity' },
  60. { key: 'readDiscreteInputs', label: '02 读取离散输入', functionCode: 0x02, inputMode: 'quantity' },
  61. { key: 'readHolding', label: '03 读取保持寄存器', functionCode: 0x03, inputMode: 'quantity' },
  62. { key: 'readInput', label: '04 读取输入寄存器', functionCode: 0x04, inputMode: 'quantity' },
  63. { key: 'writeCoil', label: '05 写单线圈', functionCode: 0x05, inputMode: 'coil' },
  64. { key: 'writeRegister', label: '06 写单寄存器', functionCode: 0x06, inputMode: 'single' },
  65. { key: 'writeRegisters', label: '10 写多寄存器', functionCode: 0x10, inputMode: 'multiple' }
  66. ]
  67. const bluetoothErrorMap = {
  68. 10000: '蓝牙模块未初始化,请重新扫描',
  69. 10001: '蓝牙不可用,请开启手机蓝牙',
  70. 10002: '未找到指定设备,请重新扫描',
  71. 10003: '连接失败,请靠近设备后重试',
  72. 10004: '未发现设备服务',
  73. 10005: '未发现设备特征值',
  74. 10006: '当前连接已断开',
  75. 10007: '当前特征值不支持此操作',
  76. 10008: '系统蓝牙异常,请稍后重试',
  77. 10009: '当前系统不支持 BLE',
  78. 10012: '蓝牙操作超时,请重试',
  79. 10013: '设备 ID 无效,请重新扫描'
  80. }
  81. const state = {
  82. adapterAvailable: false,
  83. adapterOpened: false,
  84. characteristicText: '未选择',
  85. connectedDevice: null,
  86. connectedServiceCount: 0,
  87. connectingDeviceId: '',
  88. devices: [],
  89. errorText: '',
  90. isAwaitingResponse: false,
  91. isConnecting: false,
  92. isDiscovering: false,
  93. isSending: false,
  94. logScrollTarget: '',
  95. logs: [],
  96. commandIndex: 2,
  97. commandValue: '0001',
  98. commandValueLabel: '读取数量',
  99. coilEnabled: true,
  100. generatedHex: '',
  101. rxCount: 0,
  102. sendHex: '',
  103. sendQueueLength: 0,
  104. protocolCommands: MODBUS_COMMANDS,
  105. protocolErrorText: '',
  106. registerAddress: '0000',
  107. showCoilValue: false,
  108. showCommandValue: true,
  109. systemTip: '',
  110. txCount: 0,
  111. slaveAddress: 'F0',
  112. writeCharacteristicId: '',
  113. writeServiceId: '',
  114. writeType: ''
  115. }
  116. let initialized = false
  117. let scanTimer = null
  118. let rssiTimer = null
  119. let isReadingRssi = false
  120. let pendingRequest = null
  121. let sendQueue = []
  122. let isProcessingSendQueue = false
  123. let sendQueueGeneration = 0
  124. let sendJobSequence = 0
  125. let deviceMap = {}
  126. let deviceSequence = 0
  127. let logSequence = 0
  128. const subscribers = []
  129. const rawResponseSubscribers = []
  130. function setState(changedData) {
  131. Object.assign(state, changedData)
  132. subscribers.slice().forEach((subscriber) => {
  133. subscriber(getState())
  134. })
  135. }
  136. function getState() {
  137. return {
  138. ...state,
  139. devices: state.devices.slice(),
  140. logs: state.logs.slice()
  141. }
  142. }
  143. function subscribe(subscriber) {
  144. if (typeof subscriber !== 'function') return () => {}
  145. subscribers.push(subscriber)
  146. subscriber(getState())
  147. return () => {
  148. const index = subscribers.indexOf(subscriber)
  149. if (index >= 0) subscribers.splice(index, 1)
  150. }
  151. }
  152. function subscribeRawResponse(subscriber) {
  153. if (typeof subscriber !== 'function') return () => {}
  154. rawResponseSubscribers.push(subscriber)
  155. return () => {
  156. const index = rawResponseSubscribers.indexOf(subscriber)
  157. if (index >= 0) rawResponseSubscribers.splice(index, 1)
  158. }
  159. }
  160. function callWx(apiName, params = {}) {
  161. return new Promise((resolve, reject) => {
  162. const api = wx[apiName]
  163. if (typeof api !== 'function') {
  164. reject(new Error(`${apiName} 不可用`))
  165. return
  166. }
  167. api({
  168. ...params,
  169. success: resolve,
  170. fail: reject
  171. })
  172. })
  173. }
  174. function formatBluetoothError(error) {
  175. if (!error) return '操作失败'
  176. const message = bluetoothErrorMap[error.errCode]
  177. if (message) return message
  178. return error.errMsg || error.message || '蓝牙操作失败'
  179. }
  180. function normalizeDevice(device) {
  181. const advertisServiceUUIDs = device.advertisServiceUUIDs || []
  182. const displayName = String(device.name || device.localName || '').trim() || '未命名设备'
  183. const packetSize = inferPacketSize({
  184. displayName,
  185. localName: device.localName,
  186. name: device.name
  187. })
  188. const isTargetAdvertised = hasTargetAdvertisedUuid({
  189. advertisServiceUUIDs
  190. })
  191. return {
  192. deviceId: device.deviceId,
  193. name: device.name || '',
  194. localName: device.localName || '',
  195. RSSI: device.RSSI,
  196. advertisServiceUUIDs,
  197. displayName,
  198. isTargetAdvertised,
  199. packetSize,
  200. signalText: formatSignalText(device.RSSI),
  201. serviceText: advertisServiceUUIDs.length ? advertisServiceUUIDs.join(', ') : '未广播服务',
  202. targetText: isTargetAdvertised ? '广播含目标 UUID' : '',
  203. lastSeenAt: Date.now()
  204. }
  205. }
  206. function formatSignalText(RSSI) {
  207. return typeof RSSI === 'number' ? `${RSSI} dBm` : '--'
  208. }
  209. function inferPacketSize(device = {}) {
  210. const text = [device.displayName, device.name, device.localName]
  211. .map((value) => String(value || ''))
  212. .join(' ')
  213. .toUpperCase()
  214. for (const item of MODULE_PACKET_SIZES) {
  215. const matchedPattern = (item.patterns || []).some((pattern) => pattern.test(text))
  216. if (matchedPattern) {
  217. return item.packetSize
  218. }
  219. }
  220. return DEFAULT_PACKET_SIZE
  221. }
  222. function resolvePacketSize(packetSize, frameLength) {
  223. if (packetSize === 0) return frameLength || DEFAULT_PACKET_SIZE
  224. if (Number.isInteger(packetSize) && packetSize > 0) return packetSize
  225. return DEFAULT_PACKET_SIZE
  226. }
  227. function normalizeUuid(value) {
  228. return String(value || '').replace(/-/g, '').toUpperCase()
  229. }
  230. function isTargetUuid(value) {
  231. const uuid = normalizeUuid(value)
  232. return TARGET_BLE_UUIDS.some((target) => uuid.indexOf(target) >= 0)
  233. }
  234. function hasTargetAdvertisedUuid(device) {
  235. return (device.advertisServiceUUIDs || []).some(isTargetUuid)
  236. }
  237. function mergeAdvertisedServiceUUIDs(left = [], right = []) {
  238. const uuidMap = {}
  239. const uuids = []
  240. left.concat(right).forEach((uuid) => {
  241. const key = normalizeUuid(uuid)
  242. if (!key || uuidMap[key]) return
  243. uuidMap[key] = true
  244. uuids.push(uuid)
  245. })
  246. return uuids
  247. }
  248. function normalizeHex(value) {
  249. return String(value || '')
  250. .replace(/0x/gi, '')
  251. .replace(/[\s,;:_-]/g, '')
  252. .toUpperCase()
  253. }
  254. function validateHex(value) {
  255. const trimmed = String(value || '').trim()
  256. const withoutPrefix = trimmed.replace(/0x/gi, '')
  257. const compact = normalizeHex(trimmed)
  258. if (!compact) return '请输入要发送的十六进制数据'
  259. if (/[^0-9a-fA-F\s,;:_-]/.test(withoutPrefix)) return '只支持十六进制字符'
  260. if (compact.length % 2 !== 0) return '十六进制长度必须为偶数'
  261. return ''
  262. }
  263. function parseHexNumber(value, label, maxValue) {
  264. const text = String(value || '').trim().replace(/^0x/i, '')
  265. if (!text || !/^[0-9a-fA-F]+$/.test(text)) {
  266. throw new Error(`${label}请输入十六进制数值`)
  267. }
  268. const parsedValue = parseInt(text, 16)
  269. if (parsedValue > maxValue) {
  270. throw new Error(`${label}超出范围`)
  271. }
  272. return parsedValue
  273. }
  274. function parseRegisterValues(value) {
  275. const text = String(value || '').trim()
  276. if (!text) throw new Error('请输入寄存器写入值')
  277. return text.split(/[\s,;]+/)
  278. .filter(Boolean)
  279. .map((item) => parseHexNumber(item, '写入值', 0xFFFF))
  280. }
  281. function normalizeMaxFrameBytes(maxFrameBytes) {
  282. const numberValue = Number(maxFrameBytes)
  283. if (Number.isFinite(numberValue) && Math.round(numberValue) === 0) return 0
  284. if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue)
  285. return MAX_MODBUS_DMA_BYTES
  286. }
  287. function getResponseBufferLimit(expected, maxFrameBytes) {
  288. const responseLength = expected
  289. ? getReadResponseByteLength(expected.functionCode, expected.quantity)
  290. : 0
  291. const frameLimit = normalizeMaxFrameBytes(maxFrameBytes)
  292. if (frameLimit === 0) {
  293. return Math.max(MAX_RESPONSE_BUFFER_BYTES, responseLength + 8)
  294. }
  295. return Math.max(MAX_RESPONSE_BUFFER_BYTES, frameLimit + 8, responseLength + 8)
  296. }
  297. function getCommand(index) {
  298. return MODBUS_COMMANDS[index] || MODBUS_COMMANDS[0]
  299. }
  300. function getDefaultCommandValue(command) {
  301. if (command.inputMode === 'quantity') return '0001'
  302. if (command.inputMode === 'coil') return 'ON'
  303. if (command.inputMode === 'multiple') return '0000'
  304. return '0000'
  305. }
  306. function generateModbusFrame(command, slaveAddress, registerAddress, commandValue, coilEnabled) {
  307. const slave = parseHexNumber(slaveAddress, '从站地址', 0xFF)
  308. const address = parseHexNumber(registerAddress, '协议寄存器', 0xFFFF)
  309. if (command.inputMode === 'quantity') {
  310. const quantity = parseHexNumber(commandValue, '读取数量', 0xFFFF)
  311. return buildReadFrame(slave, command.functionCode, address, quantity)
  312. }
  313. if (command.inputMode === 'coil') {
  314. return buildWriteSingleCoilFrame(slave, address, coilEnabled)
  315. }
  316. if (command.inputMode === 'single') {
  317. return buildWriteSingleRegisterFrame(slave, address, parseHexNumber(commandValue, '写入值', 0xFFFF))
  318. }
  319. return buildWriteMultipleRegistersFrame(slave, address, parseRegisterValues(commandValue))
  320. }
  321. function createProtocolState(commandIndex, slaveAddress, registerAddress, commandValue, coilEnabled) {
  322. const command = getCommand(commandIndex)
  323. const commandValueLabel = command.inputMode === 'quantity' ? '读取数量' : '写入值'
  324. try {
  325. return {
  326. commandValueLabel,
  327. generatedHex: formatHex(generateModbusFrame(command, slaveAddress, registerAddress, commandValue, coilEnabled)),
  328. protocolErrorText: '',
  329. showCoilValue: command.inputMode === 'coil',
  330. showCommandValue: command.inputMode !== 'coil'
  331. }
  332. } catch (error) {
  333. return {
  334. commandValueLabel,
  335. generatedHex: '',
  336. protocolErrorText: error.message,
  337. showCoilValue: command.inputMode === 'coil',
  338. showCommandValue: command.inputMode !== 'coil'
  339. }
  340. }
  341. }
  342. function hexToArrayBuffer(hexText) {
  343. const hex = normalizeHex(hexText)
  344. const buffer = new ArrayBuffer(hex.length / 2)
  345. const view = new Uint8Array(buffer)
  346. for (let index = 0; index < view.length; index += 1) {
  347. view[index] = parseInt(hex.substr(index * 2, 2), 16)
  348. }
  349. return buffer
  350. }
  351. function arrayBufferToHex(buffer) {
  352. if (!buffer) return ''
  353. return Array.prototype.map.call(new Uint8Array(buffer), (item) => item.toString(16).padStart(2, '0')).join(' ').toUpperCase()
  354. }
  355. function parseModbusResponse(bytes) {
  356. if (!Array.isArray(bytes) || bytes.length < 5 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null
  357. const slaveAddress = bytes[0]
  358. const functionCode = bytes[1]
  359. if (functionCode & 0x80) {
  360. return {
  361. exceptionCode: bytes[2],
  362. functionCode,
  363. isException: true,
  364. slaveAddress,
  365. sourceFunctionCode: functionCode & 0x7F
  366. }
  367. }
  368. if (functionCode === 0x01 || functionCode === 0x02) {
  369. const byteCount = bytes[2]
  370. const dataEnd = 3 + byteCount
  371. if (bytes.length < dataEnd + 2) return null
  372. return {
  373. byteCount,
  374. dataBytes: bytes.slice(3, dataEnd),
  375. functionCode,
  376. isException: false,
  377. slaveAddress
  378. }
  379. }
  380. if (functionCode === 0x03 || functionCode === 0x04) {
  381. const byteCount = bytes[2]
  382. const dataEnd = 3 + byteCount
  383. if (bytes.length < dataEnd + 2) return null
  384. return {
  385. byteCount,
  386. dataBytes: bytes.slice(3, dataEnd),
  387. functionCode,
  388. isException: false,
  389. slaveAddress,
  390. words: bytesToWords(bytes.slice(3, dataEnd))
  391. }
  392. }
  393. if (functionCode === 0x05 || functionCode === 0x06 || functionCode === 0x10) {
  394. return {
  395. address: ((bytes[2] << 8) | bytes[3]) & 0xFFFF,
  396. functionCode,
  397. isException: false,
  398. quantityOrValue: ((bytes[4] << 8) | bytes[5]) & 0xFFFF,
  399. slaveAddress
  400. }
  401. }
  402. return {
  403. functionCode,
  404. isException: false,
  405. slaveAddress
  406. }
  407. }
  408. function parseModbusRequest(bytes) {
  409. if (!Array.isArray(bytes) || bytes.length < 6 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) return null
  410. const slaveAddress = bytes[0]
  411. const functionCode = bytes[1]
  412. const address = ((bytes[2] << 8) | bytes[3]) & 0xFFFF
  413. let quantity = 1
  414. let value
  415. if (functionCode === 0x01 || functionCode === 0x02 || functionCode === 0x03 || functionCode === 0x04 || functionCode === 0x10) {
  416. quantity = ((bytes[4] << 8) | bytes[5]) & 0xFFFF
  417. }
  418. if (functionCode === 0x05 || functionCode === 0x06) {
  419. value = ((bytes[4] << 8) | bytes[5]) & 0xFFFF
  420. }
  421. return {
  422. address,
  423. functionCode,
  424. kind: 'raw-hex',
  425. quantity,
  426. value,
  427. slaveAddress
  428. }
  429. }
  430. function validateDmaFrameLength(bytes, expected) {
  431. const maxFrameBytes = normalizeMaxFrameBytes(expected && expected.maxFrameBytes)
  432. if (maxFrameBytes === 0) return ''
  433. if (bytes.length > maxFrameBytes) {
  434. return `发送帧长度 ${bytes.length} 字节,超过最大包长 ${maxFrameBytes} 字节限制`
  435. }
  436. if (!expected) return ''
  437. const responseLength = getReadResponseByteLength(expected.functionCode, expected.quantity)
  438. if (responseLength > maxFrameBytes) {
  439. return `预计返回帧长度 ${responseLength} 字节,超过最大包长 ${maxFrameBytes} 字节限制`
  440. }
  441. return ''
  442. }
  443. function formatTime(timestamp) {
  444. const date = new Date(timestamp)
  445. const pad = (value, length = 2) => String(value).padStart(length, '0')
  446. return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`
  447. }
  448. function getCharacteristicRole(properties = {}) {
  449. const canWrite = !!(properties.write || properties.writeNoResponse)
  450. const canNotify = !!(properties.notify || properties.indicate)
  451. if (canWrite && canNotify) return '收发'
  452. if (canWrite) return '发送'
  453. if (canNotify) return '接收'
  454. if (properties.read) return '读取'
  455. return '其他'
  456. }
  457. function buildCharacteristicText(serviceId, characteristicId) {
  458. if (!serviceId || !characteristicId) return '未选择'
  459. return `${serviceId.slice(0, 8)} / ${characteristicId.slice(0, 8)}`
  460. }
  461. function hasTargetCharacteristic(discovery) {
  462. return (discovery.services || []).some((service) => (
  463. isTargetUuid(service.uuid) || (service.characteristics || []).some((item) => isTargetUuid(item.uuid))
  464. ))
  465. }
  466. function getExceptionText(code) {
  467. return MODBUS_EXCEPTION_MESSAGES[code] || '未知异常'
  468. }
  469. function addLog(direction, payload, note = '') {
  470. logSequence += 1
  471. const logItem = {
  472. id: `log-${Date.now()}-${logSequence}`,
  473. direction,
  474. note,
  475. payload,
  476. time: formatTime(Date.now())
  477. }
  478. const nextLogs = state.logs.concat(logItem).slice(-MAX_LOG_COUNT)
  479. setState({
  480. logScrollTarget: logItem.id,
  481. logs: nextLogs
  482. })
  483. }
  484. function getReceiveCrcState(rawBytes) {
  485. if (!rawBytes || rawBytes.length < 4) return ''
  486. if (isBootloaderFrame(rawBytes)) {
  487. const expectedLength = getBootloaderResponseLength(rawBytes)
  488. if (expectedLength && rawBytes.length < expectedLength) return '片段'
  489. return hasValidCrc16Ccitt(rawBytes, { byteOrder: BYTE_ORDER_HIGH }) ? 'CRC OK' : 'CRC ERR'
  490. }
  491. return hasValidCrc16Modbus(rawBytes, MODBUS_CRC_OPTIONS) ? 'CRC OK' : (pendingRequest ? '片段' : 'CRC ERR')
  492. }
  493. function showCommandAlert(title, content) {
  494. const message = content || title || '操作失败'
  495. notifyPageToast(message, 'error')
  496. setState({
  497. errorText: message
  498. })
  499. }
  500. function clearScanTimer() {
  501. if (!scanTimer) return
  502. clearTimeout(scanTimer)
  503. scanTimer = null
  504. }
  505. async function stopScan() {
  506. clearScanTimer()
  507. try {
  508. await callWx('stopBluetoothDevicesDiscovery')
  509. } catch (error) {
  510. if (error.errCode !== 10000) {
  511. setState({
  512. errorText: formatBluetoothError(error)
  513. })
  514. }
  515. }
  516. setState({
  517. isDiscovering: false
  518. })
  519. }
  520. function resetScanTimer() {
  521. clearScanTimer()
  522. scanTimer = setTimeout(() => {
  523. stopScan()
  524. if (!state.devices.length) {
  525. setState({
  526. systemTip: '安卓真机请确认系统定位已开启,并允许微信使用附近设备或位置信息。'
  527. })
  528. }
  529. }, SCAN_TIMEOUT)
  530. }
  531. function mergeDevices(devices) {
  532. if (!devices.length) return
  533. devices.forEach((device) => {
  534. if (!device.deviceId) return
  535. const previousDevice = deviceMap[device.deviceId] || {}
  536. const nextDevice = normalizeDevice(device)
  537. const advertisServiceUUIDs = mergeAdvertisedServiceUUIDs(
  538. previousDevice.advertisServiceUUIDs,
  539. nextDevice.advertisServiceUUIDs
  540. )
  541. const isTargetAdvertised = !!previousDevice.isTargetAdvertised || hasTargetAdvertisedUuid({
  542. advertisServiceUUIDs
  543. })
  544. const isTargetDevice = !!previousDevice.isTargetDevice
  545. const seenIndex = previousDevice.seenIndex || (deviceSequence += 1)
  546. deviceMap[device.deviceId] = {
  547. ...previousDevice,
  548. ...nextDevice,
  549. advertisServiceUUIDs,
  550. displayName: nextDevice.displayName === '未命名设备' && previousDevice.displayName
  551. ? previousDevice.displayName
  552. : nextDevice.displayName,
  553. isTargetAdvertised,
  554. isTargetDevice,
  555. packetSize: nextDevice.packetSize || previousDevice.packetSize || DEFAULT_PACKET_SIZE,
  556. seenIndex,
  557. serviceText: advertisServiceUUIDs.length ? advertisServiceUUIDs.join(', ') : '未广播服务',
  558. targetText: isTargetDevice ? '已发现目标特征' : (isTargetAdvertised ? '广播含目标 UUID' : '')
  559. }
  560. })
  561. refreshDeviceList()
  562. }
  563. function refreshDeviceList() {
  564. const deviceList = getSortedDeviceList()
  565. setState({
  566. devices: deviceList.slice(0, 30)
  567. })
  568. }
  569. function getSortedDeviceList() {
  570. return Object.keys(deviceMap)
  571. .map((deviceId) => deviceMap[deviceId])
  572. .sort((left, right) => {
  573. const leftIndex = Number(left.seenIndex) || 0
  574. const rightIndex = Number(right.seenIndex) || 0
  575. return leftIndex - rightIndex
  576. })
  577. }
  578. function clearPendingRequest() {
  579. if (!pendingRequest) return null
  580. const pending = pendingRequest
  581. clearTimeout(pendingRequest.timer)
  582. pendingRequest = null
  583. setState({
  584. isAwaitingResponse: false
  585. })
  586. return pending
  587. }
  588. function cancelPendingRequest() {
  589. const pending = clearPendingRequest()
  590. if (pending) {
  591. pending.resolve(false)
  592. }
  593. }
  594. function clearSendQueue() {
  595. if (!sendQueue.length) return
  596. const queuedJobs = sendQueue.splice(0)
  597. queuedJobs.forEach((job) => {
  598. job.resolve(false)
  599. })
  600. setState({
  601. sendQueueLength: 0
  602. })
  603. }
  604. function resetSendRuntimeState() {
  605. sendQueueGeneration += 1
  606. cancelPendingRequest()
  607. clearSendQueue()
  608. isProcessingSendQueue = false
  609. setState({
  610. isAwaitingResponse: false,
  611. isSending: false,
  612. sendQueueLength: 0
  613. })
  614. }
  615. function clearConnectedState(changedData = {}) {
  616. stopRssiRefresh()
  617. resetSendRuntimeState()
  618. setState({
  619. characteristicText: '未选择',
  620. connectedDevice: null,
  621. connectedServiceCount: 0,
  622. connectingDeviceId: '',
  623. isConnecting: false,
  624. writeCharacteristicId: '',
  625. writeServiceId: '',
  626. writeType: '',
  627. ...changedData
  628. })
  629. }
  630. function stopRssiRefresh() {
  631. if (rssiTimer) {
  632. clearInterval(rssiTimer)
  633. rssiTimer = null
  634. }
  635. isReadingRssi = false
  636. }
  637. function applyRssiUpdate(deviceId, rssi) {
  638. if (!state.connectedDevice || state.connectedDevice.deviceId !== deviceId || typeof rssi !== 'number') {
  639. return
  640. }
  641. const signalText = formatSignalText(rssi)
  642. const updatedDevice = {
  643. ...state.connectedDevice,
  644. RSSI: rssi,
  645. lastSeenAt: Date.now(),
  646. signalText
  647. }
  648. deviceMap[deviceId] = {
  649. ...(deviceMap[deviceId] || {}),
  650. RSSI: rssi,
  651. lastSeenAt: updatedDevice.lastSeenAt,
  652. signalText
  653. }
  654. setState({
  655. connectedDevice: updatedDevice,
  656. devices: getSortedDeviceList().slice(0, 30)
  657. })
  658. }
  659. async function refreshConnectedRssi() {
  660. const { connectedDevice } = state
  661. if (!connectedDevice || typeof wx === 'undefined' || typeof wx.getBLEDeviceRSSI !== 'function') return
  662. if (isReadingRssi) return
  663. isReadingRssi = true
  664. try {
  665. const result = await callWx('getBLEDeviceRSSI', {
  666. deviceId: connectedDevice.deviceId
  667. })
  668. if (!state.connectedDevice || state.connectedDevice.deviceId !== connectedDevice.deviceId) return
  669. applyRssiUpdate(connectedDevice.deviceId, result && result.RSSI)
  670. } catch (error) {
  671. if (isConnectionLostError(error)) {
  672. clearConnectedState({
  673. errorText: formatBluetoothError(error)
  674. })
  675. }
  676. } finally {
  677. isReadingRssi = false
  678. }
  679. }
  680. function startRssiRefresh() {
  681. stopRssiRefresh()
  682. if (!state.connectedDevice || typeof wx === 'undefined' || typeof wx.getBLEDeviceRSSI !== 'function') {
  683. return
  684. }
  685. refreshConnectedRssi()
  686. rssiTimer = setInterval(() => {
  687. refreshConnectedRssi()
  688. }, RSSI_REFRESH_INTERVAL)
  689. }
  690. function isConnectionLostError(error) {
  691. if (!error) return false
  692. if ([10000, 10001, 10006, 10013].includes(error.errCode)) return true
  693. const message = String(error.errMsg || error.message || '').toLowerCase()
  694. return message.includes('disconnect') || message.includes('not connected')
  695. }
  696. function isExpectedResponse(response, expected) {
  697. if (response.functionCode === 0x01 || response.functionCode === 0x02) {
  698. return Array.isArray(response.dataBytes) && response.dataBytes.length >= Math.ceil(expected.quantity / 8)
  699. }
  700. if (response.functionCode === 0x03 || response.functionCode === 0x04) {
  701. return Array.isArray(response.words) && response.words.length >= expected.quantity
  702. }
  703. if (response.functionCode === 0x10) {
  704. return response.address === expected.address && response.quantityOrValue === expected.quantity
  705. }
  706. if (response.functionCode === 0x05 || response.functionCode === 0x06) {
  707. if (response.address !== expected.address) return false
  708. if (Number.isInteger(expected.value)) return response.quantityOrValue === expected.value
  709. return true
  710. }
  711. return true
  712. }
  713. function getExpectedResponseLength(expected, responseFunctionCode, responseBytes) {
  714. if (!expected) return 0
  715. if (responseFunctionCode === (expected.functionCode | 0x80)) {
  716. return 5
  717. }
  718. if (responseFunctionCode === 0x01 || responseFunctionCode === 0x02) {
  719. if (responseBytes.length < 3) return 0
  720. return 3 + Number(responseBytes[2] || 0) + 2
  721. }
  722. if (responseFunctionCode === 0x03 || responseFunctionCode === 0x04) {
  723. if (responseBytes.length < 3) return 0
  724. return 3 + Number(responseBytes[2] || 0) + 2
  725. }
  726. if (responseFunctionCode === 0x05 || responseFunctionCode === 0x06 || responseFunctionCode === 0x10) {
  727. return 8
  728. }
  729. return 0
  730. }
  731. function alignResponseBuffer(buffer, expected) {
  732. if (!Array.isArray(buffer) || !buffer.length || !expected) return
  733. const expectedFunctionCodes = [expected.functionCode, expected.functionCode | 0x80]
  734. let matchIndex = -1
  735. for (let index = 0; index < buffer.length - 1; index += 1) {
  736. if (buffer[index] !== expected.slaveAddress) continue
  737. if (!expectedFunctionCodes.includes(buffer[index + 1])) continue
  738. matchIndex = index
  739. break
  740. }
  741. if (matchIndex > 0) {
  742. buffer.splice(0, matchIndex)
  743. } else if (matchIndex < 0 && buffer.length > 2) {
  744. buffer.splice(0, buffer.length - 1)
  745. }
  746. }
  747. function finishPendingRequest(resolveValue) {
  748. const pending = clearPendingRequest()
  749. if (pending) {
  750. pending.resolve(resolveValue)
  751. }
  752. }
  753. function consumePendingResponseBuffer() {
  754. const pending = pendingRequest
  755. if (!pending || !Array.isArray(pending.responseBuffer)) return
  756. const buffer = pending.responseBuffer
  757. alignResponseBuffer(buffer, pending.expected)
  758. if (buffer.length < 2) return
  759. const responseFunctionCode = buffer[1]
  760. const responseLength = getExpectedResponseLength(pending.expected, responseFunctionCode, buffer)
  761. if (!responseLength) return
  762. const frameLimit = normalizeMaxFrameBytes(pending.expected && pending.expected.maxFrameBytes)
  763. if (frameLimit > 0 && responseLength > frameLimit) {
  764. const content = `${pending.label} 返回帧长度 ${responseLength} 字节,超过最大包长 ${frameLimit} 字节限制,已丢弃`
  765. addLog('SYS', content)
  766. finishPendingRequest(false)
  767. if (pending.showModal) {
  768. showCommandAlert('通讯异常', content)
  769. }
  770. return
  771. }
  772. if (buffer.length < responseLength) return
  773. const frameBytes = buffer.slice(0, responseLength)
  774. const response = parseModbusResponse(frameBytes)
  775. if (!response) {
  776. const content = `${pending.label} 收到无效响应帧,已丢弃`
  777. addLog('SYS', content)
  778. finishPendingRequest(false)
  779. if (pending.showModal) {
  780. showCommandAlert('通讯异常', content)
  781. }
  782. return
  783. }
  784. const responseCode = response.isException ? response.sourceFunctionCode : response.functionCode
  785. if (response.slaveAddress !== pending.expected.slaveAddress || responseCode !== pending.expected.functionCode) {
  786. buffer.shift()
  787. consumePendingResponseBuffer()
  788. return
  789. }
  790. if (response.isException) {
  791. const exceptionText = getExceptionText(response.exceptionCode)
  792. const content = `设备返回异常帧:功能码 0x${padHex(response.sourceFunctionCode, 2)},异常码 0x${padHex(response.exceptionCode, 2)}(${exceptionText})`
  793. addLog('SYS', content)
  794. finishPendingRequest(false)
  795. if (pending.showModal) {
  796. showCommandAlert('设备返回故障帧', content)
  797. }
  798. return
  799. }
  800. if (!isExpectedResponse(response, pending.expected)) {
  801. const content = `${pending.label} 收到不匹配响应,已丢弃`
  802. addLog('SYS', content)
  803. finishPendingRequest(false)
  804. if (pending.showModal) {
  805. showCommandAlert('通讯异常', content)
  806. }
  807. return
  808. }
  809. buffer.splice(0, responseLength)
  810. finishPendingRequest(response)
  811. if (buffer.length) {
  812. consumePendingResponseBuffer()
  813. }
  814. }
  815. function handleModbusResponse(bytes) {
  816. if (!pendingRequest || !Array.isArray(bytes) || !bytes.length) return
  817. pendingRequest.responseBuffer = pendingRequest.responseBuffer.concat(bytes)
  818. const bufferLimit = pendingRequest.responseBufferLimit || MAX_RESPONSE_BUFFER_BYTES
  819. if (pendingRequest.responseBuffer.length > bufferLimit) {
  820. const pending = pendingRequest
  821. const content = `${pending.label} 返回数据超过缓冲区,已丢弃`
  822. addLog('SYS', content)
  823. finishPendingRequest(false)
  824. if (pending.showModal) {
  825. showCommandAlert('通讯异常', content)
  826. }
  827. return
  828. }
  829. consumePendingResponseBuffer()
  830. }
  831. function createPendingRequest(label, expected, options = {}) {
  832. return new Promise((resolve) => {
  833. const timer = setTimeout(() => {
  834. const pending = clearPendingRequest()
  835. if (!pending) return
  836. addLog('SYS', `${label} 超时`)
  837. if (options.showModal !== false) {
  838. showCommandAlert('通讯超时', `${label} 1秒内没有收到回复`)
  839. }
  840. resolve(false)
  841. }, options.timeout || RESPONSE_TIMEOUT)
  842. pendingRequest = {
  843. expected,
  844. label,
  845. resolve,
  846. timer,
  847. responseBufferLimit: getResponseBufferLimit(expected, options.maxFrameBytes),
  848. showModal: options.showModal !== false,
  849. responseBuffer: []
  850. }
  851. setState({
  852. isAwaitingResponse: true
  853. })
  854. })
  855. }
  856. function init() {
  857. if (initialized) return
  858. wx.onBluetoothDeviceFound((res) => {
  859. mergeDevices(res.devices || [])
  860. })
  861. wx.onBluetoothAdapterStateChange((res) => {
  862. setState({
  863. adapterAvailable: !!res.available,
  864. isDiscovering: !!res.discovering
  865. })
  866. if (!res.available) {
  867. clearScanTimer()
  868. clearConnectedState({
  869. adapterAvailable: false,
  870. adapterOpened: false,
  871. errorText: '请开启手机蓝牙后重新扫描',
  872. isDiscovering: false,
  873. sendQueueLength: 0
  874. })
  875. }
  876. })
  877. wx.onBLEConnectionStateChange((res) => {
  878. const { connectedDevice } = state
  879. if (!connectedDevice || connectedDevice.deviceId !== res.deviceId) return
  880. if (!res.connected) {
  881. addLog('SYS', '连接已断开')
  882. clearConnectedState({
  883. errorText: '',
  884. sendQueueLength: 0
  885. })
  886. }
  887. })
  888. wx.onBLECharacteristicValueChange((res) => {
  889. const hex = arrayBufferToHex(res.value)
  890. const byteLength = res.value ? res.value.byteLength : 0
  891. const rawBytes = Array.prototype.slice.call(new Uint8Array(res.value || new ArrayBuffer(0)))
  892. const crcState = getReceiveCrcState(rawBytes)
  893. setState({
  894. rxCount: state.rxCount + byteLength
  895. })
  896. addLog('RX', hex, crcState)
  897. rawResponseSubscribers.slice().forEach((subscriber) => {
  898. subscriber(rawBytes, res)
  899. })
  900. handleModbusResponse(rawBytes)
  901. })
  902. initialized = true
  903. }
  904. async function getAuthSetting() {
  905. return callWx('getSetting')
  906. .then((res) => res.authSetting || {})
  907. .catch(() => ({}))
  908. }
  909. function showPermissionModal(title, content) {
  910. return new Promise((resolve, reject) => {
  911. wx.showModal({
  912. title,
  913. content,
  914. confirmText: '去设置',
  915. success: async (res) => {
  916. if (!res.confirm) {
  917. reject(new Error('用户取消授权'))
  918. return
  919. }
  920. try {
  921. await callWx('openSetting')
  922. resolve()
  923. } catch (error) {
  924. reject(error)
  925. }
  926. },
  927. fail: reject
  928. })
  929. })
  930. }
  931. async function ensureBluetoothAuthorized() {
  932. const authSetting = await getAuthSetting()
  933. if (authSetting['scope.bluetooth']) return
  934. if (authSetting['scope.bluetooth'] === false) {
  935. await showPermissionModal('需要蓝牙权限', '请在设置中允许使用蓝牙,用于扫描并连接附近设备。')
  936. return
  937. }
  938. try {
  939. await callWx('authorize', {
  940. scope: 'scope.bluetooth'
  941. })
  942. } catch (error) {
  943. await showPermissionModal('需要蓝牙权限', '请在设置中允许使用蓝牙,用于扫描并连接附近设备。')
  944. }
  945. }
  946. async function ensureAndroidLocationAuthorized() {
  947. const systemInfo = wx.getSystemInfoSync ? wx.getSystemInfoSync() : wx.getDeviceInfo()
  948. if (systemInfo.platform !== 'android') return
  949. const authSetting = await getAuthSetting()
  950. if (authSetting['scope.userLocation']) return
  951. setState({
  952. systemTip: '安卓系统扫描 BLE 设备通常需要开启系统定位权限。'
  953. })
  954. if (authSetting['scope.userLocation'] === false) {
  955. await showPermissionModal('需要定位权限', '安卓系统要求定位权限开启后才能搜索附近 BLE 设备。')
  956. return
  957. }
  958. try {
  959. await callWx('authorize', {
  960. scope: 'scope.userLocation'
  961. })
  962. } catch (error) {
  963. await showPermissionModal('需要定位权限', '安卓系统要求定位权限开启后才能搜索附近 BLE 设备。')
  964. }
  965. }
  966. async function openAdapter() {
  967. if (state.adapterOpened) {
  968. try {
  969. const adapterState = await callWx('getBluetoothAdapterState')
  970. setState({
  971. adapterAvailable: !!adapterState.available,
  972. isDiscovering: !!adapterState.discovering
  973. })
  974. if (adapterState.available) return
  975. } catch (error) {
  976. setState({
  977. adapterAvailable: false,
  978. adapterOpened: false
  979. })
  980. }
  981. }
  982. try {
  983. await callWx('openBluetoothAdapter', {
  984. mode: 'central'
  985. })
  986. const adapterState = await callWx('getBluetoothAdapterState')
  987. setState({
  988. adapterAvailable: !!adapterState.available,
  989. adapterOpened: true,
  990. isDiscovering: !!adapterState.discovering
  991. })
  992. if (!adapterState.available) {
  993. throw {
  994. errCode: 10001,
  995. errMsg: 'bluetooth adapter not available'
  996. }
  997. }
  998. } catch (error) {
  999. if (error.errCode === 10001) {
  1000. setState({
  1001. adapterOpened: true,
  1002. adapterAvailable: false
  1003. })
  1004. }
  1005. throw error
  1006. }
  1007. }
  1008. async function startDiscovery() {
  1009. try {
  1010. await callWx('startBluetoothDevicesDiscovery', {
  1011. allowDuplicatesKey: true,
  1012. interval: 600,
  1013. powerLevel: 'high'
  1014. })
  1015. } catch (error) {
  1016. await callWx('startBluetoothDevicesDiscovery', {
  1017. allowDuplicatesKey: true,
  1018. interval: 600
  1019. })
  1020. }
  1021. }
  1022. async function startScan() {
  1023. if (state.isConnecting) return
  1024. deviceMap = {}
  1025. deviceSequence = 0
  1026. setState({
  1027. devices: [],
  1028. errorText: ''
  1029. })
  1030. try {
  1031. init()
  1032. await ensureBluetoothAuthorized()
  1033. await ensureAndroidLocationAuthorized()
  1034. await openAdapter()
  1035. await startDiscovery()
  1036. setState({
  1037. isDiscovering: true
  1038. })
  1039. resetScanTimer()
  1040. addLog('SYS', '开始扫描 BLE 设备')
  1041. } catch (error) {
  1042. clearScanTimer()
  1043. setState({
  1044. isDiscovering: false,
  1045. errorText: formatBluetoothError(error)
  1046. })
  1047. }
  1048. }
  1049. function clearDevices() {
  1050. deviceMap = {}
  1051. deviceSequence = 0
  1052. setState({
  1053. devices: [],
  1054. errorText: ''
  1055. })
  1056. }
  1057. async function closeConnectedDevice(nextDeviceId, options = {}) {
  1058. const { connectedDevice } = state
  1059. if (!connectedDevice) {
  1060. resetSendRuntimeState()
  1061. return
  1062. }
  1063. if (connectedDevice.deviceId === nextDeviceId && !options.force) return
  1064. resetSendRuntimeState()
  1065. try {
  1066. await callWx('closeBLEConnection', {
  1067. deviceId: connectedDevice.deviceId
  1068. })
  1069. } catch (error) {
  1070. if (error.errCode !== 10006) throw error
  1071. }
  1072. clearConnectedState()
  1073. }
  1074. async function discoverCharacteristics(deviceId) {
  1075. const serviceResult = await callWx('getBLEDeviceServices', {
  1076. deviceId
  1077. })
  1078. const services = []
  1079. let writeServiceId = ''
  1080. let writeCharacteristicId = ''
  1081. let writeType = ''
  1082. let notifyServiceId = ''
  1083. let notifyCharacteristicId = ''
  1084. for (const service of serviceResult.services || []) {
  1085. const characteristicResult = await callWx('getBLEDeviceCharacteristics', {
  1086. deviceId,
  1087. serviceId: service.uuid
  1088. })
  1089. const characteristics = (characteristicResult.characteristics || []).map((item) => ({
  1090. uuid: item.uuid,
  1091. role: getCharacteristicRole(item.properties),
  1092. properties: item.properties || {}
  1093. }))
  1094. services.push({
  1095. uuid: service.uuid,
  1096. primary: service.isPrimary,
  1097. characteristics
  1098. })
  1099. characteristics.forEach((item) => {
  1100. const isPreferredService = isTargetUuid(service.uuid)
  1101. const isPreferredCharacteristic = isTargetUuid(item.uuid)
  1102. const canWrite = item.properties.write || item.properties.writeNoResponse
  1103. const canNotify = item.properties.notify || item.properties.indicate
  1104. if (isPreferredService && isPreferredCharacteristic && canWrite) {
  1105. writeServiceId = service.uuid
  1106. writeCharacteristicId = item.uuid
  1107. writeType = item.properties.write ? 'write' : 'writeNoResponse'
  1108. }
  1109. if (isPreferredService && isPreferredCharacteristic && canNotify) {
  1110. notifyServiceId = service.uuid
  1111. notifyCharacteristicId = item.uuid
  1112. }
  1113. if (!writeCharacteristicId && canWrite) {
  1114. writeServiceId = service.uuid
  1115. writeCharacteristicId = item.uuid
  1116. writeType = item.properties.write ? 'write' : 'writeNoResponse'
  1117. }
  1118. if (!notifyCharacteristicId && canNotify) {
  1119. notifyServiceId = service.uuid
  1120. notifyCharacteristicId = item.uuid
  1121. }
  1122. })
  1123. }
  1124. return {
  1125. services,
  1126. writeServiceId,
  1127. writeCharacteristicId,
  1128. writeType,
  1129. notifyServiceId,
  1130. notifyCharacteristicId
  1131. }
  1132. }
  1133. async function enableNotify(deviceId, serviceId, characteristicId) {
  1134. try {
  1135. await callWx('notifyBLECharacteristicValueChange', {
  1136. deviceId,
  1137. serviceId,
  1138. characteristicId,
  1139. state: true
  1140. })
  1141. addLog('SYS', `已开启通知 ${characteristicId}`)
  1142. return true
  1143. } catch (error) {
  1144. addLog('SYS', `开启通知失败:${formatBluetoothError(error)}`)
  1145. if (isConnectionLostError(error)) {
  1146. throw error
  1147. }
  1148. return false
  1149. }
  1150. }
  1151. async function connectDeviceById(deviceId) {
  1152. const device = deviceMap[deviceId]
  1153. if (!device || state.isConnecting) return
  1154. resetSendRuntimeState()
  1155. setState({
  1156. connectingDeviceId: deviceId,
  1157. errorText: '',
  1158. isConnecting: true
  1159. })
  1160. try {
  1161. await stopScan()
  1162. await closeConnectedDevice(deviceId, {
  1163. force: state.connectedDevice && state.connectedDevice.deviceId === deviceId
  1164. })
  1165. await openAdapter()
  1166. await callWx('createBLEConnection', {
  1167. deviceId,
  1168. timeout: CONNECT_TIMEOUT
  1169. })
  1170. const discovery = await discoverCharacteristics(deviceId)
  1171. const notifyEnabled = discovery.notifyServiceId && discovery.notifyCharacteristicId
  1172. ? await enableNotify(deviceId, discovery.notifyServiceId, discovery.notifyCharacteristicId)
  1173. : false
  1174. const isTargetDevice = hasTargetCharacteristic(discovery)
  1175. const connectedDevice = {
  1176. ...device,
  1177. isTargetDevice,
  1178. packetSize: device.packetSize || inferPacketSize(device),
  1179. targetText: isTargetDevice ? '已发现目标特征' : device.targetText
  1180. }
  1181. deviceMap[deviceId] = connectedDevice
  1182. refreshDeviceList()
  1183. setState({
  1184. characteristicText: buildCharacteristicText(discovery.writeServiceId, discovery.writeCharacteristicId),
  1185. connectedDevice,
  1186. connectedServiceCount: discovery.services.length,
  1187. connectingDeviceId: '',
  1188. errorText: discovery.writeServiceId
  1189. ? (notifyEnabled ? '' : '已连接,但未成功开启通知,可能收不到设备回复')
  1190. : '已连接,但未找到可写特征值',
  1191. isConnecting: false,
  1192. writeCharacteristicId: discovery.writeCharacteristicId,
  1193. writeServiceId: discovery.writeServiceId,
  1194. writeType: discovery.writeType
  1195. })
  1196. startRssiRefresh()
  1197. addLog('SYS', `已连接 ${device.displayName}`)
  1198. } catch (error) {
  1199. resetSendRuntimeState()
  1200. setState({
  1201. connectingDeviceId: '',
  1202. isConnecting: false,
  1203. errorText: formatBluetoothError(error)
  1204. })
  1205. }
  1206. }
  1207. async function disconnectDevice() {
  1208. const { connectedDevice } = state
  1209. if (!connectedDevice) return
  1210. try {
  1211. await callWx('closeBLEConnection', {
  1212. deviceId: connectedDevice.deviceId
  1213. })
  1214. } catch (error) {
  1215. if (error.errCode !== 10006) {
  1216. setState({
  1217. errorText: formatBluetoothError(error)
  1218. })
  1219. return
  1220. }
  1221. }
  1222. addLog('SYS', '主动断开连接')
  1223. clearConnectedState({
  1224. errorText: '',
  1225. sendQueueLength: 0
  1226. })
  1227. }
  1228. async function refreshNativeConnectionState() {
  1229. if (!state.connectedDevice || typeof wx.getConnectedBluetoothDevices !== 'function') return true
  1230. try {
  1231. const services = state.writeServiceId ? [state.writeServiceId] : []
  1232. const result = await callWx('getConnectedBluetoothDevices', {
  1233. services
  1234. })
  1235. const isConnected = (result.devices || []).some((device) => device.deviceId === state.connectedDevice.deviceId)
  1236. if (isConnected) return true
  1237. addLog('SYS', '蓝牙连接状态已失效')
  1238. clearConnectedState({
  1239. errorText: '蓝牙连接已失效,请重新连接'
  1240. })
  1241. return false
  1242. } catch (error) {
  1243. if (isConnectionLostError(error)) {
  1244. clearConnectedState({
  1245. errorText: formatBluetoothError(error)
  1246. })
  1247. return false
  1248. }
  1249. return true
  1250. }
  1251. }
  1252. function handleAppHide() {
  1253. clearScanTimer()
  1254. stopRssiRefresh()
  1255. resetSendRuntimeState()
  1256. if (state.isDiscovering) {
  1257. stopScan()
  1258. }
  1259. }
  1260. async function handleAppShow() {
  1261. init()
  1262. const connected = await refreshNativeConnectionState()
  1263. if (connected && state.connectedDevice) {
  1264. startRssiRefresh()
  1265. }
  1266. }
  1267. function setSendHex(sendHex) {
  1268. setState({
  1269. sendHex,
  1270. errorText: ''
  1271. })
  1272. }
  1273. function setCommandIndex(value) {
  1274. const commandIndex = Number(value)
  1275. const command = getCommand(commandIndex)
  1276. const commandValue = getDefaultCommandValue(command)
  1277. const nextState = {
  1278. commandIndex,
  1279. commandValue,
  1280. coilEnabled: true
  1281. }
  1282. setState({
  1283. ...nextState,
  1284. ...createProtocolState(
  1285. nextState.commandIndex,
  1286. state.slaveAddress,
  1287. state.registerAddress,
  1288. nextState.commandValue,
  1289. nextState.coilEnabled
  1290. )
  1291. })
  1292. }
  1293. function setProtocolInput(changedData) {
  1294. const nextState = {
  1295. commandIndex: state.commandIndex,
  1296. slaveAddress: state.slaveAddress,
  1297. registerAddress: state.registerAddress,
  1298. commandValue: state.commandValue,
  1299. coilEnabled: state.coilEnabled,
  1300. ...changedData
  1301. }
  1302. setState({
  1303. ...changedData,
  1304. ...createProtocolState(
  1305. nextState.commandIndex,
  1306. nextState.slaveAddress,
  1307. nextState.registerAddress,
  1308. nextState.commandValue,
  1309. nextState.coilEnabled
  1310. )
  1311. })
  1312. }
  1313. function buildGeneratedExpectedResponse() {
  1314. try {
  1315. const command = getCommand(state.commandIndex)
  1316. const address = parseHexNumber(state.registerAddress, '协议寄存器', 0xFFFF)
  1317. const slaveAddress = parseHexNumber(state.slaveAddress, '从站地址', 0xFF)
  1318. const quantity = command.inputMode === 'quantity'
  1319. ? parseHexNumber(state.commandValue, '读取数量', 0xFFFF)
  1320. : (command.inputMode === 'multiple' ? parseRegisterValues(state.commandValue).length : 1)
  1321. const value = command.inputMode === 'coil'
  1322. ? (state.coilEnabled ? 0xFF00 : 0x0000)
  1323. : (command.inputMode === 'single' ? parseHexNumber(state.commandValue, '写入值', 0xFFFF) : undefined)
  1324. return {
  1325. address,
  1326. functionCode: command.functionCode,
  1327. kind: 'manual-rtu',
  1328. quantity,
  1329. value,
  1330. slaveAddress
  1331. }
  1332. } catch (error) {
  1333. return null
  1334. }
  1335. }
  1336. function clearInput() {
  1337. setState({
  1338. sendHex: '',
  1339. errorText: ''
  1340. })
  1341. }
  1342. function clearLogs() {
  1343. setState({
  1344. logScrollTarget: '',
  1345. logs: [],
  1346. rxCount: 0,
  1347. txCount: 0
  1348. })
  1349. }
  1350. function enqueueSendFrame(hexFrame, source, options = {}) {
  1351. if (!state.connectedDevice) {
  1352. setState({
  1353. errorText: '请先连接蓝牙透传设备'
  1354. })
  1355. return Promise.resolve(false)
  1356. }
  1357. if (!state.writeServiceId || !state.writeCharacteristicId) {
  1358. setState({
  1359. errorText: '当前设备没有可写特征值'
  1360. })
  1361. return Promise.resolve(false)
  1362. }
  1363. const errorText = validateHex(hexFrame)
  1364. if (errorText) {
  1365. setState({
  1366. errorText
  1367. })
  1368. return Promise.resolve(false)
  1369. }
  1370. const buffer = hexToArrayBuffer(hexFrame)
  1371. const bytes = new Uint8Array(buffer)
  1372. const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options.expected)
  1373. if (dmaFrameLengthError) {
  1374. setState({
  1375. errorText: dmaFrameLengthError
  1376. })
  1377. return Promise.resolve(false)
  1378. }
  1379. return new Promise((resolve) => {
  1380. sendJobSequence += 1
  1381. sendQueue.push({
  1382. id: sendJobSequence,
  1383. hexFrame,
  1384. options,
  1385. resolve,
  1386. source
  1387. })
  1388. setState({
  1389. sendQueueLength: sendQueue.length
  1390. })
  1391. processSendQueue()
  1392. })
  1393. }
  1394. async function processSendQueue() {
  1395. if (isProcessingSendQueue) return
  1396. const generation = sendQueueGeneration
  1397. isProcessingSendQueue = true
  1398. try {
  1399. while (sendQueue.length && generation === sendQueueGeneration) {
  1400. const job = sendQueue.shift()
  1401. setState({
  1402. sendQueueLength: sendQueue.length
  1403. })
  1404. let result = false
  1405. try {
  1406. result = await executeSendFrame(job.hexFrame, job.source, job.options)
  1407. } catch (error) {
  1408. cancelPendingRequest()
  1409. setState({
  1410. errorText: error.message || '发送失败'
  1411. })
  1412. }
  1413. job.resolve(result)
  1414. if (!state.connectedDevice) {
  1415. clearSendQueue()
  1416. break
  1417. }
  1418. }
  1419. } finally {
  1420. if (generation === sendQueueGeneration) {
  1421. isProcessingSendQueue = false
  1422. }
  1423. }
  1424. }
  1425. async function executeSendFrame(hexFrame, source, options = {}) {
  1426. const {
  1427. connectedDevice,
  1428. writeCharacteristicId,
  1429. writeServiceId,
  1430. writeType
  1431. } = state
  1432. const errorText = validateHex(hexFrame)
  1433. if (!connectedDevice) {
  1434. setState({
  1435. errorText: '请先连接蓝牙透传设备'
  1436. })
  1437. return false
  1438. }
  1439. if (!writeServiceId || !writeCharacteristicId) {
  1440. setState({
  1441. errorText: '当前设备没有可写特征值'
  1442. })
  1443. return false
  1444. }
  1445. if (errorText) {
  1446. setState({
  1447. errorText
  1448. })
  1449. return false
  1450. }
  1451. const buffer = hexToArrayBuffer(hexFrame)
  1452. const bytes = new Uint8Array(buffer)
  1453. const dmaFrameLengthError = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options.expected)
  1454. if (dmaFrameLengthError) {
  1455. setState({
  1456. errorText: dmaFrameLengthError
  1457. })
  1458. return false
  1459. }
  1460. const chunkSize = resolvePacketSize(
  1461. options.chunkSize === undefined ? connectedDevice.packetSize : options.chunkSize,
  1462. bytes.length
  1463. )
  1464. const waitResponse = !!options.expected
  1465. const responsePromise = waitResponse
  1466. ? createPendingRequest(source, options.expected, options)
  1467. : null
  1468. setState({
  1469. isSending: true,
  1470. errorText: ''
  1471. })
  1472. try {
  1473. for (let offset = 0; offset < bytes.length; offset += chunkSize) {
  1474. const chunk = bytes.slice(offset, offset + chunkSize)
  1475. await callWx('writeBLECharacteristicValue', {
  1476. deviceId: connectedDevice.deviceId,
  1477. serviceId: writeServiceId,
  1478. characteristicId: writeCharacteristicId,
  1479. value: chunk.buffer,
  1480. writeType
  1481. })
  1482. }
  1483. setState({
  1484. txCount: state.txCount + bytes.length
  1485. })
  1486. addLog('TX', arrayBufferToHex(buffer), source)
  1487. if (waitResponse) {
  1488. return responsePromise
  1489. }
  1490. return true
  1491. } catch (error) {
  1492. if (waitResponse) {
  1493. cancelPendingRequest()
  1494. }
  1495. if (isConnectionLostError(error)) {
  1496. clearConnectedState({
  1497. errorText: formatBluetoothError(error)
  1498. })
  1499. } else {
  1500. setState({
  1501. errorText: formatBluetoothError(error)
  1502. })
  1503. }
  1504. return false
  1505. } finally {
  1506. setState({
  1507. isSending: false
  1508. })
  1509. }
  1510. }
  1511. function sendManagedFrame(frameBytes, label, expected, options = {}) {
  1512. return enqueueSendFrame(formatHex(frameBytes), label, {
  1513. expected: expected ? {
  1514. ...expected,
  1515. maxFrameBytes: options.maxFrameBytes === undefined ? MAX_MODBUS_DMA_BYTES : options.maxFrameBytes
  1516. } : expected,
  1517. showModal: options.showModal !== false,
  1518. timeout: options.timeout || RESPONSE_TIMEOUT,
  1519. maxFrameBytes: options.maxFrameBytes === undefined ? MAX_MODBUS_DMA_BYTES : options.maxFrameBytes
  1520. })
  1521. }
  1522. function sendRawFrameExact(frameBytes, source) {
  1523. const bytes = frameBytes instanceof Uint8Array
  1524. ? frameBytes
  1525. : new Uint8Array(frameBytes || [])
  1526. return enqueueSendFrame(formatHex(Array.prototype.slice.call(bytes)), source, {
  1527. chunkSize: 0,
  1528. skipDmaCheck: true
  1529. })
  1530. }
  1531. function sendHexFrame() {
  1532. const errorText = validateHex(state.sendHex)
  1533. const expected = errorText ? null : parseModbusRequest(Array.prototype.slice.call(new Uint8Array(hexToArrayBuffer(state.sendHex))))
  1534. return enqueueSendFrame(state.sendHex, 'HEX', expected ? {
  1535. expected
  1536. } : {})
  1537. }
  1538. function sendGeneratedFrame() {
  1539. if (!state.generatedHex) return false
  1540. const expected = buildGeneratedExpectedResponse()
  1541. return enqueueSendFrame(state.generatedHex, 'RTU', expected ? {
  1542. expected
  1543. } : {})
  1544. }
  1545. setState(createProtocolState(
  1546. state.commandIndex,
  1547. state.slaveAddress,
  1548. state.registerAddress,
  1549. state.commandValue,
  1550. state.coilEnabled
  1551. ))
  1552. module.exports = {
  1553. clearDevices,
  1554. clearInput,
  1555. clearLogs,
  1556. connectDeviceById,
  1557. disconnectDevice,
  1558. getState,
  1559. handleAppHide,
  1560. handleAppShow,
  1561. init,
  1562. sendGeneratedFrame,
  1563. sendHexFrame,
  1564. sendManagedFrame,
  1565. sendRawFrameExact,
  1566. setCommandIndex,
  1567. setProtocolInput,
  1568. setSendHex,
  1569. showCommandAlert,
  1570. startScan,
  1571. stopScan,
  1572. subscribe,
  1573. subscribeRawResponse
  1574. }