1
0

ble-transport.js 42 KB

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