@esptool/mac 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +22 -8
  2. package/bin/cli.js +55 -120
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,22 +1,34 @@
1
1
  # esptool.mac(@esptool/mac)
2
2
 
3
- MacBook 用户专用的 ESP32 主机侧工具集,发布为 `@esptool/mac`。当前提供基于 `@stoprocent/noble` 的 BLE 扫描(经本机蓝牙适配器发现周边 BLE 广播设备)。
3
+ MacBook 用户专用的 ESP32 主机侧工具集,发布为 `@esptool/mac`。当前提供基于 `@stoprocent/noble` 的 BLE 扫描与 GATT 枚举。
4
4
 
5
5
  ## 使用(消费约定与 @esptool/cli 相同)
6
6
 
7
7
  ```bash
8
- npx -y @esptool/mac scan # 扫描 10s(⏳ 待发布,发布前用下面的本地方式)
8
+ npx -y @esptool/mac ble scan # 扫描周边 BLE 设备 10s
9
+ npx -y @esptool/mac ble info <id|name> # 连接设备并枚举 GATT 服务/特征
9
10
  ```
10
11
 
11
12
  本地开发:
12
13
 
13
14
  ```bash
14
15
  bun install
15
- bun bin/cli.js scan -t 8 # 指定时长;-t 0 持续扫描直到 Ctrl+C
16
- bun bin/cli.js scan -n S3 # 按设备名过滤
17
- bun bin/cli.js scan -u ffff # 按广播 service UUID 过滤(BluFi 设备指纹)
18
- bun bin/cli.js scan -o # 只显示本仓库设备(规则见 OUR_SIGNATURES)
19
- bun bin/cli.js scan -j # JSON 输出(不含表头与实时行,可直接管道给 jq/node)
16
+ bun bin/cli.js ble scan -t 8 # 指定时长;-t 0 持续扫描直到 Ctrl+C
17
+ bun bin/cli.js ble scan -n S3 # 按设备名过滤
18
+ bun bin/cli.js ble scan -u ffff # 按广播 service UUID 过滤(BluFi 设备指纹)
19
+ bun bin/cli.js ble scan -o # 只显示本仓库设备(规则见 OUR_SIGNATURES)
20
+ bun bin/cli.js ble scan -j # JSON 输出(不含表头与实时行,可直接管道给 jq/node)
21
+ bun bin/cli.js ble info 85B4MT # 连接指定设备(id 或名字),枚举服务与特征
22
+ ```
23
+
24
+ ## 结构
25
+
26
+ ```
27
+ bin/cli.js # 入口:命令树分发(<域> <命令>)
28
+ src/ble.js # noble 封装:适配器就绪等待、扫描聚合(scanDevices)
29
+ src/scan.js # ble scan:过滤、表格/JSON 输出
30
+ src/info.js # ble info:查找、连接、GATT 枚举
31
+ src/signatures.js # 自家设备识别签名(见 docs/2026-10-25-own-device-recognition.md)
20
32
  ```
21
33
 
22
34
  ## 说明
@@ -24,9 +36,11 @@ bun bin/cli.js scan -j # JSON 输出(不含表头与实时行,可直
24
36
  - 运行时兼容 node 与 bun:bin 入口 `#!/usr/bin/env node`(npx 场景),本地也可 `bun bin/cli.js` 直跑。
25
37
  - macOS 下设备 id 是 CoreBluetooth 生成的随机 UUID,非真实 MAC。
26
38
  - 依赖安装时 bun 会阻止 `bluetooth-hci-socket` 的 postinstall(Linux 原生模块),macOS 无需信任,不影响使用。
27
- - 发布:`bun run release`(npm version patch + bun publish)。
39
+ - 发布:`bun run release`(npm version patch + npm publish)。
28
40
 
29
41
  ## 文档
30
42
 
31
43
  - [BLE 扫描 PoC 工作报告](docs/2026-10-25-scan-poc-report.md)
32
44
  - [BLE 广播标识统一规范提案](docs/2026-10-25-ble-advertising-spec-proposal.md)
45
+ - [自家设备识别规则](docs/2026-10-25-own-device-recognition.md)
46
+ - [工具规划提案](docs/2026-10-25-esptool-mac-tooling-proposal.md)
package/bin/cli.js CHANGED
@@ -1,158 +1,93 @@
1
1
  #!/usr/bin/env node
2
- import noble from '@stoprocent/noble'
2
+ import { bleScan } from '../src/scan.js'
3
+ import { bleInfo } from '../src/info.js'
3
4
 
