# Plugin Capabilities

## Capability Types

Declare required capabilities in `package.json`:

Capability | Description | Status
--- | --- | ---
`dataRead` | Read Signal K data model | Supported
`dataWrite` | Emit delta messages | Supported
`storage` | Write to VFS (`vfs-only`) | Supported
`httpEndpoints` | Register custom HTTP endpoints | Supported
`staticFiles` | Serve HTML/CSS/JS from `public/` folder | Supported
`network` | HTTP requests (via as-fetch) | Supported (AssemblyScript only)
`putHandlers` | Register PUT handlers for vessel control | Supported
`rawSockets` | UDP socket access for radar, NMEA, etc. | Supported
`serialPorts` | Serial port access | Planned

## Network API (AssemblyScript)

AssemblyScript plugins can make HTTP requests using the `as-fetch` library integrated into the SDK.

Requirements:

- Plugin must declare `"network": true` in manifest
- Server must be running Node.js 18+ (for native fetch support)
- Import network functions from SDK
- Must add `"transform": ["as-fetch/transform"]` to `asconfig.json` options
- Must set `"exportRuntime": true` in `asconfig.json` options

Example: HTTP GET Request

    import {
      httpGet,
      hasNetworkCapability
    } from '@signalk/assemblyscript-plugin-sdk/assembly/network'
    import { debug, setError } from '@signalk/assemblyscript-plugin-sdk/assembly'

    class MyPlugin extends Plugin {
      start(config: string): i32 {
        // Always check capability first
        if (!hasNetworkCapability()) {
          setError('Network capability not granted')
          return 1
        }

        // Make HTTP GET request
        const response = httpGet('https://api.example.com/data')
        if (response === null) {
          setError('HTTP request failed')
          return 1
        }

        debug('Received: ' + response)
        return 0
      }
    }

Available Network Functions:

    // Check if network capability is granted
    hasNetworkCapability(): boolean

    // HTTP GET request - returns response body or null on error
    httpGet(url: string): string | null

    // HTTP POST request - returns status code or -1 on error
    httpPost(url: string, body: string): i32

    // HTTP POST with response - returns response body or null
    httpPostWithResponse(url: string, body: string): string | null

    // HTTP PUT request - returns status code or -1 on error
    httpPut(url: string, body: string): i32

    // HTTP DELETE request - returns status code or -1 on error
    httpDelete(url: string): i32

    // Advanced HTTP request with full control
    httpRequest(
      url: string,
      method: string,
      body: string | null,
      contentType: string | null
    ): HttpResponse | null

Build Configuration (asconfig.json):

For plugins using network capability:

    {
      "targets": {
        "release": {
          "outFile": "build/plugin.wasm",
          "optimize": true,
          "shrinkLevel": 2,
          "runtime": "stub"
        }
      },
      "options": {
        "bindings": "esm",
        "exportRuntime": true,
        "transform": ["as-fetch/transform"]
      }
    }

Manifest Configuration:

    {
      "name": "my-plugin",
      "wasmCapabilities": {
        "network": true
      },
      "dependencies": {
        "@signalk/assemblyscript-plugin-sdk": "^0.2.0",
        "as-fetch": "^2.1.4"
      }
    }

## Raw Sockets API (UDP)

The `rawSockets` capability enables direct UDP socket access for plugins that need to communicate with devices like:

- Marine radars (Navico, Raymarine, Furuno, Garmin)
- NMEA 0183 over UDP
- AIS receivers
- Other marine electronics using UDP multicast

Requirements:

- Plugin must declare `"rawSockets": true` in manifest
- Sockets are non-blocking (poll-based receive)
- Automatic cleanup when plugin stops

Manifest Configuration:

    {
      "name": "my-radar-plugin",
      "wasmManifest": "plugin.wasm",
      "wasmCapabilities": {
        "rawSockets": true,
        "dataWrite": true
      }
    }

FFI Functions Available:

