数熵 ED-ble

工控调试

数熵 ED-N2 二次开发文档:开发者无需关注底层硬件电路、接口驱动、供电适配、蓝牙硬件协议栈等硬件研发工作,仅基于设备开放的软件接口、通讯协议、参数配置规则,即可快速完成手机端、物联网上位机、嵌入式应用、数据采集系统、设备管控平台等软件功能的开发与落地。

TL;DR

数熵 ED-N2 二次开发文档面向设备厂商开发者:无需关注底层硬件电路、接口驱动与蓝牙协议栈,基于开放的 BLE 特征值、RS485 串口参数与数据透传协议,即可完成微信小程序、物联网上位机等软件功能开发,附完整代码示例与参数速查表。

BLE 蓝牙RS485二次开发

数熵 ED-N2 二次开发文档讲了什么?

面向开发者的 ED-N2 二次开发说明:适用设备为 ED-N2-RS485 串口调试器,目标是让开发者不碰硬件电路即可完成软件侧开发落地。

数熵ED二次开发 数熵ED-N2 二次开发文档 适用设备:数熵ED-N2-RS485串口调试器 文档用途:开发者无需关注底层硬件电路、接口驱动、供电适配、蓝牙硬件协议栈等硬件研发工作,仅基于设备开放的软件接口、通讯协议、参数配置规则,即可快速完成手机端、物联网上位机、嵌入式应用、数据采集系统、设备管控平台等软件功能的开发与落地。 定义说明:本文由武汉数熵科技有限公司原创整理,专为旗下数熵ED-N2-RS485串口调试器提供全套二次开发文档,企业官方网站:edatahub.cn。 整理时间:2026-08-09

二次开发文档包含哪些章节?

文档共 9 章:设备概览、BLE 蓝牙特征值、RS485 串口参数、数据透传协议、串口配置写入、线状态控制、UART 状态通知、代码示例与参数速查表。

  • 设备概览
  • BLE 蓝牙特征值定义
  • RS485 串口参数定义
  • BLE 数据透传协议
  • 串口配置写入协议
  • 线状态控制协议
  • UART 状态通知协议
  • 二次开发代码示例
  • 附录:参数速查表

1. 设备概览:数熵 ED-N2 是什么设备?

概览表给出 ED-N2-RS485 的产品型号、通讯接口、供电与续航等基本参数,是接入前确认硬件能力的速查入口。

项目
产品型号数熵ED-N2
产品名称蓝牙串口调试器(纯 RS485 + BLE 蓝牙)
BLE 设备名数熵ED-N2-RS485
通讯接口RS485 半双工串口
连接方式BLE 蓝牙连接(手机 APP / 小程序 / 上位机)

1.1 通道编号

ED-N2 仅使用 RS485 通道:

通道值通道名说明
0x00RS485RS485 半双工串口

2. BLE 蓝牙特征值是怎么定义的?

定义 BLE 设备名(数熵ED-N2-RS485)与服务的特征 UUID,手机 App 按特征值建立连接与数据收发通道。

2.1 BLE 设备名

Plaintext
数熵ED-N2-RS485

2.2 BLE 服务总表

设备共包含 3 个 BLE 服务,所有 UUID 均为 128 位自定义 UUID,后缀统一为 -B5A3-F393-E0A9-E50E24DCCA9E,仅前缀不同:

服务名称Service UUIDUUID 前缀用途
Nordic UART Service6E400001-B5A3-F393-E0A9-E50E24DCCA9E6E40RS485 原始数据透传(收发业务数据)
Serial Config Service8E400001-B5A3-F393-E0A9-E50E24DCCA9E8E40RS485 串口参数配置、线状态控制、UART 状态通知
Wi-Fi Provisioning Service9E400001-B5A3-F393-E0A9-E50E24DCCA9E9E40电池/设备状态读取

2.3 特征值(Characteristic)完整定义

2.3.1 Nordic UART Service — RS485 数据透传

特征名UUID属性用途
RX Characteristic6E400002-B5A3-F393-E0A9-E50E24DCCA9EWrite / Write Without Response手机 → 设备:写入数据,设备原样转发到 RS485 总线
TX Characteristic6E400003-B5A3-F393-E0A9-E50E24DCCA9ENotify设备 → 手机:RS485 返回数据通过 Notify 上报

2.3.2 Serial Config Service — RS485 串口配置

