File size: 7,066 Bytes
7932636
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// Synapse Agriculture — Sensor Module Interface
// This WIT definition is the IMMORTAL CONTRACT between:
//   - Guest: sensor WASM modules (runs on MCU/gateway/host)
//   - Host:  wasm3 (MCU), wasmtime (gateway/host), browser VM
//
// Adding a new sensor type = add to this file, regen bindings,
// type-check catches every integration point at compile time.
//
// Changing this file is a BREAKING CHANGE across the entire stack.
// Treat it like a database migration — versioned, reviewed, irreversible.

package synapse:sensor@0.1.0;

/// Types shared across all sensor modules and host runtimes.
/// These compile into synapse-core and are used everywhere.
interface types {
    /// Sensor reading with metadata for provenance tracking
    record reading {
        /// Unix timestamp in milliseconds (from host clock or RTC)
        timestamp-ms: u64,
        /// Sensor channel identifier (maps to physical probe)
        channel: u8,
        /// Raw ADC or digital value before calibration
        raw-value: s32,
        /// Calibrated value as fixed-point (value * 1000)
        /// Using s32 instead of f32 because wasm3 soft-float
        /// on Cortex-M is slow and we don't need the precision
        calibrated-value: s32,
        /// Unit of measurement after calibration
        unit: measurement-unit,
        /// Quality/confidence flag from self-diagnostics
        quality: reading-quality,
    }

    /// Fixed-point calibration coefficients for linear cal:
    ///   calibrated = (raw * slope / 1000) + (offset / 1000)
    /// Two-point cal: derive slope/offset from known standards
    record calibration {
        slope: s32,     // multiplied by 1000
        offset: s32,    // multiplied by 1000
    }

    /// What physical quantity this reading represents
    enum measurement-unit {
        /// Water chemistry
        ph,               // pH units (0-14)
        ec,               // electrical conductivity, µS/cm
        dissolved-oxygen,  // mg/L
        orp,              // mV
        temperature-water, // °C * 1000

        /// Soil
        moisture-vwc,     // volumetric water content, % * 1000
        temperature-soil, // °C * 1000

        /// Atmosphere
        temperature-air,  // °C * 1000
        humidity,         // % * 1000
        pressure,         // hPa * 1000
        light-lux,        // lux
        light-par,        // µmol/m²/s (photosynthetically active)

        /// Power (Layer 7)
        voltage,          // mV
        current,          // mA
        power,            // mW
        battery-soc,      // state of charge, % * 10
    }

    /// Self-diagnostic quality assessment
    enum reading-quality {
        good,
        degraded,     // reading taken but outside expected range
        cal-needed,   // calibration overdue or drift detected
        fault,        // sensor not responding or shorted
    }

    /// Configuration pushed from gateway to node
    record sensor-config {
        /// Sampling interval in seconds
        sample-interval-secs: u32,
        /// Which channels to read (bitmask, up to 8 channels)
        active-channels: u8,
        /// Per-channel calibration (indexed by channel number)
        calibrations: list<calibration>,
    }

    /// Compact transmission payload for LoRa
    /// Designed to fit in a single LoRa packet (<= 242 bytes at SF7)
    record transmission-payload {
        /// Node identifier (unique per site)
        node-id: u16,
        /// Sequence number for dedup and gap detection
        sequence: u16,
        /// Battery voltage in mV (for power monitoring)
        battery-mv: u16,
        /// All readings from this sample cycle
        readings: list<reading>,
    }
}

/// Host functions provided TO the sensor module BY the runtime.
/// The MCU firmware implements these against real hardware.
/// The test harness implements them as mocks.
/// The browser implements them as no-ops or simulations.
interface host {
    use types.{reading, calibration};

    /// Read raw value from I2C sensor
    /// address: 7-bit I2C device address (e.g., 0x63 for Atlas pH)
    /// register: register to read from
    /// length: bytes to read (max 32)
    /// Returns: raw bytes from device, or error
    read-i2c: func(address: u8, register: u8, length: u8) -> result<list<u8>, sensor-error>;

    /// Read ADC channel (for analog sensors)
    /// channel: ADC channel number (0-7 on RP2350)
    /// Returns: raw 12-bit ADC value (0-4095)
    read-adc: func(channel: u8) -> result<u16, sensor-error>;

    /// Get current timestamp from RTC or host clock
    get-timestamp-ms: func() -> u64;

    /// Queue a LoRa transmission
    /// payload: CBOR-encoded bytes to transmit
    /// Returns: number of bytes queued, or error
    transmit: func(payload: list<u8>) -> result<u32, sensor-error>;

    /// Enter low-power sleep for specified duration
    /// The WASM module yields execution here; host handles
    /// actual MCU sleep modes (DORMANT on RP2350)
    sleep-ms: func(duration-ms: u32);

    /// Log a diagnostic message (forwarded to gateway if possible)
    /// Compiled out / no-op on MCU builds via feature flag
    log: func(level: log-level, message: string);

    enum sensor-error {
        /// Device not responding on bus
        not-found,
        /// Bus arbitration failure
        bus-error,
        /// Device returned NAK
        nak,
        /// Read timed out
        timeout,
        /// Transmission queue full
        queue-full,
        /// Generic / unclassified
        other,
    }

    enum log-level {
        debug,
        info,
        warn,
        error,
    }
}

/// The interface that every sensor module MUST implement.
/// This is the guest-side contract — the "main" of the module.
interface guest {
    use types.{reading, sensor-config, transmission-payload};

    /// Called once at boot. Host passes stored config.
    /// Module initializes internal state, validates config.
    /// Returns: true if init succeeded, false to signal fault.
    init: func(config: sensor-config) -> bool;

    /// Called each sample cycle by the host's main loop.
    /// Module reads sensors (via host.read-i2c / host.read-adc),
    /// applies calibration, builds readings list.
    /// Returns: payload ready for LoRa transmission.
    sample: func() -> transmission-payload;

    /// Called when gateway pushes new config (e.g., new cal values).
    /// Module validates and applies, returns success/failure.
    reconfigure: func(config: sensor-config) -> bool;

    /// Self-diagnostic. Module checks sensor responsiveness,
    /// validates readings against expected ranges, reports health.
    /// Returns: list of (channel, quality) pairs.
    diagnose: func() -> list<tuple<u8, reading-quality>>;

    use host.{sensor-error};

    /// Redeclare quality enum access for diagnose return
    use types.{reading-quality};
}

/// The complete world — wires guest to host.
/// cargo-component uses this to generate the full bindings.
world sensor-node {
    import host;
    export guest;
}