4
5
  const HELP = `
5
- @esptool/mac — MacBook 专用主机工具(BLE 扫描等)
6
+ @esptool/mac — MacBook 专用主机工具(BLE 扫描 / GATT 枚举等)
6
7
 
7
- 用法: npx @esptool/mac scan [选项]
8
- 或: bun bin/cli.js scan [选项]
8
+ 用法: npx -y @esptool/mac <域> <命令> [选项]
9
9
 
10
- 选项:
10
+ BLE 命令:
11
+ mac ble scan [选项] 扫描周边 BLE 广播设备
12
+ mac ble info <id|name> 连接设备并枚举 GATT 服务/特征
13
+
14
+ ble scan 选项:
11
15
  -t, --timeout <秒> 扫描时长 (默认 10, 0 = 持续扫描直到 Ctrl+C)
12
16
  -n, --name <关键字> 按设备名过滤 (大小写不敏感)
13
17
  -u, --service <uuid> 按广播 service UUID 过滤 (如 ffff, 忽略大小写与连字符)
14
- -o, --our 只显示本仓库设备 (BluFi ffff 服务, 或 KDB- 名字前缀)
18
+ -o, --our 只显示本仓库设备 (识别规则见 docs/2026-10-25-own-device-recognition.md)
15
19
  -d, --duplicates 响应重复广播 (实时刷新 RSSI)
16
20
  -j, --json 以 JSON 输出最终结果
17
- -h, --help 显示帮助
18
- `
19
-
20
- const normalizeUuid = (uuid) => String(uuid).replace(/-/g, '').toLowerCase()
21
21
 
22
- // 自家设备识别:命中任一签名即算。多条并存是为了覆盖规范落地前后各种形态,
23
- // 随命名规范推进逐步收紧(见 docs/2026-10-25-ble-advertising-spec-proposal.md §2/§3)。
24
- const OUR_SIGNATURES = [
25
- { service: 'ffff' }, // BluFi 配网固件
26
- { namePrefix: 'kdb-' }, // 完整格式名 KDB-<产品线>-<型号>-<标识>
27
- { namePattern: /^[a-z]{2}-[0-9a-f]{2}$/ }, // 短码 <产品线2>-<MAC末1字节hex2>,如 S3-1F
28
- ]
22
+ ble info 选项:
23
+ -t, --timeout <秒> 查找设备的最长等待 (默认 10)
29
24
 