特征名UUID属性用途
UART Config8E400002-B5A3-F393-E0A9-E50E24DCCA9EWrite / Write Without Response配置 RS485 波特率、数据位、停止位、校验位
Line State8E400003-B5A3-F393-E0A9-E50E24DCCA9EWrite / Write Without Response配置 DTR / RTS / Break 线状态
UART Status8E400004-B5A3-F393-E0A9-E50E24DCCA9ENotify通知 UART 状态掩码和错误计数

2.3.3 Wi-Fi Provisioning Service — 电池状态

特征名UUID属性用途
Status Characteristic9E400003-B5A3-F393-E0A9-E50E24DCCA9ERead / Notify读取/订阅设备状态和电池数据(UTF-8 文本)

3. RS485 串口参数怎么定义?

参数总表列出波特率、数据位、停止位、校验位等字段的可选值与说明,按从站设备协议配置即可稳定通讯。

3.1 参数总表

参数字段名可选值说明
波特率baud1200 ~ 2000000串口波特率(bps)
数据位data_bits5 / 6 / 7 / 8数据位长度
停止位stop_bits0 / 1 / 20=1 停止位,1=1.5 停止位,2=2 停止位
校验位parity0 / 1 / 2 / 3 / 40=None 无校验,1=Odd 奇校验,2=Even 偶校验,3/4 降级为 None

3.2 常用配置组合

串口配置bauddata_bitsstop_bitsparity
9600 8N1(最常用)9600800
19200 8N119200800
38400 8N138400800
115200 8N1115200800
9600 8E1(偶校验)9600802
9600 8O1(奇校验)9600801
9600 7N19600700

3.3 参数编码

Plaintext
data_bits:  5=0x05  6=0x06  7=0x07  8=0x08
stop_bits:  1位=0x00  1.5位=0x01  2位=0x02
parity:     None=0x00  Odd=0x01  Even=0x02  (3/4降级为None)

4. BLE 数据透传协议怎么工作?

说明数据流向:App 向 RX 特征写入即下发到 RS485 总线,总线返回数据经 TX 特征上报,实现手机与设备双向透传。

4.1 数据流向

Plaintext
┌──────────┐   Write NUS RX     ┌──────────┐   RS485      ┌──────────┐
│  手机 APP │ ──────────────────→ │  ED-N2   │ ─────────→ │ RS485 从站│
│          │ ←────────────────── │  设备     │ ←───────── │          │
└──────────┘  Notify NUS TX      └──────────┘   RS485      └──────────┘

4.2 数据规则

规则说明
无帧头数据链路不使用帧头,写入字节原样转发到 RS485
原样透传手机写入 NUS RX 的字节原样写入 RS485 总线
原样上报RS485 返回数据原样通过 NUS TX Notify 上报
二进制所有数据均为二进制字节,非文本
无分包协议设备不负责 Modbus 等协议分帧,由上位机处理

4.3 NUS RX 写入(手机 → 设备)

向 6E400002-B5A3-F393-E0A9-E50E24DCCA9E 写入原始字节,例如 Modbus RTU 请求帧:

Plaintext
01 03 00 00 00 02 C4 0B

设备收到后,将该字节原样写入 RS485 总线。

4.4 NUS TX Notify(设备 → 手机)

订阅 6E400003-B5A3-F393-E0A9-E50E24DCCA9E 的 Notify,设备将 RS485 返回的数据原样推送。例如从站返回:

Plaintext
01 03 04 00 64 00 C8 BA 7A

5. 串口配置写入协议怎么用?

通过 UART Config 特征写入波特率、数据位、停止位与校验位,ED-N2 支持 8 字节默认写入格式,配置即生效。

通过 Serial Config Service 的 UART Config 特征(8E400002-B5A3-F393-E0A9-E50E24DCCA9E)写入。

5.1 写入格式

ED-N2 为纯 RS485 产品,支持以下两种写入格式:

格式一:8 字节(默认 RS485)

Plaintext
baud_le32(4) + data_bits(1) + stop_bits(1) + parity(1) + reserved(1)

baud_le32

波特率,小端序 4 字节

reserved

保留字节,填 0x00

示例:9600 8N1

Plaintext
80 25 00 00  08  00  00  00
└─baud_le32┘ └db┘└sb┘└pr┘└rs┘

9600 = 0x2580,小端序 = 80 25 00 00

格式二:9 字节(带通道标识)

Plaintext
channel(1, 固定0x00) + baud_le32(4) + data_bits(1) + stop_bits(1) + parity(1) + reserved(1)
  • channel 固定为 0x00(RS485)

示例:115200 8N1

