@robotical/raftjs 2.0.5 → 2.0.7

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.
@@ -296,49 +296,62 @@ export default class RaftChannelSimulated implements RaftChannel {
296
296
  dataView.setUint16(bytePos, deviceTimeMs % 65536, false);
297
297
  bytePos += 2;
298
298
 
299
- const numAttributes = attributes.length;
300
- const numSamplesPerCycle = 10;
301
- const frequencyHz = (deviceIntervalMs > 0)
302
- ? (1000 / deviceIntervalMs) / numSamplesPerCycle
303
- : 0.1;
304
- const timeRadians = deviceTimeMs * frequencyHz * (2 * Math.PI) / 1000;
305
-
306
- // Iterate through attributes and fill the payload
307
- for (let attrIdx = 0; attrIdx < numAttributes; attrIdx++) {
308
- const attr = attributes[attrIdx];
309
- const { typeCode, repeatCount, littleEndian } = this._parseAttrType(attr.t);
310
- const scaledValues = this._generateAttributeScaledValues(
311
- attr,
312
- attrIdx,
313
- repeatCount,
314
- numAttributes,
315
- timeRadians,
316
- deviceTimeMs
317
- );
318
-
319
- if (scaledValues.length !== repeatCount) {
320
- RaftLog.warn(`RaftChannelSimulated._createSimulatedDeviceInfoMsg - value count mismatch for ${attr.n}`);
321
- continue;
322
- }
323
-
324
- for (let elemIdx = 0; elemIdx < repeatCount; elemIdx++) {
325
- const scaledValue = scaledValues[elemIdx];
326
- const rawValue = this._prepareRawValue(attr, typeCode, scaledValue);
327
- const nextBytePos = this._writeRawValueToBuffer(
328
- dataView,
329
- bytePos,
330
- typeCode,
331
- littleEndian,
332
- rawValue
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;
314
+ const timeRadians = deviceTimeMs * frequencyHz * (2 * Math.PI) / 1000;
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
333
327
  );
334
328
 
335
- if (nextBytePos < 0) {
336
- RaftLog.warn(`RaftChannelSimulated._createSimulatedDeviceInfoMsg - buffer overflow writing ${attr.n}`);
337
- break;
329
+ if (scaledValues.length !== repeatCount) {
330
+ RaftLog.warn(`RaftChannelSimulated._createSimulatedDeviceInfoMsg - value count mismatch for ${attr.n}`);
331
+ continue;
338
332
  }
339
333
 
340
- bytePos = nextBytePos;
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
+ }
341
352
  }
353
+ } else {
354
+ bytePos += dataBlockSizeBytes;
342
355
  }
343
356
 
344
357
  // Convert the buffer to a byte array
@@ -471,6 +484,80 @@ export default class RaftChannelSimulated implements RaftChannel {
471
484
  return values;
472
485
  }
473
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
+ switch (deviceTypeInfo.type) {
496
+ case "LTR-329": {
497
+ if (dataBlockSizeBytes < 4) {
498
+ return false;
499
+ }
500
+
501
+ const frequencyHz = (deviceIntervalMs > 0)
502
+ ? (1000 / deviceIntervalMs) / 10
503
+ : 0.1;
504
+ const timeRadians = deviceTimeMs * frequencyHz * (2 * Math.PI) / 1000;
505
+
506
+ const range = deviceTypeInfo.resp?.a?.[0]?.r ?? [0, 64000];
507
+ const minLux = range[0] ?? 0;
508
+ const maxLux = range[1] ?? 64000;
509
+
510
+ const baseLux = (maxLux + minLux) / 4;
511
+ const amplitudeLux = (maxLux - minLux) / 6;
512
+ let combined = Math.round(baseLux + amplitudeLux * Math.sin(timeRadians));
513
+ combined = Math.max(minLux, Math.min(maxLux, combined));
514
+
515
+ const irBase = combined * 0.35;
516
+ const irVariance = (combined * 0.15) * Math.sin(timeRadians + Math.PI / 4);
517
+ let ir = Math.round(irBase + irVariance);
518
+ ir = Math.max(minLux, Math.min(combined, ir));
519
+
520
+ dataView.setUint16(bytePos, combined, true);
521
+ dataView.setUint16(bytePos + 2, ir, true);
522
+
523
+ return true;
524
+ }
525
+ case "RoboticalServo": {
526
+ if (dataBlockSizeBytes < 6) {
527
+ return false;
528
+ }
529
+
530
+ const swingAmplitudeDeg = 90;
531
+ const cycleMs = 4000;
532
+ const angularSpeedRadPerMs = (2 * Math.PI) / cycleMs;
533
+ const phaseRadians = (deviceTimeMs % cycleMs) * angularSpeedRadPerMs;
534
+
535
+ const angleDegrees = swingAmplitudeDeg * Math.sin(phaseRadians);
536
+ const velocityDegPerSec = swingAmplitudeDeg * angularSpeedRadPerMs * 1000 * Math.cos(phaseRadians);
537
+ const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
538
+
539
+ const angleRaw = clamp(Math.round(angleDegrees * 10), -1800, 1800);
540
+ let velocityRaw = Math.round(velocityDegPerSec);
541
+ velocityRaw = clamp(velocityRaw, -32768, 32767);
542
+
543
+ let currentRaw = Math.round(20 + 0.08 * Math.abs(velocityDegPerSec));
544
+ currentRaw = clamp(currentRaw, 0, 127);
545
+
546
+ const isMoving = Math.abs(velocityDegPerSec) > 5;
547
+ const stateRaw = (0x02) | (isMoving ? 0x01 : 0);
548
+
549
+ dataView.setInt16(bytePos, angleRaw, false);
550
+ dataView.setInt8(bytePos + 2, currentRaw);
551
+ dataView.setUint8(bytePos + 3, stateRaw);
552
+ dataView.setInt16(bytePos + 4, velocityRaw, false);
553
+
554
+ return true;
555
+ }
556
+ default:
557
+ return false;
558
+ }
559
+ }
560
+
474
561
  private _getGridDimensions(attr: any, repeatCount: number): { rows: number; cols: number } {
475
562
  if (attr && typeof attr.resolution === "string") {
476
563
  const match = attr.resolution.match(/(\d+)\s*x\s*(\d+)/i);
@@ -723,6 +810,102 @@ export default class RaftChannelSimulated implements RaftChannel {
723
810
  }
724
811
  ]
725
812
  }
