const {
  buildReadFrame,
  buildWriteMultipleRegistersFrame,
  buildWriteSingleCoilFrame,
  buildWriteSingleRegisterFrame,
  formatHex,
  getReadResponseByteLength,
  MODBUS_CRC_OPTIONS,
  MAX_MODBUS_DMA_BYTES,
  hasValidCrc16Modbus
} = require('./modbus-rtu')
const {
  BYTE_ORDER_HIGH,
  hasValidCrc16Ccitt
} = require('./crc')
const {
  getBootloaderResponseLength,
  isBootloaderFrame
} = require('./bootloader-frame')
const {
  notifyPageToast
} = require('./page-toast')
const {
  padHex
} = require('./base-utils')
const {
  bytesToWords
} = require('./binary-utils')

const SCAN_TIMEOUT = 15000
const CONNECT_TIMEOUT = 10000
const RSSI_REFRESH_INTERVAL = 2000
const DEFAULT_PACKET_SIZE = 20
const MODULE_PACKET_SIZES = [
  {
    packetSize: 0,
    patterns: [/HC[-_ ]?05/i]
  },
  {
    packetSize: 320,
    patterns: [/BT[-_ ]?24/i, /\bBT24\b/i]
  }
]
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: 'readDiscreteInputs', label: '02 读取离散输入', functionCode: 0x02, 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: 2,
  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 rssiTimer = null
let isReadingRssi = false
let pendingRequest = null
let sendQueue = []
let isProcessingSendQueue = false
let sendQueueGeneration = 0
let sendJobSequence = 0
let deviceMap = {}
let deviceSequence = 0
let logSequence = 0
const subscribers = []
const rawResponseSubscribers = []

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 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 subscribeRawResponse(subscriber) {
  if (typeof subscriber !== 'function') return () => {}

  rawResponseSubscribers.push(subscriber)

  return () => {
    const index = rawResponseSubscribers.indexOf(subscriber)
    if (index >= 0) rawResponseSubscribers.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 packetSize = inferPacketSize({
    displayName,
    localName: device.localName,
    name: device.name
  })
  const isTargetAdvertised = hasTargetAdvertisedUuid({
    advertisServiceUUIDs
  })

  return {
    deviceId: device.deviceId,
    name: device.name || '',
    localName: device.localName || '',
    RSSI: device.RSSI,
    advertisServiceUUIDs,
    displayName,
    isTargetAdvertised,
    packetSize,
    signalText: formatSignalText(device.RSSI),
    serviceText: advertisServiceUUIDs.length ? advertisServiceUUIDs.join(', ') : '未广播服务',
    targetText: isTargetAdvertised ? '广播含目标 UUID' : '',
    lastSeenAt: Date.now()
  }
}

function formatSignalText(RSSI) {
  return typeof RSSI === 'number' ? `${RSSI} dBm` : '--'
}

function inferPacketSize(device = {}) {
  const text = [device.displayName, device.name, device.localName]
    .map((value) => String(value || ''))
    .join(' ')
    .toUpperCase()

  for (const item of MODULE_PACKET_SIZES) {
    const matchedPattern = (item.patterns || []).some((pattern) => pattern.test(text))
    if (matchedPattern) {
      return item.packetSize
    }
  }

  return DEFAULT_PACKET_SIZE
}

function resolvePacketSize(packetSize, frameLength) {
  if (packetSize === 0) return frameLength || DEFAULT_PACKET_SIZE
  if (Number.isInteger(packetSize) && packetSize > 0) return packetSize

  return DEFAULT_PACKET_SIZE
}

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 normalizeMaxFrameBytes(maxFrameBytes) {
  const numberValue = Number(maxFrameBytes)
  if (Number.isFinite(numberValue) && Math.round(numberValue) === 0) return 0
  if (Number.isFinite(numberValue) && numberValue > 0) return Math.round(numberValue)

  return MAX_MODBUS_DMA_BYTES
}

function getResponseBufferLimit(expected, maxFrameBytes) {
  const responseLength = expected
    ? getReadResponseByteLength(expected.functionCode, expected.quantity)
    : 0
  const frameLimit = normalizeMaxFrameBytes(maxFrameBytes)

  if (frameLimit === 0) {
    return Math.max(MAX_RESPONSE_BUFFER_BYTES, responseLength + 8)
  }

  return Math.max(MAX_RESPONSE_BUFFER_BYTES, frameLimit + 8, responseLength + 8)
}

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 parseModbusResponse(bytes) {
  if (!Array.isArray(bytes) || bytes.length < 5 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) 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 || functionCode === 0x02) {
    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 || !hasValidCrc16Modbus(bytes, MODBUS_CRC_OPTIONS)) 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 === 0x02 || 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) {
  const maxFrameBytes = normalizeMaxFrameBytes(expected && expected.maxFrameBytes)
  if (maxFrameBytes === 0) return ''

  if (bytes.length > maxFrameBytes) {
    return `发送帧长度 ${bytes.length} 字节，超过最大包长 ${maxFrameBytes} 字节限制`
  }

  if (!expected) return ''

  const responseLength = getReadResponseByteLength(expected.functionCode, expected.quantity)

  if (responseLength > maxFrameBytes) {
    return `预计返回帧长度 ${responseLength} 字节，超过最大包长 ${maxFrameBytes} 字节限制`
  }

  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 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 getReceiveCrcState(rawBytes) {
  if (!rawBytes || rawBytes.length < 4) return ''

  if (isBootloaderFrame(rawBytes)) {
    const expectedLength = getBootloaderResponseLength(rawBytes)
    if (expectedLength && rawBytes.length < expectedLength) return '片段'

    return hasValidCrc16Ccitt(rawBytes, { byteOrder: BYTE_ORDER_HIGH }) ? 'CRC OK' : 'CRC ERR'
  }

  return hasValidCrc16Modbus(rawBytes, MODBUS_CRC_OPTIONS) ? 'CRC OK' : (pendingRequest ? '片段' : 'CRC ERR')
}

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,
      packetSize: nextDevice.packetSize || previousDevice.packetSize || DEFAULT_PACKET_SIZE,
      seenIndex,
      serviceText: advertisServiceUUIDs.length ? advertisServiceUUIDs.join(', ') : '未广播服务',
      targetText: isTargetDevice ? '已发现目标特征' : (isTargetAdvertised ? '广播含目标 UUID' : '')
    }
  })

  refreshDeviceList()
}

