signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
701 lines • 35.3 kB
JavaScript
;
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* WASM Radar Provider Support
*
* Handles radar provider registration and handler invocation for WASM plugins.
* Integrates with Signal K's Radar API at /signalk/v2/api/vessels/self/radars
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.wasmRadarProviders = void 0;
exports.callWasmRadarHandler = callWasmRadarHandler;
exports.updateRadarProviderInstance = updateRadarProviderInstance;
exports.cleanupRadarProviders = cleanupRadarProviders;
exports.createRadarProviderBinding = createRadarProviderBinding;
exports.createRadarEmitSpokesBinding = createRadarEmitSpokesBinding;
const debug_1 = __importDefault(require("debug"));
const debug = (0, debug_1.default)('signalk:wasm:radar-provider');
/**
* Registered radar providers from WASM plugins
* Key: pluginId
*/
exports.wasmRadarProviders = new Map();
/**
* Call a WASM radar handler function
* Handles both AssemblyScript and Rust plugins with Asyncify support for async operations
*/
async function callWasmRadarHandler(pluginInstance, handlerName, requestJson) {
try {
const asLoader = pluginInstance.asLoader;
// Use the wrapped exports which have proper WASI initialization
// Fall back to raw instance exports if wrapped exports don't have the handler
const _wrappedExports = pluginInstance.exports;
const rawExports = pluginInstance.instance?.exports;
// Debug: list available exports when handler is not found
if (rawExports) {
const exportNames = Object.keys(rawExports).filter((k) => k.startsWith('radar_'));
debug(`[${pluginInstance.pluginId}] Looking for ${handlerName}, available radar_ exports: ${exportNames.join(', ')}`);
}
else {
debug(`[${pluginInstance.pluginId}] No rawExports available`);
}
if (asLoader && typeof asLoader.exports[handlerName] === 'function') {
// AssemblyScript: allocate string in WASM memory, pass pointer, get string pointer back
// Need to handle Asyncify for handlers that call fetchSync
const requestPtr = asLoader.exports.__newString(requestJson);
// Set up Asyncify resume handling
let resumePromiseResolve = null;
const resumePromise = new Promise((resolve) => {
resumePromiseResolve = resolve;
});
// Store the result pointer from the handler call
let handlerResultPtr = null;
if (pluginInstance.setAsyncifyResume) {
pluginInstance.setAsyncifyResume(() => {
debug(`Re-calling ${handlerName} to resume from rewind state`);
const resumeResultPtr = asLoader.exports[handlerName](requestPtr);
const result = asLoader.exports.__getString(resumeResultPtr);
if (resumePromiseResolve) {
resumePromiseResolve(result);
}
return resumeResultPtr;
});
}
// Call the handler
handlerResultPtr = asLoader.exports[handlerName](requestPtr);
// Check if we're in Asyncify unwind state
if (typeof asLoader.exports.asyncify_get_state === 'function') {
const state = asLoader.exports.asyncify_get_state();
debug(`Asyncify state after ${handlerName}: ${state}`);
if (state === 1) {
// State 1 = unwound, waiting for async operation
debug(`${handlerName} is in unwound state - waiting for async operation to complete`);
const result = await resumePromise;
debug(`${handlerName} async operation completed`);
if (pluginInstance.setAsyncifyResume) {
pluginInstance.setAsyncifyResume(null);
}
return result;
}
else {
// Not in async state, clean up
if (pluginInstance.setAsyncifyResume) {
pluginInstance.setAsyncifyResume(null);
}
}
}
// Normal synchronous return
return asLoader.exports.__getString(handlerResultPtr);
}
else if (rawExports && typeof rawExports[handlerName] === 'function') {
// Rust: buffer-based string passing
if (typeof rawExports.allocate !== 'function') {
debug(`Plugin ${pluginInstance.pluginId} missing allocate export`);
return null;
}
const responseMaxLen = 65536; // 64KB response buffer
const responsePtr = rawExports.allocate(responseMaxLen);
const memory = rawExports.memory;
let writtenLen;
// radar_get_radars takes only output buffer params: (output_ptr, output_len) -> written_len
if (handlerName === 'radar_get_radars') {
writtenLen = rawExports[handlerName](responsePtr, responseMaxLen);
}
else {
// Other handlers take request + output: (request_ptr, request_len, response_ptr, response_max_len) -> written_len
const requestBytes = Buffer.from(requestJson, 'utf8');
const requestPtr = rawExports.allocate(requestBytes.length);
// Write request to WASM memory
const memView = new Uint8Array(memory.buffer);
memView.set(requestBytes, requestPtr);
writtenLen = rawExports[handlerName](requestPtr, requestBytes.length, responsePtr, responseMaxLen);
// Deallocate request buffer
if (typeof rawExports.deallocate === 'function') {
rawExports.deallocate(requestPtr, requestBytes.length);
}
}
// Read response from WASM memory
const responseBytes = new Uint8Array(memory.buffer, responsePtr, writtenLen);
const responseJson = new TextDecoder('utf-8').decode(responseBytes);
// Deallocate response buffer
if (typeof rawExports.deallocate === 'function') {
rawExports.deallocate(responsePtr, responseMaxLen);
}
return responseJson;
}
debug(`Handler ${handlerName} not found in plugin ${pluginInstance.pluginId}`);
return null;
}
catch (error) {
debug(`Error calling radar handler ${handlerName}: ${error}`);
return null;
}
}
/**
* Update radar provider references with a newly loaded plugin instance
*/
function updateRadarProviderInstance(pluginId, pluginInstance) {
const provider = exports.wasmRadarProviders.get(pluginId);
if (provider) {
provider.pluginInstance = pluginInstance;
debug(`Updated radar provider ${pluginId} with plugin instance`);
}
}
/**
* Clean up radar provider registrations for a plugin
* @param pluginId The plugin ID
* @param app The Signal K app (optional, if provided will also unregister from RadarApi)
*/
function cleanupRadarProviders(pluginId, app) {
if (exports.wasmRadarProviders.has(pluginId)) {
debug(`Removing radar provider registration: ${pluginId}`);
exports.wasmRadarProviders.delete(pluginId);
}
// Also unregister from Signal K RadarApi
if (app && app.radarApi && typeof app.radarApi.unRegister === 'function') {
try {
app.radarApi.unRegister(pluginId);
debug(`Unregistered ${pluginId} from RadarApi`);
}
catch (error) {
debug(`Error unregistering from RadarApi: ${error}`);
}
}
}
/**
* Create the sk_register_radar_provider host binding
*
* WASM plugins call this to register as a radar provider.
* The plugin must export handler functions:
* - radar_get_radars() -> JSON array of radar IDs
* - radar_get_radar_info(requestJson) -> RadarInfo JSON
* - radar_set_power(requestJson) -> boolean success
* - radar_set_range(requestJson) -> boolean success
* - radar_set_gain(requestJson) -> boolean success
* - radar_set_controls(requestJson) -> boolean success
*/
function createRadarProviderBinding(pluginId, capabilities, app, readUtf8String) {
return (namePtr, nameLen) => {
try {
const providerName = readUtf8String(namePtr, nameLen);
debug(`[${pluginId}] Registering as radar provider: ${providerName}`);
// Check if plugin has radarProvider capability
if (!capabilities.radarProvider) {
debug(`[${pluginId}] radarProvider capability not granted`);
return 0; // Failure
}
// Check if app and radarApi are available
if (!app || !app.radarApi) {
debug(`[${pluginId}] app.radarApi not available`);
return 0;
}
// Store the registration (we'll update the pluginInstance reference after instance creation)
exports.wasmRadarProviders.set(pluginId, {
pluginId,
providerName,
pluginInstance: null // Will be set after full instance creation
});
// Create RadarProvider object that calls into WASM handlers
const radarProvider = {
name: providerName,
methods: {
pluginId: pluginId,
/**
* Get list of radar IDs this provider manages
*/
getRadars: async () => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return [];
}
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_get_radars', '{}');
if (result) {
try {
return JSON.parse(result);
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_get_radars response: ${e}`);
return [];
}
}
return [];
},
/**
* Get radar info for a specific radar
* @param radarId The radar ID
*/
getRadarInfo: async (radarId) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return null;
}
const requestJson = JSON.stringify({ radarId });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_get_radar_info', requestJson);
if (result) {
try {
return JSON.parse(result);
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_get_radar_info response: ${e}`);
return null;
}
}
return null;
},
/**
* Set radar power state
* @param radarId The radar ID
* @param state Power state
*/
setPower: async (radarId, state) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return false;
}
const requestJson = JSON.stringify({ radarId, state });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_set_power', requestJson);
if (result) {
try {
return JSON.parse(result) === true;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_set_power response: ${e}`);
return false;
}
}
return false;
},
/**
* Set radar range
* @param radarId The radar ID
* @param range Range in meters
*/
setRange: async (radarId, range) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return false;
}
const requestJson = JSON.stringify({ radarId, range });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_set_range', requestJson);
if (result) {
try {
return JSON.parse(result) === true;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_set_range response: ${e}`);
return false;
}
}
return false;
},
/**
* Set radar gain
* @param radarId The radar ID
* @param gain Gain settings
*/
setGain: async (radarId, gain) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return false;
}
const requestJson = JSON.stringify({ radarId, gain });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_set_gain', requestJson);
if (result) {
try {
return JSON.parse(result) === true;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_set_gain response: ${e}`);
return false;
}
}
return false;
},
/**
* Set radar sea clutter
* @param radarId The radar ID
* @param sea Sea clutter settings
*/
setSea: async (radarId, sea) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return false;
}
const requestJson = JSON.stringify({ radarId, sea });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_set_sea', requestJson);
if (result) {
try {
return JSON.parse(result) === true;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_set_sea response: ${e}`);
return false;
}
}
return false;
},
/**
* Set radar rain clutter
* @param radarId The radar ID
* @param rain Rain clutter settings
*/
setRain: async (radarId, rain) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return false;
}
const requestJson = JSON.stringify({ radarId, rain });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_set_rain', requestJson);
if (result) {
try {
return JSON.parse(result) === true;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_set_rain response: ${e}`);
return false;
}
}
return false;
},
/**
* Set multiple radar controls at once
* @param radarId The radar ID
* @param controls Controls to update
*/
setControls: async (radarId, controls) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return false;
}
const requestJson = JSON.stringify({ radarId, controls });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_set_controls', requestJson);
if (result) {
try {
return JSON.parse(result) === true;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_set_controls response: ${e}`);
return false;
}
}
return false;
},
/**
* Get capability manifest for a radar
* @param radarId The radar ID
*/
getCapabilities: async (radarId) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return null;
}
const requestJson = JSON.stringify({ radarId });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_get_capabilities', requestJson);
if (result) {
try {
const parsed = JSON.parse(result);
if (parsed.error) {
debug(`[${pluginId}] radar_get_capabilities error: ${parsed.error}`);
return null;
}
return parsed;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_get_capabilities response: ${e}`);
return null;
}
}
return null;
},
/**
* Get current state
* @param radarId The radar ID
*/
getState: async (radarId) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return null;
}
const requestJson = JSON.stringify({ radarId });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_get_state', requestJson);
if (result) {
try {
const parsed = JSON.parse(result);
if (parsed.error) {
debug(`[${pluginId}] radar_get_state error: ${parsed.error}`);
return null;
}
return parsed;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_get_state response: ${e}`);
return null;
}
}
return null;
},
/**
* Get a single control value
* @param radarId The radar ID
* @param controlId The control ID
*/
getControl: async (radarId, controlId) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return null;
}
const requestJson = JSON.stringify({ radarId, controlId });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_get_control', requestJson);
if (result) {
try {
const parsed = JSON.parse(result);
if (parsed.error) {
debug(`[${pluginId}] radar_get_control error: ${parsed.error}`);
return null;
}
return parsed;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_get_control response: ${e}`);
return null;
}
}
return null;
},
/**
* Set a single control value
* @param radarId The radar ID
* @param controlId The control ID
* @param value The value to set
*/
setControl: async (radarId, controlId, value) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return { success: false, error: 'Provider not ready' };
}
const requestJson = JSON.stringify({ radarId, controlId, value });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_set_control', requestJson);
if (result) {
try {
return JSON.parse(result);
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_set_control response: ${e}`);
return { success: false, error: 'Invalid response' };
}
}
return { success: false, error: 'No response' };
},
// ============================================
// ARPA Target Methods
// ============================================
/**
* Get all tracked ARPA targets
* @param radarId The radar ID
*/
getTargets: async (radarId) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return null;
}
const requestJson = JSON.stringify({ radarId });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_get_targets', requestJson);
if (result) {
try {
const parsed = JSON.parse(result);
if (parsed.error) {
debug(`[${pluginId}] radar_get_targets error: ${parsed.error}`);
return null;
}
return parsed;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_get_targets response: ${e}`);
return null;
}
}
return null;
},
/**
* Manually acquire a target at the specified position
* @param radarId The radar ID
* @param bearing Bearing in degrees
* @param distance Distance in meters
*/
acquireTarget: async (radarId, bearing, distance) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return { success: false, error: 'Provider not ready' };
}
const requestJson = JSON.stringify({ radarId, bearing, distance });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_acquire_target', requestJson);
if (result) {
try {
return JSON.parse(result);
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_acquire_target response: ${e}`);
return { success: false, error: 'Invalid response' };
}
}
return { success: false, error: 'No response' };
},
/**
* Cancel tracking of a target
* @param radarId The radar ID
* @param targetId The target ID to cancel
*/
cancelTarget: async (radarId, targetId) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return false;
}
const requestJson = JSON.stringify({ radarId, targetId });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_cancel_target', requestJson);
if (result) {
try {
return JSON.parse(result) === true;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_cancel_target response: ${e}`);
return false;
}
}
return false;
},
/**
* Get ARPA settings
* @param radarId The radar ID
*/
getArpaSettings: async (radarId) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return null;
}
const requestJson = JSON.stringify({ radarId });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_get_arpa_settings', requestJson);
if (result) {
try {
const parsed = JSON.parse(result);
if (parsed.error) {
debug(`[${pluginId}] radar_get_arpa_settings error: ${parsed.error}`);
return null;
}
return parsed;
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_get_arpa_settings response: ${e}`);
return null;
}
}
return null;
},
/**
* Update ARPA settings
* @param radarId The radar ID
* @param settings Partial settings to update
*/
setArpaSettings: async (radarId, settings) => {
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return { success: false, error: 'Provider not ready' };
}
const requestJson = JSON.stringify({ radarId, settings });
const result = await callWasmRadarHandler(provider.pluginInstance, 'radar_set_arpa_settings', requestJson);
if (result) {
try {
return JSON.parse(result);
}
catch (e) {
debug(`[${pluginId}] Failed to parse radar_set_arpa_settings response: ${e}`);
return { success: false, error: 'Invalid response' };
}
}
return { success: false, error: 'No response' };
}
}
};
// Register with Signal K RadarApi
app.radarApi.register(pluginId, radarProvider);
debug(`[${pluginId}] Successfully registered as radar provider: ${providerName}`);
return 1; // Success
}
catch (error) {
debug(`Plugin register radar provider error: ${error}`);
return 0;
}
};
}
/**
* Create the sk_radar_emit_spokes host binding
*
* Convenience wrapper for radar plugins to emit binary spoke data.
* Maps to sk_emit_binary_stream with "radars/{radarId}" stream ID format.
*
* @param pluginId - Plugin identifier
* @param capabilities - Plugin capabilities
* @param app - SignalK application instance
* @param readUtf8String - Function to read UTF-8 strings from WASM memory
* @param readBinaryData - Function to read binary data from WASM memory
* @returns FFI binding function
*/
function createRadarEmitSpokesBinding(pluginId, capabilities, app, readUtf8String, readBinaryData) {
return (radarIdPtr, radarIdLen, spokeDataPtr, spokeDataLen) => {
try {
// Check radar provider capability
if (!capabilities.radarProvider) {
debug(`[${pluginId}] radarProvider capability not granted`);
return 0;
}
// Extract radar ID and spoke data from WASM memory
const radarId = readUtf8String(radarIdPtr, radarIdLen);
const spokeData = readBinaryData(spokeDataPtr, spokeDataLen);
// Only log periodically to avoid flooding logs (every ~1000 calls)
if (Math.random() < 0.001) {
debug(`[${pluginId}] sk_radar_emit_spokes: radarId="${radarId}", ` +
`dataLen=${spokeDataLen} bytes`);
}
// Validate radar belongs to this plugin
const provider = exports.wasmRadarProviders.get(pluginId);
if (!provider || !provider.pluginInstance) {
debug(`[${pluginId}] Radar provider instance not ready`);
return 0;
}
// Use general binary stream with radar stream ID format
const streamId = `radars/${radarId}`;
if (app && app.binaryStreamManager) {
app.binaryStreamManager.emitData(streamId, spokeData);
return 1; // Success
}
else {
debug(`[${pluginId}] Binary stream manager not available`);
return 0; // Failure
}
}
catch (error) {
debug(`[${pluginId}] sk_radar_emit_spokes error: ${error}`);
return 0; // Failure
}
};
}
//# sourceMappingURL=radar-provider.js.map