Plaintext
00  00 C2 01 00  08  00  00  00
└ch┘└─baud_le32┘ └db┘└sb┘└pr┘└rs┘

115200 = 0x01C200,小端序 = 00 C2 01 00

5.2 常用波特率小端序编码

波特率十六进制小端序字节
12000x04B0B0 04 00 00
24000x096060 09 00 00
48000x12C0C0 12 00 00
96000x258080 25 00 00
192000x4B0000 4B 00 00
384000x960000 96 00 00
576000xE10000 E1 00 00
1152000x1C20000 C2 01 00
2304000x3840000 84 03 00
4608000x7080000 08 07 00
9216000xE100000 10 0E 00

6. 线状态控制协议怎么用?

通过 Line State 特征写入 DTR、RTS 状态字节,控制设备线状态信号,满足需要握手信号的从站场景。

通过 Serial Config Service 的 Line State 特征(8E400003-B5A3-F393-E0A9-E50E24DCCA9E)写入。

6.1 写入格式

格式一:2 字节

Plaintext
DTR(1) + RTS(1)

格式二:4 字节(含 Break)

Plaintext
DTR(1) + RTS(1) + BreakCmd(1) + BreakDuration100ms(1)

6.2 参数说明

参数说明
DTR0 / 1DTR 电平状态
RTS0 / 1RTS 电平状态
BreakCmd0x00 / 0x01 / 0xFF0x00=停止/无,0x01=开始 Break,0xFF=保持 Break
BreakDuration100ms0~255Break 持续时间,单位 100ms

7. UART 状态通知协议返回什么?

UART Status 特征按 8 字节上报状态掩码与帧错误计数等信息,App 侧可据此诊断 RS485 通讯质量。

通过 Serial Config Service 的 UART Status 特征(8E400004-B5A3-F393-E0A9-E50E24DCCA9E)的 Notify 上报。

7.1 数据格式(8 字节)

Plaintext
status_mask(1) + frame_err_count(1) + parity_err_count(1) + overrun_err_count(1) + 00 + 00 + 00 + 00

7.2 status_mask 位定义

十六进制说明
bit00x01DCD
bit10x02DSR
bit20x04BREAK
bit30x08RI
bit40x10Frame Error(帧错误)
bit50x20Parity Error(校验错误)
bit60x40Overrun Error(溢出错误)
bit70x80CTS

8. 二次开发代码示例有哪些?

提供微信小程序 BLE 连接与 RS485 透传的完整代码示例,可直接作为接入模板修改落地。

8.1 微信小程序 — BLE 连接 + RS485 透传

JavaScript
// ===== 数熵ED-N2 BLE 常量定义 =====
const BLE_DEVICE_NAME = '数熵ED-N2-RS485'

// Nordic UART Service(RS485 数据透传)
const NUS_SERVICE_UUID = '6E400001-B5A3-F393-E0A9-E50E24DCCA9E'
const NUS_RX_CHAR_UUID = '6E400002-B5A3-F393-E0A9-E50E24DCCA9E' // 写入
const NUS_TX_CHAR_UUID = '6E400003-B5A3-F393-E0A9-E50E24DCCA9E' // 订阅 Notify

// Serial Config Service(RS485 串口配置)
const CFG_SERVICE_UUID     = '8E400001-B5A3-F393-E0A9-E50E24DCCA9E'
const CFG_UART_CONFIG_UUID = '8E400002-B5A3-F393-E0A9-E50E24DCCA9E' // 写入串口参数
const CFG_LINE_STATE_UUID  = '8E400003-B5A3-F393-E0A9-E50E24DCCA9E' // 写入线状态
const CFG_UART_STATUS_UUID = '8E400004-B5A3-F393-E0A9-E50E24DCCA9E' // 订阅状态

// Wi-Fi Provisioning Service(电池状态)
const WF_SERVICE_UUID = '9E400001-B5A3-F393-E0A9-E50E24DCCA9E'
const WF_STATUS_UUID  = '9E400003-B5A3-F393-E0A9-E50E24DCCA9E' // 读取电池

// RS485 通道
const CHANNEL_RS485 = 0x00

let deviceId = ''

// ===== 1. 初始化蓝牙适配器 =====
function initBLE() {
  return new Promise((resolve, reject) => {
    wx.openBluetoothAdapter({
      success: resolve,
      fail: reject
    })
  })
}