813
+ },
814
+ "LTR-329": {
815
+ "name": "LTR-329",
816
+ "desc": "Visible light and IR Sensor",
817
+ "manu": "Lite On",
818
+ "type": "LTR-329",
819
+ "clas": ["LGHT"],
820
+ "resp": {
821
+ "b": 4,
822
+ "a": [
823
+ {
824
+ "n": "ir",
825
+ "t": "<h",
826
+ "u": "lux",
827
+ "r": [0, 64000],
828
+ "f": "d",
829
+ "o": "uint16"
830
+ },
831
+ {
832
+ "n": "visible",
833
+ "t": "<h",
834
+ "u": "lux",
835
+ "r": [0, 64000],
836
+ "f": "d",
837
+ "o": "uint16"
838
+ }
839
+ ],
840
+ "c": {
841
+ "n": "ltr329_light_calc",
842
+ "c": "int combined = buf[0] + (((uint16_t)buf[1])<<8); out.ir = buf[2] + (((uint16_t)buf[3])<<8); out.visible = combined - out.ir;",
843
+ "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));"
844
+ }
845
+ }
846
+ },
847
+ "RoboticalServo": {
848
+ "name": "Robotical Servo",
849
+ "desc": "Servo",
850
+ "manu": "Robotical",
851
+ "type": "RoboticalServo",
852
+ "clas": ["SRVO"],
853
+ "resp": {
854
+ "b": 6,
855
+ "a": [
856
+ {
857
+ "n": "angle",
858
+ "t": ">h",
859
+ "r": [-180.0, 180.0],
860
+ "f": ".1f",
861
+ "d": 10,
862
+ "o": "int16",
863
+ "u": "degrees"
864
+ },
865
+ {
866
+ "n": "current",
867
+ "t": "b",
868
+ "r": [-128, 127],
869
+ "f": "d",
870
+ "o": "int8"
871
+ },
872
+ {
873
+ "n": "state",
874
+ "t": "B",
875
+ "r": [0, 255],
876
+ "f": "02x",
877
+ "o": "uint8"
878
+ },
879
+ {
880
+ "n": "velocity",
881
+ "t": ">h",
882
+ "r": [-32768, 32767],
883
+ "f": "d",
884
+ "o": "int16"
885
+ }
886
+ ]
887
+ },
888
+ "actions": [
889
+ {
890
+ "n": "angle",
891
+ "t": ">h",
892
+ "w": "0001",
893
+ "wz": "0064",
894
+ "f": ".1f",
895
+ "mul": 10,
896
+ "sub": 0,
897
+ "r": [-180.0, 180.0],
898
+ "d": 0
899
+ },
900
+ {
901
+ "n": "enable",
902
+ "t": "B",
903
+ "w": "20",
904
+ "f": "b",
905
+ "r": [0, 1],
906
+ "d": 1
907
+ }
908
+ ]
726
909
  }
