loupedeck-commander 1.2.3 → 1.2.4
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/LICENSE +201 -0
- package/README.md +131 -131
- package/VERSION.md +32 -32
- package/common/ApplicationConfig.mjs +219 -84
- package/common/BaseLoupeDeckHandler.mjs +333 -326
- package/common/cmd-executer.mjs +16 -16
- package/common/index.mjs +5 -5
- package/common/touchbuttons.mjs +534 -534
- package/common/utils.mjs +30 -29
- package/config.json +8 -8
- package/eslint.config.mjs +8 -8
- package/example/ExampleDeviceHandler.mjs +44 -44
- package/example/example.mjs +21 -21
- package/index.mjs +3 -3
- package/interfaces/baseif.mjs +69 -69
- package/interfaces/httpif.mjs +90 -81
- package/interfaces/opcuaif.mjs +296 -291
- package/interfaces/shellif.mjs +53 -47
- package/package.json +29 -29
- package/profile-1.json +373 -280
- package/test.mjs +26 -22
package/common/utils.mjs
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Read a JSON File
|
|
5
|
-
*/
|
|
6
|
-
export function readJSONFile (fileName) {
|
|
7
|
-
let data
|
|
8
|
-
try {
|
|
9
|
-
data = readFileSync(fileName, 'utf8')
|
|
10
|
-
return JSON.parse(data)
|
|
11
|
-
} catch (error) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (data
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read a JSON File
|
|
5
|
+
*/
|
|
6
|
+
export function readJSONFile (fileName) {
|
|
7
|
+
let data
|
|
8
|
+
try {
|
|
9
|
+
data = readFileSync(fileName, 'utf8')
|
|
10
|
+
return JSON.parse(data)
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.info(`Error reading File: ${fileName}`)
|
|
13
|
+
return data
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Write a JSON File
|
|
19
|
+
*/
|
|
20
|
+
export function writeJSONFile (fileName, jsonObj) {
|
|
21
|
+
const data = JSON.stringify(jsonObj, null, 4)
|
|
22
|
+
writeFileSync(fileName, data)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function calcDelta (data, delta, max = 100) {
|
|
26
|
+
data = data + delta
|
|
27
|
+
if (data > max) { data = max }
|
|
28
|
+
if (data < 0) { data = 0 }
|
|
29
|
+
return data
|
|
30
|
+
}
|
package/config.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
{
|
|
2
|
-
"application": "Example",
|
|
3
|
-
"profiles": [
|
|
4
|
-
{
|
|
5
|
-
"name": "
|
|
6
|
-
"file": "profile-1.json"
|
|
7
|
-
}
|
|
8
|
-
]
|
|
1
|
+
{
|
|
2
|
+
"application": "Example",
|
|
3
|
+
"profiles": [
|
|
4
|
+
{
|
|
5
|
+
"name": "profile-1",
|
|
6
|
+
"file": "profile-1.json"
|
|
7
|
+
}
|
|
8
|
+
]
|
|
9
9
|
}
|
package/eslint.config.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import globals from "globals";
|
|
2
|
-
import pluginJs from "@eslint/js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/** @type {import('eslint').Linter.Config[]} */
|
|
6
|
-
export default [
|
|
7
|
-
{languageOptions: { globals: globals.browser }},
|
|
8
|
-
pluginJs.configs.recommended,
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import pluginJs from "@eslint/js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/** @type {import('eslint').Linter.Config[]} */
|
|
6
|
+
export default [
|
|
7
|
+
{languageOptions: { globals: globals.browser }},
|
|
8
|
+
pluginJs.configs.recommended,
|
|
9
9
|
];
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import { HAPTIC } from 'loupedeck'
|
|
2
|
-
import { BaseLoupeDeckHandler } from '../common/BaseLoupeDeckHandler.mjs'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Our Special-Handler just used the Default - and adds Vibration after triggers through Button-Releases
|
|
6
|
-
*/
|
|
7
|
-
export class ExampleDeviceHandler extends BaseLoupeDeckHandler {
|
|
8
|
-
/**
|
|
9
|
-
* Handle different Vibration-Feedback on OK (true), and NOK (false)
|
|
10
|
-
* @param {*} ok
|
|
11
|
-
*/
|
|
12
|
-
async vibrateHandler (ok) {
|
|
13
|
-
if (ok) { await this.device.vibrate(HAPTIC.REV_FASTEST) } else {
|
|
14
|
-
this.device.vibrate(HAPTIC.RUMBLE2)
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Handle Button-Up Events with Vibration Handler
|
|
20
|
-
* @param {*} event
|
|
21
|
-
*/
|
|
22
|
-
async onButtonUp (event) {
|
|
23
|
-
const res = await super.onButtonUp(event)
|
|
24
|
-
await this.vibrateHandler(res)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Handle Knob-Rotation Events with Vibration Handler
|
|
29
|
-
* @param {*} event
|
|
30
|
-
*/
|
|
31
|
-
async onRotate (event) {
|
|
32
|
-
const res = await super.onRotate(event)
|
|
33
|
-
await this.vibrateHandler(res)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Handle Touch-End Events with Vibration Handler
|
|
38
|
-
* @param {*} event
|
|
39
|
-
*/
|
|
40
|
-
async onTouchEnd (event) {
|
|
41
|
-
const res = await super.onTouchEnd(event)
|
|
42
|
-
await this.vibrateHandler(res)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
1
|
+
import { HAPTIC } from 'loupedeck'
|
|
2
|
+
import { BaseLoupeDeckHandler } from '../common/BaseLoupeDeckHandler.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Our Special-Handler just used the Default - and adds Vibration after triggers through Button-Releases
|
|
6
|
+
*/
|
|
7
|
+
export class ExampleDeviceHandler extends BaseLoupeDeckHandler {
|
|
8
|
+
/**
|
|
9
|
+
* Handle different Vibration-Feedback on OK (true), and NOK (false)
|
|
10
|
+
* @param {*} ok
|
|
11
|
+
*/
|
|
12
|
+
async vibrateHandler (ok) {
|
|
13
|
+
if (ok) { await this.device.vibrate(HAPTIC.REV_FASTEST) } else {
|
|
14
|
+
this.device.vibrate(HAPTIC.RUMBLE2)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handle Button-Up Events with Vibration Handler
|
|
20
|
+
* @param {*} event
|
|
21
|
+
*/
|
|
22
|
+
async onButtonUp (event) {
|
|
23
|
+
const res = await super.onButtonUp(event)
|
|
24
|
+
await this.vibrateHandler(res)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handle Knob-Rotation Events with Vibration Handler
|
|
29
|
+
* @param {*} event
|
|
30
|
+
*/
|
|
31
|
+
async onRotate (event) {
|
|
32
|
+
const res = await super.onRotate(event)
|
|
33
|
+
await this.vibrateHandler(res)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handle Touch-End Events with Vibration Handler
|
|
38
|
+
* @param {*} event
|
|
39
|
+
*/
|
|
40
|
+
async onTouchEnd (event) {
|
|
41
|
+
const res = await super.onTouchEnd(event)
|
|
42
|
+
await this.vibrateHandler(res)
|
|
43
|
+
}
|
|
44
|
+
}
|
package/example/example.mjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { ExampleDeviceHandler } from './ExampleDeviceHandler.mjs'
|
|
2
|
-
|
|
3
|
-
const handler = new ExampleDeviceHandler('config.json')
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Stop the handlers when a signal like SIGINT or SIGTERM arrive
|
|
8
|
-
* @param {*} signal
|
|
9
|
-
*/
|
|
10
|
-
const stopHandler = async(signal) => {
|
|
11
|
-
console.log(`Receiving ${signal} => Stopping processes.`)
|
|
12
|
-
await handler.stop()
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Initiating the signal handlers:
|
|
16
|
-
// see https://www.tutorialspoint.com/unix/unix-signals-traps.htm
|
|
17
|
-
process.on('SIGINT', async (signal) => { stopHandler(signal) })
|
|
18
|
-
process.on('SIGTERM', async (signal) => { stopHandler(signal) })
|
|
19
|
-
|
|
20
|
-
// Initiating a process
|
|
21
|
-
await handler.start()
|
|
1
|
+
import { ExampleDeviceHandler } from './ExampleDeviceHandler.mjs'
|
|
2
|
+
|
|
3
|
+
const handler = new ExampleDeviceHandler('config.json')
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Stop the handlers when a signal like SIGINT or SIGTERM arrive
|
|
8
|
+
* @param {*} signal
|
|
9
|
+
*/
|
|
10
|
+
const stopHandler = async(signal) => {
|
|
11
|
+
console.log(`Receiving ${signal} => Stopping processes.`)
|
|
12
|
+
await handler.stop()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Initiating the signal handlers:
|
|
16
|
+
// see https://www.tutorialspoint.com/unix/unix-signals-traps.htm
|
|
17
|
+
process.on('SIGINT', async (signal) => { stopHandler(signal) })
|
|
18
|
+
process.on('SIGTERM', async (signal) => { stopHandler(signal) })
|
|
19
|
+
|
|
20
|
+
// Initiating a process
|
|
21
|
+
await handler.start()
|
package/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { BaseLoupeDeckHandler } from './common/BaseLoupeDeckHandler.mjs'
|
|
2
|
-
|
|
3
|
-
export { BaseLoupeDeckHandler }
|
|
1
|
+
import { BaseLoupeDeckHandler } from './common/BaseLoupeDeckHandler.mjs'
|
|
2
|
+
|
|
3
|
+
export { BaseLoupeDeckHandler }
|
package/interfaces/baseif.mjs
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
import format from 'string-template'
|
|
2
|
-
import { EventEmitter } from 'node:events'
|
|
3
|
-
|
|
4
|
-
export class BaseIf extends EventEmitter {
|
|
5
|
-
formattedCommand
|
|
6
|
-
cmd
|
|
7
|
-
options
|
|
8
|
-
call (cmd, options = {}) {
|
|
9
|
-
var res = this.Check(options)
|
|
10
|
-
if (res < 0){
|
|
11
|
-
LogError("Missing essential options in dictionary => Quitting\n",res,options)
|
|
12
|
-
return false
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
this.cmd = cmd
|
|
16
|
-
this.options = options
|
|
17
|
-
this.formattedCommand = this.formatString(cmd, options)
|
|
18
|
-
return this.formattedCommand
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async stop (){
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
formatString (cmd, options = {}) {
|
|
26
|
-
let f =""
|
|
27
|
-
try{
|
|
28
|
-
f = format(cmd, options)
|
|
29
|
-
}catch(e){}
|
|
30
|
-
return f
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
Check(options) {
|
|
34
|
-
if (!"id" in options)
|
|
35
|
-
return -1
|
|
36
|
-
if (!"key" in options)
|
|
37
|
-
return -2
|
|
38
|
-
if (!"state" in options)
|
|
39
|
-
return -3
|
|
40
|
-
if (!"min" in options)
|
|
41
|
-
return -4
|
|
42
|
-
if (!"max" in options)
|
|
43
|
-
return -5
|
|
44
|
-
if (!"color" in options)
|
|
45
|
-
return -6
|
|
46
|
-
if (!"image" in options)
|
|
47
|
-
return -7
|
|
48
|
-
return 0
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
LogError(...args){
|
|
52
|
-
let str = new String(args)
|
|
53
|
-
process.stderr.write(str.toString())
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
LogDebug(...args){
|
|
57
|
-
if (this.options && this.options.verbose){
|
|
58
|
-
let str = new String(args)
|
|
59
|
-
process.stdout.write(str.toString())
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
LogInfo(...args){
|
|
64
|
-
let str = new String(args)
|
|
65
|
-
process.stdout.write(str.toString())
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
import format from 'string-template'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class BaseIf extends EventEmitter {
|
|
5
|
+
formattedCommand
|
|
6
|
+
cmd
|
|
7
|
+
options
|
|
8
|
+
call (cmd, options = {}) {
|
|
9
|
+
var res = this.Check(options)
|
|
10
|
+
if (res < 0){
|
|
11
|
+
this.LogError("Missing essential options in dictionary => Quitting\n",res,options)
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
this.cmd = cmd
|
|
16
|
+
this.options = options
|
|
17
|
+
this.formattedCommand = this.formatString(cmd, options)
|
|
18
|
+
return this.formattedCommand
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async stop (){
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
formatString (cmd, options = {}) {
|
|
26
|
+
let f =""
|
|
27
|
+
try{
|
|
28
|
+
f = format(cmd, options)
|
|
29
|
+
}catch(e){}
|
|
30
|
+
return f
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Check(options) {
|
|
34
|
+
if (!"id" in options)
|
|
35
|
+
return -1
|
|
36
|
+
if (!"key" in options)
|
|
37
|
+
return -2
|
|
38
|
+
if (!"state" in options)
|
|
39
|
+
return -3
|
|
40
|
+
if (!"min" in options)
|
|
41
|
+
return -4
|
|
42
|
+
if (!"max" in options)
|
|
43
|
+
return -5
|
|
44
|
+
if (!"color" in options)
|
|
45
|
+
return -6
|
|
46
|
+
if (!"image" in options)
|
|
47
|
+
return -7
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
LogError(...args){
|
|
52
|
+
let str = new String(args)
|
|
53
|
+
process.stderr.write(str.toString())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
LogDebug(...args){
|
|
57
|
+
if (this.options && this.options.verbose){
|
|
58
|
+
let str = new String(args)
|
|
59
|
+
process.stdout.write(str.toString())
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
LogInfo(...args){
|
|
64
|
+
let str = new String(args)
|
|
65
|
+
process.stdout.write(str.toString())
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
package/interfaces/httpif.mjs
CHANGED
|
@@ -1,81 +1,90 @@
|
|
|
1
|
-
import * as http from 'node:http'
|
|
2
|
-
import url from 'node:url'
|
|
3
|
-
import { BaseIf } from './baseif.mjs'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Our Special-Handler just used the Default - and adds Vibration after triggers through Button-Releases
|
|
7
|
-
*/
|
|
8
|
-
export class HTTPif extends BaseIf {
|
|
9
|
-
async call (url1, options = {}) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
response.on('
|
|
69
|
-
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
1
|
+
import * as http from 'node:http'
|
|
2
|
+
import url from 'node:url'
|
|
3
|
+
import { BaseIf } from './baseif.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Our Special-Handler just used the Default - and adds Vibration after triggers through Button-Releases
|
|
7
|
+
*/
|
|
8
|
+
export class HTTPif extends BaseIf {
|
|
9
|
+
async call (url1, options = {}) {
|
|
10
|
+
var res = this.Check(options)
|
|
11
|
+
if (res<0){
|
|
12
|
+
// this.LogError(`HTTPif call: Missing essential options in dictionary => Quitting`,res,"\n")
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
let urlFormatted = super.call(url1, options)
|
|
16
|
+
let myURL
|
|
17
|
+
try {
|
|
18
|
+
myURL = new url.URL(urlFormatted)
|
|
19
|
+
await this.get(myURL, options)
|
|
20
|
+
} catch (e) {
|
|
21
|
+
this.LogError(`HTTPif: error with URL: ${e.message}\n`)
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async stop(){
|
|
28
|
+
this.LogInfo("HTTPif: Stopping\n")
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Check(options) {
|
|
33
|
+
var res= super.Check(options)
|
|
34
|
+
if (res <0){
|
|
35
|
+
this.LogError(`HTTPif: mandatory parameter missing\n`)
|
|
36
|
+
return res
|
|
37
|
+
}
|
|
38
|
+
if (!options.hostname){
|
|
39
|
+
this.LogError(`HTTPif: mandatory parameter hostname missing\n`)
|
|
40
|
+
return -21
|
|
41
|
+
}
|
|
42
|
+
return 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handle a HTTP Get request with Basic Authentification
|
|
47
|
+
* @param {*} myURL Uri
|
|
48
|
+
*/
|
|
49
|
+
async get (myURL) {
|
|
50
|
+
const auth = 'Basic ' + Buffer.from(myURL.username + ':' + myURL.password).toString('base64')
|
|
51
|
+
const getOptions = {
|
|
52
|
+
hostname: myURL.hostname,
|
|
53
|
+
port: myURL.port,
|
|
54
|
+
path: myURL.pathname,
|
|
55
|
+
agent: false, // Create a new agent just for this one request
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: auth
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.LogInfo(`HTTPIf: call URL ${myURL} ${getOptions}\n`)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
const prom = new Promise((resolve, reject) => {
|
|
65
|
+
const req = http.get(getOptions, (response) => {
|
|
66
|
+
const chunksOfData = []
|
|
67
|
+
|
|
68
|
+
response.on('data', (fragments) => {
|
|
69
|
+
chunksOfData.push(fragments)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
response.on('end', () => {
|
|
73
|
+
const responseBody = Buffer.concat(chunksOfData)
|
|
74
|
+
resolve(responseBody.toString())
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
response.on('error', (error) => {
|
|
78
|
+
resolve("")
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
req.on('error', (e) => {
|
|
83
|
+
this.LogError(`HTTPif: ignore other errors like ERRNOTCONNECTED: ${e.message}\n`)
|
|
84
|
+
return false
|
|
85
|
+
});
|
|
86
|
+
}).catch(function (error) { // (*)
|
|
87
|
+
return false
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|