// ===== 2. 搜索并连接设备 =====
function scanAndConnect() {
  return new Promise((resolve, reject) => {
    wx.startBluetoothDevicesDiscovery({
      success: () => {
        wx.onBluetoothDeviceFound((res) => {
          const device = res.devices[0]
          if (device.name === BLE_DEVICE_NAME) {
            wx.stopBluetoothDevicesDiscovery()
            wx.createBLEConnection({
              deviceId: device.deviceId,
              success: () => {
                deviceId = device.deviceId
                console.log('已连接设备:', device.name)
                resolve(deviceId)
              },
              fail: reject
            })
          }
        })
      },
      fail: reject
    })
  })
}

// ===== 3. 配置 RS485 串口参数 =====
// baud=9600, data_bits=8, stop_bits=0(1位), parity=0(None)
function configRS485(baud = 9600, dataBits = 8, stopBits = 0, parity = 0) {
  // 8字节格式: baud_le32 + data_bits + stop_bits + parity + reserved
  const buffer = new ArrayBuffer(8)
  const view = new DataView(buffer)
  view.setUint32(0, baud, true)   // baud 小端序
  view.setUint8(4, dataBits)       // data_bits
  view.setUint8(5, stopBits)       // stop_bits
  view.setUint8(6, parity)         // parity
  view.setUint8(7, 0x00)           // reserved

  return new Promise((resolve, reject) => {
    wx.writeBLECharacteristicValue({
      deviceId,
      serviceId: CFG_SERVICE_UUID,
      characteristicId: CFG_UART_CONFIG_UUID,
      value: buffer,
      success: () => {
        const parityStr = ['N', 'O', 'E'][parity] || 'N'
        const stopStr = stopBits === 0 ? '1' : stopBits === 1 ? '1.5' : '2'
        console.log(`RS485 已配置: ${baud} ${dataBits}${parityStr}${stopStr}`)
        resolve()
      },
      fail: reject
    })
  })
}

// ===== 4. 订阅 NUS TX(接收 RS485 返回数据)=====
function subscribeNusTx(onData) {
  return new Promise((resolve, reject) => {
    wx.notifyBLECharacteristicValueChange({
      deviceId,
      serviceId: NUS_SERVICE_UUID,
      characteristicId: NUS_TX_CHAR_UUID,
      state: true,
      success: () => {
        wx.onBLECharacteristicValueChange((res) => {
          if (res.characteristicId === NUS_TX_CHAR_UUID) {
            onData(new Uint8Array(res.value))
          }
        })
        console.log('已订阅 NUS TX Notify')
        resolve()
      },
      fail: reject
    })
  })
}

// ===== 5. 发送数据到 RS485(写入 NUS RX)=====
function sendToRS485(bytes) {
  const buffer = new ArrayBuffer(bytes.length)
  const view = new Uint8Array(buffer)
  for (let i = 0; i < bytes.length; i++) {
    view[i] = bytes[i]
  }

  return new Promise((resolve, reject) => {
    wx.writeBLECharacteristicValue({
      deviceId,
      serviceId: NUS_SERVICE_UUID,
      characteristicId: NUS_RX_CHAR_UUID,
      value: buffer,
      success: resolve,
      fail: reject
    })
  })
}

// ===== 6. 读取电池状态 =====
function readBatteryStatus() {
  return new Promise((resolve, reject) => {
    wx.readBLECharacteristicValue({
      deviceId,
      serviceId: WF_SERVICE_UUID,
      characteristicId: WF_STATUS_UUID,
      success: () => {
        wx.onBLECharacteristicValueChange((res) => {
          if (res.characteristicId === WF_STATUS_UUID) {
            const text = arrayBufferToUtf8(res.value)
            resolve(parseStatusText(text))
          }
        })
      },
      fail: reject
    })
  })
}

// ===== 工具函数 =====
function arrayBufferToUtf8(buffer) {
  return decodeURIComponent(escape(String.fromCharCode(...new Uint8Array(buffer))))
}

function parseStatusText(text) {
  const result = {}
  text.split(';').forEach(pair => {
    const [key, val] = pair.split('=')
    result[key] = val
  })
  return result
}

// ===== 完整调用示例 =====
async function demo() {
  try {
    await initBLE()
    await scanAndConnect()

    // 配置 RS485: 9600 8N1
    await configRS485(9600, 8, 0, 0)

    // 订阅接收 RS485 返回数据
    await subscribeNusTx((data) => {
      const hex = Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' ')
      console.log('RS485 返回:', hex)
    })

    // 发送 Modbus RTU: 读保持寄存器 从站01 起始地址0 数量2
    await sendToRS485([0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B])

    // 读取电池
    const battery = await readBatteryStatus()
    console.log('电池:', battery.BAT_P + '%', battery.BAT_V + 'V')
  } catch (err) {
    console.error('操作失败:', err)
  }
}