727
910
  };
728
911
 
@@ -7,23 +7,32 @@
7
7
  //
8
8
  /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9
9
 
10
- import { DeviceTypePollRespMetadata } from "./RaftDeviceInfo";
10
+ import { CustomFunctionDefinition, DeviceTypePollRespMetadata } from "./RaftDeviceInfo";
11
+
12
+ type CustomAttrJsFn = (
13
+ buf: Uint8Array,
14
+ attrValues: Record<string, number[]>,
15
+ attrValueVecs: number[][],
16
+ pollRespMetadata: DeviceTypePollRespMetadata,
17
+ msgBuffer: Uint8Array,
18
+ msgBufIdx: number,
19
+ numMsgBytes: number
20
+ ) => void;
11
21
 
12
22
  export default class CustomAttrHandler {
23
+
24
+ private _jsFunctionCache = new Map<string, CustomAttrJsFn>();
13
25
 
14
26
  public handleAttr(pollRespMetadata: DeviceTypePollRespMetadata, msgBuffer: Uint8Array, msgBufIdx: number): number[][] {
15
27
 
16
- // Implement the pseudo-code:
17
- // 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;}
18
-
19
- // Number of bytes in the each message
28
+ // Number of bytes in each message
20
29
  const numMsgBytes = pollRespMetadata.b;
21
30
 
22
31
  // Create a vector for each attribute in the metadata
23
- const attrValueVecs: [][] = [];
32
+ const attrValueVecs: number[][] = [];
24
33
 
25
34
  // Reference to each vector by attribute name
26
- const attrValues: { [key: string]: number[] } = {};
35
+ const attrValues: Record<string, number[]> = {};
27
36
 
28
37
  // Add attributes to the vector
29
38
  for (let attrIdx = 0; attrIdx < pollRespMetadata.a.length; attrIdx++) {
@@ -31,41 +40,78 @@ export default class CustomAttrHandler {
31
40
  attrValues[pollRespMetadata.a[attrIdx].n] = attrValueVecs[attrIdx];
32
41
  }
33
42
 
34
- // Custom code for each device type
35
- if (pollRespMetadata.c!.n === "max30101_fifo") {
36
- // Hex dump msgBuffer
37
- // console.log(`CustomAttrHandler handleAttr ${pollRespMetadata.c!.n} msgBuffer: ${msgBuffer.toString('hex')}`);
38
- const buf = msgBuffer.slice(msgBufIdx);
39
- if (buf.length < numMsgBytes) {
40
- return [];
43
+ const customFnDef = pollRespMetadata.c;
44
+ if (!customFnDef) {
45
+ return attrValueVecs;
46
+ }
47
+
48
+ // Provide the message buffer sliced to the data portion
49
+ const buf = msgBuffer.slice(msgBufIdx);
50
+ if (buf.length < numMsgBytes) {
51
+ return [];
52
+ }
53
+
54
+ // Execute supplied JS implementation if provided
55
+ if (customFnDef.j && customFnDef.j.trim().length > 0) {
56
+ const jsFn = this.getOrCompileJsFunction(customFnDef);
57
+ if (!jsFn) {
58
+ return attrValueVecs;
59
+ }
60
+ try {
61
+ jsFn(buf, attrValues, attrValueVecs, pollRespMetadata, msgBuffer, msgBufIdx, numMsgBytes);
62
+ } catch (err) {
63
+ console.error(`CustomAttrHandler JS function ${customFnDef.n} execution failed`, err);
41
64
  }
65
+ return attrValueVecs;
66
+ }
42
67
 
68
+ // Custom code for each device type handled natively
69
+ if (customFnDef.n === "max30101_fifo") {
43
70
  // Generated code ...
44
- const N=(buf[0]+32-buf[2])%32;
45
- let k=3;
46
- let i=0;
47
- while (i<N) {
48
- attrValues['Red'].push(0); attrValues['Red'][attrValues['Red'].length-1] =(buf[k]<<16)|(buf[k+1]<<8)|buf[k+2];
49
- attrValues['IR'].push(0); attrValues['IR'][attrValues['IR'].length-1] =(buf[k+3]<<16)|(buf[k+4]<<8)|buf[k+5];
50
- k+=6;
71
+ const N = (buf[0] + 32 - buf[2]) % 32;
72
+ let k = 3;
73
+ let i = 0;
74
+ while (i < N) {
75
+ attrValues["Red"].push(0);
76
+ attrValues["Red"][attrValues["Red"].length - 1] = (buf[k] << 16) | (buf[k + 1] << 8) | buf[k + 2];
77
+ attrValues["IR"].push(0);
78
+ attrValues["IR"][attrValues["IR"].length - 1] = (buf[k + 3] << 16) | (buf[k + 4] << 8) | buf[k + 5];
79
+ k += 6;
51
80
  i++;
52
- ;
53
- }
54
- } else if (pollRespMetadata.c!.n === "gravity_o2_calc") {
55
- // Get the buffer
56
- const buf = msgBuffer.slice(msgBufIdx);
57
- if (buf.length < numMsgBytes) {
58
- return [];
59
81
  }
60
-
61
- // Implement the pseudo-code:
62
- // float key = 20.9/120.0; float val = key * (buf[0] + (buf[1]/10.0) + (buf[2]/100.0)); out.oxygen = val;
82
+ } else if (customFnDef.n === "gravity_o2_calc") {
63
83
  const key = 20.9 / 120.0;
64
- const val = key * (buf[0] + (buf[1] / 10.0) + (buf[2] / 100.0));
65
-
66
- // Add the value to the oxygen attribute
67
- attrValues['oxygen'].push(val);
84
+ const val = key * (buf[0] + buf[1] / 10.0 + buf[2] / 100.0);
85
+ attrValues["oxygen"].push(val);
68
86
  }
69
87
  return attrValueVecs;
70
88
  }
89
+
90
+ private getOrCompileJsFunction(customFnDef: CustomFunctionDefinition): CustomAttrJsFn | null {
91
+ if (!customFnDef.j) {
92
+ return null;
93
+ }
94
+ const cacheKey = `${customFnDef.n}::${customFnDef.j}`;
95
+ const cachedFn = this._jsFunctionCache.get(cacheKey);
96
+ if (cachedFn) {
97
+ return cachedFn;
98
+ }
99
+ try {
100
+ const compiledFn = new Function(
101
+ "buf",
102
+ "attrValues",
103
+ "attrValueVecs",
104
+ "pollRespMetadata",
105
+ "msgBuffer",
106
+ "msgBufIdx",
107
+ "numMsgBytes",
108
+ customFnDef.j
109
+ ) as CustomAttrJsFn;
110
+ this._jsFunctionCache.set(cacheKey, compiledFn);
111
+ return compiledFn;
112
+ } catch (err) {
113
+ console.error(`CustomAttrHandler failed to compile JS function ${customFnDef.n}`, err);
114
+ return null;
115
+ }
116
+ }
71
117
  }
@@ -79,6 +79,7 @@ export interface DeviceTypeAttribute {
79
79
  export interface CustomFunctionDefinition {
80
80
  n: string; // Function name
81
81
  c: string; // Function pseudo-code
82
+ j?: string; // Optional JavaScript implementation
82
83
  }
83
84
 
84
85
  export interface DeviceTypePollRespMetadata {