Skip to content

Commit dbff3fe

Browse files
committed
2 parents 43aa43b + ae563ab commit dbff3fe

9 files changed

Lines changed: 2201 additions & 0 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 934 additions & 0 deletions
Large diffs are not rendered by default.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ Thumbs.db
66
.temp/
77
.idea/
88
.vscode/
9+
libraries-linux.json
10+
libraries-linux-ai.json
11+
libraries-linux-index.json
12+
package-folder-map-linux.json
13+
*/info.json

scripts/generate-info.js

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
'use strict';
2+
3+
const fs = require('node:fs');
4+
const path = require('node:path');
5+
const {
6+
ROOT,
7+
compareStrings,
8+
getPublishablePackages,
9+
isCoreDirectory,
10+
readJson,
11+
writeJson,
12+
} = require('./genjson');
13+
14+
const CATALOG_FILE = path.join('catalog', 'python-libraries.json');
15+
const TAG_CATEGORIES = {
16+
audio: 'audio',
17+
camera: 'vision-ai',
18+
clock: 'system',
19+
communication: 'network',
20+
core: 'core',
21+
data: 'data-science',
22+
'data-processing': 'data-science',
23+
display: 'display',
24+
io: 'hardware-io',
25+
sensor: 'sensors',
26+
storage: 'system',
27+
system: 'system',
28+
};
29+
30+
// 型号优先,避免包含温度等辅助属性的复合传感器被归成单一类型。
31+
const EXACT_HARDWARE_TYPES = {
32+
ads1x15: ['adc'],
33+
ahtx0: ['temperature', 'humidity'],
34+
apds9960: ['gesture', 'color', 'proximity'],
35+
bme280: ['temperature', 'humidity', 'pressure'],
36+
bme680: ['temperature', 'humidity', 'pressure', 'gas'],
37+
bmp280: ['temperature', 'pressure'],
38+
bno055: ['imu', 'accelerometer', 'gyroscope', 'magnetometer'],
39+
ccs811: ['gas'],
40+
dht: ['temperature', 'humidity'],
41+
ds18b20: ['temperature'],
42+
ds3231: ['rtc'],
43+
ina219: ['power'],
44+
lis3dh: ['accelerometer'],
45+
mcp3xxx: ['adc'],
46+
mlx90614: ['temperature'],
47+
mpu6050: ['imu', 'accelerometer', 'gyroscope'],
48+
neopixel: ['led'],
49+
pca9685: ['servo'],
50+
pn532: ['rfid'],
51+
scd4x: ['temperature', 'humidity', 'gas'],
52+
sgp30: ['gas'],
53+
tsl2591: ['light'],
54+
vl53l0x: ['distance'],
55+
vl53l1x: ['distance'],
56+
w1thermsensor: ['temperature'],
57+
};
58+
59+
const HARDWARE_TYPE_KEYWORDS = {
60+
imu: ['imu', '惯性', '6-axis', '9-axis', '六轴', '九轴'],
61+
accelerometer: ['accelerometer', 'accel', '加速度'],
62+
gyroscope: ['gyroscope', 'gyro', '陀螺仪'],
63+
magnetometer: ['magnetometer', 'compass', '磁力计', '指南针'],
64+
temperature: ['temperature sensor', 'thermometer', '温度传感器'],
65+
humidity: ['humidity sensor', '湿度传感器'],
66+
pressure: ['pressure sensor', 'barometer', '气压传感器', '压力传感器'],
67+
gas: ['gas sensor', 'air quality', 'co2 sensor', 'voc sensor', '气体传感器', '空气质量'],
68+
distance: ['distance sensor', 'ranging', '距离传感器', '测距', 'tof'],
69+
ultrasonic: ['ultrasonic', '超声波'],
70+
proximity: ['proximity sensor', '接近传感器'],
71+
light: ['light sensor', 'lux', 'ambient light', '光线传感器'],
72+
color: ['color sensor', 'rgb sensor', '颜色传感器'],
73+
gesture: ['gesture sensor', '手势传感器'],
74+
touch: ['touch sensor', 'capacitive touch', '触摸传感器'],
75+
oled: ['oled'],
76+
lcd: ['lcd', 'character display', '液晶'],
77+
tft: ['tft display', '彩屏'],
78+
led: ['led strip', 'neopixel', '灯带', '灯珠'],
79+
'led-matrix': ['led matrix', '点阵'],
80+
servo: ['servo', '舵机'],
81+
stepper: ['stepper', '步进'],
82+
'dc-motor': ['dc motor', 'motor driver', 'motorkit', '直流电机'],
83+
relay: ['relay', '继电器'],
84+
buzzer: ['buzzer', '蜂鸣器'],
85+
rfid: ['rfid reader', 'nfc reader'],
86+
gps: ['gps module', 'gnss module', '定位模块'],
87+
rtc: ['rtc module', 'real-time clock', 'real time clock', '实时时钟'],
88+
camera: ['camera', '摄像头'],
89+
microphone: ['microphone', '麦克风'],
90+
speaker: ['speaker', '扬声器'],
91+
'sd-card': ['sd card', 'microsd', 'tf card'],
92+
power: ['power monitor', '电源监测'],
93+
adc: ['analog-to-digital', 'analog to digital', 'adc'],
94+
};
95+
96+
const COMMUNICATION_KEYWORDS = {
97+
i2c: ['i2c', 'iic', 'twi'],
98+
spi: ['spi'],
99+
uart: ['uart', 'serial port', 'serial-port', 'pyserial', 'lib-serial', 'minimalmodbus', 'python-periphery', '串口'],
100+
onewire: ['onewire', 'one-wire', 'one wire', '1-wire', '单总线'],
101+
gpio: ['gpio', 'digital pin'],
102+
pwm: ['pwm'],
103+
analog: ['analog', 'adc', 'iio'],
104+
wifi: ['wifi', 'http', 'httpx', 'aiohttp', 'mqtt', 'websocket', 'websockets', 'tcp', 'udp', 'coap', 'aiocoap'],
105+
ble: ['ble', 'bluetooth'],
106+
can: ['canbus', 'can bus', 'socketcan', 'python-can', 'cantools'],
107+
usb: ['usb', 'hidapi', 'pyftdi', 'pyusb'],
108+
};
109+
110+
function uniqueStrings(values, fieldName) {
111+
if (!Array.isArray(values)) {
112+
throw new Error(`${fieldName} 必须是数组`);
113+
}
114+
if (values.some((value) => typeof value !== 'string' || value === '')) {
115+
throw new Error(`${fieldName} 必须只包含非空字符串`);
116+
}
117+
return [...new Set(values)];
118+
}
119+
120+
function loadCatalog(rootDir = ROOT) {
121+
const catalogPath = path.join(rootDir, CATALOG_FILE);
122+
const catalog = readJson(catalogPath);
123+
if (!Array.isArray(catalog.libraries)) {
124+
throw new Error(`${catalogPath} 缺少 libraries 数组`);
125+
}
126+
127+
const entries = new Map();
128+
for (const entry of catalog.libraries) {
129+
if (!entry || typeof entry.id !== 'string' || entry.id === '') {
130+
throw new Error(`${catalogPath} 包含无效的 library.id`);
131+
}
132+
if (entries.has(entry.id)) {
133+
throw new Error(`${catalogPath} 包含重复 library.id: ${entry.id}`);
134+
}
135+
entries.set(entry.id, entry);
136+
}
137+
return entries;
138+
}
139+
140+
function fallbackCategory(folderName, packageJson) {
141+
if (isCoreDirectory(folderName)) {
142+
return 'core';
143+
}
144+
for (const tag of packageJson.tags || []) {
145+
if (TAG_CATEGORIES[tag]) {
146+
return TAG_CATEGORIES[tag];
147+
}
148+
}
149+
return 'utility';
150+
}
151+
152+
function matchKeyword(text, keyword) {
153+
const lowerText = text.toLowerCase();
154+
const lowerKeyword = keyword.toLowerCase();
155+
if (lowerKeyword.length > 3) {
156+
return lowerText.includes(lowerKeyword);
157+
}
158+
159+
const escapedKeyword = lowerKeyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
160+
return new RegExp(`(?:^|[^a-z0-9])${escapedKeyword}(?:[^a-z0-9]|$)`, 'i').test(lowerText);
161+
}
162+
163+
function matchCommunicationKeyword(text, keyword) {
164+
const escapedKeyword = keyword.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
165+
return new RegExp(`(?:^|[^a-z0-9])${escapedKeyword}(?:[^a-z0-9]|$)`, 'i').test(text);
166+
}
167+
168+
function detectHardwareType(text, folderName) {
169+
const lowerText = text.toLowerCase();
170+
const lowerName = folderName.toLowerCase();
171+
const hardwareTypes = new Set();
172+
173+
for (const [model, types] of Object.entries(EXACT_HARDWARE_TYPES)) {
174+
if (lowerName.includes(model) || lowerText.includes(model)) {
175+
types.forEach((type) => hardwareTypes.add(type));
176+
}
177+
}
178+
if (hardwareTypes.size > 0) {
179+
return [...hardwareTypes];
180+
}
181+
182+
for (const [type, keywords] of Object.entries(HARDWARE_TYPE_KEYWORDS)) {
183+
if (keywords.some((keyword) => matchKeyword(lowerText, keyword))) {
184+
hardwareTypes.add(type);
185+
}
186+
}
187+
return [...hardwareTypes];
188+
}
189+
190+
function detectCommunication(text) {
191+
const communications = [];
192+
for (const [communication, keywords] of Object.entries(COMMUNICATION_KEYWORDS)) {
193+
if (keywords.some((keyword) => matchCommunicationKeyword(text, keyword))) {
194+
communications.push(communication);
195+
}
196+
}
197+
return communications;
198+
}
199+
200+
function buildInfo(folderName, packageJson, catalogEntry) {
201+
const supportedCores = uniqueStrings(
202+
packageJson.compatibility && packageJson.compatibility.type,
203+
`${folderName}.compatibility.type`,
204+
);
205+
if (supportedCores.length === 0) {
206+
throw new Error(`${folderName}.compatibility.type 不能为空`);
207+
}
208+
209+
const voltage = packageJson.compatibility.voltage === undefined
210+
? []
211+
: packageJson.compatibility.voltage;
212+
if (!Array.isArray(voltage)) {
213+
throw new Error(`${folderName}.compatibility.voltage 必须是数组`);
214+
}
215+
216+
const tags = uniqueStrings(packageJson.tags || [], `${folderName}.tags`)
217+
.sort(compareStrings);
218+
const metadataText = [
219+
folderName,
220+
packageJson.name,
221+
packageJson.nickname || '',
222+
packageJson.description || '',
223+
packageJson.description_en || '',
224+
...(packageJson.keywords || []),
225+
...tags,
226+
catalogEntry && catalogEntry.module ? catalogEntry.module : '',
227+
...(catalogEntry && Array.isArray(catalogEntry.callables) ? catalogEntry.callables : []),
228+
...(catalogEntry && Array.isArray(catalogEntry.methods) ? catalogEntry.methods : []),
229+
...(catalogEntry && Array.isArray(catalogEntry.attributes) ? catalogEntry.attributes : []),
230+
].join(' ');
231+
232+
return {
233+
$schema: 'library-info-schema',
234+
name: packageJson.name.replace('@aily-project/', ''),
235+
displayName: packageJson.nickname || packageJson.name.replace('@aily-project/', ''),
236+
category: catalogEntry && catalogEntry.category
237+
? catalogEntry.category
238+
: fallbackCategory(folderName, packageJson),
239+
subcategory: '',
240+
supportedCores,
241+
communication: detectCommunication(metadataText),
242+
voltage: [...new Set(voltage)],
243+
functions: [],
244+
hardwareType: detectHardwareType(metadataText, folderName),
245+
tags,
246+
};
247+
}
248+
249+
function parseArguments(args) {
250+
const regenerateAll = args.includes('--all');
251+
const dryRun = args.includes('--dry-run');
252+
const unknownOption = args.find((argument) => argument.startsWith('--')
253+
&& argument !== '--all'
254+
&& argument !== '--dry-run');
255+
if (unknownOption) {
256+
throw new Error(`未知参数: ${unknownOption}`);
257+
}
258+
259+
const libraries = args.filter((argument) => !argument.startsWith('--'));
260+
if (libraries.length > 1) {
261+
throw new Error('一次只能指定一个库目录');
262+
}
263+
return { regenerateAll, dryRun, specificLibrary: libraries[0] };
264+
}
265+
266+
function generateInfoFiles(rootDir = ROOT, options = {}) {
267+
const catalog = loadCatalog(rootDir);
268+
const allPackages = getPublishablePackages(rootDir);
269+
const packages = options.specificLibrary
270+
? allPackages.filter(({ folderName }) => folderName === options.specificLibrary)
271+
: allPackages;
272+
273+
if (options.specificLibrary && packages.length === 0) {
274+
throw new Error(`没有找到可发布库目录: ${options.specificLibrary}`);
275+
}
276+
277+
let written = 0;
278+
let skipped = 0;
279+
for (const { folderName, packageJson } of packages) {
280+
const infoPath = path.join(rootDir, folderName, 'info.json');
281+
const info = buildInfo(folderName, packageJson, catalog.get(folderName));
282+
if (options.dryRun) {
283+
continue;
284+
}
285+
if (!options.regenerateAll && fs.existsSync(infoPath)) {
286+
skipped += 1;
287+
continue;
288+
}
289+
writeJson(infoPath, info, 2);
290+
written += 1;
291+
}
292+
293+
if (packages.length !== (options.dryRun ? packages.length : written + skipped)) {
294+
throw new Error(`info.json 处理数量与可发布包数量不一致`);
295+
}
296+
return { count: packages.length, skipped, written };
297+
}
298+
299+
function main() {
300+
const options = parseArguments(process.argv.slice(2));
301+
const result = generateInfoFiles(ROOT, options);
302+
console.log(`info.json 处理完成:库 ${result.count},写入 ${result.written},跳过 ${result.skipped}`);
303+
}
304+
305+
if (require.main === module) {
306+
try {
307+
main();
308+
} catch (error) {
309+
console.error(error.message);
310+
process.exitCode = 1;
311+
}
312+
}
313+
314+
module.exports = {
315+
buildInfo,
316+
detectCommunication,
317+
detectHardwareType,
318+
generateInfoFiles,
319+
loadCatalog,
320+
parseArguments,
321+
};

0 commit comments

Comments
 (0)