loupedeck-commander 1.3.0 → 1.3.2

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/common/button.mjs CHANGED
@@ -20,9 +20,10 @@ export class Button {
20
20
  //#data
21
21
  #index = 0
22
22
  #enforcedIndex = -1
23
+ #enforcedBlink = false
23
24
  #event
24
25
  #keys
25
- #states
26
+ #states
26
27
  // Timestamp when button was pressed
27
28
  timeStampPressed
28
29
  // Timestamp when button was released
@@ -31,48 +32,49 @@ export class Button {
31
32
  timeHold
32
33
  #counter = 0
33
34
 
34
- constructor (id, width, height, data={},key,profile = {}) {
35
+ constructor(id, width, height, data = {}, key, profile = {}) {
35
36
  this.#states = {}
36
37
  this.#keys = []
37
-
38
+
38
39
  this.id = id
39
40
  this.#index = 0
40
41
  this.#enforcedIndex = -1
42
+ this.#enforcedBlink = false
41
43
 
42
44
  this.#profile = profile
43
45
  this.#params = {
44
- "key" : key,
45
- "state" : '',
46
- "width" : width,
47
- "height" : height,
48
- "min" : 0,
49
- "max" : 100,
50
- "x" : 0,
51
- "y" : 0,
52
- "color" : '#000000',
53
- "text" : '',
54
- "textColor" : '#ffffff',
55
- "textAlign" : 'center',
56
- "textBaseline": 'top',
57
- "font" :'16px Arial',
58
- "value" : 50,
59
- "minPressed": 25,
60
- "moveLeft" : false,
61
- "moveRight" : false,
62
- "moveUp" : false,
63
- "moveDown" : false,
64
- "nodeid" : '',
65
- "type" : ButtonType.TOGGLE,
66
- "group" : key,
67
- "filter" : "",
68
- "blink" : false,
69
- "vibrate" : false,
70
- "profile" : "",
71
- "brightness" : undefined,
72
- "default" : "0"
73
- }
74
-
75
- if (profile.parameters){
46
+ "key": key,
47
+ "state": '',
48
+ "width": width,
49
+ "height": height,
50
+ "min": 0,
51
+ "max": 100,
52
+ "x": 0,
53
+ "y": 0,
54
+ "color": '#000000',
55
+ "text": '',
56
+ "textColor": '#ffffff',
57
+ "textAlign": 'center',
58
+ "textBaseline": 'top',
59
+ "font": '16px Arial',
60
+ "value": 50,
61
+ "minPressed": 25,
62
+ "moveLeft": false,
63
+ "moveRight": false,
64
+ "moveUp": false,
65
+ "moveDown": false,
66
+ "nodeid": '',
67
+ "type": ButtonType.TOGGLE,
68
+ "group": key,
69
+ "filter": "",
70
+ "blink": false,
71
+ "vibrate": false,
72
+ "profile": "",
73
+ "brightness": undefined,
74
+ "default": "0"
75
+ }
76
+
77
+ if (profile.parameters) {
76
78
  let params = Object.keys(profile.parameters)
77
79
  for (let i = 0; i < params.length; i++) {
78
80
  const k = params[i]
@@ -83,7 +85,7 @@ export class Button {
83
85
  this.#params.key = key
84
86
 
85
87
  // New approach: Use all data from data.params:
86
- if (data.params){
88
+ if (data.params) {
87
89
  this.#params = {
88
90
  ...this.#params,
89
91
  ...data.params
@@ -98,16 +100,16 @@ export class Button {
98
100
  this.#states = data.states
99
101
  this.#keys = Object.keys(this.#states)
100
102
 
101
- var defaultIndex = this.#keys.indexOf(this.#params.default)
103
+ var defaultIndex = this.#keys.indexOf(this.#params.default)
102
104
  if (defaultIndex >= 0) {
103
105
  this.#index = defaultIndex
104
106
  } else {
105
107
  this.#params.default = this.#keys[0]
106
- console.info(` button ${id} default set to ${this.#params.default}, available states`, this.#keys)
108
+ // console.info(` button ${id} default set to ${this.#params.default}, available states`, this.#keys)
107
109
  }
108
110
  this.#params.state = this.#keys[this.#index]
109
111
  }
110
-
112
+
111
113
  // Set the default value for the button:
112
114
 
113
115
  // Legacy approach: Use data directly:
@@ -119,11 +121,11 @@ export class Button {
119
121
  console.warn("Legacy approach: text is deprecated, use params.text instead")
120
122
  this.#params.text = data.text
121
123
  }
122
- if (data.nodeid){
124
+ if (data.nodeid) {
123
125
  this.#params.nodeid = format(data.nodeid, this.getParams({}))
124
- console.warn("Legacy approach: nodeid is deprecated, use params.nodeid instead",this.#params.nodeid)
126
+ console.warn("Legacy approach: nodeid is deprecated, use params.nodeid instead", this.#params.nodeid)
125
127
  }
126
- if (data.type){
128
+ if (data.type) {
127
129
  console.warn("Legacy approach: type is deprecated, use params.type instead")
128
130
  this.#params.type = data.type
129
131
  }
@@ -133,18 +135,32 @@ export class Button {
133
135
  this.#index = this.#keys.indexOf(data.default)
134
136
  }
135
137
 
136
- if (data.group){
138
+ if (data.group) {
137
139
  console.warn("Legacy approach: group is deprecated, use params.group instead")
138
140
  this.#params.group = format(data.group, this.getParams({}))
139
141
  }
140
142
  // End Legacy approach
141
143
  }
142
144
 
143
- setState (index = 0) {
145
+ setState(index = 0) {
144
146
  this.#index = index
145
147
  }
146
148
 
147
- async drawPhysical (device, id) {
149
+ /**
150
+ *
151
+ * @returns Group attribute of the current button
152
+ */
153
+ getGroup() {
154
+ return this.#params.group
155
+ }
156
+
157
+ /**
158
+ * Draw a physical button (color in RGBA format) on a device
159
+ * @param {*} device
160
+ * @param {*} id - id of the physical button
161
+ * @returns
162
+ */
163
+ async drawPhysical(device, id) {
148
164
  const elem = this.getCurrentElement()
149
165
  if (!elem || !elem.color) { return }
150
166
 
@@ -156,52 +172,95 @@ export class Button {
156
172
  var idx = parseInt(id, 10);
157
173
 
158
174
  const val = {
159
- id:idx,
175
+ id: idx,
160
176
  color: `rgba(${r}, ${g}, ${b})`
161
177
  }
162
- // console.log(' Set Button Color',id, val.id,elem.color, val.color)
178
+ // console.log(' Set Button Color',id, val.id,elem.color, val.color)
163
179
  device.setButtonColor(val)
164
180
  } catch (error) {
165
181
  console.error(' Error', error)
166
182
  }
167
183
  }
168
184
 
169
- async draw (row, column, ctx) {
185
+ /**
186
+ * Draw the Touch-Screen Elementat the given row and column
187
+ * @param {*} row
188
+ * @param {*} column
189
+ * @param {*} ctx
190
+ */
191
+ async draw(row, column, ctx) {
192
+ // Calculate the x/y position based on the row and column and the width/height properties of the button
170
193
  const x = column * this.#params.width
171
194
  const y = row * this.#params.height
172
195
 
173
- const elem = this.getCurrentElement()
174
- let params = this.getParams(elem)
175
-
176
- if (elem) {
177
- if(elem.blink && this.#counter % 2 == 0) {
178
- // If blink is set, we switch the color to inverted color
179
- let oldColor = elem.color
180
- elem.color = params.textColor
181
- elem.textColor = oldColor
182
- //console.log("Blinking Button",this.#index,elem.color)
183
- }
196
+ // Modifications in draw:
197
+ // - Support color and textColor settings also in params section which
198
+ // can be overwriten by button state property. This way color and textColor
199
+ // can be defined overall for all states without repeating them in every state.
200
+ // - Same for blink property.
184
201
 
185
- ctx.fillStyle = params.color
186
- ctx.fillRect(x, y, params.width, params.height)
187
-
188
- if (elem.imgBuffer) {
189
- ctx.drawImage(elem.imgBuffer, x, y, params.width, params.height)
190
- }
202
+ let params = this.getParams(this.getCurrentElement())
203
+
204
+ let color = params.color
205
+ let textColor = params.textColor
206
+
207
+ if ((params.blink || this.#enforcedBlink) && this.#counter % 2 == 0) {
208
+ // If blink is set, we switch textcolor and background color:
209
+ color = params.textColor
210
+ textColor = params.color
191
211
  }
192
- if (params.text != undefined && params.text != '') {
193
- ctx.fillStyle = params.textColor
212
+
213
+ // Apply the color as fillStyle:
214
+ ctx.fillStyle = color
215
+ // Draw the background of the button::
216
+ ctx.fillRect(x, y, params.width, params.height)
217
+
218
+ // if we have an icon/image draw it on top of the button
219
+ if (params.imgBuffer) {
220
+ ctx.drawImage(params.imgBuffer, x, y, params.width, params.height)
221
+ }
222
+ // if we have a text defined, draw it on top:
223
+ if (params.text != undefined && params.text != '') {
224
+ ctx.fillStyle = textColor
194
225
  ctx.font = params.font
195
226
  ctx.textBaseline = params.textBaseline;
196
227
  ctx.textAlign = params.textAlign;
197
- let dynamicText = format(params.text, this.getParams(elem))
198
- ctx.fillText(dynamicText,x+6,y+6)
228
+ let dynamicText = format(params.text, params)
229
+ let tx = x
230
+ let ty = y
231
+ // Handle Text Horizontal Alignment
232
+ switch (params.textAlign) {
233
+ case "center":
234
+ tx += this.#params.width / 2;
235
+ break;
236
+ case "left":
237
+ tx += 6;
238
+ break;
239
+ case "right":
240
+ tx += this.#params.width - 6;
241
+ break;
242
+ }
243
+ // Handle Text Vertical Alignment
244
+ switch (params.textBaseline) {
245
+ case "middle":
246
+ ty += this.#params.height / 2;
247
+ break;
248
+ case "top":
249
+ ty += 6;
250
+ ctx.textBaseline = "top";
251
+ break;
252
+ case "bottom":
253
+ ty += this.#params.height - 6;
254
+ ctx.textBaseline = "bottom";
255
+ break;
256
+ }
257
+ ctx.fillText(dynamicText, x + 6, y + 6)
199
258
  }
200
259
 
201
260
  this.#counter = this.#counter + 1
202
261
  }
203
262
 
204
- async load (globalConfig) {
263
+ async load(globalConfig) {
205
264
  this.#profile = globalConfig
206
265
  for (let i = 0; i < this.#keys.length; i++) {
207
266
  const key = this.#keys[i]
@@ -218,34 +277,41 @@ export class Button {
218
277
  }
219
278
  }
220
279
 
221
- getCurrentElement () {
280
+ getCurrentElement() {
222
281
  const state = this.#keys[this.#index]
223
282
  return this.#states[state]
224
283
  }
225
284
 
226
- getLastElement () {
227
- let i=this.#index-1
228
- if (i < 0) { i = this.#keys.length - 1 }
229
-
230
- const lastState = this.#keys[i]
231
- return this.#states[lastState]
232
- }
233
-
234
- pressed () {
285
+ /**
286
+ * Triggered when either a physical button, touchscreen button or Knob Button was pressed
287
+ * Updates the timeStampPressed and timeHold, and checks if the button was hold long enough (minPressed attribute)
288
+ * If the button type was not TOGGLE, it will decrease the index to the previous state.
289
+ * @returns
290
+ */
291
+ pressed() {
235
292
  this.timeStampPressed = Date.now()
236
-
293
+ this.updateState(this.#index, "pressed", true)
237
294
  this.#index++
238
295
 
239
- if (this.#enforcedIndex>=0){
240
- console.log("Enforced Index",this.#enforcedIndex )
296
+ if (this.#enforcedIndex >= 0) {
297
+ console.log("Enforced Index", this.#enforcedIndex)
241
298
  this.#index = this.#enforcedIndex
242
299
  }
243
300
 
244
- this.updateState(this.#index,"pressed",true)
301
+ // Update the State according to the correctly pressed state
302
+ if (this.#index < 0) { this.#index = this.#keys.length - 1 }
303
+ this.#index %= this.#keys.length
304
+
245
305
  return true
246
306
  }
247
307
 
248
- released () {
308
+ /**
309
+ * Triggered when either a physical button, touchscreen button or Knob Button was released
310
+ * Updates the timeStampReleased and timeHold, and checks if the button was hold long enough (minPressed attribute)
311
+ * If the button type was not TOGGLE, it will decrease the index to the previous state.
312
+ * @returns
313
+ */
314
+ released() {
249
315
  let elem = this.getCurrentElement()
250
316
  if (!elem) { return false }
251
317
  this.timeStampReleased = Date.now()
@@ -255,7 +321,6 @@ export class Button {
255
321
  if (this.timeHold < this.#params.minPressed) {
256
322
  // Update the State according to the not correct pressed state
257
323
  console.log('Did not hold minimum time of ', this.#params.minPressed, 'only', this.timeHold)
258
- //if (this.#index < 0) { this.#index = this.#keys.length - 1 }
259
324
  bExecute = false
260
325
  }
261
326
 
@@ -266,34 +331,41 @@ export class Button {
266
331
  break
267
332
  default:
268
333
  this.#index--
269
- //if (this.#index < 0) { this.#index = this.#keys.length - 1 }
270
334
  break
271
335
  }
272
336
 
273
- if (this.#enforcedIndex>=0){
274
- console.log("Enforced Index",this.#enforcedIndex )
337
+ if (this.#enforcedIndex >= 0 && this.#enforcedIndex != this.#index) {
338
+ // If we have an enforced index, we set it to the enforced index:
339
+ console.log("Enforcing Index", this.#enforcedIndex, "instead of", this.#index)
275
340
  this.#index = this.#enforcedIndex
276
341
  }
277
-
278
-
279
- this.updateState(this.#index,"released",bExecute)
280
-
281
- return true // this.runCommand()
282
- }
283
342
 
284
- updateState(index,eventType,bExecute){
285
- this.#index = index
286
- this.#event = eventType
287
343
  // Update the State according to the correctly pressed state
288
344
  if (this.#index < 0) { this.#index = this.#keys.length - 1 }
289
345
  this.#index %= this.#keys.length
346
+
347
+
348
+ this.updateState(this.#index, "released", bExecute)
349
+
350
+ return true
351
+ }
352
+
353
+
354
+ updateState(index, eventType, bExecute) {
355
+ this.#index = index
356
+ this.#event = eventType
290
357
  if (bExecute)
291
358
  this.runCommand()
292
- //console.log("TODO: expect newState", newState)
293
- return true // this.runCommand()
359
+ return true
294
360
  }
295
361
 
296
- async rotated (delta) {
362
+ /**
363
+ * Triggered when one of the physical KNOBS is rotated
364
+ * Calculates the new value based on the delta, and store it in the params.value
365
+ * @param {*} delta
366
+ * @returns
367
+ */
368
+ async rotated(delta) {
297
369
  if (!this.getCurrentElement()) { return false }
298
370
 
299
371
  this.#event = "rotated"
@@ -301,50 +373,51 @@ export class Button {
301
373
  return this.runCommand()
302
374
  }
303
375
 
304
- async changed(buttonID,nodeid,val){
305
- // Only handle updates within the same group identified by nodeid
306
- if (nodeid !== this.#params.nodeid){
307
- return
308
- }
309
-
310
- this.#index = 0;
311
- this.#enforcedIndex = -1;
312
- for (let i = 0; i < this.#keys.length; i++) {
313
- let key = this.#keys[i]
314
- // check if the state-name is same as the value we get from outside:
315
- if (val == key){
316
- this.#index = i;
317
- console.info("enforce index", buttonID,nodeid,val,i)
318
- this.#enforcedIndex = this.#index;
319
- break;
320
- }
321
-
322
- // check if the nodeid is the same and the value is one of the states
323
- // todo überarbeiten
324
- let state = this.#states[key]
325
- if (state.value === undefined)
326
- continue
327
-
328
- const params = {
329
- id: buttonID,
330
- key: buttonID,
331
- state : key,
332
- ...state
333
- }
334
- if (state && state.value !== undefined){
335
- let strVal = state.value.toString()
336
- let val1 = format(strVal,params)
337
- if (val1 === val?.toString()){
376
+ /**
377
+ * This function is called when the value of a button is changed from outside (e.g. coming from an opcua subscription)
378
+ * @param {*} buttonID: the ID of the button that caused the change
379
+ * @param {*} nodeid: the nodeid that changed
380
+ * @param {*} val : the current value of the given nodeid
381
+ * @returns
382
+ */
383
+ async buttonStateChanged(buttonID, attribute, nodeid, val) {
384
+
385
+ //
386
+ if (attribute == "nodeid" && nodeid == this.#params.nodeid) {
387
+ this.#index = 0;
388
+ this.#enforcedIndex = -1;
389
+ for (let i = 0; i < this.#keys.length; i++) {
390
+ let stateStr = this.#keys[i].toString()
391
+ let valStr = val.toString()
392
+ // check if the state-name is same as the value we get from outside:
393
+ if (valStr == stateStr) {
394
+ if (i !== this.#index) {
395
+ console.info("Changed index", this.id, nodeid, val, i)
338
396
  this.#index = i;
339
- break;
397
+ console.info("enforce State index", this.id, nodeid, val, i)
398
+ this.#enforcedIndex = this.#index;
340
399
  }
400
+ break;
341
401
  }
342
- //break;
402
+ }
403
+ }
404
+
405
+ if (attribute == "blink") {
406
+ console.info("Changed blink", buttonID, nodeid, val)
407
+ this.#enforcedBlink = val;
343
408
  }
344
409
  }
345
410
 
346
- async touchmove (x, y) {
347
- // if (!this.getCurrentElement()) { return false }
411
+ /**
412
+ * Handle a touchmove event on a touchscreen button
413
+ * This function calculates the new value based on the x and y coordinates of the touchmove event.
414
+ * It updates the moveLeft, moveRight, moveUp and moveDown parameters based on the x and y coordinates.
415
+ * It also calculates the delta for the value based on the y coordinate and updates the value parameter.
416
+ * @param {*} x
417
+ * @param {*} y
418
+ * @returns
419
+ */
420
+ async touchmove(x, y) {
348
421
 
349
422
  let delta = 0
350
423
  if (x > this.#params.x) {
@@ -365,8 +438,8 @@ export class Button {
365
438
  delta = 1
366
439
  }
367
440
 
368
- this.#params.x = (x%100)
369
- this.#params.y = (y%100)
441
+ this.#params.x = (x % 100)
442
+ this.#params.y = (y % 100)
370
443
 
371
444
  // Calculate delta for value no touchmove up/down
372
445
  this.#params.value = calcDelta(this.#params.value, delta, this.#params.min, this.#params.max)
@@ -375,7 +448,7 @@ export class Button {
375
448
  return false
376
449
  }
377
450
 
378
- getParams(elem){
451
+ getParams(elem) {
379
452
  // Call an action - include dynamic parameters
380
453
  // and also all attributes of elem + global config
381
454
  const params = {
@@ -385,71 +458,58 @@ export class Button {
385
458
  id: this.id,
386
459
  //key: this.key,
387
460
  event: this.#event,
388
- pressed : this.#event == "pressed",
389
- released : this.#event == "released",
390
- rotated : this.#event == "rotated",
391
- // state: this.#keys[this.#index]
461
+ pressed: this.#event == "pressed",
462
+ released: this.#event == "released",
463
+ rotated: this.#event == "rotated",
392
464
  }
393
465
  return params
394
466
  }
395
467
 
396
- async runCommand () {
397
- const elem = this.getCurrentElement()
468
+ /**
469
+ * Run a command based on the current element's parameters.
470
+ * This function checks if the current element has a command, http request or opcua request defined.
471
+ * If so, it will execute the command using the respective interface (shell, http or opcua).
472
+ * It will also emit profile and brightness changes if defined in the parameters.
473
+ * @returns
474
+ */
475
+ async runCommand() {
476
+ //const elem = this.getCurrentElement()
477
+ let params = this.getParams(this.getCurrentElement())
478
+
398
479
  // Only continue, if we have an element:
399
- if (!elem) {
480
+ if (!params) {
400
481
  return
401
482
  }
402
483
  // Filter for Event Type:
403
- if (elem.filter && elem.filter != this.#event){
484
+ if (params.filter && params.filter != this.#event) {
404
485
  return
405
486
  }
406
487
 
407
- if (elem.profile !== undefined) {
408
- profileEmitter.emit("profileChanged", elem.profile)
488
+ if (params.profile !== undefined && params.profile !== '') {
489
+ profileEmitter.emit("profileChanged", params.profile)
409
490
  }
410
491
 
411
- if (elem.brightness !== undefined) {
412
- profileEmitter.emit("brightnessChanged", elem.brightness)
492
+ if (params.brightness !== undefined && params.brightness !== '') {
493
+ // If brightness is set, we emit a brightnessChanged event with the given params
494
+ // This is used to change the brightness of the device
495
+ profileEmitter.emit("brightnessChanged", params.brightness)
413
496
  }
414
497
 
415
- if (elem.vibrate !== undefined) {
416
- profileEmitter.emit("vibrate", elem.vibrate)
498
+ if (params.vibrate !== undefined && params.vibrate !== '' && params.vibrate !== false) {
499
+ // If vibrate is set, we emit a vibrate event with the given params
500
+ // This is used to trigger a vibration on the device
501
+ profileEmitter.emit("vibrate", params.vibrate)
417
502
  }
418
503
 
419
- // Only continue, if we have an element, that contains some kind of command:
420
- if (!elem.cmd && !elem.http && !elem.opcua) {
421
- return
422
- }
423
-
424
- let params = this.getParams(elem)
425
504
  let res = ''
426
- if ('cmd' in elem) {
427
- if (shellinterface){
428
- res = await shellinterface.call(elem.cmd, params)
429
- }else{
430
- console.warn("shellinterface not started")
431
- }
505
+ if ('cmd' in params && shellinterface) {
506
+ res = await shellinterface.call(params.cmd, params)
432
507
  }
433
- if ('http' in elem) {
434
- if (httpinterface){
435
- res = await httpinterface.call(elem.http, params)
436
- }else{
437
- console.warn("httpinterface not started")
438
- }
508
+ if ('http' in params && httpinterface) {
509
+ res = await httpinterface.call(params.http, params)
439
510
  }
440
- if ('opcua' in elem) {
441
- if (opcuainterface){
442
- res = await opcuainterface.call(elem.opcua, params)
443
-
444
- /** TODO: CHECK THIS - don't think it's ever used any more */
445
- /*if (this.#data.statenodeid){
446
- let stateParams = params
447
- params.value = params.state
448
- res = await opcuainterface.call(this.#data.statenodeid, params)
449
- }*/
450
- }else{
451
- console.warn("opcuainterface not started")
452
- }
511
+ if ('opcua' in params && opcuainterface) {
512
+ res = await opcuainterface.call(params.opcua, params)
453
513
  }
454
514
 
455
515
  return res
package/common/utils.mjs CHANGED
@@ -1,17 +1,29 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs'
2
+ import YAML from 'yaml'
2
3
 
3
4
  /**
4
- * Read a JSON File
5
+ * Read a Config file in either JSON or YAML Fileformat
6
+ * @param {string} fileName - The name of the file to read
7
+ * @returns {object} - The parsed object from the file
5
8
  */
6
- export function readJSONFile (fileName) {
7
- let data
9
+ export function readConfigFile (fileName) {
10
+ let obj = undefined
8
11
  try {
9
- data = readFileSync(fileName, 'utf8')
10
- return JSON.parse(data)
12
+ //console.log("Reading File:", fileName)
13
+ let data = readFileSync(fileName, 'utf8')
14
+ // If the file is a YAML file, parse it
15
+ if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) {
16
+ obj = YAML.parse(data)
17
+ } else if (fileName.endsWith('.json')) {
18
+ // If the file is a JSON file, parse it
19
+ obj = JSON.parse(data)
20
+ // automatically convert all json to yaml
21
+ writeYAMLFile(fileName.replace(".json",".yaml"), obj)
22
+ }
11
23
  } catch (error) {
12
- console.info(`Error reading File: ${fileName}`)
13
- return data
24
+ console.info(`Error reading File: ${fileName}`, error)
14
25
  }
26
+ return obj
15
27
  }
16
28
 
17
29
  /**
@@ -22,6 +34,15 @@ export function writeJSONFile (fileName, jsonObj) {
22
34
  writeFileSync(fileName, data)
23
35
  }
24
36
 
37
+ /**
38
+ * Write a JSON File
39
+ */
40
+ export function writeYAMLFile (fileName, jsonObj) {
41
+ const data = YAML.stringify(jsonObj)
42
+ writeFileSync(fileName, data)
43
+ }
44
+
45
+
25
46
  /**
26
47
  * Calculate the delta of a value within a range between min and max with overflow
27
48
  * @param {*} data