30
- const isOurs = (name, services) => {
31
- const lowerName = name.toLowerCase()
32
- return OUR_SIGNATURES.some(
33
- (sig) =>
34
- (sig.service && services.some((s) => s === sig.service || s.endsWith(sig.service))) ||
35
- (sig.namePrefix && lowerName.startsWith(sig.namePrefix)) ||
36
- (sig.namePattern && sig.namePattern.test(lowerName))
37
- )
38
- }
25
+ 其他:
26
+ -h, --help 显示帮助
27
+ `
39
28
 
40
- const parseArgs = (argv) => {
29
+ const parseScanArgs = (argv) => {
41
30
  const opts = { timeout: 10, name: '', service: '', our: false, duplicates: false, json: false }
42
31
  for (let i = 0; i < argv.length; i++) {
43
32
  const a = argv[i]
44
33
  if (a === '-t' || a === '--timeout') opts.timeout = Number(argv[++i])
45
34
  else if (a === '-n' || a === '--name') opts.name = argv[++i]
46
- else if (a === '-u' || a === '--service') opts.service = normalizeUuid(argv[++i])
35
+ else if (a === '-u' || a === '--service') opts.service = argv[++i]
47
36
  else if (a === '-o' || a === '--our') opts.our = true
48
37
  else if (a === '-d' || a === '--duplicates') opts.duplicates = true
49
38
  else if (a === '-j' || a === '--json') opts.json = true
50
- else if (a === '-h' || a === '--help') {
51
- console.log(HELP)
52
- process.exit(0)
53
- } else {
54
- console.error(`未知参数: ${a}`)
55
- console.log(HELP)
56
- process.exit(1)
57
- }
39
+ else throw new Error(`未知参数: ${a}`)
58
40
  }
59
41
  return opts
60
42
  }
61
43
 
62
- const toHex = (buf) => (Buffer.isBuffer(buf) ? buf.toString('hex') : '')
63
-
64
- const toRecords = (devices) =>
65
- [...devices.values()].sort((x, y) => y.rssi - x.rssi)
66
-
67
- const printTable = (records, elapsed) => {
68
- console.log(`\n共发现 ${records.length} 台设备 (耗时 ${elapsed}s),按信号强度排序:\n`)
69
- const pad = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s.padEnd(n))
70
- console.log(` ${'RSSI'.padStart(8)} ${pad('UUID / ADDRESS', 38)}${pad('NAME', 24)}${pad('SERVICES', 10)}MFG DATA`)
71
- for (const d of records) {
72
- const services = d.services.join(',')
73
- console.log(
74
- ` ${`${d.rssi} dBm`.padStart(8)} ${pad(d.id, 38)}${pad(d.name || '(未命名)', 24)}${pad(services, 10)}${d.mfg}`
75
- )
44
+ const parseInfoArgs = (argv) => {
45
+ const opts = { target: '', timeout: 10 }
46
+ for (let i = 0; i < argv.length; i++) {
47
+ const a = argv[i]
48
+ if (a === '-t' || a === '--timeout') opts.timeout = Number(argv[++i])
49
+ else if (!a.startsWith('-')) opts.target = a
50
+ else throw new Error(`未知参数: ${a}`)
76
51
  }
52
+ if (!opts.target) throw new Error('缺少目标设备 (id 或设备名)')
53
+ return opts
77
54
  }
78
55
 
79
- const scan = async (opts) => {
80
- const devices = new Map()
81
- const startedAt = Date.now()
82
-
83
- noble.on('discover', (p) => {
84
- const name = p.advertisement?.localName || ''
85
- if (opts.name && !name.toLowerCase().includes(opts.name.toLowerCase())) return
86
-
87
- const services = (p.advertisement?.serviceUuids ?? []).map(normalizeUuid)
88
- if (opts.service && !services.some((s) => s === opts.service || s.endsWith(opts.service))) return
89
- if (opts.our && !isOurs(name, services)) return
90
-
91
- const prev = devices.get(p.id)
92
- if (prev) {
93
- prev.rssi = p.rssi
94
- prev.count += 1
95
- if (!prev.name && name) prev.name = name
96
- if (!prev.services.length && services.length) prev.services = services
97
- if (!opts.duplicates) return
98
- } else {
99
- devices.set(p.id, {
100
- id: p.id,
101
- name,
102
- rssi: p.rssi,
103
- connectable: p.connectable ?? null,
104
- mfg: toHex(p.advertisement?.manufacturerData),
105
- services,
106
- firstSeen: Date.now() - startedAt,
107
- count: 1,
108
- })
109
- if (!opts.json) {
110
- const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1)
111
- console.log(` [${elapsed.padStart(5)}s] ${String(p.rssi).padStart(4)} dBm ${p.id} ${name || '(未命名)'}`)
112
- }
56
+ const route = async ([domain, cmd, ...rest]) => {
57
+ if (domain === 'ble') {
58
+ if (cmd === 'scan') {
59
+ await bleScan(parseScanArgs(rest))
60
+ return true
113
61
  }
114
- })
115
-
116
- await new Promise((resolve, reject) => {
117
- const onState = (state) => (state === 'poweredOn' ? resolve() : undefined)
118
- noble.on('stateChange', onState)
119
- noble.on('warning', (w) => console.error(`[noble] ${w}`))
120
- if (noble.state === 'poweredOn') resolve()
121
- setTimeout(() => reject(new Error(`蓝牙适配器未就绪 (state: ${noble.state})`)), 5000)
122
- })
123
-
124
- if (!opts.json) {
125
- console.log(`开始扫描 (${opts.timeout === 0 ? '持续模式, Ctrl+C 结束' : `${opts.timeout}s`})…\n`)
126
- }
127
- await noble.startScanningAsync([], opts.duplicates)
128
-
129
- const finish = async () => {
130
- noble.removeAllListeners('discover')
131
- await noble.stopScanningAsync().catch(() => {})
132
- const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1)
133
- const records = toRecords(devices)
134
- if (opts.json) {
135
- console.log(JSON.stringify({ elapsed: Number(elapsed), count: records.length, devices: records }, null, 2))
136
- } else {
137
- printTable(records, elapsed)
62
+ if (cmd === 'info') {
63
+ await bleInfo(parseInfoArgs(rest))
64
+ return true
138
65
  }
139
- process.exit(0)
140
66
  }
141
-
142
- process.on('SIGINT', finish)
143
- if (opts.timeout > 0) setTimeout(finish, opts.timeout * 1000)
67
+ // 旧入口过渡: 单独的 scan 自动路由到 ble scan,下个版本移除
68
+ if (domain === 'scan') {
69
+ console.error('[提示] scan 已迁移为 ble scan,本次自动路由(旧入口将在下个版本移除)\n')
70
+ await bleScan(parseScanArgs([cmd, ...rest]))
71
+ return true
72
+ }
73
+ return false
144
74
  }
145
75
 
146
76
  const main = async () => {
147
- const [cmd, ...rest] = process.argv.slice(2)
148
- if (cmd !== 'scan') {
77
+ const argv = process.argv.slice(2)
78
+ if (argv[0] === '-h' || argv[0] === '--help' || !argv.length) {
149
79
  console.log(HELP)
150
- process.exit(cmd ? 1 : 0)
80
+ return
81
+ }
82
+ if (!(await route(argv))) {
83
+ console.log(HELP)
84
+ process.exit(1)
151
85
  }
152
- await scan(parseArgs(rest))
153
86
  }
154
87
 
155
- main().catch((err) => {
156
- console.error(`错误: ${err.message}`)
157
- process.exit(1)
158
- })
88
+ main()
89
+ .then(() => process.exit(0))
90
+ .catch((err) => {
91
+ console.error(`错误: ${err.message}`)
92
+ process.exit(1)
93
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esptool/mac",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "MacBook host-side tools for ESP32 workflow - BLE scanner and more",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,8 +11,8 @@
11
11
  "README.md"
12
12
  ],
13
13
  "scripts": {
14
- "scan": "bun bin/cli.js scan",
15
- "test": "node bin/cli.js scan -t 3",
14
+ "scan": "bun bin/cli.js ble scan",
15
+ "test": "node bin/cli.js ble scan -t 3",
16
16
  "release": "npm version patch && npm publish --access public"
17
17
  },
18
18
  "dependencies": {