@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.
- package/README.md +22 -8
- package/bin/cli.js +55 -120
- 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
|
|
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
|
|
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 +
|
|
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
|
|
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
|
|
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 只显示本仓库设备 (
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
|
148
|
-
if (
|
|
77
|
+
const argv = process.argv.slice(2)
|
|
78
|
+
if (argv[0] === '-h' || argv[0] === '--help' || !argv.length) {
|
|
149
79
|
console.log(HELP)
|
|
150
|
-
|
|
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()
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
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": {
|