| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698 |
- const {
- buildReadFrame,
- buildWriteMultipleRegistersFrame,
- buildWriteSingleCoilFrame,
- buildWriteSingleRegisterFrame,
- formatHex,
- getReadResponseByteLength,
- MAX_MODBUS_DMA_BYTES,
- hasValidCrc
- } = require('./modbus-rtu')
- const {
- notifyPageToast
- } = require('./page-toast')
- const SCAN_TIMEOUT = 15000
- const CONNECT_TIMEOUT = 10000
- const DEFAULT_PACKET_SIZE = 20
- const RESPONSE_TIMEOUT = 1000
- const MAX_RESPONSE_BUFFER_BYTES = 128
- const MAX_LOG_COUNT = 100
- const TARGET_BLE_UUIDS = ['FFE0', 'FFE1']
- const MODBUS_EXCEPTION_MESSAGES = {
- 0x01: '非法功能',
- 0x02: '非法数据地址',
- 0x03: '非法数据值',
- 0x04: '从站设备故障',
- 0x05: '确认',
- 0x06: '从站设备忙',
- 0x08: '存储奇偶性错误',
- 0x0A: '网关路径不可用',
- 0x0B: '网关目标设备响应失败'
- }
- const MODBUS_COMMANDS = [
- { key: 'readCoils', label: '01 读取线圈', functionCode: 0x01, inputMode: 'quantity' },
- { key: 'readHolding', label: '03 读取保持寄存器', functionCode: 0x03, inputMode: 'quantity' },
- { key: 'readInput', label: '04 读取输入寄存器', functionCode: 0x04, inputMode: 'quantity' },
- { key: 'writeCoil', label: '05 写单线圈', functionCode: 0x05, inputMode: 'coil' },
- { key: 'writeRegister', label: '06 写单寄存器', functionCode: 0x06, inputMode: 'single' },
- { key: 'writeRegisters', label: '10 写多寄存器', functionCode: 0x10, inputMode: 'multiple' }
- ]
- const bluetoothErrorMap = {
- 10000: '蓝牙模块未初始化,请重新扫描',
- 10001: '蓝牙不可用,请开启手机蓝牙',
- 10002: '未找到指定设备,请重新扫描',
- 10003: '连接失败,请靠近设备后重试',
- 10004: '未发现设备服务',
- 10005: '未发现设备特征值',
- 10006: '当前连接已断开',
- 10007: '当前特征值不支持此操作',
- 10008: '系统蓝牙异常,请稍后重试',
- 10009: '当前系统不支持 BLE',
- 10012: '蓝牙操作超时,请重试',
- 10013: '设备 ID 无效,请重新扫描'
- }
- const state = {
- adapterAvailable: false,
- adapterOpened: false,
- characteristicText: '未选择',
- connectedDevice: null,
- connectedServiceCount: 0,
- connectingDeviceId: '',
- devices: [],
- errorText: '',
- isAwaitingResponse: false,
- isConnecting: false,
- isDiscovering: false,
- isSending: false,
- logScrollTarget: '',
- logs: [],
- commandIndex: 1,
- commandValue: '0001',
- commandValueLabel: '读取数量',
- coilEnabled: true,
- generatedHex: '',
- rxCount: 0,
- sendHex: '',
- sendQueueLength: 0,
- protocolCommands: MODBUS_COMMANDS,
- protocolErrorText: '',
- registerAddress: '0000',
- showCoilValue: false,
- showCommandValue: true,
- systemTip: '',
- txCount: 0,
- slaveAddress: 'F0',
- writeCharacteristicId: '',
- writeServiceId: '',
- writeType: ''
- }
- let initialized = false
- let scanTimer = null
- let pendingRequest = null
- let sendQueue = []
- let isProcessingSendQueue = false
- let sendQueueGeneration = 0
- let sendJobSequence = 0
- let deviceMap = {}
- let deviceSequence = 0
- let logSequence = 0
- const subscribers = []
- function setState(changedData) {
- Object.assign(state, changedData)
- subscribers.slice().forEach((subscriber) => {
- subscriber(getState())
- })
- }
- function getState() {
- return {
- ...state,
- devices: state.devices.slice(),
- logs: state.logs.slice()
- }
- }
- function getSlaveAddress() {
- return parseHexNumber(state.slaveAddress, '从站地址', 0xFF)
- }
- function subscribe(subscriber) {
- if (typeof subscriber !== 'function') return () => {}
- subscribers.push(subscriber)
- subscriber(getState())
- return () => {
- const index = subscribers.indexOf(subscriber)
- if (index >= 0) subscribers.splice(index, 1)
- }
- }
- function callWx(apiName, params = {}) {
- return new Promise((resolve, reject) => {
- const api = wx[apiName]
- if (typeof api !== 'function') {
- reject(new Error(`${apiName} 不可用`))
- return
- }
- api({
- ...params,
- success: resolve,
- fail: reject
- })
- })
- }
- function formatBluetoothError(error) {
- if (!error) return '操作失败'
- const message = bluetoothErrorMap[error.errCode]
- if (message) return message
- return error.errMsg || error.message || '蓝牙操作失败'
- }
- function normalizeDevice(device) {
- const advertisServiceUUIDs = device.advertisServiceUUIDs || []
- const displayName = String(device.name || device.localName || '').trim() || '未命名设备'
- const isTargetAdvertised = hasTargetAdvertisedUuid({
- advertisServiceUUIDs
- })
- return {
- deviceId: device.deviceId,
- name: device.name || '',
- localName: device.localName || '',
- RSSI: device.RSSI,
- advertisServiceUUIDs,
- displayName,
- isTargetAdvertised,
- signalText: typeof device.RSSI === 'number' ? `${device.RSSI} dBm` : '--',
- serviceText: advertisServiceUUIDs.length ? advertisServiceUUIDs.join(', ') : '未广播服务',
- targetText: isTargetAdvertised ? '广播含目标 UUID' : '',
- lastSeenAt: Date.now()
- }
- }
- function normalizeUuid(value) {
- return String(value || '').replace(/-/g, '').toUpperCase()
- }
- function isTargetUuid(value) {
- const uuid = normalizeUuid(value)
- return TARGET_BLE_UUIDS.some((target) => uuid.indexOf(target) >= 0)
- }
- function hasTargetAdvertisedUuid(device) {
- return (device.advertisServiceUUIDs || []).some(isTargetUuid)
- }
- function mergeAdvertisedServiceUUIDs(left = [], right = []) {
- const uuidMap = {}
- const uuids = []
- left.concat(right).forEach((uuid) => {
- const key = normalizeUuid(uuid)
- if (!key || uuidMap[key]) return
- uuidMap[key] = true
- uuids.push(uuid)
- })
- return uuids
- }
- function normalizeHex(value) {
- return String(value || '')
- .replace(/0x/gi, '')
- .replace(/[\s,;:_-]/g, '')
- .toUpperCase()
- }
- function validateHex(value) {
- const trimmed = String(value || '').trim()
- const withoutPrefix = trimmed.replace(/0x/gi, '')
- const compact = normalizeHex(trimmed)
- if (!compact) return '请输入要发送的十六进制数据'
- if (/[^0-9a-fA-F\s,;:_-]/.test(withoutPrefix)) return '只支持十六进制字符'
- if (compact.length % 2 !== 0) return '十六进制长度必须为偶数'
- return ''
- }
- function parseHexNumber(value, label, maxValue) {
- const text = String(value || '').trim().replace(/^0x/i, '')
- if (!text || !/^[0-9a-fA-F]+$/.test(text)) {
- throw new Error(`${label}请输入十六进制数值`)
- }
- const parsedValue = parseInt(text, 16)
- if (parsedValue > maxValue) {
- throw new Error(`${label}超出范围`)
- }
- return parsedValue
- }
- function parseRegisterValues(value) {
- const text = String(value || '').trim()
- if (!text) throw new Error('请输入寄存器写入值')
- return text.split(/[\s,;]+/)
- .filter(Boolean)
- .map((item) => parseHexNumber(item, '写入值', 0xFFFF))
- }
- function getCommand(index) {
- return MODBUS_COMMANDS[index] || MODBUS_COMMANDS[0]
- }
- function getDefaultCommandValue(command) {
- if (command.inputMode === 'quantity') return '0001'
- if (command.inputMode === 'coil') return 'ON'
- if (command.inputMode === 'multiple') return '0000'
- return '0000'
- }
- function generateModbusFrame(command, slaveAddress, registerAddress, commandValue, coilEnabled) {
- const slave = parseHexNumber(slaveAddress, '从站地址', 0xFF)
- const address = parseHexNumber(registerAddress, '协议寄存器', 0xFFFF)
- if (command.inputMode === 'quantity') {
- const quantity = parseHexNumber(commandValue, '读取数量', 0xFFFF)
- return buildReadFrame(slave, command.functionCode, address, quantity)
- }
- if (command.inputMode === 'coil') {
- return buildWriteSingleCoilFrame(slave, address, coilEnabled)
- }
- if (command.inputMode === 'single') {
- return buildWriteSingleRegisterFrame(slave, address, parseHexNumber(commandValue, '写入值', 0xFFFF))
- }
- return buildWriteMultipleRegistersFrame(slave, address, parseRegisterValues(commandValue))
- }
- function createProtocolState(commandIndex, slaveAddress, registerAddress, commandValue, coilEnabled) {
- const command = getCommand(commandIndex)
- const commandValueLabel = command.inputMode === 'quantity' ? '读取数量' : '写入值'
- try {
- return {
- commandValueLabel,
- generatedHex: formatHex(generateModbusFrame(command, slaveAddress, registerAddress, commandValue, coilEnabled)),
- protocolErrorText: '',
- showCoilValue: command.inputMode === 'coil',
- showCommandValue: command.inputMode !== 'coil'
- }
- } catch (error) {
- return {
- commandValueLabel,
- generatedHex: '',
- protocolErrorText: error.message,
- showCoilValue: command.inputMode === 'coil',
- showCommandValue: command.inputMode !== 'coil'
- }
- }
- }
- function hexToArrayBuffer(hexText) {
- const hex = normalizeHex(hexText)
- const buffer = new ArrayBuffer(hex.length / 2)
- const view = new Uint8Array(buffer)
- for (let index = 0; index < view.length; index += 1) {
- view[index] = parseInt(hex.substr(index * 2, 2), 16)
- }
- return buffer
- }
- function arrayBufferToHex(buffer) {
- if (!buffer) return ''
- return Array.prototype.map.call(new Uint8Array(buffer), (item) => item.toString(16).padStart(2, '0')).join(' ').toUpperCase()
- }
- function bytesToWords(bytes) {
- const words = []
- for (let index = 0; index + 1 < bytes.length; index += 2) {
- words.push(((bytes[index] << 8) | bytes[index + 1]) & 0xFFFF)
- }
- return words
- }
- function parseModbusResponse(bytes) {
- if (!Array.isArray(bytes) || bytes.length < 5 || !hasValidCrc(bytes)) return null
- const slaveAddress = bytes[0]
- const functionCode = bytes[1]
- if (functionCode & 0x80) {
- return {
- exceptionCode: bytes[2],
- functionCode,
- isException: true,
- slaveAddress,
- sourceFunctionCode: functionCode & 0x7F
- }
- }
- if (functionCode === 0x01) {
- const byteCount = bytes[2]
- const dataEnd = 3 + byteCount
- if (bytes.length < dataEnd + 2) return null
- return {
- byteCount,
- dataBytes: bytes.slice(3, dataEnd),
- functionCode,
- isException: false,
- slaveAddress
- }
- }
- if (functionCode === 0x03 || functionCode === 0x04) {
- const byteCount = bytes[2]
- const dataEnd = 3 + byteCount
- if (bytes.length < dataEnd + 2) return null
- return {
- byteCount,
- dataBytes: bytes.slice(3, dataEnd),
- functionCode,
- isException: false,
- slaveAddress,
- words: bytesToWords(bytes.slice(3, dataEnd))
- }
- }
- if (functionCode === 0x05 || functionCode === 0x06 || functionCode === 0x10) {
- return {
- address: ((bytes[2] << 8) | bytes[3]) & 0xFFFF,
- functionCode,
- isException: false,
- quantityOrValue: ((bytes[4] << 8) | bytes[5]) & 0xFFFF,
- slaveAddress
- }
- }
- return {
- functionCode,
- isException: false,
- slaveAddress
- }
- }
- function parseModbusRequest(bytes) {
- if (!Array.isArray(bytes) || bytes.length < 6 || !hasValidCrc(bytes)) return null
- const slaveAddress = bytes[0]
- const functionCode = bytes[1]
- const address = ((bytes[2] << 8) | bytes[3]) & 0xFFFF
- let quantity = 1
- let value
- if (functionCode === 0x01 || functionCode === 0x03 || functionCode === 0x04 || functionCode === 0x10) {
- quantity = ((bytes[4] << 8) | bytes[5]) & 0xFFFF
- }
- if (functionCode === 0x05 || functionCode === 0x06) {
- value = ((bytes[4] << 8) | bytes[5]) & 0xFFFF
- }
- return {
- address,
- functionCode,
- kind: 'raw-hex',
- quantity,
- value,
- slaveAddress
- }
- }
- function validateDmaFrameLength(bytes, expected) {
- if (bytes.length > MAX_MODBUS_DMA_BYTES) {
- return `发送帧长度 ${bytes.length} 字节,超过下位机 DMA ${MAX_MODBUS_DMA_BYTES} 字节限制`
- }
- if (!expected) return ''
- const responseLength = getReadResponseByteLength(expected.functionCode, expected.quantity)
- if (responseLength > MAX_MODBUS_DMA_BYTES) {
- return `预计返回帧长度 ${responseLength} 字节,超过下位机 DMA ${MAX_MODBUS_DMA_BYTES} 字节限制`
- }
- return ''
- }
- function formatTime(timestamp) {
- const date = new Date(timestamp)
- const pad = (value, length = 2) => String(value).padStart(length, '0')
- return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`
- }
- function getCharacteristicRole(properties = {}) {
- const canWrite = !!(properties.write || properties.writeNoResponse)
- const canNotify = !!(properties.notify || properties.indicate)
- if (canWrite && canNotify) return '收发'
- if (canWrite) return '发送'
- if (canNotify) return '接收'
- if (properties.read) return '读取'
- return '其他'
- }
- function buildCharacteristicText(serviceId, characteristicId) {
- if (!serviceId || !characteristicId) return '未选择'
- return `${serviceId.slice(0, 8)} / ${characteristicId.slice(0, 8)}`
- }
- function hasTargetCharacteristic(discovery) {
- return (discovery.services || []).some((service) => (
- isTargetUuid(service.uuid) || (service.characteristics || []).some((item) => isTargetUuid(item.uuid))
- ))
- }
- function padHex(value, length = 4) {
- return Number(value || 0).toString(16).toUpperCase().padStart(length, '0')
- }
- function getExceptionText(code) {
- return MODBUS_EXCEPTION_MESSAGES[code] || '未知异常'
- }
- function addLog(direction, payload, note = '') {
- logSequence += 1
- const logItem = {
- id: `log-${Date.now()}-${logSequence}`,
- direction,
- note,
- payload,
- time: formatTime(Date.now())
- }
- const nextLogs = state.logs.concat(logItem).slice(-MAX_LOG_COUNT)
- setState({
- logScrollTarget: logItem.id,
- logs: nextLogs
- })
- }
- function showCommandAlert(title, content) {
- const message = content || title || '操作失败'
- notifyPageToast(message, 'error')
- setState({
- errorText: message
- })
- }
- function clearScanTimer() {
- if (!scanTimer) return
- clearTimeout(scanTimer)
- scanTimer = null
- }
- async function stopScan() {
- clearScanTimer()
- try {
- await callWx('stopBluetoothDevicesDiscovery')
- } catch (error) {
- if (error.errCode !== 10000) {
- setState({
- errorText: formatBluetoothError(error)
- })
- }
- }
- setState({
- isDiscovering: false
- })
- }
- function resetScanTimer() {
- clearScanTimer()
- scanTimer = setTimeout(() => {
- stopScan()
- if (!state.devices.length) {
- setState({
- systemTip: '安卓真机请确认系统定位已开启,并允许微信使用附近设备或位置信息。'
- })
- }
- }, SCAN_TIMEOUT)
- }
- function mergeDevices(devices) {
- if (!devices.length) return
- devices.forEach((device) => {
- if (!device.deviceId) return
- const previousDevice = deviceMap[device.deviceId] || {}
- const nextDevice = normalizeDevice(device)
- const advertisServiceUUIDs = mergeAdvertisedServiceUUIDs(
- previousDevice.advertisServiceUUIDs,
- nextDevice.advertisServiceUUIDs
- )
- const isTargetAdvertised = !!previousDevice.isTargetAdvertised || hasTargetAdvertisedUuid({
- advertisServiceUUIDs
- })
- const isTargetDevice = !!previousDevice.isTargetDevice
- const seenIndex = previousDevice.seenIndex || (deviceSequence += 1)
- deviceMap[device.deviceId] = {
- ...previousDevice,
- ...nextDevice,
- advertisServiceUUIDs,
- displayName: nextDevice.displayName === '未命名设备' && previousDevice.displayName
- ? previousDevice.displayName
- : nextDevice.displayName,
- isTargetAdvertised,
- isTargetDevice,
- seenIndex,
- serviceText: advertisServiceUUIDs.length ? advertisServiceUUIDs.join(', ') : '未广播服务',
- targetText: isTargetDevice ? '已发现目标特征' : (isTargetAdvertised ? '广播含目标 UUID' : '')
- }
- })
- refreshDeviceList()
- }
- function refreshDeviceList() {
- const deviceList = Object.keys(deviceMap)
- .map((deviceId) => deviceMap[deviceId])
- .sort((left, right) => {
- const leftIndex = Number(left.seenIndex) || 0
- const rightIndex = Number(right.seenIndex) || 0
- return leftIndex - rightIndex
- })
- setState({
- devices: deviceList.slice(0, 30)
- })
- }
- function clearPendingRequest() {
- if (!pendingRequest) return null
- const pending = pendingRequest
- clearTimeout(pendingRequest.timer)
- pendingRequest = null
- setState({
- isAwaitingResponse: false
- })
- return pending
- }
- function cancelPendingRequest() {
- const pending = clearPendingRequest()
- if (pending) {
- pending.resolve(false)
- }
- }
- function clearSendQueue() {
- if (!sendQueue.length) return
- const queuedJobs = sendQueue.splice(0)
- queuedJobs.forEach((job) => {
- job.resolve(false)
- })
- setState({
- sendQueueLength: 0
- })
- }
- function resetSendRuntimeState() {
- sendQueueGeneration += 1
- cancelPendingRequest()
- clearSendQueue()
- isProcessingSendQueue = false
- setState({
- isAwaitingResponse: false,
- isSending: false,
- sendQueueLength: 0
- })
- }
- function clearConnectedState(changedData = {}) {
- resetSendRuntimeState()
- setState({
- characteristicText: '未选择',
- connectedDevice: null,
- connectedServiceCount: 0,
- connectingDeviceId: '',
- isConnecting: false,
- writeCharacteristicId: '',
- writeServiceId: '',
- writeType: '',
- ...changedData
- })
- }
- function isConnectionLostError(error) {
- if (!error) return false
- if ([10000, 10001, 10006, 10013].includes(error.errCode)) return true
- const message = String(error.errMsg || error.message || '').toLowerCase()
- return message.includes('disconnect') || message.includes('not connected')
- }
- function isExpectedResponse(response, expected) {
- if (response.functionCode === 0x01) {
- return Array.isArray(response.dataBytes) && response.dataBytes.length >= Math.ceil(expected.quantity / 8)
- }
- if (response.functionCode === 0x03 || response.functionCode === 0x04) {
- return Array.isArray(response.words) && response.words.length >= expected.quantity
- }
- if (response.functionCode === 0x10) {
- return response.address === expected.address && response.quantityOrValue === expected.quantity
- }
- if (response.functionCode === 0x05 || response.functionCode === 0x06) {
- if (response.address !== expected.address) return false
- if (Number.isInteger(expected.value)) return response.quantityOrValue === expected.value
- return true
- }
- return true
- }
- function getExpectedResponseLength(expected, responseFunctionCode, responseBytes) {
- if (!expected) return 0
- if (responseFunctionCode === (expected.functionCode | 0x80)) {
- return 5
- }
- if (responseFunctionCode === 0x01) {
- if (responseBytes.length < 3) return 0
- return 3 + Number(responseBytes[2] || 0) + 2
- }
- if (responseFunctionCode === 0x03 || responseFunctionCode === 0x04) {
- if (responseBytes.length < 3) return 0
- return 3 + Number(responseBytes[2] || 0) + 2
- }
- if (responseFunctionCode === 0x05 || responseFunctionCode === 0x06 || responseFunctionCode === 0x10) {
- return 8
- }
- return 0
- }
- function alignResponseBuffer(buffer, expected) {
- if (!Array.isArray(buffer) || !buffer.length || !expected) return
- const expectedFunctionCodes = [expected.functionCode, expected.functionCode | 0x80]
- let matchIndex = -1
- for (let index = 0; index < buffer.length - 1; index += 1) {
- if (buffer[index] !== expected.slaveAddress) continue
- if (!expectedFunctionCodes.includes(buffer[index + 1])) continue
- matchIndex = index
- break
- }
- if (matchIndex > 0) {
- buffer.splice(0, matchIndex)
- } else if (matchIndex < 0 && buffer.length > 2) {
- buffer.splice(0, buffer.length - 1)
- }
- }
- function finishPendingRequest(resolveValue) {
- const pending = clearPendingRequest()
- if (pending) {
- pending.resolve(resolveValue)
- }
- }
- function consumePendingResponseBuffer() {
- const pending = pendingRequest
- if (!pending || !Array.isArray(pending.responseBuffer)) return
- const buffer = pending.responseBuffer
- alignResponseBuffer(buffer, pending.expected)
- if (buffer.length < 2) return
- const responseFunctionCode = buffer[1]
- const responseLength = getExpectedResponseLength(pending.expected, responseFunctionCode, buffer)
- if (!responseLength) return
- if (responseLength > MAX_MODBUS_DMA_BYTES) {
- const content = `${pending.label} 返回帧长度 ${responseLength} 字节,超过 DMA 限制,已丢弃`
- addLog('SYS', content)
- finishPendingRequest(false)
- if (pending.showModal) {
- showCommandAlert('通讯异常', content)
- }
- return
- }
- if (buffer.length < responseLength) return
- const frameBytes = buffer.slice(0, responseLength)
- const response = parseModbusResponse(frameBytes)
- if (!response) {
- const content = `${pending.label} 收到无效响应帧,已丢弃`
- addLog('SYS', content)
- finishPendingRequest(false)
- if (pending.showModal) {
- showCommandAlert('通讯异常', content)
- }
- return
- }
- const responseCode = response.isException ? response.sourceFunctionCode : response.functionCode
- if (response.slaveAddress !== pending.expected.slaveAddress || responseCode !== pending.expected.functionCode) {
- buffer.shift()
- consumePendingResponseBuffer()
- return
- }
- if (response.isException) {
- const exceptionText = getExceptionText(response.exceptionCode)
- const content = `设备返回异常帧:功能码 0x${padHex(response.sourceFunctionCode, 2)},异常码 0x${padHex(response.exceptionCode, 2)}(${exceptionText})`
- addLog('SYS', content)
- finishPendingRequest(false)
- if (pending.showModal) {
- showCommandAlert('设备返回故障帧', content)
- }
- return
- }
- if (!isExpectedResponse(response, pending.expected)) {
- const content = `${pending.label} 收到不匹配响应,已丢弃`
- addLog('SYS', content)
- finishPendingRequest(false)
- if (pending.showModal) {
- showCommandAlert('通讯异常', content)
- }
- return
- }
- buffer.splice(0, responseLength)
- finishPendingRequest(response)
- if (buffer.length) {
- consumePendingResponseBuffer()
- }
- }
- function handleModbusResponse(bytes) {
- if (!pendingRequest || !Array.isArray(bytes) || !bytes.length) return
- pendingRequest.responseBuffer = pendingRequest.responseBuffer.concat(bytes)
- if (pendingRequest.responseBuffer.length > MAX_RESPONSE_BUFFER_BYTES) {
- const pending = pendingRequest
- const content = `${pending.label} 返回数据超过缓冲区,已丢弃`
- addLog('SYS', content)
- finishPendingRequest(false)
- if (pending.showModal) {
- showCommandAlert('通讯异常', content)
- }
- return
- }
- consumePendingResponseBuffer()
- }
- function createPendingRequest(label, expected, options = {}) {
- return new Promise((resolve) => {
- const timer = setTimeout(() => {
- const pending = clearPendingRequest()
- if (!pending) return
- addLog('SYS', `${label} 超时`)
- if (options.showModal !== false) {
- showCommandAlert('通讯超时', `${label} 1秒内没有收到回复`)
- }
- resolve(false)
- }, options.timeout || RESPONSE_TIMEOUT)
- pendingRequest = {
- expected,
- label,
- resolve,
- timer,
- showModal: options.showModal !== false,
- responseBuffer: []
- }
- setState({
- isAwaitingResponse: true
- })
- })
- }
- function init() {
- if (initialized) return
- wx.onBluetoothDeviceFound((res) => {
- mergeDevices(res.devices || [])
- })
- wx.onBluetoothAdapterStateChange((res) => {
- setState({
- adapterAvailable: !!res.available,
- isDiscovering: !!res.discovering
- })
- if (!res.available) {
- clearScanTimer()
- clearConnectedState({
- adapterAvailable: false,
- adapterOpened: false,
- errorText: '请开启手机蓝牙后重新扫描',
- isDiscovering: false,
- sendQueueLength: 0
- })
- }
- })
- wx.onBLEConnectionStateChange((res) => {
- const { connectedDevice } = state
- if (!connectedDevice || connectedDevice.deviceId !== res.deviceId) return
- if (!res.connected) {
- addLog('SYS', '连接已断开')
- clearConnectedState({
- errorText: '',
- sendQueueLength: 0
- })
- }
- })
- wx.onBLECharacteristicValueChange((res) => {
- const hex = arrayBufferToHex(res.value)
- const byteLength = res.value ? res.value.byteLength : 0
- const rawBytes = Array.prototype.slice.call(new Uint8Array(res.value || new ArrayBuffer(0)))
- const crcState = rawBytes.length >= 4
- ? (hasValidCrc(rawBytes) ? 'CRC OK' : (pendingRequest ? '片段' : 'CRC ERR'))
- : ''
- setState({
- rxCount: state.rxCount + byteLength
- })
- addLog('RX', hex, crcState)
- handleModbusResponse(rawBytes)
- })
- initialized = true
- }
- async function getAuthSetting() {
- return callWx('getSetting')
- .then((res) => res.authSetting || {})
- .catch(() => ({}))
- }
- function showPermissionModal(title, content) {
- return new Promise((resolve, reject) => {
- wx.showModal({
- title,
- content,
- confirmText: '去设置',
- success: async (res) => {
- if (!res.confirm) {
- reject(new Error('用户取消授权'))
- return
- }
- try {
- await callWx('openSetting')
- resolve()
- } catch (error) {
- reject(error)
- }
- },
- fail: reject
- })
- })
- }
- async function ensureBluetoothAuthorized() {
- const authSetting = await getAuthSetting()
- if (authSetting['scope.bluetooth']) return
- if (authSetting['scope.bluetooth'] === false) {
- await showPermissionModal('需要蓝牙权限', '请在设置中允许使用蓝牙,用于扫描并连接附近设备。')
- return
- }
- try {
- await callWx('authorize', {
- scope: 'scope.bluetooth'
- })
- } catch (error) {
- await showPermissionModal('需要蓝牙权限', '请在设置中允许使用蓝牙,用于扫描并连接附近设备。')
- }
- }
- async function ensureAndroidLocationAuthorized() {
- const systemInfo = wx.getSystemInfoSync ? wx.getSystemInfoSync() : wx.getDeviceInfo()
- if (systemInfo.platform !== 'android') return
- const authSetting = await getAuthSetting()
- if (authSetting['scope.userLocation']) return
- setState({
- systemTip: '安卓系统扫描 BLE 设备通常需要开启系统定位权限。'
- })
- if (authSetting['scope.userLocation'] === false) {
- await showPermissionModal('需要定位权限', '安卓系统要求定位权限开启后才能搜索附近 BLE 设备。')
- return
- }
- try {
- await callWx('authorize', {
- scope: 'scope.userLocation'
- })
- } catch (error) {
- await showPermissionModal('需要定位权限', '安卓系统要求定位权限开启后才能搜索附近 BLE 设备。')
- }
- }
- async function openAdapter() {
- if (state.adapterOpened) {
- try {
- const adapterState = await callWx('getBluetoothAdapterState')
- setState({
- adapterAvailable: !!adapterState.available,
- isDiscovering: !!adapterState.discovering
- })
- if (adapterState.available) return
- } catch (error) {
- setState({
- adapterAvailable: false,
- adapterOpened: false
- })
- }
- }
- try {
- await callWx('openBluetoothAdapter', {
- mode: 'central'
- })
- const adapterState = await callWx('getBluetoothAdapterState')
- setState({
- adapterAvailable: !!adapterState.available,
- adapterOpened: true,
- isDiscovering: !!adapterState.discovering
- })
- if (!adapterState.available) {
- throw {
- errCode: 10001,
- errMsg: 'bluetooth adapter not available'
- }
- }
- } catch (error) {
- if (error.errCode === 10001) {
- setState({
- adapterOpened: true,
- adapterAvailable: false
- })
- }
- throw error
- }
- }
- async function startDiscovery() {
- try {
- await callWx('startBluetoothDevicesDiscovery', {
- allowDuplicatesKey: true,
- interval: 600,
- powerLevel: 'high'
- })
- } catch (error) {
- await callWx('startBluetoothDevicesDiscovery', {
- allowDuplicatesKey: true,
- interval: 600
- })
- }
- }
- async function startScan() {
- if (state.isConnecting) return
- deviceMap = {}
- deviceSequence = 0
- setState({
- devices: [],
- errorText: ''
- })
- try {
- init()
- await ensureBluetoothAuthorized()
- await ensureAndroidLocationAuthorized()
- await openAdapter()
- await startDiscovery()
- setState({
- isDiscovering: true
- })
- resetScanTimer()
- addLog('SYS', '开始扫描 BLE 设备')
- } catch (error) {
- clearScanTimer()
- setState({
- isDiscovering: false,
- errorText: formatBluetoothError(error)
- })
- }
- }
- function clearDevices() {
- deviceMap = {}
- deviceSequence = 0
- setState({
- devices: [],
- errorText: ''
- })
- }
- async function closeConnectedDevice(nextDeviceId, options = {}) {
- const { connectedDevice } = state
- if (!connectedDevice) {
- resetSendRuntimeState()
- return
- }
- if (connectedDevice.deviceId === nextDeviceId && !options.force) return
- resetSendRuntimeState()
- try {
- await callWx('closeBLEConnection', {
- deviceId: connectedDevice.deviceId
- })
- } catch (error) {
- if (error.errCode !== 10006) throw error
- }
- clearConnectedState()
- }
- async function discoverCharacteristics(deviceId) {
- const serviceResult = await callWx('getBLEDeviceServices', {
- deviceId
- })
- const services = []
- let writeServiceId = ''
- let writeCharacteristicId = ''
- let writeType = ''
- let notifyServiceId = ''
- let notifyCharacteristicId = ''
- for (const service of serviceResult.services || []) {
- const characteristicResult = await callWx('getBLEDeviceCharacteristics', {
- deviceId,
- serviceId: service.uuid
- })
- const characteristics = (characteristicResult.characteristics || []).map((item) => ({
- uuid: item.uuid,
- role: getCharacteristicRole(item.properties),
- properties: item.properties || {}
- }))
- services.push({
- uuid: service.uuid,
- primary: service.isPrimary,
- characteristics
- })
- characteristics.forEach((item) => {
- const isPreferredService = isTargetUuid(service.uuid)
- const isPreferredCharacteristic = isTargetUuid(item.uuid)
- const canWrite = item.properties.write || item.properties.writeNoResponse
- const canNotify = item.properties.notify || item.properties.indicate
- if (isPreferredService && isPreferredCharacteristic && canWrite) {
- writeServiceId = service.uuid
- writeCharacteristicId = item.uuid
- writeType = item.properties.write ? 'write' : 'writeNoResponse'
- }
- if (isPreferredService && isPreferredCharacteristic && canNotify) {
- notifyServiceId = service.uuid
- notifyCharacteristicId = item.uuid
- }
- if (!writeCharacteristicId && canWrite) {
- writeServiceId = service.uuid
- writeCharacteristicId = item.uuid
- writeType = item.properties.write ? 'write' : 'writeNoResponse'
- }
- if (!notifyCharacteristicId && canNotify) {
- notifyServiceId = service.uuid
- notifyCharacteristicId = item.uuid
- }
- })
- }
- return {
- services,
- writeServiceId,
- writeCharacteristicId,
- writeType,
- notifyServiceId,
- notifyCharacteristicId
- }
- }
- async function enableNotify(deviceId, serviceId, characteristicId) {
- try {
- await callWx('notifyBLECharacteristicValueChange', {
- deviceId,
- serviceId,
- characteristicId,
- state: true
- })
- addLog('SYS', `已开启通知 ${characteristicId}`)
- return true
- } catch (error) {
- addLog('SYS', `开启通知失败:${formatBluetoothError(error)}`)
- if (isConnectionLostError(error)) {
- throw error
- }
- return false
- }
- }
- async function connectDeviceById(deviceId) {
- const device = deviceMap[deviceId]
- if (!device || state.isConnecting) return
- resetSendRuntimeState()
- setState({
- connectingDeviceId: deviceId,
- errorText: '',
- isConnecting: true
- })
- try {
- await stopScan()
- await closeConnectedDevice(deviceId, {
- force: state.connectedDevice && state.connectedDevice.deviceId === deviceId
- })
- await openAdapter()
- await callWx('createBLEConnection', {
- deviceId,
- timeout: CONNECT_TIMEOUT
- })
- const discovery = await discoverCharacteristics(deviceId)
- const notifyEnabled = discovery.notifyServiceId && discovery.notifyCharacteristicId
- ? await enableNotify(deviceId, discovery.notifyServiceId, discovery.notifyCharacteristicId)
- : false
- const isTargetDevice = hasTargetCharacteristic(discovery)
- const connectedDevice = {
- ...device,
- isTargetDevice,
- targetText: isTargetDevice ? '已发现目标特征' : device.targetText
- }
- deviceMap[deviceId] = connectedDevice
- refreshDeviceList()
- setState({
- characteristicText: buildCharacteristicText(discovery.writeServiceId, discovery.writeCharacteristicId),
- connectedDevice,
- connectedServiceCount: discovery.services.length,
- connectingDeviceId: '',
- errorText: discovery.writeServiceId
- ? (notifyEnabled ? '' : '已连接,但未成功开启通知,可能收不到设备回复')
- : '已连接,但未找到可写特征值',
- isConnecting: false,
- writeCharacteristicId: discovery.writeCharacteristicId,
- writeServiceId: discovery.writeServiceId,
- writeType: discovery.writeType
- })
- addLog('SYS', `已连接 ${device.displayName}`)
- } catch (error) {
- resetSendRuntimeState()
- setState({
- connectingDeviceId: '',
- isConnecting: false,
- errorText: formatBluetoothError(error)
- })
- }
- }
- async function disconnectDevice() {
- const { connectedDevice } = state
- if (!connectedDevice) return
- try {
- await callWx('closeBLEConnection', {
- deviceId: connectedDevice.deviceId
- })
- } catch (error) {
- if (error.errCode !== 10006) {
- setState({
- errorText: formatBluetoothError(error)
- })
- return
- }
- }
- addLog('SYS', '主动断开连接')
- clearConnectedState({
- errorText: '',
- sendQueueLength: 0
- })
- }
- async function refreshNativeConnectionState() {
- if (!state.connectedDevice || typeof wx.getConnectedBluetoothDevices !== 'function') return true
- try {
- const services = state.writeServiceId ? [state.writeServiceId] : []
- const result = await callWx('getConnectedBluetoothDevices', {
- services
- })
- const isConnected = (result.devices || []).some((device) => device.deviceId === state.connectedDevice.deviceId)
- if (isConnected) return true
- addLog('SYS', '蓝牙连接状态已失效')
- clearConnectedState({
- errorText: '蓝牙连接已失效,请重新连接'
- })
- return false
- } catch (error) {
- if (isConnectionLostError(error)) {
- clearConnectedState({
- errorText: formatBluetoothError(error)
- })
- return false
- }
- return true
- }
- }
- function handleAppHide() {
- clearScanTimer()
- resetSendRuntimeState()
- if (state.isDiscovering) {
- stopScan()
- }
- }
- function handleAppShow() {
- init()
- refreshNativeConnectionState()
- }
- async function openSetting() {
- try {
- await callWx('openSetting')
- setState({
- errorText: ''
- })
- } catch (error) {
- setState({
- errorText: formatBluetoothError(error)
- })
- }
- }
- function setSendHex(sendHex) {
- setState({
- sendHex,
- errorText: ''
- })
- }
- function setCommandIndex(value) {
- const commandIndex = Number(value)
- const command = getCommand(commandIndex)
- const commandValue = getDefaultCommandValue(command)
- const nextState = {
- commandIndex,
- commandValue,
- coilEnabled: true
- }
- setState({
- ...nextState,
- ...createProtocolState(
- nextState.commandIndex,
- state.slaveAddress,
- state.registerAddress,
- nextState.commandValue,
- nextState.coilEnabled
- )
- })
- }
- function setProtocolInput(changedData) {
- const nextState = {
- commandIndex: state.commandIndex,
- slaveAddress: state.slaveAddress,
- registerAddress: state.registerAddress,
- commandValue: state.commandValue,
- coilEnabled: state.coilEnabled,
- ...changedData
- }
- setState({
- ...changedData,
- ...createProtocolState(
- nextState.commandIndex,
- nextState.slaveAddress,
- nextState.registerAddress,
- nextState.commandValue,
- nextState.coilEnabled
- )
- })
- }
- function buildGeneratedExpectedResponse() {
- try {
- const command = getCommand(state.commandIndex)
- const address = parseHexNumber(state.registerAddress, '协议寄存器', 0xFFFF)
- const slaveAddress = parseHexNumber(state.slaveAddress, '从站地址', 0xFF)
- const quantity = command.inputMode === 'quantity'
- ? parseHexNumber(state.commandValue, '读取数量', 0xFFFF)
- : (command.inputMode === 'multiple' ? parseRegisterValues(state.commandValue).length : 1)
- const value = command.inputMode === 'coil'
- ? (state.coilEnabled ? 0xFF00 : 0x0000)
- : (command.inputMode === 'single' ? parseHexNumber(state.commandValue, '写入值', 0xFFFF) : undefined)
- return {
- address,
- functionCode: command.functionCode,
- kind: 'manual-rtu',
- quantity,
- value,
- slaveAddress
- }
- } catch (error) {
- return null
- }
- }
- function clearInput() {
- setState({
- sendHex: '',
- errorText: ''
- })
- }
- function clearLogs() {
- setState({
- logScrollTarget: '',
- logs: [],
- rxCount: 0,
- txCount: 0
- })
- }
- function enqueueSendFrame(hexFrame, source, options = {}) {
- if (!state.connectedDevice) {
- setState({
- errorText: '请先连接蓝牙透传设备'
- })
- return Promise.resolve(false)
- }
- if (!state.writeServiceId || !state.writeCharacteristicId) {
- setState({
- errorText: '当前设备没有可写特征值'
- })
- return Promise.resolve(false)
- }
- const errorText = validateHex(hexFrame)
- if (errorText) {
- setState({
- errorText
- })
- return Promise.resolve(false)
- }
- const buffer = hexToArrayBuffer(hexFrame)
- const bytes = new Uint8Array(buffer)
- const dmaFrameLengthError = validateDmaFrameLength(bytes, options.expected)
- if (dmaFrameLengthError) {
- setState({
- errorText: dmaFrameLengthError
- })
- return Promise.resolve(false)
- }
- return new Promise((resolve) => {
- sendJobSequence += 1
- sendQueue.push({
- id: sendJobSequence,
- hexFrame,
- options,
- resolve,
- source
- })
- setState({
- sendQueueLength: sendQueue.length
- })
- processSendQueue()
- })
- }
- async function processSendQueue() {
- if (isProcessingSendQueue) return
- const generation = sendQueueGeneration
- isProcessingSendQueue = true
- try {
- while (sendQueue.length && generation === sendQueueGeneration) {
- const job = sendQueue.shift()
- setState({
- sendQueueLength: sendQueue.length
- })
- let result = false
- try {
- result = await executeSendFrame(job.hexFrame, job.source, job.options)
- } catch (error) {
- cancelPendingRequest()
- setState({
- errorText: error.message || '发送失败'
- })
- }
- job.resolve(result)
- if (!state.connectedDevice) {
- clearSendQueue()
- break
- }
- }
- } finally {
- if (generation === sendQueueGeneration) {
- isProcessingSendQueue = false
- }
- }
- }
- async function executeSendFrame(hexFrame, source, options = {}) {
- const {
- connectedDevice,
- writeCharacteristicId,
- writeServiceId,
- writeType
- } = state
- const errorText = validateHex(hexFrame)
- if (!connectedDevice) {
- setState({
- errorText: '请先连接蓝牙透传设备'
- })
- return false
- }
- if (!writeServiceId || !writeCharacteristicId) {
- setState({
- errorText: '当前设备没有可写特征值'
- })
- return false
- }
- if (errorText) {
- setState({
- errorText
- })
- return false
- }
- const buffer = hexToArrayBuffer(hexFrame)
- const bytes = new Uint8Array(buffer)
- const dmaFrameLengthError = validateDmaFrameLength(bytes, options.expected)
- if (dmaFrameLengthError) {
- setState({
- errorText: dmaFrameLengthError
- })
- return false
- }
- const chunkSize = DEFAULT_PACKET_SIZE
- const waitResponse = !!options.expected
- const responsePromise = waitResponse
- ? createPendingRequest(source, options.expected, options)
- : null
- setState({
- isSending: true,
- errorText: ''
- })
- try {
- for (let offset = 0; offset < bytes.length; offset += chunkSize) {
- const chunk = bytes.slice(offset, offset + chunkSize)
- await callWx('writeBLECharacteristicValue', {
- deviceId: connectedDevice.deviceId,
- serviceId: writeServiceId,
- characteristicId: writeCharacteristicId,
- value: chunk.buffer,
- writeType
- })
- }
- setState({
- txCount: state.txCount + bytes.length
- })
- addLog('TX', arrayBufferToHex(buffer), source)
- if (waitResponse) {
- return responsePromise
- }
- return true
- } catch (error) {
- if (waitResponse) {
- cancelPendingRequest()
- }
- if (isConnectionLostError(error)) {
- clearConnectedState({
- errorText: formatBluetoothError(error)
- })
- } else {
- setState({
- errorText: formatBluetoothError(error)
- })
- }
- return false
- } finally {
- setState({
- isSending: false
- })
- }
- }
- function sendManagedFrame(frameBytes, label, expected, options = {}) {
- return enqueueSendFrame(formatHex(frameBytes), label, {
- expected,
- showModal: options.showModal !== false,
- timeout: options.timeout || RESPONSE_TIMEOUT
- })
- }
- function sendFrame(hexFrame, source, options = {}) {
- return enqueueSendFrame(hexFrame, source, options)
- }
- function sendHexFrame() {
- const errorText = validateHex(state.sendHex)
- const expected = errorText ? null : parseModbusRequest(Array.prototype.slice.call(new Uint8Array(hexToArrayBuffer(state.sendHex))))
- return enqueueSendFrame(state.sendHex, 'HEX', expected ? {
- expected
- } : {})
- }
- function sendGeneratedFrame() {
- if (!state.generatedHex) return false
- const expected = buildGeneratedExpectedResponse()
- return enqueueSendFrame(state.generatedHex, 'RTU', expected ? {
- expected
- } : {})
- }
- setState(createProtocolState(
- state.commandIndex,
- state.slaveAddress,
- state.registerAddress,
- state.commandValue,
- state.coilEnabled
- ))
- module.exports = {
- arrayBufferToHex,
- buildReadFrame,
- buildWriteMultipleRegistersFrame,
- buildWriteSingleCoilFrame,
- buildWriteSingleRegisterFrame,
- clearDevices,
- clearInput,
- clearLogs,
- connectDeviceById,
- disconnectDevice,
- formatHex,
- getState,
- getSlaveAddress,
- handleAppHide,
- handleAppShow,
- hexToArrayBuffer,
- init,
- normalizeHex,
- openSetting,
- parseModbusRequest,
- sendFrame,
- sendGeneratedFrame,
- sendHexFrame,
- sendManagedFrame,
- setCommandIndex,
- setProtocolInput,
- setSendHex,
- showCommandAlert,
- startScan,
- stopScan,
- subscribe,
- validateHex
- }
|