@springfield/ham-radio-registry 1.0.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/CHANGELOG.md +19 -0
- package/README.md +93 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/registry/config-registry.d.ts +51 -0
- package/dist/registry/config-registry.d.ts.map +1 -0
- package/dist/registry/config-registry.js +280 -0
- package/dist/registry/config-registry.js.map +1 -0
- package/dist/registry/shared-components.d.ts +14 -0
- package/dist/registry/shared-components.d.ts.map +1 -0
- package/dist/registry/shared-components.js +68 -0
- package/dist/registry/shared-components.js.map +1 -0
- package/dist/types/plugin-module.d.ts +90 -0
- package/dist/types/plugin-module.d.ts.map +1 -0
- package/dist/types/plugin-module.js +2 -0
- package/dist/types/plugin-module.js.map +1 -0
- package/dist/types/radio-config.d.ts +40 -0
- package/dist/types/radio-config.d.ts.map +1 -0
- package/dist/types/radio-config.js +2 -0
- package/dist/types/radio-config.js.map +1 -0
- package/dist/utils/npm-client.d.ts +27 -0
- package/dist/utils/npm-client.d.ts.map +1 -0
- package/dist/utils/npm-client.js +111 -0
- package/dist/utils/npm-client.js.map +1 -0
- package/package.json +73 -0
- package/src/index.ts +37 -0
- package/src/registry/config-registry.ts +379 -0
- package/src/registry/shared-components.ts +80 -0
- package/src/types/plugin-module.ts +96 -0
- package/src/types/radio-config.ts +43 -0
- package/src/utils/npm-client.ts +167 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import type { ILogLayer } from 'loglayer';
|
|
2
|
+
import type { RadioModelId, RadioCodec, ValidationResult } from '@springfield/ham-radio-api';
|
|
3
|
+
import type { RegistryRadio } from '../types/radio-config.js';
|
|
4
|
+
import type { PluginModule } from '../types/plugin-module.js';
|
|
5
|
+
import type { SharedComponentManager } from '@springfield/ham-radio-api';
|
|
6
|
+
import type { NpmClient } from '../utils/npm-client.js';
|
|
7
|
+
import { DefaultSharedComponentManager } from './shared-components.js';
|
|
8
|
+
import { DefaultNpmClient } from '../utils/npm-client.js';
|
|
9
|
+
import { readdir, readFile } from 'fs/promises';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Radio configuration registry interface
|
|
15
|
+
*/
|
|
16
|
+
export interface RadioConfigRegistry {
|
|
17
|
+
// Discover all available radio configurations from npm modules
|
|
18
|
+
discoverConfigurations(): Promise<RegistryRadio[]>;
|
|
19
|
+
|
|
20
|
+
// Get configuration by ID
|
|
21
|
+
getConfiguration(configId: string): Promise<RegistryRadio | null>;
|
|
22
|
+
|
|
23
|
+
// Get configurations by manufacturer
|
|
24
|
+
getConfigurationsByManufacturer(manufacturer: string): Promise<RegistryRadio[]>;
|
|
25
|
+
|
|
26
|
+
// Get configurations by module
|
|
27
|
+
getConfigurationsByModule(moduleId: string): Promise<RegistryRadio[]>;
|
|
28
|
+
|
|
29
|
+
// Validate configuration
|
|
30
|
+
validateConfiguration(config: RegistryRadio): ValidationResult;
|
|
31
|
+
|
|
32
|
+
// Register a new configuration
|
|
33
|
+
registerConfiguration(config: RegistryRadio): Promise<void>;
|
|
34
|
+
|
|
35
|
+
// Install and load a new plugin module
|
|
36
|
+
installPlugin(moduleId: string): Promise<void>;
|
|
37
|
+
|
|
38
|
+
// List installed plugin modules
|
|
39
|
+
listInstalledPlugins(): Promise<PluginModule[]>;
|
|
40
|
+
|
|
41
|
+
// Get codec for a radio model
|
|
42
|
+
getCodec(modelId: RadioModelId): Promise<RadioCodec | null>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* NPM-based configuration registry implementation
|
|
47
|
+
*/
|
|
48
|
+
export class NpmBasedConfigRegistry implements RadioConfigRegistry {
|
|
49
|
+
private configCache = new Map<string, RegistryRadio>();
|
|
50
|
+
private pluginCache = new Map<string, PluginModule>();
|
|
51
|
+
private codecCache = new Map<string, RadioCodec>();
|
|
52
|
+
private sharedComponentManager: SharedComponentManager;
|
|
53
|
+
private npmClient: NpmClient;
|
|
54
|
+
private logger: ILogLayer;
|
|
55
|
+
|
|
56
|
+
constructor(logger: ILogLayer) {
|
|
57
|
+
this.logger = logger;
|
|
58
|
+
this.sharedComponentManager = new DefaultSharedComponentManager(logger);
|
|
59
|
+
this.npmClient = new DefaultNpmClient(logger);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async discoverConfigurations(): Promise<RegistryRadio[]> {
|
|
63
|
+
const configs: RegistryRadio[] = [];
|
|
64
|
+
|
|
65
|
+
// Discover plugin modules from node_modules
|
|
66
|
+
const pluginModules = await this.discoverPluginModules();
|
|
67
|
+
|
|
68
|
+
for (const plugin of pluginModules) {
|
|
69
|
+
try {
|
|
70
|
+
const pluginConfigs = await this.loadConfigurationsFromPlugin(plugin);
|
|
71
|
+
configs.push(...pluginConfigs);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
this.logger.withError(error).warn('Failed to load configurations from plugin ' + plugin.name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return configs;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async getConfiguration(configId: string): Promise<RegistryRadio | null> {
|
|
81
|
+
// Check cache first
|
|
82
|
+
if (this.configCache.has(configId)) {
|
|
83
|
+
return this.configCache.get(configId)!;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Discover configurations if cache is empty
|
|
87
|
+
if (this.configCache.size === 0) {
|
|
88
|
+
await this.discoverConfigurations();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return this.configCache.get(configId) || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getConfigurationsByManufacturer(manufacturer: string): Promise<RegistryRadio[]> {
|
|
95
|
+
const configs = await this.discoverConfigurations();
|
|
96
|
+
return configs.filter((config) => config.id.manufacturer.toLowerCase() === manufacturer.toLowerCase());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getConfigurationsByModule(moduleId: string): Promise<RegistryRadio[]> {
|
|
100
|
+
const configs = await this.discoverConfigurations();
|
|
101
|
+
return configs.filter((config) => config.metadata.moduleId === moduleId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
validateConfiguration(config: RegistryRadio): ValidationResult {
|
|
105
|
+
const errors: string[] = [];
|
|
106
|
+
const warnings: string[] = [];
|
|
107
|
+
|
|
108
|
+
// Basic validation
|
|
109
|
+
if (!config.id?.model) {
|
|
110
|
+
errors.push('Configuration must have a valid model ID');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!config.serialConfig) {
|
|
114
|
+
errors.push('Configuration must have serial configuration');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!config.memoryConfig) {
|
|
118
|
+
errors.push('Configuration must have memory configuration');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!config.readMemory || config.readMemory.length === 0) {
|
|
122
|
+
errors.push('Configuration must have read memory protocol');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!config.writeMemory || config.writeMemory.length === 0) {
|
|
126
|
+
errors.push('Configuration must have write memory protocol');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Schema validation
|
|
130
|
+
if (!config.settingsSchema) {
|
|
131
|
+
errors.push('Configuration must have settings schema');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
isValid: errors.length === 0,
|
|
136
|
+
errors,
|
|
137
|
+
warnings,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async registerConfiguration(config: RegistryRadio): Promise<void> {
|
|
142
|
+
const validation = this.validateConfiguration(config);
|
|
143
|
+
if (!validation.isValid) {
|
|
144
|
+
throw new Error(`Invalid configuration: ${validation.errors.join(', ')}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.configCache.set(config.id.model, config);
|
|
148
|
+
this.logger.withMetadata({ modelId: config.id.model }).info('Registered configuration');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async installPlugin(moduleId: string): Promise<void> {
|
|
152
|
+
// Validate plugin before installation
|
|
153
|
+
const validation = await this.validatePlugin(moduleId);
|
|
154
|
+
if (!validation.isValid) {
|
|
155
|
+
throw new Error(`Plugin validation failed: ${validation.errors.join(', ')}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Install using yarn
|
|
159
|
+
await this.runPackageManager(['add', moduleId]);
|
|
160
|
+
|
|
161
|
+
// Refresh configuration registry
|
|
162
|
+
await this.discoverConfigurations();
|
|
163
|
+
|
|
164
|
+
this.logger.withMetadata({ moduleId }).info('Installed plugin');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async listInstalledPlugins(): Promise<PluginModule[]> {
|
|
168
|
+
return Array.from(this.pluginCache.values());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async getCodec(modelId: RadioModelId): Promise<RadioCodec | null> {
|
|
172
|
+
// Check cache first
|
|
173
|
+
if (this.codecCache.has(modelId)) {
|
|
174
|
+
return this.codecCache.get(modelId)!;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Find configuration for this model
|
|
178
|
+
const config = await this.getConfiguration(modelId);
|
|
179
|
+
if (!config || !config.codec) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Load codec based on configuration
|
|
184
|
+
const codec = await this.loadCodecFromConfig(config);
|
|
185
|
+
if (codec) {
|
|
186
|
+
this.codecCache.set(modelId, codec);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return codec;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async discoverPluginModules(): Promise<PluginModule[]> {
|
|
193
|
+
const plugins: PluginModule[] = [];
|
|
194
|
+
|
|
195
|
+
// Scan node_modules for radio modules
|
|
196
|
+
const nodeModulesPath = join(process.cwd(), 'node_modules');
|
|
197
|
+
|
|
198
|
+
if (!existsSync(nodeModulesPath)) {
|
|
199
|
+
return plugins;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const entries = await readdir(nodeModulesPath, { withFileTypes: true });
|
|
203
|
+
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
if (entry.isDirectory()) {
|
|
206
|
+
const packageJsonPath = join(nodeModulesPath, entry.name, 'package.json');
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
210
|
+
|
|
211
|
+
// Check if this is a radio module
|
|
212
|
+
if (this.isRadioModule(packageJson)) {
|
|
213
|
+
const plugin = await this.loadPluginModule(entry.name, packageJson);
|
|
214
|
+
plugins.push(plugin);
|
|
215
|
+
this.pluginCache.set(entry.name, plugin);
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// Skip invalid packages
|
|
219
|
+
this.logger.withMetadata({ name: entry.name, error } as any).debug('Skipped invalid package');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return plugins;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private isRadioModule(packageJson: any): boolean {
|
|
228
|
+
const name = packageJson.name || '';
|
|
229
|
+
|
|
230
|
+
// Check naming convention
|
|
231
|
+
const isNamedCorrectly = name.includes('radio-module') || name.startsWith('@springfield/radio-module-');
|
|
232
|
+
|
|
233
|
+
// Check springfield plugin field
|
|
234
|
+
const hasSpringfieldField = packageJson.springfield?.pluginType === 'radio-module';
|
|
235
|
+
|
|
236
|
+
// Check keywords
|
|
237
|
+
const hasKeywords = packageJson.keywords?.includes('radio-module');
|
|
238
|
+
|
|
239
|
+
return isNamedCorrectly || hasSpringfieldField || hasKeywords;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async loadPluginModule(moduleName: string, packageJson: any): Promise<PluginModule> {
|
|
243
|
+
const modulePath = join(process.cwd(), 'node_modules', moduleName);
|
|
244
|
+
const springfieldConfig = packageJson.springfield || {};
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
name: moduleName,
|
|
248
|
+
version: packageJson.version,
|
|
249
|
+
manufacturer: springfieldConfig.manufacturer,
|
|
250
|
+
configPath: join(modulePath, springfieldConfig.configPath || 'configs'),
|
|
251
|
+
sharedPath: join(modulePath, springfieldConfig.sharedPath || 'shared'),
|
|
252
|
+
codecFactoryPath: springfieldConfig.codecFactory ? join(modulePath, springfieldConfig.codecFactory) : undefined,
|
|
253
|
+
capabilities: springfieldConfig.capabilities || {},
|
|
254
|
+
packageJson,
|
|
255
|
+
} as PluginModule;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async loadConfigurationsFromPlugin(plugin: PluginModule): Promise<RegistryRadio[]> {
|
|
259
|
+
const configs: RegistryRadio[] = [];
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const configFiles = await this.findConfigFiles(plugin.configPath);
|
|
263
|
+
|
|
264
|
+
for (const configFile of configFiles) {
|
|
265
|
+
try {
|
|
266
|
+
const config = await this.loadConfiguration(configFile);
|
|
267
|
+
|
|
268
|
+
// Resolve shared component references
|
|
269
|
+
await this.resolveSharedComponents(config, plugin);
|
|
270
|
+
|
|
271
|
+
// Add plugin metadata
|
|
272
|
+
config.metadata = {
|
|
273
|
+
...config.metadata,
|
|
274
|
+
moduleId: plugin.name,
|
|
275
|
+
moduleVersion: plugin.version,
|
|
276
|
+
pluginPath: plugin.configPath,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
if (this.validateConfiguration(config).isValid) {
|
|
280
|
+
configs.push(config);
|
|
281
|
+
this.configCache.set(config.id.model, config);
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
this.logger.withError(error).warn(`Failed to load configuration from ${configFile}:`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
this.logger.withError(error).warn(`Failed to access plugin directory ${plugin.configPath}:`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return configs;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private async findConfigFiles(configPath: string): Promise<string[]> {
|
|
295
|
+
if (!existsSync(configPath)) {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const files = await readdir(configPath);
|
|
300
|
+
return files.filter((file) => file.endsWith('.json')).map((file) => join(configPath, file));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private async loadConfiguration(configFile: string): Promise<RegistryRadio> {
|
|
304
|
+
const content = await readFile(configFile, 'utf8');
|
|
305
|
+
return JSON.parse(content);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async resolveSharedComponents(config: RegistryRadio, plugin: PluginModule): Promise<void> {
|
|
309
|
+
// Resolve schema references
|
|
310
|
+
if (config.settingsSchema.settingsSchema && typeof config.settingsSchema.settingsSchema === 'object' && '$ref' in config.settingsSchema.settingsSchema) {
|
|
311
|
+
const schemaPath = this.sharedComponentManager.resolveReference(config.settingsSchema.settingsSchema.$ref, plugin.configPath);
|
|
312
|
+
config.settingsSchema.settingsSchema = await this.sharedComponentManager.loadSchema(schemaPath);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (config.settingsSchema.channelSchema && typeof config.settingsSchema.channelSchema === 'object' && '$ref' in config.settingsSchema.channelSchema) {
|
|
316
|
+
const schemaPath = this.sharedComponentManager.resolveReference(config.settingsSchema.channelSchema.$ref, plugin.configPath);
|
|
317
|
+
config.settingsSchema.channelSchema = await this.sharedComponentManager.loadSchema(schemaPath);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private async loadCodecFromConfig(config: RegistryRadio): Promise<RadioCodec | null> {
|
|
322
|
+
if (!config.codec || config.codec.type !== 'shared' || !config.codec.reference) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const codecPath = this.sharedComponentManager.resolveReference(config.codec.reference, config.metadata.pluginPath || '');
|
|
327
|
+
|
|
328
|
+
return this.sharedComponentManager.loadCodec(codecPath, {
|
|
329
|
+
modelId: config.id.model,
|
|
330
|
+
...config.codec.config,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private async validatePlugin(moduleId: string): Promise<ValidationResult> {
|
|
335
|
+
const errors: string[] = [];
|
|
336
|
+
const warnings: string[] = [];
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
// Get package info from npm registry
|
|
340
|
+
const packageInfo = await this.npmClient.getPackageInfo(moduleId);
|
|
341
|
+
|
|
342
|
+
// Check if it's a radio module
|
|
343
|
+
if (!this.isRadioModule(packageInfo)) {
|
|
344
|
+
errors.push('Package is not a radio module');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Check version compatibility
|
|
348
|
+
if (!this.isVersionCompatible()) {
|
|
349
|
+
errors.push('Module version is not compatible with current system');
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
errors.push(`Failed to validate plugin: ${error}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
isValid: errors.length === 0,
|
|
357
|
+
errors,
|
|
358
|
+
warnings,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private isVersionCompatible(): boolean {
|
|
363
|
+
// Basic version compatibility check
|
|
364
|
+
// Could be enhanced with semver validation
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private async runPackageManager(args: string[]): Promise<void> {
|
|
369
|
+
const { exec } = await import('child_process');
|
|
370
|
+
const { promisify } = await import('util');
|
|
371
|
+
const execAsync = promisify(exec);
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
await execAsync(`yarn ${args.join(' ')}`);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw new Error(`Package manager error: ${error}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ILogLayer } from 'loglayer';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import type { RadioCodec, SharedComponentManager } from '@springfield/ham-radio-api';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default implementation of the shared component manager
|
|
8
|
+
*/
|
|
9
|
+
export class DefaultSharedComponentManager implements SharedComponentManager {
|
|
10
|
+
private logger: ILogLayer;
|
|
11
|
+
|
|
12
|
+
constructor(logger: ILogLayer) {
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async loadSchema(schemaPath: string): Promise<Record<string, unknown>> {
|
|
17
|
+
try {
|
|
18
|
+
const content = await readFile(schemaPath, 'utf8');
|
|
19
|
+
const schema = JSON.parse(content);
|
|
20
|
+
|
|
21
|
+
this.logger.withMetadata({ schemaPath }).debug('Loaded shared schema');
|
|
22
|
+
return schema;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
this.logger.withError(error).error('Failed to load shared schema');
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async loadProtocol(protocolPath: string): Promise<Record<string, unknown>> {
|
|
30
|
+
try {
|
|
31
|
+
const content = await readFile(protocolPath, 'utf8');
|
|
32
|
+
const protocol = JSON.parse(content);
|
|
33
|
+
|
|
34
|
+
this.logger.withMetadata({ protocolPath }).debug('Loaded shared protocol');
|
|
35
|
+
return protocol;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
this.logger.withError(error).error('Failed to load shared protocol');
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async loadCodec(codecPath: string, config: Record<string, unknown>): Promise<RadioCodec> {
|
|
43
|
+
try {
|
|
44
|
+
// Dynamically load codec module
|
|
45
|
+
const codecModule = await import(codecPath);
|
|
46
|
+
|
|
47
|
+
// Check for CodecFactory export
|
|
48
|
+
if (!codecModule.CodecFactory) {
|
|
49
|
+
throw new Error(`Codec module does not export CodecFactory: ${codecPath}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const CodecFactory = codecModule.CodecFactory;
|
|
53
|
+
const factory = new CodecFactory();
|
|
54
|
+
|
|
55
|
+
// Create codec with configuration
|
|
56
|
+
const codec = await factory.createCodec(config.modelId as any, config, this.logger);
|
|
57
|
+
|
|
58
|
+
this.logger.withMetadata({ codecPath, modelId: config.modelId }).info('Loaded shared codec');
|
|
59
|
+
return codec;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.logger.withError(error).error('Failed to load shared codec');
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
resolveReference(reference: string, basePath: string): string {
|
|
67
|
+
// Handle relative paths
|
|
68
|
+
if (reference.startsWith('./') || reference.startsWith('../')) {
|
|
69
|
+
return join(basePath, reference);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle absolute paths
|
|
73
|
+
if (reference.startsWith('/')) {
|
|
74
|
+
return reference;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Default to relative path
|
|
78
|
+
return join(basePath, reference);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin module capabilities
|
|
3
|
+
*/
|
|
4
|
+
export interface PluginCapabilities {
|
|
5
|
+
dslProtocols: boolean;
|
|
6
|
+
customCodecs: boolean;
|
|
7
|
+
memoryRead: boolean;
|
|
8
|
+
memoryWrite: boolean;
|
|
9
|
+
sharedComponents: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Springfield-specific plugin configuration in package.json
|
|
14
|
+
*/
|
|
15
|
+
export interface SpringfieldPluginConfig {
|
|
16
|
+
pluginType: 'radio-module';
|
|
17
|
+
version: string;
|
|
18
|
+
manufacturer: string;
|
|
19
|
+
supportedRadios: string[];
|
|
20
|
+
capabilities: PluginCapabilities;
|
|
21
|
+
configPath: string;
|
|
22
|
+
sharedPath: string;
|
|
23
|
+
codecFactory?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Plugin module information
|
|
28
|
+
*/
|
|
29
|
+
export interface PluginModule {
|
|
30
|
+
name: string;
|
|
31
|
+
version: string;
|
|
32
|
+
manufacturer?: string;
|
|
33
|
+
configPath: string;
|
|
34
|
+
sharedPath: string;
|
|
35
|
+
codecFactoryPath?: string;
|
|
36
|
+
capabilities: Record<string, boolean>;
|
|
37
|
+
packageJson: any;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* NPM plugin information from registry
|
|
42
|
+
*/
|
|
43
|
+
export interface NpmPluginInfo {
|
|
44
|
+
name: string;
|
|
45
|
+
version: string;
|
|
46
|
+
description: string;
|
|
47
|
+
author: string;
|
|
48
|
+
license: string;
|
|
49
|
+
repository?: string;
|
|
50
|
+
downloads: number;
|
|
51
|
+
lastUpdated: string;
|
|
52
|
+
springfield?: SpringfieldPluginConfig;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Plugin manifest configuration
|
|
57
|
+
*/
|
|
58
|
+
export interface PluginManifest {
|
|
59
|
+
pluginType: 'radio-module';
|
|
60
|
+
version: string;
|
|
61
|
+
manufacturer: string;
|
|
62
|
+
author: string;
|
|
63
|
+
license: string;
|
|
64
|
+
repository?: string;
|
|
65
|
+
configurations: PluginManifestConfiguration[];
|
|
66
|
+
sharedComponents?: PluginSharedComponents;
|
|
67
|
+
security?: PluginSecurity;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Plugin manifest configuration entry
|
|
72
|
+
*/
|
|
73
|
+
export interface PluginManifestConfiguration {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
file: string;
|
|
77
|
+
description: string;
|
|
78
|
+
usesSharedComponents?: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Plugin shared components
|
|
83
|
+
*/
|
|
84
|
+
export interface PluginSharedComponents {
|
|
85
|
+
schemas?: Record<string, string>;
|
|
86
|
+
protocols?: Record<string, string>;
|
|
87
|
+
codecs?: Record<string, string>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Plugin security information
|
|
92
|
+
*/
|
|
93
|
+
export interface PluginSecurity {
|
|
94
|
+
signature?: string;
|
|
95
|
+
checksum?: string;
|
|
96
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Radio } from '@springfield/ham-radio-api';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Radio configuration metadata
|
|
5
|
+
*/
|
|
6
|
+
export interface RadioConfigMetadata {
|
|
7
|
+
moduleId: string;
|
|
8
|
+
moduleVersion: string;
|
|
9
|
+
pluginPath: string;
|
|
10
|
+
lastUpdated?: string;
|
|
11
|
+
author?: string;
|
|
12
|
+
license?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Radio capabilities
|
|
17
|
+
*/
|
|
18
|
+
export interface RadioCapabilities {
|
|
19
|
+
dslProtocols: boolean;
|
|
20
|
+
customCodecs: boolean;
|
|
21
|
+
memoryRead: boolean;
|
|
22
|
+
memoryWrite: boolean;
|
|
23
|
+
sharedComponents: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Codec configuration
|
|
28
|
+
*/
|
|
29
|
+
export interface CodecConfig {
|
|
30
|
+
type: 'shared' | 'inline';
|
|
31
|
+
reference?: string;
|
|
32
|
+
config?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Registry radio configuration
|
|
37
|
+
*/
|
|
38
|
+
export interface RegistryRadio extends Radio {
|
|
39
|
+
$schema?: string;
|
|
40
|
+
capabilities: RadioCapabilities;
|
|
41
|
+
codec?: CodecConfig;
|
|
42
|
+
metadata: RadioConfigMetadata;
|
|
43
|
+
}
|