UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

364 lines (313 loc) 9.23 kB
/** * Hello World - AssemblyScript WASM Plugin * * Demonstrates basic AssemblyScript plugin structure for Signal K */ import { Plugin, Delta, Update, PathValue, Notification, NotificationState, emit, setStatus, setError, debug } from '@signalk/assemblyscript-plugin-sdk/assembly' /** * Plugin configuration interface */ class HelloConfig { message: string = 'Hello from AssemblyScript!' updateInterval: i32 = 5000 enableDebugLogging: boolean = false } // Track elapsed time for polling (server calls poll() every ~1000ms) let elapsedMs: i32 = 0 let pollCount: i32 = 0 /** * Hello World Plugin Implementation */ class HelloPlugin extends Plugin { private config: HelloConfig = new HelloConfig() /** * Helper to conditionally log debug messages */ private logDebug(message: string): void { if (this.config.enableDebugLogging) { debug(message) } } /** * Plugin name shown in admin UI * Note: Plugin ID is derived from package.json name */ name(): string { return 'Hello AssemblyScript Plugin' } /** * JSON schema for configuration UI */ schema(): string { return `{ "type": "object", "properties": { "message": { "type": "string", "title": "Welcome Message", "default": "Hello from AssemblyScript!" }, "updateInterval": { "type": "number", "title": "Update Interval (ms)", "default": 5000 } } }` } /** * Start plugin with configuration */ start(configJson: string): i32 { // Parse configuration // Note: For production, use a JSON parser like assemblyscript-json // For this example, we do basic string parsing // Check enableDebug at root level (matches regular plugin config structure) if ( configJson.includes('"enableDebug":true') || configJson.includes('"enableDebug": true') ) { this.config.enableDebugLogging = true } // Parse updateInterval from config (basic string parsing) const intervalKey = '"updateInterval":' const intervalIdx = configJson.indexOf(intervalKey) if (intervalIdx >= 0) { const startIdx = intervalIdx + intervalKey.length let endIdx = startIdx while (endIdx < configJson.length) { const c = configJson.charCodeAt(endIdx) if (c < 48 || c > 57) break // Not a digit (0-9) endIdx++ } if (endIdx > startIdx) { const intervalStr = configJson.substring(startIdx, endIdx) this.config.updateInterval = i32(parseInt(intervalStr) as i32) } } // Reset poll counters elapsedMs = 0 pollCount = 0 this.logDebug('========================================') this.logDebug('Hello AssemblyScript plugin starting...') this.logDebug(`Plugin Name: ${this.name()}`) this.logDebug(`Configuration received: ${configJson}`) this.logDebug( `Debug logging: ${this.config.enableDebugLogging ? 'ENABLED' : 'DISABLED'}` ) this.logDebug(`Update interval: ${this.config.updateInterval}ms`) this.logDebug('========================================') setStatus('Started successfully') this.logDebug('Status set to: Started successfully') // Emit a welcome notification this.logDebug('Emitting welcome notification...') this.emitWelcomeNotification() // Emit a test delta this.logDebug('Emitting test delta with plugin info...') this.emitTestDelta() this.logDebug('========================================') this.logDebug('Hello AssemblyScript plugin started successfully!') this.logDebug('========================================') return 0 // Success } /** * Stop plugin */ stop(): i32 { this.logDebug('========================================') this.logDebug('Hello AssemblyScript plugin stopping...') setStatus('Stopped') this.logDebug('Status set to: Stopped') this.logDebug('Hello AssemblyScript plugin stopped successfully!') this.logDebug('========================================') return 0 // Success } /** * Emit a welcome notification */ private emitWelcomeNotification(): void { this.logDebug('Building welcome notification...') const notification = new Notification( NotificationState.normal, this.config.message ) const pathValue = new PathValue( 'notifications.hello', notification.toJSON() ) const update = new Update([pathValue]) const delta = new Delta('vessels.self', [update]) emit(delta) this.logDebug('✓ Welcome notification emitted to path: notifications.hello') } /** * Emit a test delta with plugin information */ private emitTestDelta(): void { this.logDebug('Building plugin info delta...') const pluginInfo = `{ "name": "${this.name()}", "language": "AssemblyScript", "version": "0.1.0" }` const pathValue = new PathValue( 'plugins.hello-assemblyscript.info', pluginInfo ) const update = new Update([pathValue]) const delta = new Delta('vessels.self', [update]) emit(delta) this.logDebug( '✓ Plugin info delta emitted to path: plugins.hello-assemblyscript.info' ) } /** * Emit a periodic heartbeat delta */ emitHeartbeat(): void { pollCount++ this.logDebug(`Heartbeat #${pollCount}`) const heartbeatValue = `{ "count": ${pollCount}, "message": "${this.config.message}", "intervalMs": ${this.config.updateInterval} }` const pathValue = new PathValue( 'plugins.hello-assemblyscript.heartbeat', heartbeatValue ) const update = new Update([pathValue]) const delta = new Delta('vessels.self', [update]) emit(delta) this.logDebug( `✓ Heartbeat delta emitted to path: plugins.hello-assemblyscript.heartbeat` ) } /** * Get update interval for polling */ getUpdateInterval(): i32 { return this.config.updateInterval } } // Export plugin instance // Signal K server will call the exported functions const plugin = new HelloPlugin() // Plugin lifecycle exports // Note: plugin_id() is no longer required - ID is derived from package.json name export function plugin_name(): string { return plugin.name() } export function plugin_schema(): string { return plugin.schema() } export function plugin_start(configPtr: usize, configLen: usize): i32 { // Read config string from memory const len = i32(configLen) const configBytes = new Uint8Array(len) for (let i: i32 = 0; i < len; i++) { configBytes[i] = load<u8>(configPtr + <usize>i) } const configJson = String.UTF8.decode(configBytes.buffer) return plugin.start(configJson) } export function plugin_stop(): i32 { return plugin.stop() } /** * Poll function - called by server every ~1000ms * Emits heartbeat delta when updateInterval has elapsed */ export function poll(): i32 { // Server calls poll() every ~1000ms elapsedMs += 1000 // Check if it's time to emit a heartbeat if (elapsedMs >= plugin.getUpdateInterval()) { plugin.emitHeartbeat() elapsedMs = 0 } return 0 // Success } /** * HTTP Endpoints (Phase 2) * Register custom REST API endpoints */ export function http_endpoints(): string { return `[ { "method": "GET", "path": "/api/info", "handler": "handle_get_info" }, { "method": "GET", "path": "/api/status", "handler": "handle_get_status" } ]` } /** * Handle GET /api/info * Returns plugin information */ export function handle_get_info(requestPtr: usize, requestLen: usize): string { // Decode request from memory (not used in this simple example) const requestBytes = new Uint8Array(i32(requestLen)) for (let i: i32 = 0; i < i32(requestLen); i++) { requestBytes[i] = load<u8>(requestPtr + <usize>i) } const requestJson = String.UTF8.decode(requestBytes.buffer) // Build response data // Note: pluginId is derived from package.json name const bodyJson = `{ "pluginName": "${plugin.name()}", "language": "AssemblyScript", "version": "0.1.0", "message": "Hello from WASM!", "capabilities": ["delta", "notifications", "http-endpoints"] }` // Escape for embedding in JSON response const escapedBody = bodyJson.replaceAll('"', '\\"').replaceAll('\n', '\\n') // Return HTTP response return `{ "statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": "${escapedBody}" }` } /** * Handle GET /api/status * Returns runtime status */ export function handle_get_status( requestPtr: usize, requestLen: usize ): string { const requestBytes = new Uint8Array(i32(requestLen)) for (let i: i32 = 0; i < i32(requestLen); i++) { requestBytes[i] = load<u8>(requestPtr + <usize>i) } const requestJson = String.UTF8.decode(requestBytes.buffer) const bodyJson = `{ "status": "running", "uptime": "N/A", "memory": "sandboxed" }` const escapedBody = bodyJson.replaceAll('"', '\\"').replaceAll('\n', '\\n') return `{ "statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": "${escapedBody}" }` }