function refreshDeviceList() {
  const deviceList = getSortedDeviceList()
  setState({
    devices: deviceList.slice(0, 30)
  })
}

function getSortedDeviceList() {
  return 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
    })
}

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 = {}) {
  stopRssiRefresh()
  resetSendRuntimeState()
  setState({
    characteristicText: '未选择',
    connectedDevice: null,
    connectedServiceCount: 0,
    connectingDeviceId: '',
    isConnecting: false,
    writeCharacteristicId: '',
    writeServiceId: '',
    writeType: '',
    ...changedData
  })
}

function stopRssiRefresh() {
  if (rssiTimer) {
    clearInterval(rssiTimer)
    rssiTimer = null
  }
  isReadingRssi = false
}

function applyRssiUpdate(deviceId, rssi) {
  if (!state.connectedDevice || state.connectedDevice.deviceId !== deviceId || typeof rssi !== 'number') {
    return
  }

  const signalText = formatSignalText(rssi)
  const updatedDevice = {
    ...state.connectedDevice,
    RSSI: rssi,
    lastSeenAt: Date.now(),
    signalText
  }

  deviceMap[deviceId] = {
    ...(deviceMap[deviceId] || {}),
    RSSI: rssi,
    lastSeenAt: updatedDevice.lastSeenAt,
    signalText
  }

  setState({
    connectedDevice: updatedDevice,
    devices: getSortedDeviceList().slice(0, 30)
  })
}