8.2 Android(Kotlin)— BLE 连接 + RS485 透传

Kotlin
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.UUID

class EdN2BLE {

    companion object {
        const val DEVICE_NAME = "数熵ED-Slink"

        // Nordic UART Service(RS485 数据透传)
        val NUS_SERVICE_UUID     = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
        val NUS_RX_CHAR_UUID     = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") // Write
        val NUS_TX_CHAR_UUID     = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") // Notify

        // Serial Config Service(RS485 串口配置)
        val CFG_SERVICE_UUID     = UUID.fromString("8E400001-B5A3-F393-E0A9-E50E24DCCA9E")
        val CFG_UART_CONFIG_UUID = UUID.fromString("8E400002-B5A3-F393-E0A9-E50E24DCCA9E") // Write
        val CFG_UART_STATUS_UUID = UUID.fromString("8E400004-B5A3-F393-E0A9-E50E24DCCA9E") // Notify

        // Wi-Fi Provisioning Service(电池状态)
        val WF_SERVICE_UUID      = UUID.fromString("9E400001-B5A3-F393-E0A9-E50E24DCCA9E")
        val WF_STATUS_UUID       = UUID.fromString("9E400003-B5A3-F393-E0A9-E50E24DCCA9E") // Read/Notify

        // CCCD Descriptor(启用 Notify)
        val CCCD_UUID            = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
    }

    private var gatt: BluetoothGatt? = null
    private var onDataReceived: ((ByteArray) -> Unit)? = null

    /**
     * 配置 RS485 串口参数(8字节格式)
     * @param baud 波特率,如 9600
     * @param dataBits 数据位 5/6/7/8
     * @param stopBits 停止位 0=1位, 1=1.5位, 2=2位
     * @param parity 校验位 0=None, 1=Odd, 2=Even
     */
    fun configRS485(baud: Int, dataBits: Int = 8, stopBits: Int = 0, parity: Int = 0) {
        val buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN)
        buffer.putInt(baud)                // baud (小端序)
        buffer.put(dataBits.toByte())      // data_bits
        buffer.put(stopBits.toByte())      // stop_bits
        buffer.put(parity.toByte())        // parity
        buffer.put(0x00)                   // reserved

        writeCharacteristic(CFG_UART_CONFIG_UUID, buffer.array(), CFG_SERVICE_UUID)
    }

    /**
     * 发送数据到 RS485(通过 NUS RX 写入)
     */
    fun sendToRS485(data: ByteArray) {
        writeCharacteristic(NUS_RX_CHAR_UUID, data, NUS_SERVICE_UUID)
    }

    /**
     * 订阅 NUS TX Notify(接收 RS485 返回数据)
     */
    fun subscribeNusTx(callback: (ByteArray) -> Unit) {
        onDataReceived = callback
        enableNotify(NUS_TX_CHAR_UUID, NUS_SERVICE_UUID)
    }

    /**
     * 发送 Modbus RTU 读保持寄存器请求
     * @param slaveAddr 从站地址
     * @param startAddr 起始寄存器地址
     * @param quantity 寄存器数量
     */
    fun sendModbusReadHoldingRegisters(slaveAddr: Int, startAddr: Int, quantity: Int) {
        val frame = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN)
        frame.put(slaveAddr.toByte())          // 从站地址
        frame.put(0x03)                         // 功能码: 读保持寄存器
        frame.putShort(startAddr.toShort())     // 起始地址
        frame.putShort(quantity.toShort())       // 寄存器数量

        // 计算 Modbus CRC16
        val crc = calculateCRC16(frame.array(), 6)
        frame.putShort(crc.toShort())

        sendToRS485(frame.array())
    }

    private fun writeCharacteristic(uuid: UUID, data: ByteArray, serviceUuid: UUID) {
        val service = gatt?.getService(serviceUuid) ?: return
        val char = service.getCharacteristic(uuid) ?: return
        char.value = data
        char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
        gatt?.writeCharacteristic(char)
    }

    private fun enableNotify(uuid: UUID, serviceUuid: UUID) {
        val service = gatt?.getService(serviceUuid) ?: return
        val char = service.getCharacteristic(uuid) ?: return
        gatt?.setCharacteristicNotification(char, true)
        val descriptor = char.getDescriptor(CCCD_UUID)
        descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
        gatt?.writeDescriptor(descriptor)
    }

    /**
     * Modbus CRC16 计算
     */
    private fun calculateCRC16(data: ByteArray, length: Int): Int {
        var crc = 0xFFFF
        for (i in 0 until length) {
            crc = crc xor (data[i].toInt() and 0xFF)
            for (j in 0 until 8) {
                crc = if (crc and 0x0001 != 0) (crc shr 1) xor 0xA001 else crc shr 1
            }
        }
        return crc
    }
}