Function | Signature | Description
--- | --- | ---
`sk_udp_create` | `(type: i32) -> i32` | Create socket (0=udp4, 1=udp6). Returns socket_id or -1
`sk_udp_bind` | `(socket_id, port) -> i32` | Bind to port (0=any). Returns 0 or -1
`sk_udp_join_multicast` | `(socket_id, addr_ptr, addr_len, iface_ptr, iface_len) -> i32` | Join multicast group
`sk_udp_leave_multicast` | `(socket_id, addr_ptr, addr_len, iface_ptr, iface_len) -> i32` | Leave multicast group
`sk_udp_set_multicast_ttl` | `(socket_id, ttl) -> i32` | Set multicast TTL
`sk_udp_set_multicast_loopback` | `(socket_id, enabled) -> i32` | Enable/disable loopback
`sk_udp_set_broadcast` | `(socket_id, enabled) -> i32` | Enable/disable broadcast
`sk_udp_send` | `(socket_id, addr_ptr, addr_len, port, data_ptr, data_len) -> i32` | Send datagram
`sk_udp_recv` | `(socket_id, buf_ptr, buf_max_len, addr_out_ptr, port_out_ptr) -> i32` | Receive datagram (non-blocking)
`sk_udp_pending` | `(socket_id) -> i32` | Get number of buffered datagrams
`sk_udp_close` | `(socket_id) -> void` | Close socket

Rust Example:

    #[link(wasm_import_module = "env")]
    extern "C" {
        fn sk_udp_create(socket_type: i32) -> i32;
        fn sk_udp_bind(socket_id: i32, port: u16) -> i32;
        fn sk_udp_join_multicast(
            socket_id: i32,
            addr_ptr: *const u8, addr_len: usize,
            iface_ptr: *const u8, iface_len: usize
        ) -> i32;
        fn sk_udp_recv(
            socket_id: i32,
            buf_ptr: *mut u8, buf_max_len: usize,
            addr_out_ptr: *mut u8, port_out_ptr: *mut u16
        ) -> i32;
        fn sk_udp_close(socket_id: i32);
    }

    // Example: Radar discovery
    fn start_radar_locator() -> i32 {
        // Create UDP socket
        let socket_id = unsafe { sk_udp_create(0) }; // udp4
        if socket_id < 0 {
            return -1;
        }

        // Bind to radar discovery port
        if unsafe { sk_udp_bind(socket_id, 6878) } < 0 {
            return -1;
        }

        // Join radar multicast group
        let group = "239.254.2.0";
        let iface = "";
        if unsafe { sk_udp_join_multicast(socket_id, group.as_ptr(), group.len(), iface.as_ptr(), iface.len()) } < 0 {
            return -1;
        }

        socket_id
    }

Important Notes:

- Receive is non-blocking - returns 0 if no data available
- Incoming datagrams are buffered (max 1000 per socket)
- Oldest datagrams are dropped if buffer is full
- All sockets are automatically closed when plugin stops
- Use `sk_udp_pending()` to check if data is available before calling `sk_udp_recv()`

## Raw Sockets API (TCP)

The `rawSockets` capability also enables TCP socket access for plugins that need persistent connections to devices:

- Marine radars with TCP control (Furuno, Garmin)
- Devices requiring handshake/login protocols
- Any marine electronics using TCP

TCP sockets support both line-buffered mode (for text protocols with `\r\n` terminators) and raw mode (for binary protocols).

FFI Functions Available:

Function | Signature | Description
--- | --- | ---
`sk_tcp_create` | `() -> i32` | Create TCP socket. Returns socket_id or -1
`sk_tcp_connect` | `(socket_id, addr_ptr, addr_len, port) -> i32` | Initiate connection (non-blocking). Returns 0 or -1
`sk_tcp_connected` | `(socket_id) -> i32` | Check connection status. Returns 1 if connected, 0 otherwise
`sk_tcp_set_line_buffering` | `(socket_id, enabled) -> i32` | Set buffering mode (1=line, 0=raw). Default: line
`sk_tcp_send` | `(socket_id, data_ptr, data_len) -> i32` | Send data. Returns bytes sent or -1
`sk_tcp_recv_line` | `(socket_id, buf_ptr, buf_max_len) -> i32` | Receive complete line (line mode). Returns len or 0
`sk_tcp_recv_raw` | `(socket_id, buf_ptr, buf_max_len) -> i32` | Receive raw data (raw mode). Returns len or 0
`sk_tcp_pending` | `(socket_id) -> i32` | Get buffered item count
`sk_tcp_close` | `(socket_id) -> void` | Close socket

