Development Guide
This guide provides practical information for developers working with the HamBench ecosystem.
Getting Started
Before diving into development, we recommend reading the Architecture Overview to understand the overall system design and the Module Comparison to understand the purpose of each module.
Development Environment Setup
Prerequisites
- Node.js 18.0.0 or higher
- Yarn package manager
- TypeScript knowledge
- Familiarity with amateur radio concepts
Required Dependencies
All modules use these core dependencies:
loglayer- Structured logging@springfield/ham-radio-api- Core types and interfacestypescript- Type safetyoxlint- Lintingchai+fishery- Testing
Module Development Patterns
1. Core API Module (@springfield/ham-radio-api)
Purpose: Define types and interfaces used by all other modules.
Key Principles:
- Keep dependencies minimal (only
loglayerandts-brand) - Use branded types for type safety
- Define clear, stable interfaces
- Maintain backward compatibility
Example Structure:
// Define branded types for type safety
export type Frequency = Brand<number, 'Frequency'>;
export type RadioChannelId = Brand<string, 'RadioChannelId'>;
// Define core interfaces
export interface RadioDriver {
readRadio(serialPortPath: string, progressIndicator: RadioProgressIndicator): Promise<RadioMemory>;
writeRadio(serialPortPath: string, memory: RadioMemory, progressIndicator: RadioProgressIndicator): Promise<void>;
}2. Driver Module (@springfield/ham-radio-driver)
Purpose: Implement radio communication logic using protocol DSL.
Key Principles:
- Use protocol interpreter pattern
- Support progress tracking and cancellation
- Handle serial communication robustly
- Provide extensible executor system
Example Usage:
import { RadioDriver } from '@springfield/ham-radio-driver';
import type { Radio, RadioProgressIndicator } from '@springfield/ham-radio-api';
const driver = new RadioDriver(radioConfig, logger);
const progressIndicator = new RadioProgressIndicator();
// Read radio memory
const memory = await driver.readRadio('/dev/ttyUSB0', progressIndicator);
// Write radio memory
await driver.writeRadio('/dev/ttyUSB0', memory, progressIndicator);3. Utilities Module (@springfield/ham-radio-utils)
Purpose: Provide shared utilities and helper functions.
Key Principles:
- Focus on reusability across modules
- Provide comprehensive test utilities
- Include schema validation
- Support UI logging and progress reporting
Example Usage:
import { SegmentedMemory, validateSchema, createUILogger } from '@springfield/ham-radio-utils';
// Memory management
const memory = new SegmentedMemory(segments);
const segment = memory.getSegment(0);
// Schema validation
const result = validateSchema(data, schema);
// UI logging
const uiLogger = createUILogger();
uiLogger.startCommand(0, 5, 'readMemory', step);4. Registry Module (@springfield/ham-radio-registry)
Purpose: Discover and manage radio module plugins.
Key Principles:
- Support npm-based plugin discovery
- Validate configurations before loading
- Manage shared components
- Provide caching for performance
Example Usage:
import { createRegistry } from '@springfield/ham-radio-registry';
const registry = createRegistry(logger);
// Discover available configurations
const configs = await registry.discoverConfigurations();
// Get specific configuration
const config = await registry.getConfiguration('baofeng:uv5r');
// Get codec for radio model
const codec = await registry.getCodec('baofeng:uv5r');5. Radio Module (radio-module-*)
Purpose: Provide radio-specific implementations.
Key Principles:
- Follow
radio-module-{manufacturer}naming - Ship JSON only: configs, schemas, memory maps
- Use
codec.type: "memoryMap"; do not add TypeScript codecs
Example Structure:
radio-module-manufacturer/
├── package.json
├── configs/
│ └── model.json
├── src/shared/
│ ├── schemas/
│ └── memory-maps/
└── README.mdTesting Patterns
Unit Testing
All modules use the Node.js native test runner with chai assertions:
import { describe, it } from 'node:test';
import { expect } from 'chai';
import { Factory } from 'fishery';
describe('MyModule', () => {
it('should handle basic operations', () => {
const data = Factory.build('testData');
const result = processData(data);
expect(result).to.be.an('object');
});
});Integration Testing
Use yarn test:integration for integration tests:
import { describe, it } from 'node:test';
import { expect } from 'chai';
describe('Integration Tests', () => {
it('should read radio memory end-to-end', async () => {
// Test complete workflow
});
});Test Data Factories
Use fishery for generating test data:
import { Factory } from 'fishery';
export const RadioChannelFactory = Factory.define<RadioChannel>(({ sequence }) => ({
id: `channel-${sequence}`,
frequency: 146.520,
name: `Channel ${sequence}`,
}));Logging Patterns
Using LogLayer
All modules use loglayer for structured logging:
import type { ILogLayer } from 'loglayer';
class MyClass {
constructor(private logger: ILogLayer) {}
async processData(data: any) {
this.logger.withMetadata({ dataSize: data.length }).debug('Processing data');
try {
// Process data
this.logger.info('Data processed successfully');
} catch (error) {
this.logger.withError(error).error('Failed to process data');
throw error;
}
}
}UI Logging
For user-facing progress reporting:
import { createUILogger } from '@springfield/ham-radio-utils';
const uiLogger = createUILogger();
uiLogger.startCommand(0, 5, 'readMemory', step);
uiLogger.logCommandSuccess(0, 5, 'readMemory', step, context);
uiLogger.logCommandFailure(0, 5, 'readMemory', step, error, context);Error Handling
Exception Types
Use specific exception types for different error conditions:
import { CancelledException } from '@springfield/ham-radio-driver';
if (progressIndicator.isCanceled) {
throw new CancelledException('Operation was cancelled by user');
}Validation Errors
Use structured validation results:
import type { ValidationResult } from '@springfield/ham-radio-api';
const result: ValidationResult = {
isValid: false,
errors: ['Invalid frequency value'],
warnings: ['Deprecated field used'],
};Performance Considerations
Memory Management
Use segmented memory for large radio memories:
import { SegmentedMemory } from '@springfield/ham-radio-utils';
const memory = new SegmentedMemory(segments);
// Efficient access to large memory structuresCaching
Implement caching for expensive operations:
class CachedRegistry {
private configCache = new Map<string, RegistryRadio>();
async getConfiguration(id: string): Promise<RegistryRadio | null> {
if (this.configCache.has(id)) {
return this.configCache.get(id)!;
}
const config = await this.loadConfiguration(id);
if (config) {
this.configCache.set(id, config);
}
return config;
}
}Progress Tracking
Support cancellation for long-running operations:
async function processLargeData(data: Uint8Array, progress: RadioProgressIndicator) {
for (let i = 0; i < data.length; i += chunkSize) {
if (progress.isCanceled) {
throw new CancelledException('Operation cancelled');
}
// Process chunk
progress.setValue(i / data.length);
}
}Security Best Practices
Input Validation
Always validate external inputs:
import { validateSchema } from '@springfield/ham-radio-utils';
const result = validateSchema(config, schema);
if (!result.isValid) {
throw new Error(`Invalid configuration: ${result.errors.join(', ')}`);
}Type Safety
Use branded types to prevent type confusion:
import type { Frequency, RadioChannelId } from '@springfield/ham-radio-api';
function processChannel(id: RadioChannelId, freq: Frequency) {
// Type-safe operations
}Sandboxing
Sandbox plugin execution where possible:
// Validate plugin before loading
const validation = await validatePlugin(moduleId);
if (!validation.isValid) {
throw new Error(`Plugin validation failed: ${validation.errors.join(', ')}`);
}Deployment Considerations
Package Management
Use yarn for consistent dependency management:
{
"packageManager": "yarn@4.9.2",
"scripts": {
"build": "tsc",
"test": "yarn test:unit && yarn test:integration"
}
}Version Compatibility
Maintain compatibility within major versions:
{
"dependencies": {
"@springfield/ham-radio-api": "^16.0.0"
}
}Build Process
Ensure proper build configuration:
{
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["src/", "dist/"]
}Contributing Guidelines
Code Style
- Use TypeScript strict mode
- Follow project linting rules
- Sort imports and object keys
- Use blank lines around code blocks
- Never use
anytype
Testing Requirements
- Write unit tests for all new functionality
- Include integration tests for complex workflows
- Use factory patterns for test data
- Maintain good test coverage
Documentation
- Update relevant documentation when adding features
- Include JSDoc comments for public APIs
- Provide usage examples
- Update architecture documentation for significant changes
Resources
- Architecture Overview - Complete system architecture
- Module Comparison - Quick reference for all modules
- Registry Documentation - Plugin system details
- Registry Examples - Code examples and patterns