dspx 0.1.1-alpha.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/.github/workflows/ci.yml +185 -0
- package/.vscode/c_cpp_properties.json +17 -0
- package/.vscode/settings.json +68 -0
- package/.vscode/tasks.json +28 -0
- package/DISCLAIMER.md +32 -0
- package/LICENSE +21 -0
- package/README.md +1803 -0
- package/ROADMAP.md +192 -0
- package/TECHNICAL_DEBT.md +165 -0
- package/binding.gyp +65 -0
- package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
- package/docs/AUTHENTICATION_SECURITY.md +396 -0
- package/docs/BACKEND_IMPROVEMENTS.md +399 -0
- package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
- package/docs/FFT_IMPLEMENTATION.md +490 -0
- package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
- package/docs/FFT_USER_GUIDE.md +494 -0
- package/docs/FILTERS_IMPLEMENTATION.md +260 -0
- package/docs/FILTER_API_GUIDE.md +418 -0
- package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
- package/docs/LOGGER_API_REFERENCE.md +350 -0
- package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
- package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
- package/docs/PHASES_5_7_SUMMARY.md +403 -0
- package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
- package/docs/SIMD_OPTIMIZATIONS.md +211 -0
- package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
- package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
- package/docs/TIMESERIES_QUICK_REF.md +85 -0
- package/docs/advanced.md +559 -0
- package/docs/time-series-guide.md +617 -0
- package/docs/time-series-migration.md +376 -0
- package/jest.config.js +37 -0
- package/package.json +42 -0
- package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
- package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
- package/scripts/test.js +24 -0
- package/src/build/dsp-ts-redis.node +0 -0
- package/src/native/DspPipeline.cc +675 -0
- package/src/native/DspPipeline.h +44 -0
- package/src/native/FftBindings.cc +817 -0
- package/src/native/FilterBindings.cc +1001 -0
- package/src/native/IDspStage.h +53 -0
- package/src/native/adapters/InterpolatorStage.h +201 -0
- package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
- package/src/native/adapters/MovingAverageStage.h +306 -0
- package/src/native/adapters/RectifyStage.h +88 -0
- package/src/native/adapters/ResamplerStage.h +238 -0
- package/src/native/adapters/RmsStage.h +299 -0
- package/src/native/adapters/SscStage.h +121 -0
- package/src/native/adapters/VarianceStage.h +307 -0
- package/src/native/adapters/WampStage.h +114 -0
- package/src/native/adapters/WaveformLengthStage.h +115 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
- package/src/native/core/FftEngine.cc +441 -0
- package/src/native/core/FftEngine.h +224 -0
- package/src/native/core/FirFilter.cc +324 -0
- package/src/native/core/FirFilter.h +149 -0
- package/src/native/core/IirFilter.cc +576 -0
- package/src/native/core/IirFilter.h +210 -0
- package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
- package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
- package/src/native/core/MovingAverageFilter.cc +18 -0
- package/src/native/core/MovingAverageFilter.h +135 -0
- package/src/native/core/MovingFftFilter.cc +291 -0
- package/src/native/core/MovingFftFilter.h +203 -0
- package/src/native/core/MovingVarianceFilter.cc +194 -0
- package/src/native/core/MovingVarianceFilter.h +114 -0
- package/src/native/core/MovingZScoreFilter.cc +215 -0
- package/src/native/core/MovingZScoreFilter.h +113 -0
- package/src/native/core/Policies.h +352 -0
- package/src/native/core/RmsFilter.cc +18 -0
- package/src/native/core/RmsFilter.h +131 -0
- package/src/native/core/SscFilter.cc +16 -0
- package/src/native/core/SscFilter.h +137 -0
- package/src/native/core/WampFilter.cc +16 -0
- package/src/native/core/WampFilter.h +101 -0
- package/src/native/core/WaveformLengthFilter.cc +17 -0
- package/src/native/core/WaveformLengthFilter.h +98 -0
- package/src/native/utils/CircularBufferArray.cc +336 -0
- package/src/native/utils/CircularBufferArray.h +62 -0
- package/src/native/utils/CircularBufferVector.cc +145 -0
- package/src/native/utils/CircularBufferVector.h +45 -0
- package/src/native/utils/NapiUtils.cc +53 -0
- package/src/native/utils/NapiUtils.h +21 -0
- package/src/native/utils/SimdOps.h +870 -0
- package/src/native/utils/SlidingWindowFilter.cc +239 -0
- package/src/native/utils/SlidingWindowFilter.h +159 -0
- package/src/native/utils/TimeSeriesBuffer.cc +205 -0
- package/src/native/utils/TimeSeriesBuffer.h +140 -0
- package/src/ts/CircularLogBuffer.ts +87 -0
- package/src/ts/DriftDetector.ts +331 -0
- package/src/ts/TopicRouter.ts +428 -0
- package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
- package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
- package/src/ts/__tests__/Chaining.test.ts +387 -0
- package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
- package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
- package/src/ts/__tests__/DriftDetector.test.ts +389 -0
- package/src/ts/__tests__/Fft.test.ts +484 -0
- package/src/ts/__tests__/ListState.test.ts +153 -0
- package/src/ts/__tests__/Logger.test.ts +208 -0
- package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
- package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
- package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
- package/src/ts/__tests__/MovingAverage.test.ts +322 -0
- package/src/ts/__tests__/RMS.test.ts +315 -0
- package/src/ts/__tests__/Rectify.test.ts +272 -0
- package/src/ts/__tests__/Redis.test.ts +456 -0
- package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
- package/src/ts/__tests__/Tap.test.ts +164 -0
- package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
- package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
- package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
- package/src/ts/__tests__/TimeSeries.test.ts +254 -0
- package/src/ts/__tests__/TopicRouter.test.ts +332 -0
- package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
- package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
- package/src/ts/__tests__/Variance.test.ts +509 -0
- package/src/ts/__tests__/WaveformLength.test.ts +147 -0
- package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
- package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
- package/src/ts/advanced-dsp.ts +566 -0
- package/src/ts/backends.ts +1137 -0
- package/src/ts/bindings.ts +1225 -0
- package/src/ts/easter-egg.ts +42 -0
- package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
- package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
- package/src/ts/examples/MovingAverage/test-state.ts +85 -0
- package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
- package/src/ts/examples/RMS/test-state.ts +97 -0
- package/src/ts/examples/RMS/test-streaming.ts +253 -0
- package/src/ts/examples/Rectify/test-state.ts +107 -0
- package/src/ts/examples/Rectify/test-streaming.ts +242 -0
- package/src/ts/examples/Variance/test-state.ts +195 -0
- package/src/ts/examples/Variance/test-streaming.ts +260 -0
- package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
- package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
- package/src/ts/examples/advanced-dsp-examples.ts +397 -0
- package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
- package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
- package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
- package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
- package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
- package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
- package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
- package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
- package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
- package/src/ts/examples/chaining/test-chaining.ts +52 -0
- package/src/ts/examples/emg-features-example.ts +284 -0
- package/src/ts/examples/fft-example.ts +309 -0
- package/src/ts/examples/fft-examples.ts +349 -0
- package/src/ts/examples/filter-examples.ts +320 -0
- package/src/ts/examples/list-state-example.ts +131 -0
- package/src/ts/examples/logger-example.ts +91 -0
- package/src/ts/examples/notch-filter-examples.ts +243 -0
- package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
- package/src/ts/examples/phase6-7/production-observability.ts +476 -0
- package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
- package/src/ts/examples/redis/redis-example.ts +202 -0
- package/src/ts/examples/redis-example.ts +202 -0
- package/src/ts/examples/simd-benchmark.ts +126 -0
- package/src/ts/examples/tap-debugging.ts +230 -0
- package/src/ts/examples/timeseries/comparison-example.ts +290 -0
- package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
- package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
- package/src/ts/examples/waveform-length-example.ts +139 -0
- package/src/ts/fft.ts +722 -0
- package/src/ts/filters.ts +1078 -0
- package/src/ts/index.ts +120 -0
- package/src/ts/types.ts +589 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 6-7: RedisTimeSeries Integration Example
|
|
3
|
+
*
|
|
4
|
+
* Shows how to pipe DSP results directly to RedisTimeSeries for:
|
|
5
|
+
* - Real-time Grafana dashboards
|
|
6
|
+
* - Historical trend analysis
|
|
7
|
+
* - Multi-sensor correlation
|
|
8
|
+
*
|
|
9
|
+
* REQUIREMENTS:
|
|
10
|
+
* - Redis Stack (includes RedisTimeSeries module)
|
|
11
|
+
* - npm install @redis/time-series
|
|
12
|
+
*
|
|
13
|
+
* Installation:
|
|
14
|
+
* docker run -d -p 6379:6379 redis/redis-stack:latest
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createDspPipeline } from "../../index.js";
|
|
18
|
+
import { createClient } from "redis";
|
|
19
|
+
|
|
20
|
+
console.log("=== Phase 6-7: RedisTimeSeries Integration ===\n");
|
|
21
|
+
|
|
22
|
+
// Simulated EMG sensor data
|
|
23
|
+
function generateEMGSample(index: number): {
|
|
24
|
+
value: number;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
} {
|
|
27
|
+
const baseSignal = Math.sin(index * 0.05) * 100;
|
|
28
|
+
const noise = (Math.random() - 0.5) * 20;
|
|
29
|
+
const timestamp = Date.now() + index * 10; // 100 Hz
|
|
30
|
+
return { value: baseSignal + noise, timestamp };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Example 1: Basic RedisTimeSeries Integration
|
|
35
|
+
*/
|
|
36
|
+
async function example1_BasicIntegration() {
|
|
37
|
+
console.log("\n--- Example 1: Basic RedisTimeSeries Integration ---\n");
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const redis = await createClient({ url: "redis://localhost:6379" })
|
|
41
|
+
.on("error", (err) => console.log("Redis Client Error", err))
|
|
42
|
+
.connect();
|
|
43
|
+
|
|
44
|
+
console.log("✅ Connected to Redis\n");
|
|
45
|
+
|
|
46
|
+
// Create DSP pipeline
|
|
47
|
+
const pipeline = createDspPipeline();
|
|
48
|
+
pipeline
|
|
49
|
+
.Rectify({ mode: "full" })
|
|
50
|
+
.MovingAverage({ mode: "moving", windowDuration: 100 }) // 100ms smoothing
|
|
51
|
+
.Rms({ mode: "moving", windowDuration: 50 }); // 50ms RMS
|
|
52
|
+
|
|
53
|
+
// Process 100 samples
|
|
54
|
+
const count = 100;
|
|
55
|
+
const samples = new Float32Array(count);
|
|
56
|
+
const timestamps = new Float32Array(count);
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < count; i++) {
|
|
59
|
+
const { value, timestamp } = generateEMGSample(i);
|
|
60
|
+
samples[i] = value;
|
|
61
|
+
timestamps[i] = timestamp;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Process through DSP pipeline
|
|
65
|
+
const processed = await pipeline.process(samples, timestamps, {
|
|
66
|
+
channels: 1,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Write results to RedisTimeSeries
|
|
70
|
+
const sensorId = "emg-sensor-001";
|
|
71
|
+
const tsKey = `myovine:${sensorId}:rms`;
|
|
72
|
+
|
|
73
|
+
console.log(`Writing ${count} samples to Redis key: ${tsKey}\n`);
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < processed.length; i++) {
|
|
76
|
+
// TS.ADD key timestamp value
|
|
77
|
+
await redis.sendCommand([
|
|
78
|
+
"TS.ADD",
|
|
79
|
+
tsKey,
|
|
80
|
+
Math.floor(timestamps[i]).toString(),
|
|
81
|
+
processed[i].toString(),
|
|
82
|
+
"ON_DUPLICATE",
|
|
83
|
+
"LAST",
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Query recent data
|
|
88
|
+
const recentData: any = await redis.sendCommand([
|
|
89
|
+
"TS.RANGE",
|
|
90
|
+
tsKey,
|
|
91
|
+
"-",
|
|
92
|
+
"+",
|
|
93
|
+
"COUNT",
|
|
94
|
+
"10",
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
console.log("📊 Last 10 samples from RedisTimeSeries:");
|
|
98
|
+
if (Array.isArray(recentData)) {
|
|
99
|
+
recentData.slice(0, 10).forEach((sample: any, idx: number) => {
|
|
100
|
+
const [ts, value] = sample;
|
|
101
|
+
console.log(
|
|
102
|
+
` [${idx}] ${new Date(Number(ts)).toISOString()} → ${Number(
|
|
103
|
+
value
|
|
104
|
+
).toFixed(3)}`
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await redis.quit();
|
|
110
|
+
console.log("\n✅ Example 1 complete\n");
|
|
111
|
+
} catch (error: any) {
|
|
112
|
+
if (error.message.includes("ECONNREFUSED")) {
|
|
113
|
+
console.error("❌ Redis not running. Start Redis Stack:");
|
|
114
|
+
console.error(" docker run -d -p 6379:6379 redis/redis-stack:latest\n");
|
|
115
|
+
} else if (error.message.includes("unknown command")) {
|
|
116
|
+
console.error("❌ RedisTimeSeries module not loaded. Use Redis Stack:");
|
|
117
|
+
console.error(" docker run -d -p 6379:6379 redis/redis-stack:latest\n");
|
|
118
|
+
} else {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Example 2: Multi-Channel EMG Monitoring
|
|
126
|
+
*/
|
|
127
|
+
async function example2_MultiChannelMonitoring() {
|
|
128
|
+
console.log("\n--- Example 2: Multi-Channel EMG Monitoring ---\n");
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const redis = await createClient({ url: "redis://localhost:6379" })
|
|
132
|
+
.on("error", (err) => console.log("Redis Client Error", err))
|
|
133
|
+
.connect();
|
|
134
|
+
|
|
135
|
+
const pipeline = createDspPipeline();
|
|
136
|
+
pipeline
|
|
137
|
+
.Rectify({ mode: "full" })
|
|
138
|
+
.MovingAverage({ mode: "moving", windowDuration: 100 })
|
|
139
|
+
.Rms({ mode: "moving", windowDuration: 50 });
|
|
140
|
+
|
|
141
|
+
const channels = 4; // 4-channel EMG
|
|
142
|
+
const samplesPerChannel = 50;
|
|
143
|
+
const totalSamples = samplesPerChannel * channels;
|
|
144
|
+
|
|
145
|
+
const samples = new Float32Array(totalSamples);
|
|
146
|
+
const timestamps = new Float32Array(samplesPerChannel);
|
|
147
|
+
|
|
148
|
+
// Generate interleaved multi-channel data
|
|
149
|
+
for (let i = 0; i < samplesPerChannel; i++) {
|
|
150
|
+
timestamps[i] = Date.now() + i * 10;
|
|
151
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
152
|
+
const { value } = generateEMGSample(i + ch * 100); // Different phases
|
|
153
|
+
samples[i * channels + ch] = value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Process all channels
|
|
158
|
+
const processed = await pipeline.process(samples, timestamps, { channels });
|
|
159
|
+
|
|
160
|
+
// Write each channel to separate RedisTimeSeries key
|
|
161
|
+
console.log("Writing multi-channel data to RedisTimeSeries:\n");
|
|
162
|
+
|
|
163
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
164
|
+
const tsKey = `myovine:emg-array:ch${ch}:rms`;
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < samplesPerChannel; i++) {
|
|
167
|
+
const value = processed[i * channels + ch];
|
|
168
|
+
await redis.sendCommand([
|
|
169
|
+
"TS.ADD",
|
|
170
|
+
tsKey,
|
|
171
|
+
Math.floor(timestamps[i]).toString(),
|
|
172
|
+
value.toString(),
|
|
173
|
+
"ON_DUPLICATE",
|
|
174
|
+
"LAST",
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(
|
|
179
|
+
` ✅ Channel ${ch}: ${samplesPerChannel} samples → ${tsKey}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Create aggregation rules for downsampling
|
|
184
|
+
console.log("\n📊 Creating aggregation rules (1-second averages):");
|
|
185
|
+
|
|
186
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
187
|
+
const sourceKey = `myovine:emg-array:ch${ch}:rms`;
|
|
188
|
+
const aggKey = `${sourceKey}:avg:1s`;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// TS.CREATERULE source dest AGGREGATION avg 1000
|
|
192
|
+
await redis.sendCommand([
|
|
193
|
+
"TS.CREATERULE",
|
|
194
|
+
sourceKey,
|
|
195
|
+
aggKey,
|
|
196
|
+
"AGGREGATION",
|
|
197
|
+
"AVG",
|
|
198
|
+
"1000", // 1 second bucket
|
|
199
|
+
]);
|
|
200
|
+
console.log(` ✅ Channel ${ch}: ${aggKey}`);
|
|
201
|
+
} catch (err: any) {
|
|
202
|
+
if (!err.message.includes("already exists")) {
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await redis.quit();
|
|
209
|
+
console.log("\n✅ Example 2 complete");
|
|
210
|
+
console.log("\n💡 Query with Grafana:");
|
|
211
|
+
console.log(" - Data source: Redis");
|
|
212
|
+
console.log(" - Query: TS.RANGE myovine:emg-array:ch0:rms - +\n");
|
|
213
|
+
} catch (error: any) {
|
|
214
|
+
if (error.message.includes("ECONNREFUSED")) {
|
|
215
|
+
console.error("❌ Redis not running. Start Redis Stack:\n");
|
|
216
|
+
console.error(" docker run -d -p 6379:6379 redis/redis-stack:latest\n");
|
|
217
|
+
} else {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Example 3: Streaming with Tap + RedisTimeSeries
|
|
225
|
+
*/
|
|
226
|
+
async function example3_StreamingWithTap() {
|
|
227
|
+
console.log("\n--- Example 3: Streaming with Tap + RedisTimeSeries ---\n");
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const redis = await createClient({ url: "redis://localhost:6379" })
|
|
231
|
+
.on("error", (err) => console.log("Redis Client Error", err))
|
|
232
|
+
.connect();
|
|
233
|
+
|
|
234
|
+
const pipeline = createDspPipeline();
|
|
235
|
+
|
|
236
|
+
// Use .tap() to intercept intermediate results
|
|
237
|
+
pipeline
|
|
238
|
+
.MovingAverage({ mode: "moving", windowDuration: 100 })
|
|
239
|
+
.tap((result, stage) => {
|
|
240
|
+
// This runs after MovingAverage, before RMS
|
|
241
|
+
console.log(` 🔍 Tap at "${stage}": ${result[0].toFixed(3)}`);
|
|
242
|
+
})
|
|
243
|
+
.Rms({ mode: "moving", windowDuration: 50 });
|
|
244
|
+
|
|
245
|
+
const count = 10;
|
|
246
|
+
const samples = new Float32Array(count);
|
|
247
|
+
const timestamps = new Float32Array(count);
|
|
248
|
+
|
|
249
|
+
console.log("Processing samples with .tap() interception:\n");
|
|
250
|
+
|
|
251
|
+
for (let i = 0; i < count; i++) {
|
|
252
|
+
const { value, timestamp } = generateEMGSample(i);
|
|
253
|
+
samples[i] = value;
|
|
254
|
+
timestamps[i] = timestamp;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const processed = await pipeline.process(samples, timestamps, {
|
|
258
|
+
channels: 1,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
console.log("\n💾 Writing final RMS values to RedisTimeSeries...");
|
|
262
|
+
|
|
263
|
+
for (let i = 0; i < processed.length; i++) {
|
|
264
|
+
await redis.sendCommand([
|
|
265
|
+
"TS.ADD",
|
|
266
|
+
"myovine:emg:rms:streaming",
|
|
267
|
+
Math.floor(timestamps[i]).toString(),
|
|
268
|
+
processed[i].toString(),
|
|
269
|
+
"ON_DUPLICATE",
|
|
270
|
+
"LAST",
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await redis.quit();
|
|
275
|
+
console.log("✅ Example 3 complete\n");
|
|
276
|
+
} catch (error: any) {
|
|
277
|
+
if (error.message.includes("ECONNREFUSED")) {
|
|
278
|
+
console.error("❌ Redis not running.\n");
|
|
279
|
+
} else {
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Example 4: Production Pattern - Helper Class
|
|
287
|
+
*/
|
|
288
|
+
class RedisTimeSeriesWriter {
|
|
289
|
+
private redis: any;
|
|
290
|
+
private connected: boolean = false;
|
|
291
|
+
|
|
292
|
+
constructor(private url: string = "redis://localhost:6379") {}
|
|
293
|
+
|
|
294
|
+
async connect(): Promise<void> {
|
|
295
|
+
this.redis = await createClient({ url: this.url })
|
|
296
|
+
.on("error", (err) => console.log("Redis Client Error", err))
|
|
297
|
+
.connect();
|
|
298
|
+
this.connected = true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async writeSamples(
|
|
302
|
+
key: string,
|
|
303
|
+
values: Float32Array,
|
|
304
|
+
timestamps: Float32Array
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
if (!this.connected) {
|
|
307
|
+
throw new Error("Not connected to Redis");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (let i = 0; i < values.length; i++) {
|
|
311
|
+
await this.redis.sendCommand([
|
|
312
|
+
"TS.ADD",
|
|
313
|
+
key,
|
|
314
|
+
Math.floor(timestamps[i]).toString(),
|
|
315
|
+
values[i].toString(),
|
|
316
|
+
"ON_DUPLICATE",
|
|
317
|
+
"LAST",
|
|
318
|
+
]);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async createAggregation(
|
|
323
|
+
sourceKey: string,
|
|
324
|
+
aggType: "AVG" | "SUM" | "MIN" | "MAX",
|
|
325
|
+
bucketMs: number
|
|
326
|
+
): Promise<string> {
|
|
327
|
+
const destKey = `${sourceKey}:${aggType.toLowerCase()}:${bucketMs}ms`;
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
await this.redis.sendCommand([
|
|
331
|
+
"TS.CREATERULE",
|
|
332
|
+
sourceKey,
|
|
333
|
+
destKey,
|
|
334
|
+
"AGGREGATION",
|
|
335
|
+
aggType,
|
|
336
|
+
bucketMs.toString(),
|
|
337
|
+
]);
|
|
338
|
+
} catch (err: any) {
|
|
339
|
+
if (!err.message.includes("already exists")) {
|
|
340
|
+
throw err;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return destKey;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async close(): Promise<void> {
|
|
348
|
+
if (this.connected) {
|
|
349
|
+
await this.redis.quit();
|
|
350
|
+
this.connected = false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function example4_ProductionPattern() {
|
|
356
|
+
console.log("\n--- Example 4: Production Pattern (Helper Class) ---\n");
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const writer = new RedisTimeSeriesWriter();
|
|
360
|
+
await writer.connect();
|
|
361
|
+
|
|
362
|
+
console.log("✅ Connected to Redis\n");
|
|
363
|
+
|
|
364
|
+
// Set up DSP pipeline
|
|
365
|
+
const pipeline = createDspPipeline();
|
|
366
|
+
pipeline
|
|
367
|
+
.Rectify({ mode: "full" })
|
|
368
|
+
.MovingAverage({ mode: "moving", windowDuration: 100 })
|
|
369
|
+
.Rms({ mode: "moving", windowDuration: 50 });
|
|
370
|
+
|
|
371
|
+
// Process data
|
|
372
|
+
const count = 50;
|
|
373
|
+
const samples = new Float32Array(count);
|
|
374
|
+
const timestamps = new Float32Array(count);
|
|
375
|
+
|
|
376
|
+
for (let i = 0; i < count; i++) {
|
|
377
|
+
const { value, timestamp } = generateEMGSample(i);
|
|
378
|
+
samples[i] = value;
|
|
379
|
+
timestamps[i] = timestamp;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const processed = await pipeline.process(samples, timestamps, {
|
|
383
|
+
channels: 1,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Write to RedisTimeSeries
|
|
387
|
+
const key = "myovine:production:emg:rms";
|
|
388
|
+
await writer.writeSamples(key, processed, timestamps);
|
|
389
|
+
console.log(`📊 Wrote ${count} samples to ${key}`);
|
|
390
|
+
|
|
391
|
+
// Create aggregation rules
|
|
392
|
+
const avg1s = await writer.createAggregation(key, "AVG", 1000);
|
|
393
|
+
const avg10s = await writer.createAggregation(key, "AVG", 10000);
|
|
394
|
+
console.log(`\n✅ Created aggregations:`);
|
|
395
|
+
console.log(` - ${avg1s}`);
|
|
396
|
+
console.log(` - ${avg10s}`);
|
|
397
|
+
|
|
398
|
+
await writer.close();
|
|
399
|
+
console.log("\n✅ Example 4 complete\n");
|
|
400
|
+
} catch (error: any) {
|
|
401
|
+
if (error.message.includes("ECONNREFUSED")) {
|
|
402
|
+
console.error("❌ Redis not running.\n");
|
|
403
|
+
} else {
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Run all examples
|
|
410
|
+
async function main() {
|
|
411
|
+
console.log(
|
|
412
|
+
"📚 These examples show how to integrate DSP results with RedisTimeSeries\n"
|
|
413
|
+
);
|
|
414
|
+
console.log("Prerequisites:");
|
|
415
|
+
console.log(
|
|
416
|
+
" 1. Install Redis Stack: docker run -d -p 6379:6379 redis/redis-stack:latest"
|
|
417
|
+
);
|
|
418
|
+
console.log(" 2. Install dependencies: npm install redis\n");
|
|
419
|
+
console.log("Press Ctrl+C if Redis is not available.\n");
|
|
420
|
+
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
422
|
+
|
|
423
|
+
await example1_BasicIntegration();
|
|
424
|
+
await example2_MultiChannelMonitoring();
|
|
425
|
+
await example3_StreamingWithTap();
|
|
426
|
+
await example4_ProductionPattern();
|
|
427
|
+
|
|
428
|
+
console.log("=== Phase 6-7 Complete ===\n");
|
|
429
|
+
console.log("✅ RedisTimeSeries integration enables:");
|
|
430
|
+
console.log(" • Real-time Grafana dashboards");
|
|
431
|
+
console.log(" • Historical trend analysis");
|
|
432
|
+
console.log(" • Automatic downsampling (aggregation rules)");
|
|
433
|
+
console.log(" • Multi-sensor correlation");
|
|
434
|
+
console.log(" • Production-ready monitoring\n");
|
|
435
|
+
|
|
436
|
+
console.log("💡 Next steps:");
|
|
437
|
+
console.log(" 1. Set up Grafana with Redis datasource");
|
|
438
|
+
console.log(" 2. Create dashboards for your EMG sensors");
|
|
439
|
+
console.log(" 3. Set up alerts for anomalies");
|
|
440
|
+
console.log(" 4. Use aggregation rules for long-term storage\n");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
main().catch((error) => {
|
|
444
|
+
console.error("Error:", error.message);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example showing how to use Redis for DSP state persistence
|
|
3
|
+
*
|
|
4
|
+
* This demonstrates:
|
|
5
|
+
* 1. Creating a pipeline with Redis configuration
|
|
6
|
+
* 2. Processing audio data
|
|
7
|
+
* 3. Saving/loading state (when Redis integration is fully implemented)
|
|
8
|
+
*
|
|
9
|
+
* Note: This example shows the intended API. Full C++ Redis integration
|
|
10
|
+
* requires implementing saveState/loadState methods in DspPipeline.cc
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createClient } from "redis";
|
|
14
|
+
import { createDspPipeline } from "../../bindings";
|
|
15
|
+
|
|
16
|
+
async function redisExample(startFresh = false) {
|
|
17
|
+
console.log("=== DSP Pipeline with Redis State Persistence ===\n");
|
|
18
|
+
|
|
19
|
+
// 1. Create Redis client
|
|
20
|
+
const redis = await createClient({ url: "redis://localhost:6379" }).connect();
|
|
21
|
+
|
|
22
|
+
// 2. Create DSP pipeline with Redis configuration
|
|
23
|
+
const stateKey = "dsp:pipeline:channel1";
|
|
24
|
+
|
|
25
|
+
// Optional: Clear previous state to start fresh
|
|
26
|
+
if (startFresh) {
|
|
27
|
+
console.log("Clearing previous state to start fresh...");
|
|
28
|
+
await redis.del(stateKey);
|
|
29
|
+
console.log("State cleared\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pipeline = createDspPipeline({
|
|
33
|
+
redisHost: "localhost",
|
|
34
|
+
redisPort: 6379,
|
|
35
|
+
stateKey: stateKey,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// 3. Build the pipeline
|
|
39
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
40
|
+
|
|
41
|
+
console.log("Pipeline created with moving average filter (window=3)");
|
|
42
|
+
|
|
43
|
+
// 4. Try to restore previous state (if exists)
|
|
44
|
+
const previousState = await redis.get(stateKey);
|
|
45
|
+
if (previousState) {
|
|
46
|
+
console.log("Found previous state in Redis, restoring...");
|
|
47
|
+
await pipeline.loadState(previousState);
|
|
48
|
+
console.log("State restored successfully!");
|
|
49
|
+
} else {
|
|
50
|
+
console.log("No previous state found in Redis");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 5. First batch of audio
|
|
54
|
+
console.log("\n--- Processing first batch ---");
|
|
55
|
+
const batch1 = new Float32Array([1, 2, 3, 4, 5]);
|
|
56
|
+
console.log("Input batch 1:", Array.from(batch1));
|
|
57
|
+
|
|
58
|
+
const output1 = await pipeline.processCopy(batch1, {
|
|
59
|
+
sampleRate: 1000,
|
|
60
|
+
channels: 1,
|
|
61
|
+
});
|
|
62
|
+
console.log("Output batch 1:", Array.from(output1));
|
|
63
|
+
|
|
64
|
+
// 6. Save state to Redis
|
|
65
|
+
console.log("\n--- Saving state to Redis ---");
|
|
66
|
+
const state = await pipeline.saveState();
|
|
67
|
+
await redis.set(stateKey, state);
|
|
68
|
+
console.log("State saved:", state);
|
|
69
|
+
|
|
70
|
+
// 7. Simulate stopping and restarting the process
|
|
71
|
+
console.log("\n--- Simulating restart ---");
|
|
72
|
+
|
|
73
|
+
// 8. Create new pipeline and restore state
|
|
74
|
+
const pipeline2 = createDspPipeline({
|
|
75
|
+
redisHost: "localhost",
|
|
76
|
+
redisPort: 6379,
|
|
77
|
+
stateKey: stateKey,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
pipeline2.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
81
|
+
|
|
82
|
+
const restoredState = await redis.get(stateKey);
|
|
83
|
+
if (restoredState) {
|
|
84
|
+
console.log("Restored state from Redis");
|
|
85
|
+
await pipeline2.loadState(restoredState);
|
|
86
|
+
console.log(
|
|
87
|
+
"State restoration complete - continuing from where we left off!"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 9. Continue processing with restored state
|
|
92
|
+
console.log("\n--- Processing second batch (after 'restart') ---");
|
|
93
|
+
const batch2 = new Float32Array([6, 7, 8]);
|
|
94
|
+
console.log("Input batch 2:", Array.from(batch2));
|
|
95
|
+
|
|
96
|
+
const output2 = await pipeline2.processCopy(batch2, {
|
|
97
|
+
sampleRate: 1000,
|
|
98
|
+
channels: 1,
|
|
99
|
+
});
|
|
100
|
+
console.log("Output batch 2:", Array.from(output2));
|
|
101
|
+
|
|
102
|
+
console.log(
|
|
103
|
+
"\nNote: With state restoration, batch2 continues the moving average from where batch1 left off!"
|
|
104
|
+
);
|
|
105
|
+
console.log(
|
|
106
|
+
"Expected output without state: [6, 6.5, 7] - but we got continuous averaging!"
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// 10. Optional: Clear state (commented out to persist between runs)
|
|
110
|
+
// console.log("\n--- Clearing state ---");
|
|
111
|
+
// await redis.del(stateKey);
|
|
112
|
+
// pipeline2.clearState();
|
|
113
|
+
// console.log("State cleared from Redis");
|
|
114
|
+
|
|
115
|
+
// Disconnect from Redis
|
|
116
|
+
await redis.disconnect();
|
|
117
|
+
} // Real-world use case example
|
|
118
|
+
async function streamingExample(startFresh = false) {
|
|
119
|
+
console.log("\n\n=== Streaming Audio Processing Example ===\n");
|
|
120
|
+
|
|
121
|
+
const redis = await createClient({ url: "redis://localhost:6379" }).connect();
|
|
122
|
+
const channelId = "audio-stream-ch1";
|
|
123
|
+
const stateKey = `dsp:stream:${channelId}`;
|
|
124
|
+
|
|
125
|
+
// Optional: Clear previous state to start fresh
|
|
126
|
+
if (startFresh) {
|
|
127
|
+
console.log("Clearing previous streaming state to start fresh...");
|
|
128
|
+
await redis.del(stateKey);
|
|
129
|
+
console.log("Streaming state cleared\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Create pipeline
|
|
133
|
+
const pipeline = createDspPipeline({
|
|
134
|
+
redisHost: "localhost",
|
|
135
|
+
redisPort: 6379,
|
|
136
|
+
stateKey: stateKey,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 5 });
|
|
140
|
+
|
|
141
|
+
// Restore state if processing was interrupted
|
|
142
|
+
const savedState = await redis.get(stateKey);
|
|
143
|
+
if (savedState) {
|
|
144
|
+
console.log("Resuming from saved state...");
|
|
145
|
+
await pipeline.loadState(savedState);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Simulate streaming audio chunks
|
|
149
|
+
const chunks = [
|
|
150
|
+
new Float32Array([1, 2, 3, 4, 5]),
|
|
151
|
+
new Float32Array([6, 7, 8, 9, 10]),
|
|
152
|
+
new Float32Array([11, 12, 13, 14, 15]),
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
console.log("Processing audio stream in chunks...\n");
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
158
|
+
const chunk = chunks[i];
|
|
159
|
+
console.log(`Chunk ${i + 1} input:`, Array.from(chunk));
|
|
160
|
+
|
|
161
|
+
const processed = await pipeline.processCopy(chunk, {
|
|
162
|
+
sampleRate: 44100,
|
|
163
|
+
channels: 1,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
console.log(`Chunk ${i + 1} output:`, Array.from(processed));
|
|
167
|
+
|
|
168
|
+
// Save state after each chunk (for crash recovery)
|
|
169
|
+
const state = await pipeline.saveState();
|
|
170
|
+
await redis.set(stateKey, state);
|
|
171
|
+
console.log(`State saved after chunk ${i + 1}\n`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log("Stream processing complete. State saved for next session.");
|
|
175
|
+
|
|
176
|
+
// Disconnect from Redis
|
|
177
|
+
await redis.disconnect();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Run examples
|
|
181
|
+
console.log("Running DSP + Redis examples...\n");
|
|
182
|
+
|
|
183
|
+
// Set to true to clear previous state and start fresh
|
|
184
|
+
// Set to false to demonstrate state persistence across runs
|
|
185
|
+
const START_FRESH = false;
|
|
186
|
+
|
|
187
|
+
redisExample(START_FRESH)
|
|
188
|
+
.then(() => streamingExample(START_FRESH))
|
|
189
|
+
.then(() => {
|
|
190
|
+
console.log("\nAll examples completed successfully!");
|
|
191
|
+
console.log(
|
|
192
|
+
"\nTip: Set START_FRESH = true to clear Redis state and start fresh"
|
|
193
|
+
);
|
|
194
|
+
console.log(
|
|
195
|
+
"Tip: Set START_FRESH = false to see state persistence across runs"
|
|
196
|
+
);
|
|
197
|
+
process.exit(0);
|
|
198
|
+
})
|
|
199
|
+
.catch((error) => {
|
|
200
|
+
console.error("\nError:", error);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
});
|