@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.
- package/dist/react-native/RaftChannelSimulated.d.ts +9 -0
- package/dist/react-native/RaftChannelSimulated.js +259 -77
- package/dist/react-native/RaftChannelSimulated.js.map +1 -1
- package/dist/react-native/RaftDeviceInfo.d.ts +1 -0
- package/dist/web/RaftChannelSimulated.d.ts +9 -0
- package/dist/web/RaftChannelSimulated.js +259 -77
- package/dist/web/RaftChannelSimulated.js.map +1 -1
- package/dist/web/RaftDeviceInfo.d.ts +1 -0
- package/package.json +1 -1
- package/src/RaftChannelSimulated.ts +325 -78
- package/src/RaftDeviceInfo.ts +1 -0
|
@@ -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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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": "°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
|
+
}
|
package/src/RaftDeviceInfo.ts
CHANGED
|
@@ -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 {
|