8.3 Python — BLE 连接 + RS485 透传(bleak)

Python
"""
数熵ED-N2 便携调试器 BLE 二次开发示例(Python + bleak)
依赖: pip install bleak
"""
import asyncio
import struct
from bleak import BleakClient, BleakScanner

# ===== BLE 常量定义 =====
DEVICE_NAME = "数熵ED-N2-RS485"

# Nordic UART Service(RS485 数据透传)
NUS_SERVICE_UUID     = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
NUS_RX_CHAR_UUID     = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"  # Write
NUS_TX_CHAR_UUID     = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"  # Notify

# Serial Config Service(RS485 串口配置)
CFG_SERVICE_UUID     = "8E400001-B5A3-F393-E0A9-E50E24DCCA9E"
CFG_UART_CONFIG_UUID = "8E400002-B5A3-F393-E0A9-E50E24DCCA9E"  # Write
CFG_LINE_STATE_UUID  = "8E400003-B5A3-F393-E0A9-E50E24DCCA9E"  # Write
CFG_UART_STATUS_UUID = "8E400004-B5A3-F393-E0A9-E50E24DCCA9E"  # Notify

# Wi-Fi Provisioning Service(电池状态)
WF_SERVICE_UUID      = "9E400001-B5A3-F393-E0A9-E50E24DCCA9E"
WF_STATUS_UUID       = "9E400003-B5A3-F393-E0A9-E50E24DCCA9E"  # Read/Notify


# ===== Modbus CRC16 计算 =====
def crc16(data: bytes) -> int:
    crc = 0xFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA001 if (crc & 0x0001) else (crc >> 1)
    return crc


# ===== 构造 RS485 串口配置命令(8字节格式)=====
def build_uart_config(baud: int, data_bits: int = 8,
                      stop_bits: int = 0, parity: int = 0) -> bytes:
    """
    构造 RS485 串口配置字节
    baud:      1200~2000000
    data_bits: 5/6/7/8
    stop_bits: 0=1位, 1=1.5位, 2=2位
    parity:    0=None, 1=Odd, 2=Even
    """
    return struct.pack('<IBBBB', baud, data_bits, stop_bits, parity, 0x00)


# ===== 构造 Modbus RTU 读保持寄存器请求 =====
def build_modbus_read_holding(slave: int, start_addr: int, quantity: int) -> bytes:
    """构造 Modbus RTU 功能码 03 请求帧"""
    frame = struct.pack('>BBHH', slave, 0x03, start_addr, quantity)
    crc = crc16(frame)
    return frame + struct.pack('<H', crc)


