@robotical/raftjs 2.0.4 → 2.0.6

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.
@@ -9,10 +9,11 @@
9
9
  /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  class CustomAttrHandler {
12
+ constructor() {
13
+ this._jsFunctionCache = new Map();
14
+ }
12
15
  handleAttr(pollRespMetadata, msgBuffer, msgBufIdx) {
13
- // Implement the pseudo-code:
14
- // int N=(buf[0]+32-buf[2])%32;int k=3;int i=0;while(i<N){out.Red=(buf[k]<<16)|(buf[k+1]<<8)|buf[k+2];out.IR=(buf[k+3]<<16)|(buf[k+4]<<8)|buf[k+5];k+=6;i++;next;}
15
- // Number of bytes in the each message
16
+ // Number of bytes in each message
16
17
  const numMsgBytes = pollRespMetadata.b;
17
18
  // Create a vector for each attribute in the metadata
18
19
  const attrValueVecs = [];
@@ -23,43 +24,70 @@ class CustomAttrHandler {
23
24
  attrValueVecs.push([]);
24
25
  attrValues[pollRespMetadata.a[attrIdx].n] = attrValueVecs[attrIdx];
25
26
  }
26
- // Custom code for each device type
27
- if (pollRespMetadata.c.n === "max30101_fifo") {
28
- // Hex dump msgBuffer
29
- // console.log(`CustomAttrHandler handleAttr ${pollRespMetadata.c!.n} msgBuffer: ${msgBuffer.toString('hex')}`);
30
- const buf = msgBuffer.slice(msgBufIdx);
31
- if (buf.length < numMsgBytes) {
32
- return [];
27
+ const customFnDef = pollRespMetadata.c;
28
+ if (!customFnDef) {
29
+ return attrValueVecs;
30
+ }
31
+ // Provide the message buffer sliced to the data portion
32
+ const buf = msgBuffer.slice(msgBufIdx);
33
+ if (buf.length < numMsgBytes) {
34
+ return [];
35
+ }
36
+ // Execute supplied JS implementation if provided
37
+ if (customFnDef.j && customFnDef.j.trim().length > 0) {
38
+ const jsFn = this.getOrCompileJsFunction(customFnDef);
39
+ if (!jsFn) {
40
+ return attrValueVecs;
33
41
  }
42
+ try {
43
+ jsFn(buf, attrValues, attrValueVecs, pollRespMetadata, msgBuffer, msgBufIdx, numMsgBytes);
44
+ }
45
+ catch (err) {
46
+ console.error(`CustomAttrHandler JS function ${customFnDef.n} execution failed`, err);
47
+ }
48
+ return attrValueVecs;
49
+ }
50
+ // Custom code for each device type handled natively
51
+ if (customFnDef.n === "max30101_fifo") {
34
52
  // Generated code ...
35
53
  const N = (buf[0] + 32 - buf[2]) % 32;
36
54
  let k = 3;
37
55
  let i = 0;
38
56
  while (i < N) {
39
- attrValues['Red'].push(0);
40
- attrValues['Red'][attrValues['Red'].length - 1] = (buf[k] << 16) | (buf[k + 1] << 8) | buf[k + 2];
41
- attrValues['IR'].push(0);
42
- attrValues['IR'][attrValues['IR'].length - 1] = (buf[k + 3] << 16) | (buf[k + 4] << 8) | buf[k + 5];
57
+ attrValues["Red"].push(0);
58
+ attrValues["Red"][attrValues["Red"].length - 1] = (buf[k] << 16) | (buf[k + 1] << 8) | buf[k + 2];
59
+ attrValues["IR"].push(0);
60
+ attrValues["IR"][attrValues["IR"].length - 1] = (buf[k + 3] << 16) | (buf[k + 4] << 8) | buf[k + 5];
43
61
  k += 6;
44
62
  i++;
45
- ;
46
63
  }
47
64
  }
48
- else if (pollRespMetadata.c.n === "gravity_o2_calc") {
49
- // Get the buffer
50
- const buf = msgBuffer.slice(msgBufIdx);
51
- if (buf.length < numMsgBytes) {
52
- return [];
53
- }
54
- // Implement the pseudo-code:
55
- // float key = 20.9/120.0; float val = key * (buf[0] + (buf[1]/10.0) + (buf[2]/100.0)); out.oxygen = val;
65
+ else if (customFnDef.n === "gravity_o2_calc") {
56
66
  const key = 20.9 / 120.0;
57
- const val = key * (buf[0] + (buf[1] / 10.0) + (buf[2] / 100.0));
58
- // Add the value to the oxygen attribute
59
- attrValues['oxygen'].push(val);
67
+ const val = key * (buf[0] + buf[1] / 10.0 + buf[2] / 100.0);
68
+ attrValues["oxygen"].push(val);
60
69
  }
61
70
  return attrValueVecs;
62
71
  }
72
+ getOrCompileJsFunction(customFnDef) {
73
+ if (!customFnDef.j) {
74
+ return null;
75
+ }
76
+ const cacheKey = `${customFnDef.n}::${customFnDef.j}`;
77
+ const cachedFn = this._jsFunctionCache.get(cacheKey);
78
+ if (cachedFn) {
79
+ return cachedFn;
80
+ }
81
+ try {
82
+ const compiledFn = new Function("buf", "attrValues", "attrValueVecs", "pollRespMetadata", "msgBuffer", "msgBufIdx", "numMsgBytes", customFnDef.j);
83
+ this._jsFunctionCache.set(cacheKey, compiledFn);
84
+ return compiledFn;
85
+ }
86
+ catch (err) {
87
+ console.error(`CustomAttrHandler failed to compile JS function ${customFnDef.n}`, err);
88
+ return null;
89
+ }
90
+ }
63
91
  }
64
92
  exports.default = CustomAttrHandler;
65
93
  //# sourceMappingURL=RaftCustomAttrHandler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"RaftCustomAttrHandler.js","sourceRoot":"","sources":["../../src/RaftCustomAttrHandler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RaftCustomAttrHandler.js","sourceRoot":"","sources":["../../src/RaftCustomAttrHandler.ts"],"names":[],"mappings":"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"}
@@ -25,10 +25,12 @@ export interface DeviceTypeAttribute {
25
25
  vf?: boolean | number;
26
26
  vft?: string;
27
27
  lut?: Array<LUTRow>;
28
+ resolution?: string;
28
29
  }
29
30
  export interface CustomFunctionDefinition {
30
31
  n: string;
31
32
  c: string;
33
+ j?: string;
32
34
  }
33
35
  export interface DeviceTypePollRespMetadata {
34
36
  b: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robotical/raftjs",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "Javascript/TS library for Raft library",
5
5
  "main": "dist/web/main.js",
6
6
  "types": "dist/web/main.d.ts",
@@ -286,7 +286,7 @@ export default class RaftChannelSimulated implements RaftChannel {
286
286
 
287
287
  const attributes = deviceTypeInfo.resp.a;
288
288
  const dataBlockSizeBytes = deviceTypeInfo.resp.b;
289
-
289
+
290
290
  // Create a buffer for the data
291
291
  const dataBuffer = new ArrayBuffer(dataBlockSizeBytes + 2);
292
292
  const dataView = new DataView(dataBuffer);
@@ -295,89 +295,68 @@ export default class RaftChannelSimulated implements RaftChannel {
295
295
  // Add 16 bit big endian deviceTimeMs mod 65536 to the buffer
296
296
  dataView.setUint16(bytePos, deviceTimeMs % 65536, false);
297
297
  bytePos += 2;
298
-
299
- // Calculate sine wave with phase offsets for each attribute
300
- const numAttributes = attributes.length;
301
- // Adjust frequency based on the device interval with N samples per cycle
302
- const numSamplesPerCycle = 10;
303
- let frequencyHz = 0.1; // Default frequency in Hz
304
- if (deviceIntervalMs > 0) {
305
- frequencyHz = (1000 / deviceIntervalMs) / numSamplesPerCycle;
306
- }
307
298
 
308
- // Amplitude of the sine wave (0 to 1)
309
- const amplitude = 0.8;
310
-
311
- // Iterate through attributes and set values
312
- for (let i = 0; i < numAttributes; i++) {
313
- const attr = attributes[i];
314
- // Calculate phase offset for this attribute
315
- const phaseOffset = (2 * Math.PI * i) / numAttributes;
316
-
317
- // Generate sine wave value
299
+ const handledByCustomGenerator = this._fillCustomRawData(
300
+ deviceTypeInfo,
301
+ dataView,
302
+ bytePos,
303
+ dataBlockSizeBytes,
304
+ deviceIntervalMs,
305
+ deviceTimeMs
306
+ );
307
+
308
+ if (!handledByCustomGenerator) {
309
+ const numAttributes = attributes.length;
310
+ const numSamplesPerCycle = 10;
311
+ const frequencyHz = (deviceIntervalMs > 0)
312
+ ? (1000 / deviceIntervalMs) / numSamplesPerCycle
313
+ : 0.1;
318
314
  const timeRadians = deviceTimeMs * frequencyHz * (2 * Math.PI) / 1000;
319
- const sinValue = Math.sin(timeRadians + phaseOffset);
320
-
321
- // Scale the value to fit within the attribute's range
322
- let scaledValue: number;
323
- if (attr.r && attr.r.length >= 2) {
324
- const minValue = attr.r[0];
325
- const maxValue = attr.r[1];
326
- const midPoint = (maxValue + minValue) / 2;
327
- const range = (maxValue - minValue) / 2;
328
- scaledValue = midPoint + sinValue * range * amplitude;
329
- } else {
330
- // Default range if not specified
331
- scaledValue = sinValue * 1000 * amplitude;
332
- }
333
-
334
- // Convert to raw integer value if needed
335
- let rawValue = scaledValue;
336
- if (attr.d) {
337
- // Multiply by the divisor to get the raw value (reverse of what happens when decoding)
338
- rawValue = scaledValue * attr.d;
339
- }
340
-
341
- // Write the value to the buffer based on its type
342
- if (attr.t === "b") {
343
- dataView.setUint8(bytePos, Math.round(rawValue));
344
- bytePos += 1;
345
- } else if (attr.t === "B") {
346
- dataView.setUint8(bytePos, Math.round(rawValue));
347
- bytePos += 1;
348
- } else if (attr.t === "c") {
349
- dataView.setInt8(bytePos, Math.round(rawValue));
350
- bytePos += 1;
351
- } else if (attr.t === "C") {
352
- dataView.setUint8(bytePos, Math.round(rawValue));
353
- bytePos += 1;
354
- } else if (attr.t === "<h") {
355
- dataView.setInt16(bytePos, Math.round(rawValue), true); // Little endian
356
- bytePos += 2;
357
- } else if (attr.t === ">h") {
358
- dataView.setInt16(bytePos, Math.round(rawValue), false); // Big endian
359
- bytePos += 2;
360
- } else if (attr.t === "<H") {
361
- dataView.setUint16(bytePos, Math.round(rawValue), true); // Little endian
362
- bytePos += 2;
363
- } else if (attr.t === ">H") {
364
- dataView.setUint16(bytePos, Math.round(rawValue), false); // Big endian
365
- bytePos += 2;
366
- } else if (attr.t === "f" || attr.t === "<f") {
367
- dataView.setFloat32(bytePos, rawValue, true); // Little endian
368
- bytePos += 4;
369
- } else if (attr.t === ">f") {
370
- dataView.setFloat32(bytePos, rawValue, false); // Big endian
371
- bytePos += 4;
372
- } else {
373
- RaftLog.warn(`RaftChannelSimulated._createSimulatedDeviceInfoMsg - unsupported attribute type ${attr.t}`);
374
- }
375
315
 
316
+ // Iterate through attributes and fill the payload
317
+ for (let attrIdx = 0; attrIdx < numAttributes; attrIdx++) {
318
+ const attr = attributes[attrIdx];
319
+ const { typeCode, repeatCount, littleEndian } = this._parseAttrType(attr.t);
320
+ const scaledValues = this._generateAttributeScaledValues(
321
+ attr,
322
+ attrIdx,
323
+ repeatCount,
324
+ numAttributes,
325
+ timeRadians,
326
+ deviceTimeMs
327
+ );
328
+
329
+ if (scaledValues.length !== repeatCount) {
330
+ RaftLog.warn(`RaftChannelSimulated._createSimulatedDeviceInfoMsg - value count mismatch for ${attr.n}`);
331
+ continue;
332
+ }
333
+
334
+ for (let elemIdx = 0; elemIdx < repeatCount; elemIdx++) {
335
+ const scaledValue = scaledValues[elemIdx];
336
+ const rawValue = this._prepareRawValue(attr, typeCode, scaledValue);
337
+ const nextBytePos = this._writeRawValueToBuffer(
338
+ dataView,
339
+ bytePos,
340
+ typeCode,
341
+ littleEndian,
342
+ rawValue
343
+ );
344
+
345
+ if (nextBytePos < 0) {
346
+ RaftLog.warn(`RaftChannelSimulated._createSimulatedDeviceInfoMsg - buffer overflow writing ${attr.n}`);
347
+ break;
348
+ }
349
+
350
+ bytePos = nextBytePos;
351
+ }
352
+ }
353
+ } else {
354
+ bytePos += dataBlockSizeBytes;
376
355
  }
377
-
356
+
378
357
  // Convert the buffer to a byte array
379
358
  const dataBytes = new Uint8Array(dataBuffer);
380
-
359
+
381
360
  // Create the JSON message structure
382
361
  const message = {
383
362
  "BUS1": {
@@ -401,6 +380,305 @@ export default class RaftChannelSimulated implements RaftChannel {
401
380
 
402
381
  }
403
382
 
383
+ private _parseAttrType(attrType: string): { typeCode: string; repeatCount: number; littleEndian: boolean } {
384
+ const repeatMatch = attrType.match(/\[(\d+)\]\s*$/);
385
+ const repeatCount = repeatMatch ? parseInt(repeatMatch[1], 10) : 1;
386
+ const coreType = repeatMatch ? attrType.slice(0, repeatMatch.index) : attrType;
387
+ let littleEndian = false;
388
+ let typeCode = coreType.trim();
389
+
390
+ if (typeCode.startsWith("<")) {
391
+ littleEndian = true;
392
+ typeCode = typeCode.slice(1);
393
+ } else if (typeCode.startsWith(">")) {
394
+ littleEndian = false;
395
+ typeCode = typeCode.slice(1);
396
+ } else if (typeCode === "f") {
397
+ // Match previous behaviour - plain "f" treated as little endian floats
398
+ littleEndian = true;
399
+ }
400
+
401
+ return { typeCode, repeatCount, littleEndian };
402
+ }
403
+
404
+ private _generateAttributeScaledValues(
405
+ attr: any,
406
+ attrIdx: number,
407
+ repeatCount: number,
408
+ numAttributes: number,
409
+ timeRadians: number,
410
+ deviceTimeMs: number
411
+ ): number[] {
412
+ const amplitude = 0.8;
413
+
414
+ if (repeatCount > 1) {
415
+ const useThermalGrid = (attr && typeof attr.resolution === "string") || repeatCount >= 16;
416
+ if (useThermalGrid) {
417
+ return this._generateThermalGridValues(attr, repeatCount, timeRadians, deviceTimeMs);
418
+ }
419
+
420
+ const values: number[] = [];
421
+ for (let elemIdx = 0; elemIdx < repeatCount; elemIdx++) {
422
+ const phaseOffset = (2 * Math.PI * (attrIdx + elemIdx / repeatCount)) / Math.max(1, numAttributes);
423
+ const sinValue = Math.sin(timeRadians + phaseOffset);
424
+
425
+ if (Array.isArray(attr.r) && attr.r.length >= 2) {
426
+ const minValue = attr.r[0];
427
+ const maxValue = attr.r[1];
428
+ const midPoint = (maxValue + minValue) / 2;
429
+ const range = (maxValue - minValue) / 2;
430
+ const value = midPoint + sinValue * range * amplitude;
431
+ values.push(Math.min(maxValue, Math.max(minValue, value)));
432
+ } else {
433
+ values.push(sinValue * 1000 * amplitude);
434
+ }
435
+ }
436
+ return values;
437
+ }
438
+
439
+ const phaseOffset = numAttributes > 0 ? (2 * Math.PI * attrIdx) / numAttributes : 0;
440
+ const sinValue = Math.sin(timeRadians + phaseOffset);
441
+
442
+ if (Array.isArray(attr.r) && attr.r.length >= 2) {
443
+ const minValue = attr.r[0];
444
+ const maxValue = attr.r[1];
445
+ const midPoint = (maxValue + minValue) / 2;
446
+ const range = (maxValue - minValue) / 2;
447
+ const value = midPoint + sinValue * range * amplitude;
448
+ return [Math.min(maxValue, Math.max(minValue, value))];
449
+ }
450
+
451
+ return [sinValue * 1000 * amplitude];
452
+ }
453
+
454
+ private _generateThermalGridValues(
455
+ attr: any,
456
+ repeatCount: number,
457
+ timeRadians: number,
458
+ deviceTimeMs: number
459
+ ): number[] {
460
+ const { rows, cols } = this._getGridDimensions(attr, repeatCount);
461
+ const values: number[] = [];
462
+ const ambientBase = 24 + 2 * Math.sin(deviceTimeMs / 7000);
463
+ const hotspotPhase = deviceTimeMs / 3200;
464
+ const hotspotRow = (Math.sin(hotspotPhase) + 1) * (rows - 1) / 2;
465
+ const hotspotCol = (Math.cos(hotspotPhase) + 1) * (cols - 1) / 2;
466
+ const hotspotAmplitude = 6;
467
+ const sigma = Math.max(rows, cols) / 3 || 1;
468
+
469
+ for (let idx = 0; idx < repeatCount; idx++) {
470
+ const row = Math.floor(idx / cols);
471
+ const col = idx % cols;
472
+ const dist = Math.hypot(row - hotspotRow, col - hotspotCol);
473
+ const hotspot = hotspotAmplitude * Math.exp(-(dist * dist) / (2 * sigma * sigma));
474
+ const gentleWave = 0.5 * Math.sin(timeRadians + row * 0.35 + col * 0.25);
475
+ let value = ambientBase + hotspot + gentleWave;
476
+
477
+ if (Array.isArray(attr.r) && attr.r.length >= 2) {
478
+ value = Math.min(attr.r[1], Math.max(attr.r[0], value));
479
+ }
480
+
481
+ values.push(value);
482
+ }
483
+
484
+ return values;
485
+ }
486
+
487
+ private _fillCustomRawData(
488
+ deviceTypeInfo: DeviceTypeInfo,
489
+ dataView: DataView,
490
+ bytePos: number,
491
+ dataBlockSizeBytes: number,
492
+ deviceIntervalMs: number,
493
+ deviceTimeMs: number
494
+ ): boolean {
495
+ if (deviceTypeInfo.type !== "LTR-329") {
496
+ return false;
497
+ }
498
+
499
+ if (dataBlockSizeBytes < 4) {
500
+ return false;
501
+ }
502
+
503
+ const frequencyHz = (deviceIntervalMs > 0)
504
+ ? (1000 / deviceIntervalMs) / 10
505
+ : 0.1;
506
+ const timeRadians = deviceTimeMs * frequencyHz * (2 * Math.PI) / 1000;
507
+
508
+ const range = deviceTypeInfo.resp?.a?.[0]?.r ?? [0, 64000];
509
+ const minLux = range[0] ?? 0;
510
+ const maxLux = range[1] ?? 64000;
511
+
512
+ const baseLux = (maxLux + minLux) / 4;
513
+ const amplitudeLux = (maxLux - minLux) / 6;
514
+ let combined = Math.round(baseLux + amplitudeLux * Math.sin(timeRadians));
515
+ combined = Math.max(minLux, Math.min(maxLux, combined));
516
+
517
+ const irBase = combined * 0.35;
518
+ const irVariance = (combined * 0.15) * Math.sin(timeRadians + Math.PI / 4);
519
+ let ir = Math.round(irBase + irVariance);
520
+ ir = Math.max(minLux, Math.min(combined, ir));
521
+
522
+ dataView.setUint16(bytePos, combined, true);
523
+ dataView.setUint16(bytePos + 2, ir, true);
524
+
525
+ return true;
526
+ }
527
+
528
+ private _getGridDimensions(attr: any, repeatCount: number): { rows: number; cols: number } {
529
+ if (attr && typeof attr.resolution === "string") {
530
+ const match = attr.resolution.match(/(\d+)\s*x\s*(\d+)/i);
531
+ if (match) {
532
+ const rows = parseInt(match[1], 10);
533
+ const cols = parseInt(match[2], 10);
534
+ if (rows > 0 && cols > 0) {
535
+ return { rows, cols };
536
+ }
537
+ }
538
+ }
539
+
540
+ const side = Math.round(Math.sqrt(repeatCount));
541
+ if (side > 0 && side * side === repeatCount) {
542
+ return { rows: side, cols: side };
543
+ }
544
+
545
+ return { rows: repeatCount, cols: 1 };
546
+ }
547
+
548
+ private _prepareRawValue(attr: any, typeCode: string, scaledValue: number): number {
549
+ if (this._isFloatType(typeCode)) {
550
+ return scaledValue;
551
+ }
552
+
553
+ let raw = scaledValue;
554
+
555
+ if (attr && typeof attr.a === "number") {
556
+ raw -= attr.a;
557
+ }
558
+ if (attr && typeof attr.d === "number") {
559
+ raw *= attr.d;
560
+ }
561
+ if (attr && typeof attr.s === "number" && attr.s !== 0) {
562
+ const shift = attr.s;
563
+ const shiftFactor = Math.pow(2, Math.abs(shift));
564
+ if (shift > 0) {
565
+ raw *= shiftFactor;
566
+ } else {
567
+ raw /= shiftFactor;
568
+ }
569
+ }
570
+
571
+ return Math.round(raw);
572
+ }
573
+
574
+ private _writeRawValueToBuffer(
575
+ dataView: DataView,
576
+ bytePos: number,
577
+ typeCode: string,
578
+ littleEndian: boolean,
579
+ rawValue: number
580
+ ): number {
581
+ const valueSize = this._byteSizeForType(typeCode);
582
+ if (valueSize <= 0 || bytePos + valueSize > dataView.byteLength) {
583
+ return -1;
584
+ }
585
+
586
+ switch (typeCode) {
587
+ case "b":
588
+ dataView.setInt8(bytePos, this._clampRawValue(rawValue, typeCode));
589
+ break;
590
+ case "c":
591
+ dataView.setInt8(bytePos, this._clampRawValue(rawValue, typeCode));
592
+ break;
593
+ case "B":
594
+ case "C":
595
+ dataView.setUint8(bytePos, this._clampRawValue(rawValue, typeCode));
596
+ break;
597
+ case "?":
598
+ dataView.setUint8(bytePos, rawValue ? 1 : 0);
599
+ break;
600
+ case "h":
601
+ dataView.setInt16(bytePos, this._clampRawValue(rawValue, typeCode), littleEndian);
602
+ break;
603
+ case "H":
604
+ dataView.setUint16(bytePos, this._clampRawValue(rawValue, typeCode), littleEndian);
605
+ break;
606
+ case "i":
607
+ case "l":
608
+ dataView.setInt32(bytePos, this._clampRawValue(rawValue, typeCode), littleEndian);
609
+ break;
610
+ case "I":
611
+ case "L":
612
+ dataView.setUint32(bytePos, this._clampRawValue(rawValue, typeCode), littleEndian);
613
+ break;
614
+ case "f":
615
+ dataView.setFloat32(bytePos, rawValue, littleEndian);
616
+ break;
617
+ case "d":
618
+ dataView.setFloat64(bytePos, rawValue, littleEndian);
619
+ break;
620
+ default:
621
+ RaftLog.warn(`RaftChannelSimulated._writeRawValueToBuffer - unsupported attribute type ${typeCode}`);
622
+ return -1;
623
+ }
624
+
625
+ return bytePos + valueSize;
626
+ }
627
+
628
+ private _byteSizeForType(typeCode: string): number {
629
+ switch (typeCode) {
630
+ case "b":
631
+ case "B":
632
+ case "c":
633
+ case "C":
634
+ case "?":
635
+ return 1;
636
+ case "h":
637
+ case "H":
638
+ return 2;
639
+ case "i":
640
+ case "I":
641
+ case "l":
642
+ case "L":
643
+ case "f":
644
+ return 4;
645
+ case "d":
646
+ return 8;
647
+ default:
648
+ return 0;
649
+ }
650
+ }
651
+
652
+ private _clampRawValue(rawValue: number, typeCode: string): number {
653
+ const value = Math.round(rawValue);
654
+
655
+ switch (typeCode) {
656
+ case "b":
657
+ case "c":
658
+ return Math.max(-128, Math.min(127, value));
659
+ case "B":
660
+ case "C":
661
+ case "?":
662
+ return Math.max(0, Math.min(255, value));
663
+ case "h":
664
+ return Math.max(-32768, Math.min(32767, value));
665
+ case "H":
666
+ return Math.max(0, Math.min(65535, value));
667
+ case "i":
668
+ case "l":
669
+ return Math.max(-2147483648, Math.min(2147483647, value));
670
+ case "I":
671
+ case "L":
672
+ return Math.max(0, Math.min(4294967295, value));
673
+ default:
674
+ return value;
675
+ }
676
+ }
677
+
678
+ private _isFloatType(typeCode: string): boolean {
679
+ return typeCode === "f" || typeCode === "d";
680
+ }
681
+
404
682
  // Helper function to convert bytes to hex string
405
683
  private _bytesToHexStr(bytes: Uint8Array): string {
406
684
  return Array.from(bytes)
@@ -411,6 +689,29 @@ export default class RaftChannelSimulated implements RaftChannel {
411
689
  // Simulated device type information - this is a copy of part of DeviceTypeInfo in RaftCore
412
690
  private _deviceTypeInfo: DeviceTypeInfoRecs =
413
691
  {
692
+ "AMG8833": {
693
+ "name": "AMG8833",
694
+ "desc": "Thermal Camera",
695
+ "manu": "Panasonic",
696
+ "type": "AMG8833",
697
+ "clas": ["TCAM"],
698
+ "resp": {
699
+ "b": 128,
700
+ "a": [
701
+ {
702
+ "n": "temp",
703
+ "t": "<h[64]",
704
+ "resolution": "8x8",
705
+ "u": "&deg;C",
706
+ "r": [-55, 125],
707
+ "s": -4,
708
+ "d": 64,
709
+ "f": ".2f",
710
+ "o": "float"
711
+ }
712
+ ]
713
+ }
714
+ },
414
715
  "LSM6DS": {
415
716
  "name": "LSM6DS",
416
717
  "desc": "6-Axis IMU",
@@ -476,7 +777,40 @@ export default class RaftChannelSimulated implements RaftChannel {
476
777
  }
477
778
  ]
478
779
  }
780
+ },
781
+ "LTR-329": {
782
+ "name": "LTR-329",
783
+ "desc": "Visible light and IR Sensor",
784
+ "manu": "Lite On",
785
+ "type": "LTR-329",
786
+ "clas": ["LGHT"],
787
+ "resp": {
788
+ "b": 4,
789
+ "a": [
790
+ {
791
+ "n": "ir",
792
+ "t": "<h",
793
+ "u": "lux",
794
+ "r": [0, 64000],
795
+ "f": "d",
796
+ "o": "uint16"
797
+ },
798
+ {
799
+ "n": "visible",
800
+ "t": "<h",
801
+ "u": "lux",
802
+ "r": [0, 64000],
803
+ "f": "d",
804
+ "o": "uint16"
805
+ }
806
+ ],
807
+ "c": {
808
+ "n": "ltr329_light_calc",
809
+ "c": "int combined = buf[0] + (((uint16_t)buf[1])<<8); out.ir = buf[2] + (((uint16_t)buf[3])<<8); out.visible = combined - out.ir;",
810
+ "j": "let combined = buf[0] + (buf[1] << 8); let ir = buf[2] + (buf[3] << 8); attrValues['ir'].push(ir); attrValues['visible'].push(Math.max(0, combined - ir));"
811
+ }
812
+ }
479
813
  }
480
814
  };
481
815
 
482
- }
816
+ }