@seeed-studio/solution-cli 0.1.0 → 0.3.0
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 +101 -48
- package/bin/cli.js +201 -4
- package/catalog/client-config.json +10 -0
- package/catalog/knowledge-core.json +9308 -0
- package/docs/cli-guide.md +264 -0
- package/lib/agent.js +6 -0
- package/lib/catalog-dto.js +69 -0
- package/lib/compatibility.js +312 -0
- package/lib/knowledge.js +516 -0
- package/package.json +5 -1
- package/skills/sensecraft/SKILL.md +30 -7
- package/skills/sensecraft/references/consult-and-select.md +120 -0
- package/skills/sensecraft/references/device-management.md +94 -0
- package/skills/sensecraft/references/hardware-bom.md +83 -0
- package/skills/sensecraft/references/secondary-development.md +87 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# SenseCraft Solution CLI 使用与集成全景指南
|
|
2
|
+
*(SenseCraft Solution CLI & AI Agent Skill Guide)*
|
|
3
|
+
|
|
4
|
+
> **定位**:本文档专门面向**方案架构师、现场应用工程师 (FAE)、嵌入式开发者以及 AI Agent (Claude Code / Codex / Antigravity / Work Buddy)**,系统性说明 `@seeed-studio/solution-cli` 的独特应用场景、安装流程、各功能指令的具体使用方式以及核心产出物。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 目录
|
|
9
|
+
|
|
10
|
+
1. [独特使用场景与核心价值](#1-独特使用场景与核心价值)
|
|
11
|
+
2. [安装与 Agent 技能挂载](#2-安装与-agent-技能挂载)
|
|
12
|
+
3. [核心功能、使用方法与具体产出](#3-核心功能使用方法与具体产出)
|
|
13
|
+
- [场景一:方案探索与工况诊断 (Hub Search)](#场景一方案探索与工况诊断-hub-search)
|
|
14
|
+
- [场景二:硬件产品规格与配件防呆评估 (Product & BOM)](#场景二硬件产品规格与配件防呆评估-product--bom)
|
|
15
|
+
- [场景三:设备状态感知与参数热修改 (Device Management)](#场景三设备状态感知与参数热修改-device-management)
|
|
16
|
+
- [场景四:自研模型替换与本地二次开发 (Secondary Dev)](#场景四自研模型替换与本地二次开发-secondary-dev)
|
|
17
|
+
4. [双轨运行机制:在线直连与离线托底](#4-双轨运行机制在线直连与离线托底)
|
|
18
|
+
5. [常见问题与环境自检 (Doctor)](#5-常见问题与环境自检-doctor)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. 独特使用场景与核心价值
|
|
23
|
+
|
|
24
|
+
在传统边缘物联网与 AI 项目落地过程中,开发者通常面临以下痛点:
|
|
25
|
+
- **方案选型盲目**:不清楚现场环境(集中还是分散、有无现有监控摄像头、有无市电)对硬件架构的致命影响;
|
|
26
|
+
- **配件采购踩坑**:工控机买回后发现电源电压不符(如 12V 误插 19V 算力板)、DC 插头尺寸不对(5.5x2.1mm 插不进 5.5x2.5mm)、M.2 接口插槽类型搞混(Key-M 固态插不进 Key-E Wi-Fi 槽);
|
|
27
|
+
- **参数变更成本高**:改个 MQTT Broker 地址或算法置信度阈值就需要重新编译镜像或重装整套系统;
|
|
28
|
+
- **AI Agent 缺乏边缘硬件感知**:纯大模型对话缺乏真实物理世界的接口约束,容易凭空编造不存在的硬件型号或非官方链接。
|
|
29
|
+
|
|
30
|
+
`@seeed-studio/solution-cli` 正是为解决这一系列问题而生,它具有三大独特角色:
|
|
31
|
+
1. **方案专家的现场诊断仪**:内置各大行业工况诊断知识库,不仅能搜方案,还能直接指导工程师向客户提出决定成本与物理拓扑的“黄金追问”;
|
|
32
|
+
2. **硬件采购防呆中枢**:内置物理规格匹配引擎,自动核算【必需配件】与【可选配件】,从源头阻断硬件返工;
|
|
33
|
+
3. **AI Agent 的物理世界触手**:一键将 SenseCraft Skill 注入到主流 AI Agent 宿主中,让 Agent 具备从方案咨询、BOM 选型到现场部署、参数热更的全生命周期驱动能力。
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. 安装与 Agent 技能挂载
|
|
38
|
+
|
|
39
|
+
### 2.1 一键免安装即时调用 (推荐)
|
|
40
|
+
无需全局安装,利用 `npx` 即可随时调用:
|
|
41
|
+
```bash
|
|
42
|
+
# 查看帮助与版本
|
|
43
|
+
npx @seeed-studio/solution-cli --help
|
|
44
|
+
|
|
45
|
+
# 交互式扫描并为本机所有 Agent 安装技能
|
|
46
|
+
npx @seeed-studio/solution-cli install
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2.2 全局安装
|
|
50
|
+
```bash
|
|
51
|
+
npm install -g @seeed-studio/solution-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2.3 跨 Agent 平台技能挂载
|
|
55
|
+
执行 `solution-cli install` 时,工具会自动检测本机存在的 Agent 宿主环境,并将 `sensecraft` Skill 和全套参考手册安全复制到对应目录:
|
|
56
|
+
|
|
57
|
+
| Agent 宿主平台 | 技能默认配置路径 | 支持能力 |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| **Claude Code** | `~/.claude/skills/sensecraft` | 完整方案咨询、工况诊断、BOM 生成、现场部署 |
|
|
60
|
+
| **Codex (OpenAI)** | `~/.agents/skills/sensecraft` 或 `~/.codex/skills/sensecraft` | 代码审查、参数重构、二次开发自动化 |
|
|
61
|
+
| **Google Antigravity (AGY)** | `~/.gemini/config/skills/sensecraft` | 端到端自主诊断、多 Agent 协同实施 |
|
|
62
|
+
| **Work Buddy / Workbody** | `~/.workbuddy/skills/sensecraft` | 桌面端交互、方案展示、一键部署联动 |
|
|
63
|
+
| **CodeBuddy** | `~/.codebuddy/skills/sensecraft` | 代码库配对开发与自研模型构建 |
|
|
64
|
+
|
|
65
|
+
**非交互式多选安装示例**:
|
|
66
|
+
```bash
|
|
67
|
+
# 同时为 Claude Code 和 Google Antigravity 部署技能,不弹窗确认
|
|
68
|
+
solution-cli install --agent claude,antigravity -y
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 3. 核心功能、使用方法与具体产出
|
|
74
|
+
|
|
75
|
+
### 场景一:方案探索与工况诊断 (Hub Search)
|
|
76
|
+
|
|
77
|
+
当工程师或客户提出模糊需求(如“想在农田做微气象监测”、“工厂产线想做划痕检测”)时使用。
|
|
78
|
+
|
|
79
|
+
#### 1. 执行命令
|
|
80
|
+
```bash
|
|
81
|
+
# 人类工程师阅读模式 (格式化终端高亮)
|
|
82
|
+
solution-cli hub search "农业"
|
|
83
|
+
|
|
84
|
+
# AI Agent 消费模式 (标准 JSON 格式的 Catalog DTO)
|
|
85
|
+
solution-cli hub search "跌倒检测" --json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### 2. 产出物内容 (What it Outputs)
|
|
89
|
+
运行该命令可产出 4 项关键资产:
|
|
90
|
+
1. **方案业务说明与技术架构**:解释为何适合在端侧运行、本地处理延迟、数据不出本地的隐私保障;
|
|
91
|
+
2. **静默商业闭环出口**:自动为该方案生成带有安全追踪标记(`utm_source=sensecraft_agent`)的官方落地页与开发 Wiki;
|
|
92
|
+
3. **官方开源仓库地址**:提供官方维护的 GitHub 源码仓库,为二次开发预留出口;
|
|
93
|
+
4. **现场工况诊断提问指引 (Diagnostic Playbook)**:直接给出当前场景下**必须向客户确认的 2~3 个核心工况问题**及其背后的技术决断理由。
|
|
94
|
+
|
|
95
|
+
#### 3. 产出示例 (以“农业”搜索为例)
|
|
96
|
+
```text
|
|
97
|
+
🔍 Solutions Hub Search Results for "农业" (typesense):
|
|
98
|
+
|
|
99
|
+
1. [Reference Design] 农业环境监测可部署方案 | LoRaWAN 自建看板 (agri_env_monitor)
|
|
100
|
+
把 SenseCAP 土壤与空气传感器接到 Home Assistant 看板上,支持本地私有 ChirpStack 运行,断网可用。
|
|
101
|
+
预设模式: cloud, tts_local, chirpstack_local
|
|
102
|
+
现场工况诊断提问指引:
|
|
103
|
+
• 监测区域是集中式连栋温室大棚(百米级),还是大面积户外大田/果园(数公里级)?
|
|
104
|
+
(决断考量: 大棚内百米级可采用 RS485 总线;广域大田推荐 LoRaWAN 远距无线,配 SenseCAP M2 网关)
|
|
105
|
+
• 现场测点是否有 220V 市电供电,还是需要野外太阳能板/锂电池长续航供电?
|
|
106
|
+
(决断考量: 市电支持持续高频数据上报;无市电需选配支持休眠唤醒的超低功耗无线传感器)
|
|
107
|
+
• 需要监测哪些核心指标?仅空气温湿度,还是包括土壤剖面水肥墒情(水分/EC/pH)?
|
|
108
|
+
(决断考量: 决定是配置多合一微气象站,还是选配工业级土壤三参数探头与变送器)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### 场景二:硬件产品规格与配件防呆评估 (Product & BOM)
|
|
114
|
+
|
|
115
|
+
当确定了方案,需要进入商务采购阶段、给客户出具设备清单时使用。
|
|
116
|
+
|
|
117
|
+
#### 1. 执行命令
|
|
118
|
+
```bash
|
|
119
|
+
# 1. 查询产品族详情与物理规格
|
|
120
|
+
solution-cli product query recomputer_j40 --json
|
|
121
|
+
|
|
122
|
+
# 2. 评估该产品族所需的官方配件与兼容性
|
|
123
|
+
solution-cli product accessories recomputer_j40 --json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### 2. 产出物内容 (What it Outputs)
|
|
127
|
+
1. **精确分类的 BOM 清单**:
|
|
128
|
+
- **【必需配件 (REQUIRED)】**:没有该配件主机无法开机或无法运行(如专属电源适配器),附带选型理由;
|
|
129
|
+
- **【可选配件 (OPTIONAL)】**:根据工况选择的外设(如 NVMe 固态硬盘、Wi-Fi 模组、导轨安装卡扣、射频天线);
|
|
130
|
+
2. **物理兼容性安全评级**:
|
|
131
|
+
- `VERIFIED`(通过):电压、引脚、尺寸完全匹配;
|
|
132
|
+
- `REQUIRES_ADAPTER`(需转接):如 M.2 2242 规格装入 2280 卡槽需加铜柱;
|
|
133
|
+
- `INCOMPATIBLE`(不兼容/危险):严禁搭配,防止烧毁板卡。
|
|
134
|
+
|
|
135
|
+
#### 3. 产出示例 (以 reComputer J40 为例)
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"family_id": "recomputer_j40",
|
|
139
|
+
"physical_spec": {
|
|
140
|
+
"power_input": {
|
|
141
|
+
"connector": "dc_jack_5.5x2.5",
|
|
142
|
+
"voltage_v": 19.0,
|
|
143
|
+
"min_current_a": 4.74,
|
|
144
|
+
"polarity": "center_positive"
|
|
145
|
+
},
|
|
146
|
+
"m2_slots": [
|
|
147
|
+
{"key": "M", "size": "2280", "interface": "PCIe Gen4 x4 NVMe"},
|
|
148
|
+
{"key": "E", "size": "2230", "interface": "WiFi/BT"}
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
"accessories": {
|
|
152
|
+
"required": [
|
|
153
|
+
{
|
|
154
|
+
"sku": "313080684",
|
|
155
|
+
"name": "19V 4.74A 电源适配器 (5.5x2.5mm)",
|
|
156
|
+
"category": "REQUIRED",
|
|
157
|
+
"compatibility": { "status": "VERIFIED" },
|
|
158
|
+
"url": "https://www.seeedstudio.com/Power-Adapter-19V-4-74A-p-5573.html?utm_source=sensecraft_agent..."
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
"optional": [
|
|
162
|
+
{
|
|
163
|
+
"sku": "110991734",
|
|
164
|
+
"name": "M.2 2280 NVMe SSD 512GB",
|
|
165
|
+
"category": "OPTIONAL",
|
|
166
|
+
"compatibility": { "status": "VERIFIED" }
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### 场景三:设备状态感知与参数热修改 (Device Management)
|
|
176
|
+
|
|
177
|
+
当方案已经在现场或测试架部署完毕,需要调整运行参数(如更换 MQTT Broker、调整告警置信度阈值)时使用。
|
|
178
|
+
|
|
179
|
+
#### 1. 核心流程与接口
|
|
180
|
+
系统支持**无需重新编译、无需重装整套系统**的零重启参数热修改:
|
|
181
|
+
1. **查询当前设备活跃部署**:
|
|
182
|
+
`GET http://<host>:5823/api/device-management/active`
|
|
183
|
+
2. **查询可修改参数**:
|
|
184
|
+
`GET http://<host>:5823/api/docker-devices/local/config/<solution_id>`
|
|
185
|
+
检查参数字段中的 `reconfigurable: true` 标记。
|
|
186
|
+
3. **提交热更新**:
|
|
187
|
+
`PUT http://<host>:5823/api/docker-devices/local/config/<solution_id>`
|
|
188
|
+
传入目标键值,容器环境无感热加载生效。
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### 场景四:自研模型替换与本地二次开发 (Secondary Dev)
|
|
193
|
+
|
|
194
|
+
当官方自带的模型无法满足客户特殊工况(如需要自研模型、特殊缺陷检测、修改前端大屏 UI)时使用。
|
|
195
|
+
|
|
196
|
+
#### 1. 开发闭环步骤
|
|
197
|
+
1. **克隆源码**:
|
|
198
|
+
从 `solution-cli hub search` 返回的 `repo_url` 将源码克隆到本地用户工作区:
|
|
199
|
+
```bash
|
|
200
|
+
git clone https://github.com/suharvest/sensecraft-solutions.git ./my-solutions
|
|
201
|
+
```
|
|
202
|
+
2. **端侧模型转换 (以 Jetson Orin 为例)**:
|
|
203
|
+
在设备上使用 TensorRT 将自定义 ONNX 导出为 Engine:
|
|
204
|
+
```bash
|
|
205
|
+
/usr/src/tensorrt/bin/trtexec --onnx=my_model.onnx --saveEngine=my_model.engine --fp16
|
|
206
|
+
```
|
|
207
|
+
3. **覆盖挂载配置**:
|
|
208
|
+
在方案目录创建 `docker-compose.override.yml` 将本地自研模型映射进容器中。
|
|
209
|
+
4. **驱动本地快速联调**:
|
|
210
|
+
使用 `--solutions-dir` 参数,强制底层引擎加载本地修改版方案,彻底不碰官方缓存:
|
|
211
|
+
```bash
|
|
212
|
+
sensecraft-engine deploy --solution fall_detection --solutions-dir ./my-solutions/solutions
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 4. 双轨运行机制:在线直连与离线托底
|
|
218
|
+
|
|
219
|
+
为了保证在各类严苛现场环境下均能稳定运行,CLI 实现了智能双轨架构:
|
|
220
|
+
|
|
221
|
+
```mermaid
|
|
222
|
+
flowchart TD
|
|
223
|
+
RunCmd["用户 / Agent 触发 CLI 命令"] --> NetCheck{"检查网络与凭证"}
|
|
224
|
+
NetCheck -- "有外网且含烘焙配置" --> Typesense["直连 Seeed Typesense 云端搜索集群"]
|
|
225
|
+
Typesense -- "返回最新上线方案与配件" --> Done["输出 Catalog DTO"]
|
|
226
|
+
NetCheck -- "纯内网 / 无凭证 / 超时" --> LocalCore["无感降级: 读取 catalog/knowledge-core.json"]
|
|
227
|
+
LocalCore -- "331 KB 本地知识库 (18场景/39方案/119硬件)" --> Done
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
1. **在线实时模式 (Online Live)**:
|
|
231
|
+
- 采用安全方案 B 在构建发布期烘焙了公开的只读 Search-Only 凭证;
|
|
232
|
+
- 自动直连 `https://rel-search.seeedstudio.com`,实时获取最新的方案、Wiki、案例及商城 SKU;
|
|
233
|
+
2. **离线安全托底 (Offline Resilience)**:
|
|
234
|
+
- 打包了全量预生成的 `catalog/knowledge-core.json`(331 KB);
|
|
235
|
+
- 在军工、内网、无公网或云端响应超过 2.5 秒时,**自动毫秒级无感降级到本地内核**,工况诊断、BOM 评估与离线部署 100% 正常可用。
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 5. 常见问题与环境自检 (Doctor)
|
|
240
|
+
|
|
241
|
+
### 1. 运行自检工具
|
|
242
|
+
在部署前或遇到环境异常时,执行:
|
|
243
|
+
```bash
|
|
244
|
+
solution-cli doctor
|
|
245
|
+
```
|
|
246
|
+
工具将逐项检测并给出修复指引:
|
|
247
|
+
- `OS Platform`: 确认宿主机操作系统及架构是否支持;
|
|
248
|
+
- `Docker Daemon`: 确认 Docker 服务是否正常运行;
|
|
249
|
+
- `Serial / Dialout Permissions`: 确认串口访问权限;
|
|
250
|
+
- `SenseCraft Engine`: 确认底座引擎完整性与版本;
|
|
251
|
+
- `Ledger Integrity`: 校验技能安装账本状态。
|
|
252
|
+
|
|
253
|
+
### 2. 状态查询
|
|
254
|
+
```bash
|
|
255
|
+
solution-cli status
|
|
256
|
+
```
|
|
257
|
+
实时查看当前 Engine 所在路径、安装版本、本地 Content 方案缓存数量以及各大 Agent 的配置状态。
|
|
258
|
+
|
|
259
|
+
### 3. 干净卸载
|
|
260
|
+
如需从某个 Agent 中移除 SenseCraft 技能:
|
|
261
|
+
```bash
|
|
262
|
+
solution-cli uninstall --agent claude,antigravity
|
|
263
|
+
```
|
|
264
|
+
工具将依据安装账本(`~/.sensecraft/skills-ledger.json`)仅删除工具自身写入的文件,绝不误删用户自定义创建的技能。
|
package/lib/agent.js
CHANGED
|
@@ -54,6 +54,12 @@ export function getKnownAgents() {
|
|
|
54
54
|
}
|
|
55
55
|
return path.join(home, '.codebuddy', 'skills');
|
|
56
56
|
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'antigravity',
|
|
60
|
+
name: 'Google Antigravity (AGY)',
|
|
61
|
+
homeDirs: [path.join(home, '.gemini', 'config'), path.join(home, '.gemini', 'antigravity-cli')],
|
|
62
|
+
getSkillsDir: () => path.join(home, '.gemini', 'config', 'skills')
|
|
57
63
|
}
|
|
58
64
|
];
|
|
59
65
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Catalog DTO definition and validator conforming to v2 schema.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const SCHEMA_VERSION = '2.0';
|
|
6
|
+
export const SCHEMA_URL = 'https://spec.seeedstudio.com/schemas/v2/catalog-dto.json';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a standardized Catalog DTO response.
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {'typesense' | 'bundled_manifest'} options.source
|
|
13
|
+
* @param {'FRESH' | 'STALE' | 'UNKNOWN'} [options.freshness='FRESH']
|
|
14
|
+
* @param {string} [options.datasetVersion='2026-10-13.1']
|
|
15
|
+
* @param {Array} options.records
|
|
16
|
+
* @param {Object} [options.fieldCompleteness]
|
|
17
|
+
* @param {Array<string>} [options.warnings=[]]
|
|
18
|
+
* @returns {Object} Validated Catalog DTO
|
|
19
|
+
*/
|
|
20
|
+
export function createCatalogDto({
|
|
21
|
+
source = 'bundled_manifest',
|
|
22
|
+
freshness = 'FRESH',
|
|
23
|
+
datasetVersion = '2026-10-13.1',
|
|
24
|
+
records = [],
|
|
25
|
+
fieldCompleteness = { spec: true, compatibility: true, offer: source === 'typesense' },
|
|
26
|
+
warnings = [],
|
|
27
|
+
}) {
|
|
28
|
+
return {
|
|
29
|
+
$schema: SCHEMA_URL,
|
|
30
|
+
schema_version: SCHEMA_VERSION,
|
|
31
|
+
dataset_version: datasetVersion,
|
|
32
|
+
generated_at: new Date().toISOString(),
|
|
33
|
+
source,
|
|
34
|
+
freshness,
|
|
35
|
+
field_completeness: fieldCompleteness,
|
|
36
|
+
record_count: Array.isArray(records) ? records.length : 0,
|
|
37
|
+
records: Array.isArray(records) ? records : [],
|
|
38
|
+
warnings: Array.isArray(warnings) ? warnings : [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validates whether an object complies with the v2 Catalog DTO structure.
|
|
44
|
+
*
|
|
45
|
+
* @param {any} dto
|
|
46
|
+
* @returns {{ valid: boolean, errors: Array<string> }}
|
|
47
|
+
*/
|
|
48
|
+
export function validateCatalogDto(dto) {
|
|
49
|
+
const errors = [];
|
|
50
|
+
if (!dto || typeof dto !== 'object') {
|
|
51
|
+
return { valid: false, errors: ['DTO must be an object'] };
|
|
52
|
+
}
|
|
53
|
+
if (dto.schema_version !== SCHEMA_VERSION) {
|
|
54
|
+
errors.push(`Invalid schema_version: expected ${SCHEMA_VERSION}, got ${dto.schema_version}`);
|
|
55
|
+
}
|
|
56
|
+
if (!['typesense', 'bundled_manifest'].includes(dto.source)) {
|
|
57
|
+
errors.push(`Invalid source: ${dto.source}`);
|
|
58
|
+
}
|
|
59
|
+
if (!['FRESH', 'STALE', 'UNKNOWN'].includes(dto.freshness)) {
|
|
60
|
+
errors.push(`Invalid freshness: ${dto.freshness}`);
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(dto.records)) {
|
|
63
|
+
errors.push('records must be an array');
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
valid: errors.length === 0,
|
|
67
|
+
errors,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Machine Compatibility Evaluator for IoT Hardware and Accessories.
|
|
3
|
+
*
|
|
4
|
+
* Implements machine-evaluable compatibility assertions across 6 physical dimensions:
|
|
5
|
+
* 1. power_supply: Voltage, tolerance, min current/watts, plug dimensions, polarity, USB-C PD profiles
|
|
6
|
+
* 2. rf_antenna: RF connector gender (N-Type, SMA, RP-SMA), impedance, frequency bands
|
|
7
|
+
* 3. m2_storage: Key-M vs Key-E, form factor (2242 vs 2280), PCIe/NVMe vs SATA
|
|
8
|
+
* 4. poe: 802.3af (15.4W) vs 802.3at (30W) vs 802.3bt
|
|
9
|
+
* 5. usb: Port generation, bandwidth, and host power output
|
|
10
|
+
* 6. os_firmware: Kernel / JetPack / firmware version constraints
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const COMPATIBILITY_STATUS = {
|
|
14
|
+
VERIFIED: 'VERIFIED',
|
|
15
|
+
REQUIRES_ADAPTER: 'REQUIRES_ADAPTER',
|
|
16
|
+
INCOMPATIBLE: 'INCOMPATIBLE',
|
|
17
|
+
UNKNOWN: 'UNKNOWN',
|
|
18
|
+
CONFLICT: 'CONFLICT',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Evaluates power supply compatibility between a host device and an adapter/cable.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} hostSpec - Host physical power requirements
|
|
25
|
+
* @param {Object} accessorySpec - Power accessory specifications
|
|
26
|
+
* @returns {{ status: string, dimension: string, reason: string, details: Object }}
|
|
27
|
+
*/
|
|
28
|
+
export function evaluatePowerCompatibility(hostSpec, accessorySpec) {
|
|
29
|
+
if (!hostSpec || !accessorySpec) {
|
|
30
|
+
return {
|
|
31
|
+
status: COMPATIBILITY_STATUS.UNKNOWN,
|
|
32
|
+
dimension: 'power_supply',
|
|
33
|
+
reason: 'Missing power specifications for host or accessory',
|
|
34
|
+
details: {},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 1. USB-C Power Delivery checks
|
|
39
|
+
if (hostSpec.connector === 'usb_c' || accessorySpec.connector === 'usb_c') {
|
|
40
|
+
if (hostSpec.connector !== accessorySpec.connector) {
|
|
41
|
+
return {
|
|
42
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
43
|
+
dimension: 'power_supply',
|
|
44
|
+
reason: `Connector mismatch: host requires ${hostSpec.connector}, accessory provides ${accessorySpec.connector}`,
|
|
45
|
+
details: { hostConnector: hostSpec.connector, accessoryConnector: accessorySpec.connector },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const hostMinWatts = hostSpec.min_watts || ((hostSpec.voltage_v || 5.0) * (hostSpec.min_current_a || 2.0));
|
|
49
|
+
const accWatts = accessorySpec.watts || ((accessorySpec.voltage_v || 5.0) * (accessorySpec.max_current_a || 3.0));
|
|
50
|
+
if (accWatts < hostMinWatts) {
|
|
51
|
+
return {
|
|
52
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
53
|
+
dimension: 'power_supply',
|
|
54
|
+
reason: `Insufficient wattage: host requires at least ${hostMinWatts}W, accessory provides ${accWatts}W`,
|
|
55
|
+
details: { requiredWatts: hostMinWatts, providedWatts: accWatts },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
status: COMPATIBILITY_STATUS.VERIFIED,
|
|
60
|
+
dimension: 'power_supply',
|
|
61
|
+
reason: `Verified USB-C power delivery: ${accWatts}W meets or exceeds required ${hostMinWatts}W`,
|
|
62
|
+
details: { requiredWatts: hostMinWatts, providedWatts: accWatts },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. DC Barrel Jack checks (Voltage, Current, Plug Dimensions, Polarity)
|
|
67
|
+
const hostV = hostSpec.voltage_v;
|
|
68
|
+
const accV = accessorySpec.voltage_v;
|
|
69
|
+
if (hostV && accV) {
|
|
70
|
+
const tolerance = hostSpec.tolerance_percent || 5.0;
|
|
71
|
+
const diffPercent = Math.abs(hostV - accV) / hostV * 100;
|
|
72
|
+
if (diffPercent > tolerance) {
|
|
73
|
+
return {
|
|
74
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
75
|
+
dimension: 'power_supply',
|
|
76
|
+
reason: `Voltage mismatch: host requires ${hostV}V (±${tolerance}%), accessory supplies ${accV}V`,
|
|
77
|
+
details: { requiredVoltage: hostV, suppliedVoltage: accV },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (hostSpec.min_current_a && accessorySpec.max_current_a) {
|
|
83
|
+
if (accessorySpec.max_current_a < hostSpec.min_current_a) {
|
|
84
|
+
return {
|
|
85
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
86
|
+
dimension: 'power_supply',
|
|
87
|
+
reason: `Current capacity too low: host requires at least ${hostSpec.min_current_a}A, adapter rated for ${accessorySpec.max_current_a}A`,
|
|
88
|
+
details: { requiredCurrent: hostSpec.min_current_a, providedCurrent: accessorySpec.max_current_a },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Plug dimension check (e.g. 5.5x2.5mm vs 5.5x2.1mm)
|
|
94
|
+
if (hostSpec.connector && accessorySpec.connector) {
|
|
95
|
+
if (hostSpec.connector !== accessorySpec.connector) {
|
|
96
|
+
return {
|
|
97
|
+
status: COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
|
|
98
|
+
dimension: 'power_supply',
|
|
99
|
+
reason: `DC Barrel plug mismatch: host requires ${hostSpec.connector}, accessory has ${accessorySpec.connector}. Requires a barrel converter adapter.`,
|
|
100
|
+
details: { hostConnector: hostSpec.connector, accessoryConnector: accessorySpec.connector },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Polarity check (center_positive vs center_negative)
|
|
106
|
+
if (hostSpec.polarity && accessorySpec.polarity) {
|
|
107
|
+
if (hostSpec.polarity !== accessorySpec.polarity) {
|
|
108
|
+
return {
|
|
109
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
110
|
+
dimension: 'power_supply',
|
|
111
|
+
reason: `Polarity inversion danger: host requires ${hostSpec.polarity}, accessory is ${accessorySpec.polarity}`,
|
|
112
|
+
details: { hostPolarity: hostSpec.polarity, accessoryPolarity: accessorySpec.polarity },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
status: COMPATIBILITY_STATUS.VERIFIED,
|
|
119
|
+
dimension: 'power_supply',
|
|
120
|
+
reason: `Power supply verified: ${accV || hostV}V @ ${accessorySpec.max_current_a || hostSpec.min_current_a}A matches requirements`,
|
|
121
|
+
details: { voltage: accV || hostV, current: accessorySpec.max_current_a || hostSpec.min_current_a },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Evaluates RF Antenna compatibility between a host device and an antenna.
|
|
127
|
+
*
|
|
128
|
+
* @param {Object} hostSpec
|
|
129
|
+
* @param {Object} antennaSpec
|
|
130
|
+
* @returns {{ status: string, dimension: string, reason: string, details: Object }}
|
|
131
|
+
*/
|
|
132
|
+
export function evaluateRfCompatibility(hostSpec, antennaSpec) {
|
|
133
|
+
if (!hostSpec?.rf_antenna || !antennaSpec) {
|
|
134
|
+
return {
|
|
135
|
+
status: COMPATIBILITY_STATUS.UNKNOWN,
|
|
136
|
+
dimension: 'rf_antenna',
|
|
137
|
+
reason: 'Missing RF or antenna specifications',
|
|
138
|
+
details: {},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const hostRf = hostSpec.rf_antenna;
|
|
143
|
+
const hostConn = (hostRf.connector || '').toLowerCase();
|
|
144
|
+
const antConn = (antennaSpec.connector || '').toLowerCase();
|
|
145
|
+
|
|
146
|
+
// 1. Connector compatibility
|
|
147
|
+
if (hostConn.includes('n-type female')) {
|
|
148
|
+
if (antConn.includes('n-type male') || antConn.includes('n-male')) {
|
|
149
|
+
// Direct mate
|
|
150
|
+
} else if (antConn.includes('sma')) {
|
|
151
|
+
return {
|
|
152
|
+
status: COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
|
|
153
|
+
dimension: 'rf_antenna',
|
|
154
|
+
reason: 'Host has N-Type Female connector, antenna has SMA connector. Requires N-Male to SMA pigtail cable / adapter.',
|
|
155
|
+
details: { hostConnector: hostRf.connector, antennaConnector: antennaSpec.connector },
|
|
156
|
+
};
|
|
157
|
+
} else if (!antConn) {
|
|
158
|
+
return {
|
|
159
|
+
status: COMPATIBILITY_STATUS.UNKNOWN,
|
|
160
|
+
dimension: 'rf_antenna',
|
|
161
|
+
reason: 'Antenna connector type not specified',
|
|
162
|
+
details: { hostConnector: hostRf.connector },
|
|
163
|
+
};
|
|
164
|
+
} else {
|
|
165
|
+
return {
|
|
166
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
167
|
+
dimension: 'rf_antenna',
|
|
168
|
+
reason: `Incompatible antenna connector: host has ${hostRf.connector}, antenna has ${antennaSpec.connector}`,
|
|
169
|
+
details: { hostConnector: hostRf.connector, antennaConnector: antennaSpec.connector },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 2. Frequency band check (e.g. EU868, US915, 2.4GHz)
|
|
175
|
+
if (hostRf.frequency_bands && antennaSpec.frequency_band) {
|
|
176
|
+
const antBand = antennaSpec.frequency_band.toUpperCase();
|
|
177
|
+
const match = hostRf.frequency_bands.some((b) => b.toUpperCase().includes(antBand) || antBand.includes(b.toUpperCase()));
|
|
178
|
+
if (!match) {
|
|
179
|
+
return {
|
|
180
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
181
|
+
dimension: 'rf_antenna',
|
|
182
|
+
reason: `Frequency band mismatch: host operating bands [${hostRf.frequency_bands.join(', ')}], antenna tuned for ${antennaSpec.frequency_band}`,
|
|
183
|
+
details: { supportedBands: hostRf.frequency_bands, antennaBand: antennaSpec.frequency_band },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
status: COMPATIBILITY_STATUS.VERIFIED,
|
|
190
|
+
dimension: 'rf_antenna',
|
|
191
|
+
reason: `Antenna verified: connector ${antennaSpec.connector || hostConn} and frequency band match host`,
|
|
192
|
+
details: { connector: antennaSpec.connector || hostConn },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Evaluates M.2 expansion slot compatibility (Key-M vs Key-E, form factor 2280 vs 2242).
|
|
198
|
+
*
|
|
199
|
+
* @param {Object} hostSpec
|
|
200
|
+
* @param {Object} moduleSpec
|
|
201
|
+
* @returns {{ status: string, dimension: string, reason: string, details: Object }}
|
|
202
|
+
*/
|
|
203
|
+
export function evaluateM2Compatibility(hostSpec, moduleSpec) {
|
|
204
|
+
if (!hostSpec?.m2_slots || !moduleSpec) {
|
|
205
|
+
return {
|
|
206
|
+
status: COMPATIBILITY_STATUS.UNKNOWN,
|
|
207
|
+
dimension: 'm2_storage',
|
|
208
|
+
reason: 'No M.2 slot or module specification available',
|
|
209
|
+
details: {},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const modKey = (moduleSpec.key || '').toUpperCase(); // 'M' or 'E'
|
|
214
|
+
const modSize = moduleSpec.size || '2280';
|
|
215
|
+
|
|
216
|
+
const matchingSlot = hostSpec.m2_slots.find((slot) => {
|
|
217
|
+
return slot.key.toUpperCase() === modKey;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (!matchingSlot) {
|
|
221
|
+
return {
|
|
222
|
+
status: COMPATIBILITY_STATUS.INCOMPATIBLE,
|
|
223
|
+
dimension: 'm2_storage',
|
|
224
|
+
reason: `M.2 Key mismatch: module requires Key-${modKey}, but host only provides [${hostSpec.m2_slots.map((s) => 'Key-' + s.key).join(', ')}]`,
|
|
225
|
+
details: { requiredKey: modKey, availableSlots: hostSpec.m2_slots },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (matchingSlot.size && matchingSlot.size !== modSize) {
|
|
230
|
+
return {
|
|
231
|
+
status: COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
|
|
232
|
+
dimension: 'm2_storage',
|
|
233
|
+
reason: `Form factor mismatch: module is ${modSize}, slot designed for ${matchingSlot.size}. May require mounting bracket / standoff.`,
|
|
234
|
+
details: { moduleSize: modSize, slotSize: matchingSlot.size },
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
status: COMPATIBILITY_STATUS.VERIFIED,
|
|
240
|
+
dimension: 'm2_storage',
|
|
241
|
+
reason: `M.2 slot verified: Key-${modKey} (${modSize}) matches slot ${matchingSlot.key}`,
|
|
242
|
+
details: { key: modKey, size: modSize, interface: matchingSlot.interface },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Master compatibility assertion runner evaluating all relevant physical dimensions.
|
|
248
|
+
*
|
|
249
|
+
* @param {Object} hostPhysicalSpec - Host physical specifications
|
|
250
|
+
* @param {Object} accessoryItem - Accessory object with specs and kind
|
|
251
|
+
* @returns {{ isCompatible: boolean, status: string, dimensions: Array<Object> }}
|
|
252
|
+
*/
|
|
253
|
+
export function assertCompatibility(hostPhysicalSpec, accessoryItem) {
|
|
254
|
+
if (!hostPhysicalSpec || !accessoryItem) {
|
|
255
|
+
return {
|
|
256
|
+
isCompatible: false,
|
|
257
|
+
status: COMPATIBILITY_STATUS.UNKNOWN,
|
|
258
|
+
dimensions: [{ dimension: 'general', status: COMPATIBILITY_STATUS.UNKNOWN, reason: 'Insufficient specification data' }],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const evaluations = [];
|
|
263
|
+
|
|
264
|
+
// Check Power
|
|
265
|
+
if (accessoryItem.power_spec || accessoryItem.type?.includes('power') || accessoryItem.id?.includes('power')) {
|
|
266
|
+
evaluations.push(evaluatePowerCompatibility(hostPhysicalSpec.power_input, accessoryItem.power_spec || {
|
|
267
|
+
connector: hostPhysicalSpec.power_input?.connector,
|
|
268
|
+
voltage_v: hostPhysicalSpec.power_input?.voltage_v,
|
|
269
|
+
max_current_a: hostPhysicalSpec.power_input?.min_current_a,
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check RF / Antenna
|
|
274
|
+
if (accessoryItem.rf_spec || accessoryItem.type?.includes('antenna') || accessoryItem.id?.includes('antenna')) {
|
|
275
|
+
evaluations.push(evaluateRfCompatibility(hostPhysicalSpec, accessoryItem.rf_spec || {
|
|
276
|
+
connector: hostPhysicalSpec.rf_antenna?.required_antenna_connector || 'SMA-J (Male)',
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check M.2 Storage / Wireless
|
|
281
|
+
if (accessoryItem.m2_spec || accessoryItem.type?.includes('ssd') || accessoryItem.type?.includes('m2')) {
|
|
282
|
+
evaluations.push(evaluateM2Compatibility(hostPhysicalSpec, accessoryItem.m2_spec || {
|
|
283
|
+
key: 'M',
|
|
284
|
+
size: '2280',
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (evaluations.length === 0) {
|
|
289
|
+
return {
|
|
290
|
+
isCompatible: true,
|
|
291
|
+
status: COMPATIBILITY_STATUS.VERIFIED,
|
|
292
|
+
dimensions: [{ dimension: 'generic', status: COMPATIBILITY_STATUS.VERIFIED, reason: 'General peripheral without physical slot constraint' }],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const hasIncompatible = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.INCOMPATIBLE);
|
|
297
|
+
const hasConflict = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.CONFLICT);
|
|
298
|
+
const hasAdapter = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.REQUIRES_ADAPTER);
|
|
299
|
+
const hasUnknown = evaluations.some((e) => e.status === COMPATIBILITY_STATUS.UNKNOWN);
|
|
300
|
+
|
|
301
|
+
let overallStatus = COMPATIBILITY_STATUS.VERIFIED;
|
|
302
|
+
if (hasIncompatible) overallStatus = COMPATIBILITY_STATUS.INCOMPATIBLE;
|
|
303
|
+
else if (hasConflict) overallStatus = COMPATIBILITY_STATUS.CONFLICT;
|
|
304
|
+
else if (hasAdapter) overallStatus = COMPATIBILITY_STATUS.REQUIRES_ADAPTER;
|
|
305
|
+
else if (hasUnknown) overallStatus = COMPATIBILITY_STATUS.UNKNOWN;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
isCompatible: overallStatus === COMPATIBILITY_STATUS.VERIFIED || overallStatus === COMPATIBILITY_STATUS.REQUIRES_ADAPTER,
|
|
309
|
+
status: overallStatus,
|
|
310
|
+
dimensions: evaluations,
|
|
311
|
+
};
|
|
312
|
+
}
|