async function refreshConnectedRssi() {
  const { connectedDevice } = state
  if (!connectedDevice || typeof wx === 'undefined' || typeof wx.getBLEDeviceRSSI !== 'function') return
  if (isReadingRssi) return

  isReadingRssi = true
  try {
    const result = await callWx('getBLEDeviceRSSI', {
      deviceId: connectedDevice.deviceId
    })
    if (!state.connectedDevice || state.connectedDevice.deviceId !== connectedDevice.deviceId) return
    applyRssiUpdate(connectedDevice.deviceId, result && result.RSSI)
  } catch (error) {
    if (isConnectionLostError(error)) {
      clearConnectedState({
        errorText: formatBluetoothError(error)
      })
    }
  } finally {
    isReadingRssi = false
  }
}

function startRssiRefresh() {
  stopRssiRefresh()

  if (!state.connectedDevice || typeof wx === 'undefined' || typeof wx.getBLEDeviceRSSI !== 'function') {
    return
  }

  refreshConnectedRssi()
  rssiTimer = setInterval(() => {
    refreshConnectedRssi()
  }, RSSI_REFRESH_INTERVAL)
}

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 || response.functionCode === 0x02) {
    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 || responseFunctionCode === 0x02) {
    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

  const frameLimit = normalizeMaxFrameBytes(pending.expected && pending.expected.maxFrameBytes)
  if (frameLimit > 0 && responseLength > frameLimit) {
    const content = `${pending.label} 返回帧长度 ${responseLength} 字节，超过最大包长 ${frameLimit} 字节限制，已丢弃`
    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)
  const bufferLimit = pendingRequest.responseBufferLimit || MAX_RESPONSE_BUFFER_BYTES
  if (pendingRequest.responseBuffer.length > bufferLimit) {
    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,
      responseBufferLimit: getResponseBufferLimit(expected, options.maxFrameBytes),
      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 = getReceiveCrcState(rawBytes)

    setState({
      rxCount: state.rxCount + byteLength
    })
    addLog('RX', hex, crcState)
    rawResponseSubscribers.slice().forEach((subscriber) => {
      subscriber(rawBytes, res)
    })
    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,
      packetSize: device.packetSize || inferPacketSize(device),
      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
    })

    startRssiRefresh()
    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()
  stopRssiRefresh()
  resetSendRuntimeState()
  if (state.isDiscovering) {
    stopScan()
  }
}

async function handleAppShow() {
  init()
  const connected = await refreshNativeConnectionState()
  if (connected && state.connectedDevice) {
    startRssiRefresh()
  }
}

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 = options.skipDmaCheck ? '' : 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 = options.skipDmaCheck ? '' : validateDmaFrameLength(bytes, options.expected)

  if (dmaFrameLengthError) {
    setState({
      errorText: dmaFrameLengthError
    })
    return false
  }

  const chunkSize = resolvePacketSize(
    options.chunkSize === undefined ? connectedDevice.packetSize : options.chunkSize,
    bytes.length
  )
  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: expected ? {
      ...expected,
      maxFrameBytes: options.maxFrameBytes === undefined ? MAX_MODBUS_DMA_BYTES : options.maxFrameBytes
    } : expected,
    showModal: options.showModal !== false,
    timeout: options.timeout || RESPONSE_TIMEOUT,
    maxFrameBytes: options.maxFrameBytes === undefined ? MAX_MODBUS_DMA_BYTES : options.maxFrameBytes
  })
}

function sendRawFrameExact(frameBytes, source) {
  const bytes = frameBytes instanceof Uint8Array
    ? frameBytes
    : new Uint8Array(frameBytes || [])

  return enqueueSendFrame(formatHex(Array.prototype.slice.call(bytes)), source, {
    chunkSize: 0,
    skipDmaCheck: true
  })
}

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 = {
  clearDevices,
  clearInput,
  clearLogs,
  connectDeviceById,
  disconnectDevice,
  getState,
  handleAppHide,
  handleAppShow,
  init,
  sendGeneratedFrame,
  sendHexFrame,
  sendManagedFrame,
  sendRawFrameExact,
  setCommandIndex,
  setProtocolInput,
  setSendHex,
  showCommandAlert,
  startScan,
  stopScan,
  subscribe,
  subscribeRawResponse
}
