@bradtech/sensor 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@bradtech/sensor",
3
+ "version": "1.1.0",
4
+ "description": "Foundational sensor abstraction, conversion interfaces, registry and replay engine for Brad sensor converters",
5
+ "author": "Olivier Lépine <olivier@lepine.fr>",
6
+ "license": "AGPL-3.0-or-later",
7
+ "copyright": "Copyright (C) 2026 Olivier Lépine",
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "registry": "https://registry.npmjs.org/"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/bradtech-oss/stack.git",
15
+ "directory": "packages/sensor"
16
+ },
17
+ "keywords": [
18
+ "iot",
19
+ "sensor",
20
+ "telemetry",
21
+ "agronomy",
22
+ "agriculture",
23
+ "brad",
24
+ "quatrain",
25
+ "replay-engine",
26
+ "agpl-v3"
27
+ ],
28
+ "main": "./dist/index.js",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "bun": "./src/index.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "src",
43
+ "LICENSE",
44
+ "README.md",
45
+ "HOWTO.md",
46
+ "!**/*.test.ts"
47
+ ],
48
+ "scripts": {
49
+ "clean": "rm -rf dist",
50
+ "build": "tsc",
51
+ "wbuild": "tsc --watch",
52
+ "test": "bun test"
53
+ },
54
+ "dependencies": {
55
+ "@bradtech/types": "workspace:*",
56
+ "@quatrain/core": "^1.2.17"
57
+ },
58
+ "devDependencies": {
59
+ "@tsconfig/recommended": "^1.0.1",
60
+ "@types/node": "^22.10.1",
61
+ "typescript": "^5.1.5"
62
+ }
63
+ }
@@ -0,0 +1,76 @@
1
+ import type { ConversionContext, ConversionOutput, SensorConverterInterface } from './types'
2
+
3
+ /**
4
+ * Abstract foundation class for all sensor converters.
5
+ * Implements defensive physical clamping and automated confidence score degradation.
6
+ *
7
+ * @template TRaw - Raw sensor payload type.
8
+ * @template TContext - Environmental and spatial context type.
9
+ */
10
+ export abstract class BaseSensorConverter<TRaw = any, TContext extends ConversionContext = ConversionContext>
11
+ implements SensorConverterInterface<TRaw, TContext>
12
+ {
13
+ /** Domain sensor family classification (e.g. 'air', 'soil', 'weather', 'power', 'acoustic') */
14
+ abstract readonly sensorFamily: string
15
+
16
+ /** Unique machine-readable algorithmic identifier */
17
+ abstract readonly modelCode: string
18
+
19
+ /** Semantic version string of the algorithm implementation */
20
+ abstract readonly modelVersion: string
21
+
22
+ /** Detailed description of conversion algorithm */
23
+ abstract readonly description: string
24
+
25
+ /**
26
+ * Executes the mathematical conversion from raw sensor signals to normalized physical outputs.
27
+ *
28
+ * @param raw - The raw sensor reading or observation payload.
29
+ * @param context - Optional environmental, soil texture, or calibration parameters.
30
+ * @returns An array of normalized physical conversion outputs.
31
+ */
32
+ abstract convert(raw: TRaw, context?: TContext): ConversionOutput[]
33
+
34
+ /**
35
+ * Clamps a numerical value within strict physical limits while calculating
36
+ * a continuous quality confidence score (between 0.0 and 1.0).
37
+ *
38
+ * @param value - The input numerical reading to evaluate.
39
+ * @param minPhysical - The absolute lower physical limit (e.g. -40°C for ambient air).
40
+ * @param maxPhysical - The absolute upper physical limit (e.g. +70°C for ambient air).
41
+ * @param minWarning - Optional soft lower warning threshold where confidence begins to degrade.
42
+ * @param maxWarning - Optional soft upper warning threshold where confidence begins to degrade.
43
+ * @returns Object containing the clamped value and the calculated confidence score.
44
+ */
45
+ protected clampWithConfidence(
46
+ value: number,
47
+ minPhysical: number,
48
+ maxPhysical: number,
49
+ minWarning?: number,
50
+ maxWarning?: number,
51
+ ): { value: number; confidence: number } {
52
+ if (isNaN(value) || !isFinite(value)) {
53
+ return { value: 0, confidence: 0.0 }
54
+ }
55
+
56
+ if (value < minPhysical) {
57
+ return { value: minPhysical, confidence: 0.1 }
58
+ }
59
+ if (value > maxPhysical) {
60
+ return { value: maxPhysical, confidence: 0.1 }
61
+ }
62
+
63
+ let confidence = 1.0
64
+
65
+ if (minWarning !== undefined && value < minWarning) {
66
+ confidence = Math.max(0.3, 1.0 - (minWarning - value) / (minWarning - minPhysical))
67
+ } else if (maxWarning !== undefined && value > maxWarning) {
68
+ confidence = Math.max(0.3, 1.0 - (value - maxWarning) / (maxPhysical - maxWarning))
69
+ }
70
+
71
+ return {
72
+ value: Number(value.toFixed(4)),
73
+ confidence: Number(confidence.toFixed(2)),
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,55 @@
1
+ import type { SensorConverterInterface } from './types'
2
+
3
+ /**
4
+ * Global registry for discovering and indexing sensor converter instances
5
+ * by sensor family and algorithmic model code.
6
+ */
7
+ export class ConverterRegistry {
8
+ /** Internal lookup map indexed by `family:modelCode` */
9
+ private static _converters: Map<string, SensorConverterInterface> = new Map()
10
+
11
+ /**
12
+ * Registers an instantiated sensor converter into the global registry.
13
+ *
14
+ * @param converter - The sensor converter instance to index.
15
+ */
16
+ static register(converter: SensorConverterInterface): void {
17
+ const key = `${converter.sensorFamily}:${converter.modelCode}`
18
+ this._converters.set(key, converter)
19
+ }
20
+
21
+ /**
22
+ * Looks up a converter by its sensor family and model code.
23
+ *
24
+ * @param sensorFamily - The domain family (e.g. 'soil', 'air').
25
+ * @param modelCode - The unique model code (e.g. 'soil-vwc-texture-calibrated').
26
+ * @returns The matching converter instance, or undefined if not found.
27
+ */
28
+ static get(sensorFamily: string, modelCode: string): SensorConverterInterface | undefined {
29
+ return this._converters.get(`${sensorFamily}:${modelCode}`)
30
+ }
31
+
32
+ /**
33
+ * Retrieves all converters registered under a specific sensor family.
34
+ *
35
+ * @param sensorFamily - The sensor family to filter by.
36
+ * @returns An array of registered converters for the family.
37
+ */
38
+ static getByFamily(sensorFamily: string): SensorConverterInterface[] {
39
+ return Array.from(this._converters.values()).filter((c) => c.sensorFamily === sensorFamily)
40
+ }
41
+
42
+ /**
43
+ * Returns all currently registered converter instances.
44
+ */
45
+ static getAll(): SensorConverterInterface[] {
46
+ return Array.from(this._converters.values())
47
+ }
48
+
49
+ /**
50
+ * Clears all registered converters from the registry.
51
+ */
52
+ static clear(): void {
53
+ this._converters.clear()
54
+ }
55
+ }
@@ -0,0 +1,133 @@
1
+ import type {
2
+ CompanyUri,
3
+ DeviceUri,
4
+ InterfaceContractUri,
5
+ OkfMetricUri,
6
+ PlotUri,
7
+ } from '@bradtech/types'
8
+ import type { ConversionContext, ConversionOutput, SensorConverterInterface } from './types'
9
+
10
+ /**
11
+ * Input contract for historical raw data points to be replayed.
12
+ */
13
+ export interface RawDataPointInput {
14
+ /** Unique UUID of the source raw data point */
15
+ id: string
16
+ /** Canonical device URI (e.g. 'probes/b25s004') */
17
+ device: DeviceUri | (string & {})
18
+ /** Canonical plot URI (e.g. 'plots/e5eadee3-...') */
19
+ plot?: PlotUri | (string & {})
20
+ /** Canonical company URI (e.g. 'companies/32c18f05-...') */
21
+ company?: CompanyUri | (string & {})
22
+ /** Raw metric identifier */
23
+ metric: OkfMetricUri | (string & {})
24
+ /** Raw numerical measurement value */
25
+ value: number
26
+ /** Time of observation */
27
+ timestamp: string | Date
28
+ /** Original measurement metadata */
29
+ metadata?: Record<string, any>
30
+ }
31
+
32
+ /**
33
+ * Output contract representing a newly computed data point with complete lineage and audit trail.
34
+ */
35
+ export interface ComputedDataPointOutput {
36
+ /** Canonical device URI */
37
+ device: DeviceUri | (string & {})
38
+ /** Canonical plot URI */
39
+ plot?: PlotUri | (string & {})
40
+ /** Canonical company URI */
41
+ company?: CompanyUri | (string & {})
42
+ /** Computed metric identifier */
43
+ metric: OkfMetricUri | (string & {})
44
+ /** Derived calculated physical value */
45
+ value: number
46
+ /** Physical unit symbol */
47
+ unit: string
48
+ /** Fixed classification indicating algorithmic computation */
49
+ kind: 'computed'
50
+ /** Numerical confidence score */
51
+ confidence: number
52
+ /** Timestamp matching the source observation */
53
+ timestamp: string | Date
54
+ /** Comprehensive lineage metadata */
55
+ metadata: {
56
+ /** Quatrain metadata interface contract identifier */
57
+ interface: InterfaceContractUri | (string & {})
58
+
59
+ /** Algorithmic model code executed during replay */
60
+ modelCode: string
61
+ /** Algorithm semver version executed during replay */
62
+ modelVersion: string
63
+ /** Source raw data point UUID providing audit traceability */
64
+ sourceDataPointId: string
65
+ /** Execution parameters, calibrations, or texture presets used */
66
+ executionParams?: Record<string, any>
67
+ [key: string]: any
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Deterministic Replay Engine.
73
+ * Allows re-processing historical timeseries data points through updated or newly introduced
74
+ * agronomic algorithms while maintaining full provenance and audit traceability.
75
+ */
76
+ export class ReplayEngine {
77
+ /**
78
+ * Re-evaluates an array of raw historical DataPoints using a specified SensorConverter
79
+ * and generates a set of fully traceable 'computed' DataPoints.
80
+ *
81
+ * @param dataPoints - Collection of raw input measurements.
82
+ * @param converter - The target conversion algorithm instance to execute.
83
+ * @param contextOverride - Optional contextual overrides (e.g. newly calibrated soil textures).
84
+ * @returns Array of transformed computed DataPoints.
85
+ */
86
+ static replay(
87
+ dataPoints: RawDataPointInput[],
88
+ converter: SensorConverterInterface,
89
+ contextOverride?: Partial<ConversionContext>,
90
+ ): ComputedDataPointOutput[] {
91
+ const results: ComputedDataPointOutput[] = []
92
+
93
+ for (const dp of dataPoints) {
94
+ const context: ConversionContext = {
95
+ deviceId: dp.device,
96
+ plotId: dp.plot,
97
+ companyId: dp.company,
98
+ timestamp: dp.timestamp,
99
+ ...contextOverride,
100
+ }
101
+
102
+ const outputs: ConversionOutput[] = converter.convert(dp.value, context)
103
+
104
+ for (const out of outputs) {
105
+ results.push({
106
+ device: dp.device,
107
+ plot: dp.plot,
108
+ company: dp.company,
109
+ metric: out.metric,
110
+ value: out.value,
111
+ unit: out.unit,
112
+ kind: 'computed',
113
+ confidence: out.confidence,
114
+ timestamp: dp.timestamp,
115
+ metadata: {
116
+ interface: '@bradtech/types:ComputedMetadataInterface',
117
+ modelCode: converter.modelCode,
118
+ modelVersion: converter.modelVersion,
119
+ sourceDataPointId: dp.id,
120
+ executionParams: {
121
+ replayedAt: new Date().toISOString(),
122
+ contextUsed: context,
123
+ rawInput: dp.value,
124
+ },
125
+ ...out.metadata,
126
+ },
127
+ })
128
+ }
129
+ }
130
+
131
+ return results
132
+ }
133
+ }
package/src/Sensor.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { Core } from '@quatrain/core'
2
+ import type { ConversionContext, ConversionOutput, SensorConverterInterface } from './types'
3
+
4
+ export type SensorAdapterRegistry = { [alias: string]: SensorConverterInterface<any, any> }
5
+
6
+ /**
7
+ * Sensor Micro-Framework Singleton Facade
8
+ * Extends Quatrain Core, manages instantiated sensor adapters and coordinates conversions.
9
+ */
10
+ export class Sensor extends Core {
11
+ /** Reference ID for the primary default sensor adapter. */
12
+ static defaultAdapter = '@default'
13
+
14
+ /** Domain-specific Core Logger. */
15
+ static logger = this.addLogger('Sensor')
16
+
17
+ /** Registry of bound, instantiated sensor adapters */
18
+ protected static _adapters: SensorAdapterRegistry = {}
19
+
20
+ /**
21
+ * Appends a new instantiated sensor adapter into the registry.
22
+ *
23
+ * @param adapter - The instantiated sensor converter/adapter.
24
+ * @param alias - The lookup alias (defaults to adapter.modelCode).
25
+ * @param setDefault - True to switch standard default adapter.
26
+ */
27
+ static addAdapter(
28
+ adapter: SensorConverterInterface<any, any>,
29
+ alias: string = adapter.modelCode,
30
+ setDefault: boolean = false,
31
+ ): typeof Sensor {
32
+ this._adapters[alias] = adapter
33
+ this.logger.debug?.(`Registered sensor adapter: '${alias}' (${adapter.constructor.name})`)
34
+
35
+ if (setDefault) {
36
+ this.defaultAdapter = alias
37
+ }
38
+ return this
39
+ }
40
+
41
+ /**
42
+ * Retrieves an instantiated adapter by alias or model code.
43
+ *
44
+ * @param alias - Adapter alias or modelCode (defaults to defaultAdapter).
45
+ * @returns Instantiated sensor adapter.
46
+ * @throws When adapter is not found in registry.
47
+ */
48
+ static getAdapter<T extends SensorConverterInterface<any, any> = SensorConverterInterface<any, any>>(
49
+ alias: string = this.defaultAdapter,
50
+ ): T {
51
+ if (this._adapters[alias]) {
52
+ return this._adapters[alias] as T
53
+ }
54
+ throw new Error(`[Sensor] Unknown sensor adapter alias: '${alias}'. Registered adapters: [${Object.keys(this._adapters).join(', ')}]`)
55
+ }
56
+
57
+ /**
58
+ * Checks if a sensor adapter is currently registered.
59
+ */
60
+ static hasAdapter(alias: string): boolean {
61
+ return Boolean(this._adapters[alias])
62
+ }
63
+
64
+ /**
65
+ * Returns the list of all registered adapter aliases.
66
+ */
67
+ static listAdapters(): string[] {
68
+ return Object.keys(this._adapters)
69
+ }
70
+
71
+ /**
72
+ * Clears all registered adapters (useful for test resets).
73
+ */
74
+ static reset(): void {
75
+ this._adapters = {}
76
+ this.defaultAdapter = '@default'
77
+ }
78
+
79
+ /**
80
+ * Direct conversion helper using a named adapter.
81
+ */
82
+ static convert<TRaw = any, TContext extends ConversionContext = ConversionContext>(
83
+ alias: string,
84
+ raw: TRaw,
85
+ context?: TContext,
86
+ ): ConversionOutput[] {
87
+ const adapter = this.getAdapter(alias)
88
+ return adapter.convert(raw, context)
89
+ }
90
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './types'
2
+ export * from './BaseSensorConverter'
3
+ export * from './ConverterRegistry'
4
+ export * from './ReplayEngine'
5
+ export * from './Sensor'
package/src/types.ts ADDED
@@ -0,0 +1,97 @@
1
+ import type {
2
+ CompanyUri,
3
+ DeviceUri,
4
+ OkfMetricUri,
5
+ PlotUri,
6
+ QudtUnitUri,
7
+ } from '@bradtech/types'
8
+
9
+ /**
10
+ * Standard output record produced by a sensor converter.
11
+ * Encapsulates the converted metric, physical value, standard unit, and quality index.
12
+ */
13
+ export interface ConversionOutput {
14
+ /** Canonical OKF metric identifier (e.g. 'okf:soil/moisture/10cm', 'okf:agronomy/microclimate/canopy_temperature') */
15
+ metric: OkfMetricUri | (string & {})
16
+ /** Calibrated, normalized physical value */
17
+ value: number
18
+ /** Human-readable unit symbol (e.g. '%', '°C', 'hPa', 'W/m²', 'V') */
19
+ unit: string
20
+ /** QUDT Semantic Web ontology URI for strict physical unit interoperability */
21
+ qudtUri: QudtUnitUri | (string & {})
22
+ /** Quality and confidence score between 0.0 (erroneous/degraded) and 1.0 (nominal) */
23
+ confidence: number
24
+ /** Supplementary calculation parameters, algorithm metadata, or intermediate variables */
25
+ metadata?: Record<string, any>
26
+ }
27
+
28
+ /**
29
+ * Mathematical parameters for linear regression calibration models ($y = a \cdot x + b$).
30
+ */
31
+ export interface LinearRegressionModel {
32
+ /** Slope coefficient ($a$ in $y = a \cdot x + b$) */
33
+ slope: number
34
+ /** Y-intercept offset ($b$ in $y = a \cdot x + b$) */
35
+ intercept: number
36
+ /** Optional coefficient of determination ($R^2$) representing model fit accuracy */
37
+ r2?: number
38
+ /** Optional human-readable description or laboratory calibration certificate reference */
39
+ modelLabel?: string
40
+ }
41
+
42
+ /**
43
+ * Environmental, spatial, and device context supplied during sensor conversion.
44
+ */
45
+ export interface ConversionContext {
46
+ /** Canonical device URI (e.g. 'probes/b25s004', 'weather-stations/b26w001') */
47
+ deviceId?: DeviceUri | (string & {})
48
+ /** Canonical plot URI (e.g. 'plots/e5eadee3-bb95-4cf0-b1f5-45db93bbfa81') */
49
+ plotId?: PlotUri | (string & {})
50
+ /** Canonical tenant/company URI */
51
+ companyId?: CompanyUri | (string & {})
52
+
53
+ /** Observation timestamp */
54
+ timestamp?: string | Date
55
+ /** Soil USDA textural class preset */
56
+ soilTexture?: 'sand' | 'loam' | 'clay' | 'silt' | 'peat' | 'default'
57
+ /** Specific custom linear regression model calibrated for the target plot */
58
+ soilLinearRegression?: LinearRegressionModel
59
+ /** Generic hardware or algorithm calibration key-value parameters */
60
+ calibration?: Record<string, any>
61
+ /** Ambient air temperature in °C for thermal compensation */
62
+ ambientTemperature?: number
63
+ /** Local atmospheric pressure in hPa */
64
+ atmosphericPressureHpa?: number
65
+ /** Elevation above sea level in meters (used for QNH and barometric reduction) */
66
+ altitudeMeters?: number
67
+ /** Battery chemistry classification */
68
+ batteryChemistry?: 'li_ion' | 'lifepo4' | 'alkaline' | 'supercap'
69
+ /** Arbitrary contextual parameters */
70
+ [key: string]: any
71
+ }
72
+
73
+ /**
74
+ * Universal interface contract for all Brad sensor conversion adapters.
75
+ *
76
+ * @template TRaw - Input payload data type (raw number, binary buffer, or composite object).
77
+ * @template TContext - Environmental and spatial context type.
78
+ */
79
+ export interface SensorConverterInterface<TRaw = any, TContext extends ConversionContext = ConversionContext> {
80
+ /** Sensor domain family classification (e.g. 'air', 'soil', 'weather', 'power', 'acoustic') */
81
+ readonly sensorFamily: string
82
+ /** Unique machine-readable algorithmic model code (e.g. 'soil-vwc-texture-calibrated') */
83
+ readonly modelCode: string
84
+ /** Semantic version string of the algorithm implementation (e.g. '1.0.0') */
85
+ readonly modelVersion: string
86
+ /** Human-readable description of the conversion algorithm and its theoretical foundation */
87
+ readonly description: string
88
+
89
+ /**
90
+ * Executes the mathematical conversion from raw sensor signals to normalized physical outputs.
91
+ *
92
+ * @param raw - The raw sensor reading or observation payload.
93
+ * @param context - Optional environmental, soil texture, or calibration parameters.
94
+ * @returns An array of normalized physical conversion outputs.
95
+ */
96
+ convert(raw: TRaw, context?: TContext): ConversionOutput[]
97
+ }