Rust Example:

    #[link(wasm_import_module = "env")]
    extern "C" {
        fn sk_tcp_create() -> i32;
        fn sk_tcp_connect(socket_id: i32, addr_ptr: *const u8, addr_len: usize, port: u16) -> i32;
        fn sk_tcp_connected(socket_id: i32) -> i32;
        fn sk_tcp_set_line_buffering(socket_id: i32, enabled: i32) -> i32;
        fn sk_tcp_send(socket_id: i32, data_ptr: *const u8, data_len: usize) -> i32;
        fn sk_tcp_recv_line(socket_id: i32, buf_ptr: *mut u8, buf_max_len: usize) -> i32;
        fn sk_tcp_recv_raw(socket_id: i32, buf_ptr: *mut u8, buf_max_len: usize) -> i32;
        fn sk_tcp_pending(socket_id: i32) -> i32;
        fn sk_tcp_close(socket_id: i32);
    }

    // Example: Furuno radar control connection
    fn connect_furuno_radar(ip: &str, port: u16) -> i32 {
        // Create TCP socket
        let socket_id = unsafe { sk_tcp_create() };
        if socket_id < 0 {
            return -1;
        }

        // Initiate connection (non-blocking)
        if unsafe { sk_tcp_connect(socket_id, ip.as_ptr(), ip.len(), port) } < 0 {
            return -1;
        }

        socket_id
    }

    fn poll_connection(socket_id: i32) {
        // Check if connected
        if unsafe { sk_tcp_connected(socket_id) } != 1 {
            return; // Still connecting
        }

        // Send command with \r\n terminator
        let cmd = "$S69,2,0,0,60,300,0\r\n";
        unsafe { sk_tcp_send(socket_id, cmd.as_ptr(), cmd.len()) };

        // Receive response line
        let mut buf = [0u8; 256];
        let len = unsafe { sk_tcp_recv_line(socket_id, buf.as_mut_ptr(), buf.len()) };
        if len > 0 {
            // Process response
        }
    }

Important Notes:

- Connection is non-blocking - poll `sk_tcp_connected()` until connected
- Line-buffered mode (default) splits incoming data on `\r\n` or `\n`
- Raw mode returns data as it arrives (for binary protocols)
- Use `sk_tcp_pending()` to check if data is available
- All sockets are automatically closed when plugin stops

## PUT Handlers API

WASM plugins can register PUT handlers to respond to PUT requests from clients, enabling vessel control and configuration management.

Requirements:

- Plugin must declare `"putHandlers": true` in manifest
- Import PUT handler functions from FFI
- Register handlers during `plugin_start()`
- Export handler functions with correct naming convention

Manifest Configuration:

    {
      "name": "my-plugin",
      "wasmManifest": "plugin.wasm",
      "wasmCapabilities": {
        "putHandlers": true
      }
    }

Handler Naming Convention:

Format: `handle_put_{context}_{path}`

- Replace all dots (`.`) with underscores (`_`)
- Convert to lowercase (recommended)

Examples:

Context | Path | Handler Function Name
--- | --- | ---
`vessels.self` | `navigation.anchor.position` | `handle_put_vessels_self_navigation_anchor_position`
`vessels.self` | `steering.autopilot.target.headingTrue` | `handle_put_vessels_self_steering_autopilot_target_headingTrue`

Response Format:

    {
      "state": "COMPLETED",
      "statusCode": 200,
      "message": "Operation successful"
    }

- `state` - Request state: `COMPLETED` or `PENDING`
- `statusCode` - HTTP status code (200, 400, 403, 500, 501)
- `message` - Human-readable message (optional)

## Storage API

Plugins have access to isolated virtual filesystem:

    use std::fs;

    fn save_state() {
        // Plugin sees "/" as its VFS root
        fs::write("/data/state.json", state_json).unwrap();
    }

    fn load_state() -> String {
        fs::read_to_string("/data/state.json").unwrap_or_default()
    }

VFS Structure:

    / (VFS root)
    ├── data/      # Persistent storage
    ├── config/    # Plugin-managed config
    └── tmp/       # Temporary files

## Delta Emission

Emit delta messages to update Signal K data:

    fn emit_position_delta() {
        let delta = r#"{
            "context": "vessels.self",
            "updates": [{
                "source": {
                    "label": "example-wasm",
                    "type": "plugin"
                },
                "timestamp": "2025-12-01T10:00:00.000Z",
                "values": [{
                    "path": "navigation.position",
                    "value": {
                        "latitude": 60.1,
                        "longitude": 24.9
                    }
                }]
            }]
        }"#;

        handle_message(&delta);
    }