@robotical/raftjs 2.0.4 → 2.0.5

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.
@@ -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,55 @@ 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
298
+
300
299
  const numAttributes = attributes.length;
301
- // Adjust frequency based on the device interval with N samples per cycle
302
300
  const numSamplesPerCycle = 10;
303
- let frequencyHz = 0.1; // Default frequency in Hz
304
- if (deviceIntervalMs > 0) {
305
- frequencyHz = (1000 / deviceIntervalMs) / numSamplesPerCycle;
306
- }
307
-
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
318
- 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}`);
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;
374
322
  }
375
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
333
+ );
334
+
335
+ if (nextBytePos < 0) {
336
+ RaftLog.warn(`RaftChannelSimulated._createSimulatedDeviceInfoMsg - buffer overflow writing ${attr.n}`);
337
+ break;
338
+ }
339
+
340
+ bytePos = nextBytePos;
341
+ }
376
342
  }
377
-
343
+
378
344
  // Convert the buffer to a byte array
379
345
  const dataBytes = new Uint8Array(dataBuffer);
380
-
346
+
381
347
  // Create the JSON message structure
382
348
  const message = {
383
349
  "BUS1": {
@@ -401,6 +367,264 @@ export default class RaftChannelSimulated implements RaftChannel {
401
367
 
402
368
  }
403
369
 
370
+ private _parseAttrType(attrType: string): { typeCode: string; repeatCount: number; littleEndian: boolean } {
371
+ const repeatMatch = attrType.match(/\[(\d+)\]\s*$/);
372
+ const repeatCount = repeatMatch ? parseInt(repeatMatch[1], 10) : 1;
373
+ const coreType = repeatMatch ? attrType.slice(0, repeatMatch.index) : attrType;
374
+ let littleEndian = false;
375
+ let typeCode = coreType.trim();
376
+
377
+ if (typeCode.startsWith("<")) {
378
+ littleEndian = true;
379
+ typeCode = typeCode.slice(1);
380
+ } else if (typeCode.startsWith(">")) {
381
+ littleEndian = false;
382
+ typeCode = typeCode.slice(1);
383
+ } else if (typeCode === "f") {
384
+ // Match previous behaviour - plain "f" treated as little endian floats
385
+ littleEndian = true;
386
+ }
387
+
388
+ return { typeCode, repeatCount, littleEndian };
389
+ }
390
+
391
+ private _generateAttributeScaledValues(
392
+ attr: any,
393
+ attrIdx: number,
394
+ repeatCount: number,
395
+ numAttributes: number,
396
+ timeRadians: number,
397
+ deviceTimeMs: number
398
+ ): number[] {
399
+ const amplitude = 0.8;
400
+
401
+ if (repeatCount > 1) {
402
+ const useThermalGrid = (attr && typeof attr.resolution === "string") || repeatCount >= 16;
403
+ if (useThermalGrid) {
404
+ return this._generateThermalGridValues(attr, repeatCount, timeRadians, deviceTimeMs);
405
+ }
406
+
407
+ const values: number[] = [];
408
+ for (let elemIdx = 0; elemIdx < repeatCount; elemIdx++) {
409
+ const phaseOffset = (2 * Math.PI * (attrIdx + elemIdx / repeatCount)) / Math.max(1, numAttributes);
410
+ const sinValue = Math.sin(timeRadians + phaseOffset);
411
+
412
+ if (Array.isArray(attr.r) && attr.r.length >= 2) {
413
+ const minValue = attr.r[0];
414
+ const maxValue = attr.r[1];
415
+ const midPoint = (maxValue + minValue) / 2;
416
+ const range = (maxValue - minValue) / 2;
417
+ const value = midPoint + sinValue * range * amplitude;
418
+ values.push(Math.min(maxValue, Math.max(minValue, value)));
419
+ } else {
420
+ values.push(sinValue * 1000 * amplitude);
421
+ }
422
+ }
423
+ return values;
424
+ }
425
+
426
+ const phaseOffset = numAttributes > 0 ? (2 * Math.PI * attrIdx) / numAttributes : 0;
427
+ const sinValue = Math.sin(timeRadians + phaseOffset);
428
+
429
+ if (Array.isArray(attr.r) && attr.r.length >= 2) {
430
+ const minValue = attr.r[0];
431
+ const maxValue = attr.r[1];
432
+ const midPoint = (maxValue + minValue) / 2;
433
+ const range = (maxValue - minValue) / 2;
434
+ const value = midPoint + sinValue * range * amplitude;
435
+ return [Math.min(maxValue, Math.max(minValue, value))];
436
+ }
437
+
438
+ return [sinValue * 1000 * amplitude];
439
+ }
440
+
441
+ private _generateThermalGridValues(
442
+ attr: any,
443
+ repeatCount: number,
444
+ timeRadians: number,
445
+ deviceTimeMs: number
446
+ ): number[] {
447
+ const { rows, cols } = this._getGridDimensions(attr, repeatCount);
448
+ const values: number[] = [];
449
+ const ambientBase = 24 + 2 * Math.sin(deviceTimeMs / 7000);
450
+ const hotspotPhase = deviceTimeMs / 3200;
451
+ const hotspotRow = (Math.sin(hotspotPhase) + 1) * (rows - 1) / 2;
452
+ const hotspotCol = (Math.cos(hotspotPhase) + 1) * (cols - 1) / 2;
453
+ const hotspotAmplitude = 6;
454
+ const sigma = Math.max(rows, cols) / 3 || 1;
455
+
456
+ for (let idx = 0; idx < repeatCount; idx++) {
457
+ const row = Math.floor(idx / cols);
458
+ const col = idx % cols;
459
+ const dist = Math.hypot(row - hotspotRow, col - hotspotCol);
460
+ const hotspot = hotspotAmplitude * Math.exp(-(dist * dist) / (2 * sigma * sigma));
461
+ const gentleWave = 0.5 * Math.sin(timeRadians + row * 0.35 + col * 0.25);
462
+ let value = ambientBase + hotspot + gentleWave;
463
+
464
+ if (Array.isArray(attr.r) && attr.r.length >= 2) {
465
+ value = Math.min(attr.r[1], Math.max(attr.r[0], value));
466
+ }
467
+
468
+ values.push(value);
469
+ }
470
+
471
+ return values;
472
+ }
473
+
474
+ private _getGridDimensions(attr: any, repeatCount: number): { rows: number; cols: number } {
475
+ if (attr && typeof attr.resolution === "string") {
476
+ const match = attr.resolution.match(/(\d+)\s*x\s*(\d+)/i);
477
+ if (match) {
478
+ const rows = parseInt(match[1], 10);
479
+ const cols = parseInt(match[2], 10);
480
+ if (rows > 0 && cols > 0) {
481
+ return { rows, cols };
482
+ }
483
+ }
484
+ }
485
+
486
+ const side = Math.round(Math.sqrt(repeatCount));
487
+ if (side > 0 && side * side === repeatCount) {
488
+ return { rows: side, cols: side };
489
+ }
490
+
491
+ return { rows: repeatCount, cols: 1 };
492
+ }
493
+
494
+ private _prepareRawValue(attr: any, typeCode: string, scaledValue: number): number {
495
+ if (this._isFloatType(typeCode)) {
496
+ return scaledValue;
497
+ }
498
+
499
+ let raw = scaledValue;
500
+
501
+ if (attr && typeof attr.a === "number") {
502
+ raw -= attr.a;
503
+ }
504
+ if (attr && typeof attr.d === "number") {
505
+ raw *= attr.d;
506
+ }
507
+ if (attr && typeof attr.s === "number" && attr.s !== 0) {
508
+ const shift = attr.s;
509
+ const shiftFactor = Math.pow(2, Math.abs(shift));
510
+ if (shift > 0) {
511
+ raw *= shiftFactor;
512
+ } else {
513
+ raw /= shiftFactor;
514
+ }
515
+ }
516
+
517
+ return Math.round(raw);
518
+ }
519
+
520
+ private _writeRawValueToBuffer(
521
+ dataView: DataView,
522
+ bytePos: number,
523
+ typeCode: string,
524
+ littleEndian: boolean,
525
+ rawValue: number
526
+ ): number {
527
+ const valueSize = this._byteSizeForType(typeCode);
528
+ if (valueSize <= 0 || bytePos + valueSize > dataView.byteLength) {
529
+ return -1;
530
+ }
531
+
532
+ switch (typeCode) {
533
+ case "b":
534
+ dataView.setInt8(bytePos, this._clampRawValue(rawValue, typeCode));
535
+ break;
536
+ case "c":
537
+ dataView.setInt8(bytePos, this._clampRawValue(rawValue, typeCode));
538
+ break;
539
+ case "B":
540
+ case "C":
541
+ dataView.setUint8(bytePos, this._clampRawValue(rawValue, typeCode));
542
+ break;
543
+ case "?":
544
+ dataView.setUint8(bytePos, rawValue ? 1 : 0);
545
+ break;
546
+ case "h":
547
+ dataView.setInt16(bytePos, this._clampRawValue(rawValue, typeCode), littleEndian);
548
+ break;
549
+ case "H":
550
+ dataView.setUint16(bytePos, this._clampRawValue(rawValue, typeCode), littleEndian);
551
+ break;
552
+ case "i":
553
+ case "l":
554
+ dataView.setInt32(bytePos, this._clampRawValue(rawValue, typeCode), littleEndian);
555
+ break;
556
+ case "I":
557
+ case "L":
558
+ dataView.setUint32(bytePos, this._clampRawValue(rawValue, typeCode), littleEndian);
559
+ break;
560
+ case "f":
561
+ dataView.setFloat32(bytePos, rawValue, littleEndian);
562
+ break;
563
+ case "d":
564
+ dataView.setFloat64(bytePos, rawValue, littleEndian);
565
+ break;
566
+ default:
567
+ RaftLog.warn(`RaftChannelSimulated._writeRawValueToBuffer - unsupported attribute type ${typeCode}`);
568
+ return -1;
569
+ }
570
+
571
+ return bytePos + valueSize;
572
+ }
573
+
574
+ private _byteSizeForType(typeCode: string): number {
575
+ switch (typeCode) {
576
+ case "b":
577
+ case "B":
578
+ case "c":
579
+ case "C":
580
+ case "?":
581
+ return 1;
582
+ case "h":
583
+ case "H":
584
+ return 2;
585
+ case "i":
586
+ case "I":
587
+ case "l":
588
+ case "L":
589
+ case "f":
590
+ return 4;
591
+ case "d":
592
+ return 8;
593
+ default:
594
+ return 0;
595
+ }
596
+ }
597
+
598
+ private _clampRawValue(rawValue: number, typeCode: string): number {
599
+ const value = Math.round(rawValue);
600
+
601
+ switch (typeCode) {
602
+ case "b":
603
+ case "c":
604
+ return Math.max(-128, Math.min(127, value));
605
+ case "B":
606
+ case "C":
607
+ case "?":
608
+ return Math.max(0, Math.min(255, value));
609
+ case "h":
610
+ return Math.max(-32768, Math.min(32767, value));
611
+ case "H":
612
+ return Math.max(0, Math.min(65535, value));
613
+ case "i":
614
+ case "l":
615
+ return Math.max(-2147483648, Math.min(2147483647, value));
616
+ case "I":
617
+ case "L":
618
+ return Math.max(0, Math.min(4294967295, value));
619
+ default:
620
+ return value;
621
+ }
622
+ }
623
+
624
+ private _isFloatType(typeCode: string): boolean {
625
+ return typeCode === "f" || typeCode === "d";
626
+ }
627
+
404
628
  // Helper function to convert bytes to hex string
405
629
  private _bytesToHexStr(bytes: Uint8Array): string {
406
630
  return Array.from(bytes)
@@ -411,6 +635,29 @@ export default class RaftChannelSimulated implements RaftChannel {
411
635
  // Simulated device type information - this is a copy of part of DeviceTypeInfo in RaftCore
412
636
  private _deviceTypeInfo: DeviceTypeInfoRecs =
413
637
  {
638
+ "AMG8833": {
639
+ "name": "AMG8833",
640
+ "desc": "Thermal Camera",
641
+ "manu": "Panasonic",
642
+ "type": "AMG8833",
643
+ "clas": ["TCAM"],
644
+ "resp": {
645
+ "b": 128,
646
+ "a": [
647
+ {
648
+ "n": "temp",
649
+ "t": "<h[64]",
650
+ "resolution": "8x8",
651
+ "u": "&deg;C",
652
+ "r": [-55, 125],
653
+ "s": -4,
654
+ "d": 64,
655
+ "f": ".2f",
656
+ "o": "float"
657
+ }
658
+ ]
659
+ }
660
+ },
414
661
  "LSM6DS": {
415
662
  "name": "LSM6DS",
416
663
  "desc": "6-Axis IMU",
@@ -479,4 +726,4 @@ export default class RaftChannelSimulated implements RaftChannel {
479
726
  }
480
727
  };
481
728
 
482
- }
729
+ }
@@ -73,6 +73,7 @@ export interface DeviceTypeAttribute {
73
73
  vf?: boolean | number; // Display attribute value in the device info panel
74
74
  vft?: string; // Attribute validity based on the value of another named attribute
75
75
  lut?: Array<LUTRow>; // Lookup table for the attribute value - each row is a lookup table for a range of values e.g. [{"r":"0x20-0x30","v":0},{"r":"1,2,3","v":42},{r:"","v":1}]
76
+ resolution?: string;
76
77
  }
77
78
 
78
79
  export interface CustomFunctionDefinition {