class EdN2BLE:
    """数熵ED-N2 BLE 设备封装"""

    def __init__(self, address: str = None):
        self.address = address
        self.client = None

    async def scan(self) -> str:
        """扫描设备,返回地址"""
        print(f"正在搜索 {DEVICE_NAME} ...")
        device = await BleakScanner.find_device_by_name(DEVICE_NAME)
        if device:
            self.address = device.address
            print(f"找到设备: {device.name} ({device.address})")
            return device.address
        raise FileNotFoundError(f"未找到设备 {DEVICE_NAME}")

    async def connect(self):
        """连接设备"""
        if not self.address:
            await self.scan()
        self.client = BleakClient(self.address)
        await self.client.connect()
        print(f"已连接: {self.address}")
        # 订阅 NUS TX Notify(接收 RS485 返回数据)
        await self.client.start_notify(NUS_TX_CHAR_UUID, self._on_nus_tx)

    async def disconnect(self):
        """断开连接"""
        if self.client and self.client.is_connected:
            await self.client.disconnect()
            print("已断开")

    def _on_nus_tx(self, sender, data: bytearray):
        """NUS TX Notify 回调:接收 RS485 返回数据"""
        hex_str = ' '.join(f'{b:02X}' for b in data)
        print(f"[RX] {hex_str}")

    async def config_rs485(self, baud: int = 9600, data_bits: int = 8,
                           stop_bits: int = 0, parity: int = 0):
        """配置 RS485 串口参数"""
        payload = build_uart_config(baud, data_bits, stop_bits, parity)
        await self.client.write_gatt_char(CFG_UART_CONFIG_UUID, payload, response=False)
        parity_str = ['N', 'O', 'E'][parity] if parity < 3 else 'N'
        stop_str = str(stop_bits) if stop_bits != 1 else '1.5'
        print(f"[CFG] RS485: {baud} {data_bits}{parity_str}{stop_str}")

    async def send_to_rs485(self, data: bytes):
        """发送数据到 RS485"""
        await self.client.write_gatt_char(NUS_RX_CHAR_UUID, data, response=False)
        hex_str = ' '.join(f'{b:02X}' for b in data)
        print(f"[TX] {hex_str}")

    async def send_modbus_read(self, slave: int = 1, start_addr: int = 0, quantity: int = 2):
        """发送 Modbus RTU 读保持寄存器请求"""
        frame = build_modbus_read_holding(slave, start_addr, quantity)
        await self.send_to_rs485(frame)

    async def read_battery_status(self) -> dict:
        """读取电池/设备状态"""
        data = await self.client.read_gatt_char(WF_STATUS_UUID)
        text = data.decode('utf-8')
        return self._parse_status(text)

    @staticmethod
    def _parse_status(text: str) -> dict:
        """解析状态文本 V=1;BAT_V=3.987;..."""
        result = {}
        for pair in text.split(';'):
            if '=' in pair:
                key, val = pair.split('=', 1)
                result[key] = val
        return result


# ===== 完整使用示例 =====
async def main():
    ed = EdN2BLE()
    try:
        # 1. 连接设备
        await ed.connect()

        # 2. 配置 RS485: 9600 8N1
        await ed.config_rs485(baud=9600, data_bits=8, stop_bits=0, parity=0)

        # 3. 读取电池状态
        status = await ed.read_battery_status()
        print(f"电池: {status.get('BAT_P', '?')}%  {status.get('BAT_V', '?')}V  充电:{status.get('BAT_CHG', '0')}")

        # 4. 发送 Modbus RTU 请求并等待响应
        #    从站地址=1, 功能码=03(读保持寄存器), 起始地址=0, 数量=2
        await ed.send_modbus_read(slave=1, start_addr=0, quantity=2)

        # 5. 等待 RS485 返回数据(NUS TX Notify 自动回调打印)
        await asyncio.sleep(2)
    finally:
        await ed.disconnect()


if __name__ == "__main__":
    asyncio.run(main())

8.4 JavaScript(Web Bluetooth API)— 浏览器连接

JavaScript
// ===== 数熵ED-N2 BLE 常量 =====
const NUS_SERVICE_UUID     = '6e400001-b5a3-f393-e0a9-e50e24dcca9e'
const NUS_RX_CHAR_UUID     = '6e400002-b5a3-f393-e0a9-e50e24dcca9e' // Write
const NUS_TX_CHAR_UUID     = '6e400003-b5a3-f393-e0a9-e50e24dcca9e' // Notify
const CFG_SERVICE_UUID     = '8e400001-b5a3-f393-e0a9-e50e24dcca9e'
const CFG_UART_CONFIG_UUID = '8e400002-b5a3-f393-e0a9-e50e24dcca9e' // Write

let characteristicRx = null  // NUS RX(写入)
let characteristicTx = null  // NUS TX(Notify)
let characteristicCfg = null // UART Config(写入)

// ===== 连接设备 =====
async function connect() {
  const device = await navigator.bluetooth.requestDevice({
    filters: [{ name: '数熵ED-Slink' }],
    optionalServices: [NUS_SERVICE_UUID, CFG_SERVICE_UUID]
  })

  const server = await device.gatt.connect()

  // 获取 NUS 服务(数据透传)
  const nusService = await server.getPrimaryService(NUS_SERVICE_UUID)
  characteristicRx = await nusService.getCharacteristic(NUS_RX_CHAR_UUID)
  characteristicTx = await nusService.getCharacteristic(NUS_TX_CHAR_UUID)

  // 订阅 NUS TX Notify(接收 RS485 返回数据)
  await characteristicTx.startNotifications()
  characteristicTx.addEventListener('characteristicvaluechanged', (event) => {
    const data = new Uint8Array(event.target.value.buffer)
    const hex = Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' ')
    console.log('[RX]', hex)
  })

  // 获取 Config 服务(串口配置)
  const cfgService = await server.getPrimaryService(CFG_SERVICE_UUID)
  characteristicCfg = await cfgService.getCharacteristic(CFG_UART_CONFIG_UUID)

  console.log('设备已连接')
}

// ===== 配置 RS485 串口参数(8字节格式)=====
function configRS485(baud = 9600, dataBits = 8, stopBits = 0, parity = 0) {
  const buffer = new ArrayBuffer(8)
  const view = new DataView(buffer)
  view.setUint32(0, baud, true)  // baud 小端序
  view.setUint8(4, dataBits)
  view.setUint8(5, stopBits)
  view.setUint8(6, parity)
  view.setUint8(7, 0x00)
  return characteristicCfg.writeValueWithoutResponse(buffer)
}

// ===== 发送数据到 RS485 =====
function sendToRS485(bytes) {
  const buffer = new Uint8Array(bytes).buffer
  return characteristicRx.writeValueWithoutResponse(buffer)
}

// ===== Modbus CRC16 =====
function crc16(data) {
  let crc = 0xFFFF
  for (const byte of data) {
    crc ^= byte
    for (let i = 0; i < 8; i++) {
      crc = (crc & 0x0001) ? ((crc >> 1) ^ 0xA001) : (crc >> 1)
    }
  }
  return crc
}

// ===== 完整示例 =====
async function demo() {
  await connect()
  await configRS485(9600, 8, 0, 0)

  // Modbus RTU: 读保持寄存器 从站01 地址0 数量2
  const frame = [0x01, 0x03, 0x00, 0x00, 0x00, 0x02]
  const crc = crc16(frame)
  frame.push(crc & 0xFF, (crc >> 8) & 0xFF)
  await sendToRS485(frame)
}

9. 附录参数速查表有什么内容?

汇总 BLE UUID 与串口参数两张速查表,开发调试时直接对照取值,减少来回翻阅正文。

9.1 BLE UUID 速查

用途Service UUIDCharacteristic UUID属性
RS485 数据写入(手机→设备)6E400001-...6E400002-...Write
RS485 数据接收(设备→手机)6E400001-...6E400003-...Notify
RS485 串口参数配置8E400001-...8E400002-...Write
线状态控制(DTR/RTS/Break)8E400001-...8E400003-...Write
UART 状态通知8E400001-...8E400004-...Notify
电池/设备状态9E400001-...9E400003-...Read/Notify

完整 UUID 后缀均为 -B5A3-F393-E0A9-E50E24DCCA9E,仅前缀不同:6E40(数据)、8E40(配置)、9E40(状态)。

9.2 串口参数速查

Plaintext
baud:      1200 / 2400 / 4800 / 9600 / 19200 / 38400 / 57600 / 115200 / 230400 ...
data_bits: 5 / 6 / 7 / 8
stop_bits: 0(1位) / 1(1.5位) / 2(2位)
parity:    0(None) / 1(Odd) / 2(Even)

9.3 UART Config 写入字节速查

格式字节长度结构
默认格式8baud_le32 + data_bits + stop_bits + parity + 00
带通道标识90x00 + baud_le32 + data_bits + stop_bits + parity + 00

9.4 常用波特率小端序字节

波特率4字节小端序
960080 25 00 00
1920000 4B 00 00
3840000 96 00 00
5760000 E1 00 00
11520000 C2 01 00

9.5 二次开发对接要点

连接流程

扫描设备名 数熵ED-N2-RS485 → 连接 → 获取 NUS 服务 → 订阅 NUS TX Notify → 配置串口参数 → 开始收发数据

数据透传

写入 NUS RX 的字节原样写入 RS485 总线,无需帧头

串口配置

每次连接后需重新配置串口参数(波特率等)

MTU 限制

BLE 单次写入建议不超过 512字节(兼容性最佳),长数据需分包

协议处理

设备只负责字节透传,Modbus 等协议分帧由上位机处理

常见问题

ED-N2 二次开发需要懂硬件电路吗?

不需要。文档已封装底层硬件电路、接口驱动、供电适配与蓝牙协议栈,开发者只基于开放的软件接口与通讯协议即可开发。

支持哪些开发形态?

可开发手机端应用、微信小程序、物联网上位机、嵌入式应用、数据采集系统与设备管控平台,文档附微信小程序 BLE 连接与 RS485 透传的完整示例。

串口参数和蓝牙特征值在哪里查?

文档第 2、3 章分别给出 BLE 特征值与 RS485 串口参数定义,第 9 章附录汇总 UUID 与参数速查表,开发时可对照取值。