diff --git a/.flake8 b/.flake8
new file mode 100644
index 00000000..f0fa22eb
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,3 @@
+[flake8]
+max-line-length = 100
+ignore = E722
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 98240553..d139e7c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,8 @@
/cc_library/.idea
__pycache__/
**/.idea
+**/pkg
+**/bin
+**/src/*
+!**/src/sciler
*/debug.log
diff --git a/.travis.yml b/.travis.yml
index ea561f43..c3b9f98f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,8 +12,7 @@ matrix:
# https://medium.com/faun/configuring-travis-ci-for-angular-application-34afee1715f
- language: node_js
- node_js:
- - "12.13.1"
+ node_js: 12.13.1
services:
- xvfb
dist: xenial
@@ -33,12 +32,15 @@ matrix:
chrome: stable
- language: go
- go_import_path: github.com/IssaHanou/BEP_1920_Q2
+ go: 1.13.4
+ go_import_path: github.com/IssaHanou/BEP_1920_Q2/back-end
before_install:
- - go get github.com/stretchr/testify/assert
- go get -u golang.org/x/lint/golint
+ - export GOPATH=$TRAVIS_BUILD_DIR/back-end
+ - cd back-end/src/sciler
+ - go get github.com/stretchr/testify/assert
script:
- - cd back-end
- - golint -set_exit_status ./..
- - go test -v ./...
- - go build
+ - cd ../..
+ - golint -set_exit_status sciler/...
+ - go test -v sciler/...
+ - go build sciler
diff --git a/back-end/.gitignore b/back-end/.gitignore
new file mode 100644
index 00000000..8b9c0447
--- /dev/null
+++ b/back-end/.gitignore
@@ -0,0 +1,2 @@
+/bin
+/pkg
\ No newline at end of file
diff --git a/back-end/.idea/vcs.xml b/back-end/.idea/vcs.xml
deleted file mode 100644
index 6c0b8635..00000000
--- a/back-end/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/back-end/README.md b/back-end/README.md
index eb797851..63f194e6 100644
--- a/back-end/README.md
+++ b/back-end/README.md
@@ -2,7 +2,11 @@
The back-end is written in Go. Tools used are go fmt, golint and Testify.
External imports that need to be retrieved should be added in `.travis.yml` in the `before_script`.
-
+Configure (project) GOPATH to /back-end folder in /BEP_1920_Q2.
+When using Goland uncheck `Use GOPATH that is defined in system environment` and check `Index entire GOPATH`
+
+run `go get ./...` in the `sciler` folder to go get all dependencies
+
### Setup gofmt and golint to run automatically:
- install golint: `go get -u golang.org/x/lint/golint`
- natigate to Settings > Tools > File Watchers
diff --git a/back-end/back-end.exe b/back-end/back-end.exe
new file mode 100644
index 00000000..0cb76b4e
Binary files /dev/null and b/back-end/back-end.exe differ
diff --git a/back-end/config/config.go b/back-end/config/config.go
deleted file mode 100644
index 5d01b784..00000000
--- a/back-end/config/config.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package config
-
-import (
- "encoding/json"
- "fmt"
- "strings"
-)
-
-// General is a struct that describes the configurations of an escape room
-type General struct {
- Name string `json:"name"`
- Duration string `json:"duration"`
-}
-
-// GetFromJSON takes in json with general info of escape room.
-func GetFromJSON(input []byte) string {
- var data General
- err := json.Unmarshal(input, &data)
- if err != nil {
- _ = fmt.Errorf(err.Error())
- }
- return "Escape room " + data.Name + " should be solved within " +
- formatDuration(data.Duration)
-}
-
-//Takes in duration string in format hh:mm:ss
-func formatDuration(duration string) string {
- vars := strings.Split(duration, ":")
- var result string
- if vars[0] != "00" {
- result += vars[0] + " hours and"
- }
- if vars[1] != "00" {
- result += vars[1] + " minutes"
- }
- if vars[2] != "00" {
- result += " and " + vars[2] + " seconds"
- }
- return result
-}
diff --git a/back-end/config/config_test.go b/back-end/config/config_test.go
deleted file mode 100644
index fa938b07..00000000
--- a/back-end/config/config_test.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package config
-
-import (
- "github.com/stretchr/testify/assert"
- "testing"
-)
-
-func TestGetFromJson(t *testing.T) {
- json := []byte(`{
- "name": "My Awesome Escape",
- "duration": "00:30:00"
- }`)
- result := GetFromJSON(json)
- expected := "Escape room My Awesome Escape should be solved within 30 minutes"
- assert.Equal(t, result, expected, "JSON should be properly converted to string")
-}
diff --git a/back-end/main.go b/back-end/main.go
deleted file mode 100644
index 70af8c4b..00000000
--- a/back-end/main.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
- "fmt"
- "github.com/IssaHanou/BEP_1920_Q2/back-end/config"
-)
-
-func main() {
- fmt.Println("Starting server")
- data := []byte(`{
- "name": "My Awesome Escape",
- "duration": "00:30:00"
- }`)
- fmt.Println(config.GetFromJSON(data))
-}
\ No newline at end of file
diff --git a/back-end/resources/room_config.json b/back-end/resources/room_config.json
index c8fe0c8b..0ff6b3d6 100644
--- a/back-end/resources/room_config.json
+++ b/back-end/resources/room_config.json
@@ -13,52 +13,52 @@
"redSwitch": "boolean",
"orangeSwitch": "boolean",
"greenSwitch": "boolean",
- "slider1": "integer",
- "slider2": "integer",
- "slider3": "integer",
+ "slider1": "numeric",
+ "slider2": "numeric",
+ "slider3": "numeric",
"mainSwitch": "boolean"
},
"output": {
"greenLight1": {
"type": "string",
"instruction": {
- "interval": "integer",
- "delay": "integer"
+ "interval": "numeric",
+ "delay": "numeric"
}
},
"greenLight2": {
"type": "string",
"instruction": {
- "interval": "integer",
- "delay": "integer"
+ "interval": "numeric",
+ "delay": "numeric"
}
},
"greenLight3": {
"type": "string",
"instruction": {
- "interval": "integer",
- "delay": "integer"
+ "interval": "numeric",
+ "delay": "numeric"
}
},
"redLight1": {
"type": "string",
"instruction": {
- "interval": "integer",
- "delay": "integer"
+ "interval": "numeric",
+ "delay": "numeric"
}
},
- "redLight2": {
+ "redLight2": {
"type": "string",
"instruction": {
- "interval": "integer",
- "delay": "integer"
+ "interval": "numeric",
+ "delay": "numeric"
}
},
- "redLight3": {
+ "redLight3": {
"type": "string",
"instruction": {
- "interval": "integer",
- "delay": "integer"
+ "interval": "numeric",
+ "delay": "numeric"
}
}
}
@@ -67,12 +67,21 @@
"id": "telephone",
"description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
"input": {
- "turningWheel": "array"
+ "turningWheel": "num-array"
},
"output": {
"audio": "string",
"ringTone": "string"
}
+ },
+ {
+ "id": "door1",
+ "description": "De deur kan open en dicht zijn en neemt geen input.",
+ "input": {
+ },
+ "output": {
+ "open": "boolean"
+ }
}
],
"puzzles": [
@@ -86,11 +95,12 @@
"conditions": [
{
"type": "device",
- "id": "telphone",
+ "type_id": "telephone",
"constraints": [
{
- "comparison": "eq",
- "turningWheel": [
+ "comp": "eq",
+ "component_id": "turningWheel",
+ "value": [
0,
1,
2,
@@ -101,7 +111,7 @@
"actions": [
{
"type": "device",
- "id": "controlBoard",
+ "type_id": "controlBoard",
"message": {
"output": {
"greenLight1": {
@@ -141,28 +151,26 @@
"conditions": [
{
"type": "device",
- "id": "controlBoard",
+ "type_id": "controlBoard",
"constraints": [
{
"component_id": "mainSwitch",
- "comparison": "eq",
+ "comp": "eq",
+ "value": true
+ },
+ {
+ "component_id": "greenSwitch",
+ "comp": "eq",
+ "value": true
+ },
+ {
+ "component_id": "redSwitch",
+ "comp": "eq",
"value": true
},
- [
- {
- "component_id": "greenSwitch",
- "comparison": "eq",
- "value": true
- },
- {
- "component_id": "redSwitch",
- "comparison": "eq",
- "value": true
- }
- ],
{
"component_id": "slider2",
- "comparison": "lt",
+ "comp": "lt",
"value": 30
}
]
@@ -171,7 +179,7 @@
"actions": [
{
"type": "device",
- "id": "telephone",
+ "type_id": "telephone",
"message": {
"output": {
"audio": "audio_1"
@@ -180,7 +188,7 @@
},
{
"type": "device",
- "id": "controlBoard",
+ "type_id": "controlBoard",
"message": {
"output": {
"greenLight1": false,
@@ -211,10 +219,10 @@
"conditions": [
{
"type": "timer",
- "id": "telephone",
+ "type_id": "timer1",
"constraints": [
{
- "comparison": "eq",
+ "comp": "eq",
"value": "00:01:00"
}
]
@@ -223,9 +231,10 @@
"actions": [
{
"type": "device",
- "id": "telephone",
+ "type_id": "telephone",
"message": {
"output": {
+ "ringTone": "standard",
"audio": "audio_1"
}
}
@@ -233,6 +242,49 @@
]
}
]
+ },
+ {
+ "name": "Stop",
+ "rules": [
+ {
+ "id": "flipSwitch",
+ "description": "Als de knop omgaat, stopt het spel: tijd stopt en deur gaat open.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "controlBoard",
+ "constraints": [
+ {
+ "component_id": "mainSwitch",
+ "comp": "eq",
+ "value": true
+ }
+ ]
+ }
+ ],
+ "actions": [
+ {
+ "type": "timer",
+ "type_id": "timer1",
+ "message": {
+ "output": {
+ "instruction": "stop"
+ }
+ }
+ },
+ {
+ "type": "device",
+ "type_id": "door1",
+ "message": {
+ "output": {
+ "open": true
+ }
+ }
+ }
+ ]
+ }
+ ]
}
]
}
\ No newline at end of file
diff --git a/back-end/resources/room_manual.md b/back-end/resources/room_manual.md
index e4cfff4f..7b3265ff 100644
--- a/back-end/resources/room_manual.md
+++ b/back-end/resources/room_manual.md
@@ -26,7 +26,7 @@ This will be a list of all devices in the room. Each device is defined as a JSON
- `id`: this is the id of a device. Write it in camelCase, e.g. "controlBoard".
- `description`: this is optional and can contain more information about the device. This can be displayed in the front-end, so should be readable and in Dutch.
- `input`: defines type of values to be expected as input. The keys are component ids and values are types of input (in string format).
- Examples are: "string", "boolean", "array", "integer" or a custom name.
+ Possible types are: "string", "boolean", "numeric", "string-array", "bool-array", "num-array" or a custom name.
- `output`: defines type of values to be expected as output. The keys are component ids and the value is a map with a `type` property
and an optional `instruction`, which defines a map with custom instruction for the device.
@@ -52,20 +52,22 @@ Rules are defined by:
- `description`: this is optional and can contain more information about the rule.
This can be displayed in the front-end, so should be readable and in Dutch.
- `limit`: this sets the number of times this rule can be triggered.
-- `conditions`: this is an array of conditions. By putting several constraints in an array within the conditions array, they will be treated as OR conditions.
+- `conditions`: this is an array of conditions. By putting several constraints in an array within the conditions array, they will be treated as OR conditions. Rule/combination pairs must be unique.
- `type`: this can `rule`, `timer` or `device`.
- - `id`: this will be the id of a timer, rule or device, depending on the type.
- - `constraints`: this is an array of constraints. By putting several constraints in an array within the constrains array, they will be treated as OR constraints.
+ - `type_id`: this will be the id of a timer, rule or device, depending on the type.
+ - `constraints`: this is an array of constraints.
- - `comparison`: this can be "eq", "lt", "gt", "cont" (contains), "lte", "gte"
- - `value`: this is the value on which the comparison is made.
+ - `comp`: this is the type of comparison and can be "eq", "lt", "gt", "contains" (contains), "lte", "gte"
+ - `value`: this is the value on which the comparison is made. This should be in the same type as specified in the input of the device.
+ If it has custom input, then enter value in preferred type and deal with it on the client.
+ In case of "timer" type, it should be in the format "hh:mm:ss"
- `component_id`: in the case of "device" type, this is the id of the component it triggers.
+ In case of "timer" type, this is non-existent.
- `actions`: this is an array of actions:
- `type`: this can be `device` or `timer`
- - `id`: the id of device or timer, depending on type respectively
- - `message`: this defines the output message sent. In case of device type, the message contains parameter `output`.
- The output defines the type of values to be expected as output. The keys are component ids
- and the value is a map with a `type` property and an optional `instruction`, which defines a map with custom instruction for the device.
+ - `type_id`: the id of device or timer, depending on type respectively
+ - `message`: this defines the `output` message sent. The output defines the type of values to be expected as output.
+ In case of device type, the keys are component ids and the value is a type. An additional "instruction" property may be defined.
In the case of timer, the message should have `instruction` specified as `stop` or `subtract`, in the latter case, a `value` should also be passed.
diff --git a/back-end/resources/testing/testCheckActionComponentType.json b/back-end/resources/testing/testCheckActionComponentType.json
new file mode 100644
index 00000000..a80f3e97
--- /dev/null
+++ b/back-end/resources/testing/testCheckActionComponentType.json
@@ -0,0 +1,45 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "general_events": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ ],
+ "actions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "message": {
+ "output": {
+ "color": 0
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/testComponentActionNotPresent.json b/back-end/resources/testing/testComponentActionNotPresent.json
new file mode 100644
index 00000000..83a6779f
--- /dev/null
+++ b/back-end/resources/testing/testComponentActionNotPresent.json
@@ -0,0 +1,57 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "component_id": "light",
+ "value": true
+ }
+ ]
+ }
+ ],
+ "actions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "message": {
+ "output": {
+ "light": "green"
+ }
+ }
+
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/testComponentConstraintNotPresent.json b/back-end/resources/testing/testComponentConstraintNotPresent.json
new file mode 100644
index 00000000..5cf651bb
--- /dev/null
+++ b/back-end/resources/testing/testComponentConstraintNotPresent.json
@@ -0,0 +1,56 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "component_id": "color",
+ "value": true
+ }
+ ]
+ }
+ ],
+ "actions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "message": {
+ "output": {
+ "color": "green"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/testDeviceActionNotPresent.json b/back-end/resources/testing/testDeviceActionNotPresent.json
new file mode 100644
index 00000000..076da362
--- /dev/null
+++ b/back-end/resources/testing/testDeviceActionNotPresent.json
@@ -0,0 +1,56 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "component_id": "light",
+ "value": true
+ }
+ ]
+ }
+ ],
+ "actions": [
+ {
+ "type": "device",
+ "type_id": "telephone",
+ "message": {
+ "output": {
+ "color": "green"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/testDeviceConstraintNotPresent.json b/back-end/resources/testing/testDeviceConstraintNotPresent.json
new file mode 100644
index 00000000..9ae39f6b
--- /dev/null
+++ b/back-end/resources/testing/testDeviceConstraintNotPresent.json
@@ -0,0 +1,57 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "telephone",
+ "constraints": [
+ {
+ "comp": "eq",
+ "component_id": "color",
+ "value": true
+ }
+ ]
+ }
+ ],
+ "actions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "message": {
+ "output": {
+ "color": "green"
+ }
+ }
+
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/testDeviceCustomType.json b/back-end/resources/testing/testDeviceCustomType.json
new file mode 100644
index 00000000..dc1a20a0
--- /dev/null
+++ b/back-end/resources/testing/testDeviceCustomType.json
@@ -0,0 +1,47 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "telephone",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "turningWheel": "my-type"
+ },
+ "output": {
+ "audio": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "telephone",
+ "constraints": [
+ {
+ "comp": "eq",
+ "component_id": "turningWheel",
+ "value": {
+ "my_key": "my_value"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/testDurationError.json b/back-end/resources/testing/testDurationError.json
new file mode 100644
index 00000000..8c4345f3
--- /dev/null
+++ b/back-end/resources/testing/testDurationError.json
@@ -0,0 +1,8 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": 30,
+ "host": "192.0.0.84",
+ "port": 1883
+ }
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/testIncorrectTypeAction.json b/back-end/resources/testing/testIncorrectTypeAction.json
new file mode 100644
index 00000000..e2c57429
--- /dev/null
+++ b/back-end/resources/testing/testIncorrectTypeAction.json
@@ -0,0 +1,45 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ ],
+ "actions": [
+ {
+ "type": "my-type",
+ "type_id": "color",
+ "message": {
+ "output": {
+ "color": "green"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/testIncorrectTypeCondition.json b/back-end/resources/testing/testIncorrectTypeCondition.json
new file mode 100644
index 00000000..1e021ce6
--- /dev/null
+++ b/back-end/resources/testing/testIncorrectTypeCondition.json
@@ -0,0 +1,45 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "mytype",
+ "type_id": "timer1",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": "30:00"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/testNoComponentIDBoolArray.json b/back-end/resources/testing/testNoComponentIDBoolArray.json
new file mode 100644
index 00000000..109ec594
--- /dev/null
+++ b/back-end/resources/testing/testNoComponentIDBoolArray.json
@@ -0,0 +1,57 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "component_id": "light",
+ "value": true
+ }
+ ]
+ }
+ ],
+ "actions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "message": {
+ "output": {
+ "color": "green"
+ }
+ }
+
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/test_config.json b/back-end/resources/testing/test_config.json
new file mode 100644
index 00000000..1d02af75
--- /dev/null
+++ b/back-end/resources/testing/test_config.json
@@ -0,0 +1,350 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "controlBoard",
+ "description": "Control board with three switches, three slides with lights and one main switch.",
+ "input": {
+ "redSwitch": "boolean",
+ "orangeSwitch": "boolean",
+ "greenSwitch": "boolean",
+ "slider1": "numeric",
+ "slider2": "numeric",
+ "slider3": "numeric",
+ "mainSwitch": "boolean"
+ },
+ "output": {
+ "greenLight1": {
+ "type": "string",
+ "instruction": {
+ "interval": "numeric",
+ "delay": "numeric"
+ }
+ },
+ "greenLight2": {
+ "type": "string",
+ "instruction": {
+ "interval": "numeric",
+ "delay": "numeric"
+ }
+ },
+ "greenLight3": {
+ "type": "string",
+ "instruction": {
+ "interval": "numeric",
+ "delay": "numeric"
+ }
+ },
+ "redLight1": {
+ "type": "string",
+ "instruction": {
+ "interval": "numeric",
+ "delay": "numeric"
+ }
+ },
+ "redLight2": {
+ "type": "string",
+ "instruction": {
+ "interval": "numeric",
+ "delay": "numeric"
+ }
+ },
+ "redLight3": {
+ "type": "string",
+ "instruction": {
+ "interval": "numeric",
+ "delay": "numeric"
+ }
+ }
+ }
+ },
+ {
+ "id": "telephone",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "turningWheel": "num-array"
+ },
+ "output": {
+ "audio": "string",
+ "ringTone": "string"
+ }
+ },
+ {
+ "id": "testDevice",
+ "description": "Device with all types of input for testing",
+ "input": {
+ "string": "string",
+ "bool": "bool",
+ "numeric": "numeric",
+ "num-array": "num-array",
+ "bool-array": "bool-array",
+ "string-array": "string-array",
+ "custom": "custom",
+ "timer": "timer"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "telephone",
+ "constraints": [
+ {
+ "comp": "eq",
+ "component_id": "turningWheel",
+ "value": [
+ 0,
+ 1,
+ 2,
+ 7
+ ]
+ }
+ ],
+ "actions": [
+ {
+ "type": "device",
+ "type_id": "controlBoard",
+ "message": {
+ "output": {
+ "greenLight1": {
+ "instruction": "blink",
+ "interval": 0.5,
+ "delay": 0
+ },
+ "greenLight2": false,
+ "greenLight3": false,
+ "redLight1": false,
+ "redLight2": false,
+ "redLight3": {
+ "instruction": "blink",
+ "interval": 0.5,
+ "delay": 0
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "hints": [
+ "De knop verzend jouw volgorde",
+ "Heb je al even gewacht?"
+ ]
+ },
+ {
+ "name": "Control puzzel",
+ "rules": [
+ {
+ "id": "controlSwitch",
+ "description": "Als de grote switch wordt geflipt, worden alle condities gecheckt (tweede schuif en groene of rode switch aan) en gaat het lichtje branden en de telefoon ringt.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "controlBoard",
+ "constraints": [
+ {
+ "component_id": "mainSwitch",
+ "comp": "eq",
+ "value": true
+ },
+ {
+ "component_id": "greenSwitch",
+ "comp": "eq",
+ "value": true
+ },
+ {
+ "component_id": "redSwitch",
+ "comp": "eq",
+ "value": true
+ },
+ {
+ "component_id": "slider2",
+ "comp": "lt",
+ "value": 30
+ }
+ ]
+ }
+ ],
+ "actions": [
+ {
+ "type": "device",
+ "type_id": "telephone",
+ "message": {
+ "output": {
+ "audio": "audio_1"
+ }
+ }
+ },
+ {
+ "type": "device",
+ "type_id": "controlBoard",
+ "message": {
+ "output": {
+ "greenLight1": false,
+ "greenLight2": true,
+ "greenLight3": false,
+ "redLight1": false,
+ "redLight2": false,
+ "redLight3": false
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "hints": [
+ "Zet de schuiven nauwkeurig"
+ ]
+ },
+ {
+ "name": "Test puzzel",
+ "rules": [
+ {
+ "id": "testCondition",
+ "description": "Condition with all component type constraints for testing",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "testDevice",
+ "constraints": [
+ {
+ "comp": "contains",
+ "value": "mystring",
+ "component_id": "string"
+ },
+ {
+ "comp": "eq",
+ "value": [
+ "mystring1",
+ "mystring2"
+ ],
+ "component_id": "string-array"
+ },
+ {
+ "comp": "lte",
+ "value": 2.5,
+ "component_id": "numeric"
+ },
+ {
+ "comp": "contains",
+ "value": [
+ 0,
+ -1,
+ 0.5,
+ 25,
+ 9.12
+ ],
+ "component_id": "num-array"
+ },
+ {
+ "comp": "contains",
+ "value": [
+ true,
+ false,
+ true
+ ],
+ "component_id": "bool-array"
+ },
+ {
+ "comp": "gt",
+ "value": {
+ "instruction": "test"
+ },
+ "component_id": "custom"
+ }
+ ]
+ }
+ ],
+ "actions": [],
+ "hints": []
+ }
+ ]
+ }
+ ],
+ "general_events": [
+ {
+ "name": "Start",
+ "rules": [
+ {
+ "id": "telephoneRings",
+ "description": "Als het spel start, moet de telefoon na 1 minuut ringen.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "timer",
+ "type_id": "timer1",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": "00:01:00"
+ }
+ ]
+ }
+ ],
+ "actions": [
+ {
+ "type": "device",
+ "type_id": "telephone",
+ "message": {
+ "output": {
+ "audio": "audio_1"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Stop",
+ "rules": [
+ {
+ "id": "flipSwitch",
+ "description": "Als de knop omgaat, stopt het spel.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "controlBoard",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": true,
+ "component_id": "mainSwitch"
+ }
+ ]
+ }
+ ],
+ "actions": [
+ {
+ "type": "timer",
+ "type_id": "timer1",
+ "message": {
+ "output": {
+ "instruction": "stop"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/timer/testTimerIncorrectInstruction.json b/back-end/resources/testing/timer/testTimerIncorrectInstruction.json
new file mode 100644
index 00000000..c4b5e049
--- /dev/null
+++ b/back-end/resources/testing/timer/testTimerIncorrectInstruction.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ ],
+ "actions": [
+ {
+ "type": "timer",
+ "type_id": "timer1",
+ "message": {
+ "output": {
+ "instruction": "test",
+ "value": "Test"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/timer/testTimerIncorrectSubtractValue.json b/back-end/resources/testing/timer/testTimerIncorrectSubtractValue.json
new file mode 100644
index 00000000..9f508095
--- /dev/null
+++ b/back-end/resources/testing/timer/testTimerIncorrectSubtractValue.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ ],
+ "actions": [
+ {
+ "type": "timer",
+ "type_id": "timer1",
+ "message": {
+ "output": {
+ "instruction": "subtract",
+ "value": true
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/timer/testTimerInvalidSubtractValue.json b/back-end/resources/testing/timer/testTimerInvalidSubtractValue.json
new file mode 100644
index 00000000..05bf2272
--- /dev/null
+++ b/back-end/resources/testing/timer/testTimerInvalidSubtractValue.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ ],
+ "actions": [
+ {
+ "type": "timer",
+ "type_id": "timer1",
+ "message": {
+ "output": {
+ "instruction": "subtract",
+ "value": "test"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/timer/testTimerNoInstruction.json b/back-end/resources/testing/timer/testTimerNoInstruction.json
new file mode 100644
index 00000000..b3796e26
--- /dev/null
+++ b/back-end/resources/testing/timer/testTimerNoInstruction.json
@@ -0,0 +1,45 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ ],
+ "actions": [
+ {
+ "type": "timer",
+ "type_id": "timer1",
+ "message": {
+ "output": {
+ "value": "Test"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/timer/testTimerNoSubtractValue.json b/back-end/resources/testing/timer/testTimerNoSubtractValue.json
new file mode 100644
index 00000000..012e7b94
--- /dev/null
+++ b/back-end/resources/testing/timer/testTimerNoSubtractValue.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ ],
+ "actions": [
+ {
+ "type": "timer",
+ "type_id": "timer1",
+ "message": {
+ "output": {
+ "instruction": "subtract",
+ "random": "lol"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testBoolArrayIncorrectType.json b/back-end/resources/testing/wrong-types/testBoolArrayIncorrectType.json
new file mode 100644
index 00000000..3e58551c
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testBoolArrayIncorrectType.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "bool-array"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": [30],
+ "component_id": "light"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testBoolArrayIncorrectType2.json b/back-end/resources/testing/wrong-types/testBoolArrayIncorrectType2.json
new file mode 100644
index 00000000..475f68c4
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testBoolArrayIncorrectType2.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "bool-array"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": 30,
+ "component_id": "light"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testBooleanIncorrectType.json b/back-end/resources/testing/wrong-types/testBooleanIncorrectType.json
new file mode 100644
index 00000000..cbbc39b1
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testBooleanIncorrectType.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": 30,
+ "component_id": "light"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testDeviceInputWrongTypeError.json b/back-end/resources/testing/wrong-types/testDeviceInputWrongTypeError.json
new file mode 100644
index 00000000..51537684
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testDeviceInputWrongTypeError.json
@@ -0,0 +1,21 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "telephone",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "turningWheel": 0
+ },
+ "output": {
+ "audio": "audio1",
+ "ringTone": "string"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testDeviceOutputWrongTypeError.json b/back-end/resources/testing/wrong-types/testDeviceOutputWrongTypeError.json
new file mode 100644
index 00000000..18b9b686
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testDeviceOutputWrongTypeError.json
@@ -0,0 +1,21 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "telephone",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "turningWheel": "int-array"
+ },
+ "output": {
+ "audio": 0,
+ "ringTone": "string"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testIncorrectTypeComparison.json b/back-end/resources/testing/wrong-types/testIncorrectTypeComparison.json
new file mode 100644
index 00000000..ef167a4c
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testIncorrectTypeComparison.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": 30,
+ "value": "30:00",
+ "component_id": "light"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testIncorrectTypeComponentID.json b/back-end/resources/testing/wrong-types/testIncorrectTypeComponentID.json
new file mode 100644
index 00000000..5a651dfc
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testIncorrectTypeComponentID.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": "30:00",
+ "component_id": 30
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testInvalidTypeComparison.json b/back-end/resources/testing/wrong-types/testInvalidTypeComparison.json
new file mode 100644
index 00000000..737504c7
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testInvalidTypeComparison.json
@@ -0,0 +1,45 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "timer",
+ "type_id": "timer1",
+ "constraints": [
+ {
+ "comp": "test",
+ "value": "30:00"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testNumArrayIncorrectType.json b/back-end/resources/testing/wrong-types/testNumArrayIncorrectType.json
new file mode 100644
index 00000000..d7d8bdf9
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testNumArrayIncorrectType.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "num-array"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": [true],
+ "component_id": "light"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testNumArrayIncorrectType2.json b/back-end/resources/testing/wrong-types/testNumArrayIncorrectType2.json
new file mode 100644
index 00000000..5fdfdcd3
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testNumArrayIncorrectType2.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "num-array"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": "test",
+ "component_id": "light"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testNumericIncorrectType.json b/back-end/resources/testing/wrong-types/testNumericIncorrectType.json
new file mode 100644
index 00000000..32d0ea3e
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testNumericIncorrectType.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "numeric"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": "30",
+ "component_id": "light"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testStringArrayIncorrectType.json b/back-end/resources/testing/wrong-types/testStringArrayIncorrectType.json
new file mode 100644
index 00000000..ded6b4c7
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testStringArrayIncorrectType.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "string-array"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": [30],
+ "component_id": "light"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testStringArrayIncorrectType2.json b/back-end/resources/testing/wrong-types/testStringArrayIncorrectType2.json
new file mode 100644
index 00000000..6e924e8a
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testStringArrayIncorrectType2.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "string-array"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": 30,
+ "component_id": "light"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testStringIncorrectType.json b/back-end/resources/testing/wrong-types/testStringIncorrectType.json
new file mode 100644
index 00000000..90f15bb7
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testStringIncorrectType.json
@@ -0,0 +1,46 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "string"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "device",
+ "type_id": "ledLights",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": 30,
+ "component_id": "light"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testTimerIncorrectPattern.json b/back-end/resources/testing/wrong-types/testTimerIncorrectPattern.json
new file mode 100644
index 00000000..7c16c715
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testTimerIncorrectPattern.json
@@ -0,0 +1,45 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "general_events": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "timer",
+ "type_id": "timer1",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": "30:00"
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/resources/testing/wrong-types/testTimerIncorrectType.json b/back-end/resources/testing/wrong-types/testTimerIncorrectType.json
new file mode 100644
index 00000000..215fd469
--- /dev/null
+++ b/back-end/resources/testing/wrong-types/testTimerIncorrectType.json
@@ -0,0 +1,45 @@
+{
+ "general": {
+ "name": "Escape X",
+ "duration": "00:30:00",
+ "host": "192.0.0.84",
+ "port": 1883
+ },
+ "devices": [
+ {
+ "id": "ledLights",
+ "description": "The telephone can ring and display a message. It will also record the numbers turned, and send these as sequence",
+ "input": {
+ "light": "boolean"
+ },
+ "output": {
+ "color": "string"
+ }
+ }
+ ],
+ "puzzles": [
+ {
+ "name": "Telefoon puzzels",
+ "rules": [
+ {
+ "id": "correctSequence",
+ "description": "De juiste volgorde van cijfers moet gedraaid worden.",
+ "limit": 1,
+ "conditions": [
+ {
+ "type": "timer",
+ "type_id": "timer1",
+ "constraints": [
+ {
+ "comp": "eq",
+ "value": 30
+ }
+ ]
+ }
+ ],
+ "actions": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/back-end/src/sciler/communication/communication.go b/back-end/src/sciler/communication/communication.go
new file mode 100644
index 00000000..4b9f1b42
--- /dev/null
+++ b/back-end/src/sciler/communication/communication.go
@@ -0,0 +1,78 @@
+package communication
+
+import (
+ "errors"
+ "fmt"
+ mqtt "github.com/eclipse/paho.mqtt.golang"
+ logger "github.com/sirupsen/logrus"
+ "time"
+)
+
+// Communicator is a type that maintains communication with the front-end and the client computers.
+type Communicator struct {
+ client mqtt.Client
+ topicsOfInterest []string
+}
+
+// NewCommunicator is a constructor for a Communicator
+func NewCommunicator(host string, port int, topicsOfInterest []string) *Communicator {
+ opts := mqtt.NewClientOptions()
+ opts.AddBroker(fmt.Sprintf("%s://%s:%d", "tcp", host, port))
+ opts.SetClientID("back-end")
+ opts.SetConnectionLostHandler(onConnectionLost)
+ client := mqtt.NewClient(opts)
+ return &Communicator{client, topicsOfInterest}
+}
+
+// Start is a function that will start the communication by connecting to the broker and subscribing to all topics of interest
+func (communicator *Communicator) Start(handler mqtt.MessageHandler) {
+ _ = action(communicator.client.Connect, "connect", -1)
+ topics := make(map[string]byte)
+ for _, topic := range communicator.topicsOfInterest {
+ topics[topic] = byte(0)
+ }
+ _ = action(func() mqtt.Token {
+ return communicator.client.SubscribeMultiple(topics, handler)
+ }, "subscribing", -1)
+}
+
+func onConnectionLost(client mqtt.Client, e error) {
+ logger.Warn(fmt.Sprintf("Connection lost : %v", e))
+ if client.IsConnected() {
+ client.Disconnect(500)
+ }
+ // TODO reconnect
+}
+
+// Publish is a method that will send a message to a specific topic
+func (communicator *Communicator) Publish(topic string, message string, retrials int) {
+ err := action(func() mqtt.Token {
+ return communicator.client.Publish(topic, byte(0), false, message)
+ }, "publish", retrials)
+ if err == errors.New("action failed") {
+ // TODO reconnect
+ }
+}
+
+// action is a function that will execute a communication action to the broker
+// actionType is the description of the action in one word for logging
+// retrials is the maximum number of times the action re-executed when failing, when retrials < 0, it is tried forever
+func action(action func() mqtt.Token, actionType string, retrials int) error {
+ for i := 0; i < retrials || retrials < 0; i++ {
+ token := action()
+ var err error = nil
+ if token.Wait() && token.Error() != nil {
+ logger.Warnf("Fail to %s, %v", actionType, token.Error())
+ time.Sleep(1 * time.Second)
+
+ logger.Infof("Retry %d to %s", i+1, actionType)
+ err = errors.New("action eventually successful")
+ continue
+ } else {
+ logger.Infof("%s successful!", actionType)
+ return err
+ }
+ }
+ logger.Errorf("All retries to %s failed, giving up :(", actionType)
+ return errors.New("action failed")
+}
diff --git a/back-end/src/sciler/communication/communication_test.go b/back-end/src/sciler/communication/communication_test.go
new file mode 100644
index 00000000..21117015
--- /dev/null
+++ b/back-end/src/sciler/communication/communication_test.go
@@ -0,0 +1,173 @@
+package communication
+
+import (
+ "errors"
+ "fmt"
+ "github.com/eclipse/paho.mqtt.golang"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "testing"
+ "time"
+)
+
+func TestNewCommunicator(t *testing.T) {
+ type args struct {
+ host string
+ port int
+ topicsOfInterest []string
+ }
+
+ optionsLocal := mqtt.NewClientOptions()
+ optionsLocal.AddBroker(fmt.Sprintf("%s://%s:%d", "tcp", "localhost", 1883))
+ optionsLocal.SetClientID("back-end")
+ optionsLocal.SetConnectionLostHandler(onConnectionLost)
+
+ tests := []struct {
+ name string
+ args args
+ want *Communicator
+ }{
+ {
+ name: "two topics",
+ args: args{
+ host: "localhost",
+ port: 1883,
+ topicsOfInterest: []string{"test", "back-end"},
+ },
+ want: &Communicator{
+ client: mqtt.NewClient(optionsLocal),
+ topicsOfInterest: []string{"test", "back-end"},
+ },
+ },
+ {
+ name: "no topics",
+ args: args{
+ host: "localhost",
+ port: 1883,
+ topicsOfInterest: nil,
+ },
+ want: &Communicator{
+ client: mqtt.NewClient(optionsLocal),
+ topicsOfInterest: nil,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := NewCommunicator(tt.args.host, tt.args.port, tt.args.topicsOfInterest)
+ assert.Equal(t, got.topicsOfInterest, tt.want.topicsOfInterest)
+ })
+ }
+}
+
+type TokenMockSuccess struct {
+ mock.Mock
+}
+
+func (t TokenMockSuccess) Wait() bool {
+ return false
+}
+
+func (t TokenMockSuccess) WaitTimeout(time.Duration) bool {
+ return true
+}
+
+func (t TokenMockSuccess) Error() error {
+ return nil
+}
+
+type TokenMockFailure struct {
+ mock.Mock
+}
+
+func (t TokenMockFailure) Wait() bool {
+ return true
+}
+
+func (t TokenMockFailure) WaitTimeout(time.Duration) bool {
+ return true
+}
+
+func (t TokenMockFailure) Error() error {
+ return errors.New("testing error of TokenMockFailure")
+}
+
+type ClientMock struct {
+ mock.Mock
+}
+
+func (c ClientMock) IsConnected() bool {
+ return true
+}
+
+func (c ClientMock) IsConnectionOpen() bool {
+ return true
+}
+
+func (c ClientMock) Connect() mqtt.Token {
+ args := c.Called()
+ return args.Get(0).(mqtt.Token)
+}
+
+func (c ClientMock) Disconnect(quiesce uint) {
+ return
+}
+
+func (c ClientMock) Publish(topic string, qos byte, retained bool, payload interface{}) mqtt.Token {
+ args := c.Called(topic, qos, retained, payload)
+ return args.Get(0).(mqtt.Token)
+}
+
+func (c ClientMock) Subscribe(topic string, qos byte, callback mqtt.MessageHandler) mqtt.Token {
+ return new(TokenMockSuccess)
+}
+
+func (c ClientMock) SubscribeMultiple(filters map[string]byte, callback mqtt.MessageHandler) mqtt.Token {
+ return new(TokenMockSuccess)
+}
+
+func (c ClientMock) Unsubscribe(topics ...string) mqtt.Token {
+ return new(TokenMockSuccess)
+}
+
+func (c ClientMock) AddRoute(topic string, callback mqtt.MessageHandler) {
+ return
+}
+
+func (c ClientMock) OptionsReader() mqtt.ClientOptionsReader {
+ return *new(mqtt.ClientOptionsReader)
+}
+
+func TestCommunicator_Start(t *testing.T) {
+ client := new(ClientMock)
+ communicator := Communicator{
+ client: client,
+ topicsOfInterest: []string{"back-end", "test"},
+ }
+
+ client.On("Connect").Return(new(TokenMockSuccess)).Once()
+ communicator.Start(func(client mqtt.Client, message mqtt.Message) {})
+ client.AssertExpectations(t)
+}
+
+func TestCommunicator_Publish(t *testing.T) {
+ client := new(ClientMock)
+ communicator := Communicator{
+ client: client,
+ topicsOfInterest: []string{"back-end", "test"},
+ }
+ client.On("Publish", "test", byte(0), false, "json").Return(new(TokenMockSuccess)).Once()
+ communicator.Publish("test", "json", 3)
+ client.AssertExpectations(t)
+}
+
+func TestCommunicator_PublishFailure(t *testing.T) {
+ client := new(ClientMock)
+ communicator := Communicator{
+ client: client,
+ topicsOfInterest: []string{"back-end", "test"},
+ }
+ client.On("Publish", "test", byte(0), false, "json").Return(new(TokenMockFailure)).Times(3)
+ communicator.Publish("test", "json", 3)
+ client.AssertExpectations(t)
+}
diff --git a/back-end/src/sciler/config/configHandler.go b/back-end/src/sciler/config/configHandler.go
new file mode 100644
index 00000000..96b67cf7
--- /dev/null
+++ b/back-end/src/sciler/config/configHandler.go
@@ -0,0 +1,201 @@
+package config
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "reflect"
+ "regexp"
+)
+
+// ReadFile reads filename and call readJSON on contents.
+func ReadFile(filename string) WorkingConfig {
+ dat, err := ioutil.ReadFile(filename)
+ if err != nil {
+ panic(errors.New("Could not read file " + filename).Error())
+ }
+ config := ReadJSON(dat)
+ return config
+}
+
+// ReadJSON transforms json file into config object.
+func ReadJSON(input []byte) WorkingConfig {
+ var config ReadConfig
+ jsonErr := json.Unmarshal(input, &config)
+ if jsonErr != nil {
+ panic(jsonErr.Error())
+ }
+ newConfig, configErr := generateDataStructures(config)
+ if configErr != nil {
+ panic(configErr.Error())
+ }
+ return newConfig
+}
+
+// Creates additional structures: forms device and rule maps;
+// and maps for actions and constraints (retrieved from puzzles and general events), with condition pointer as key
+func generateDataStructures(readConfig ReadConfig) (WorkingConfig, error) {
+ var config WorkingConfig
+ // Copy information from read config to working config.
+ config.General = readConfig.General
+ config.Puzzles = readConfig.Puzzles
+ config.GeneralEvents = readConfig.GeneralEvents
+ config.Devices = make(map[string]Device)
+ for _, d := range readConfig.Devices {
+ config.Devices[d.ID] = d
+ }
+
+ // Create additional data structures.
+ config.Rules = make(map[string]Rule)
+ config.ActionMap = make(map[string][]Action)
+ config.ConstraintMap = make(map[string]map[string][]interface{})
+ for i, p := range config.Puzzles {
+ for j, r := range p.Rules {
+ config.Rules[r.ID] = r
+ actions, err := checkActions(config.Devices, r.Actions)
+ if err != nil {
+ return config, err
+ }
+ for k, c := range r.Conditions {
+ // Set both original pointer's and current pointer's conditions
+ config.Puzzles[i].Rules[j].Conditions[k].RuleID = r.ID
+ c.RuleID = r.ID
+ config.ActionMap[c.GetID()] = actions
+ constraints, err := makeConstraints(config.Devices, c)
+ if err != nil {
+ return config, err
+ }
+ config.ConstraintMap[c.GetID()] = constraints
+ }
+ }
+ }
+ for i, e := range config.GeneralEvents {
+ for j, r := range e.Rules {
+ config.Rules[r.ID] = r
+ actions, err := checkActions(config.Devices, r.Actions)
+ if err != nil {
+ return config, err
+ }
+ for k, c := range r.Conditions {
+ // Set both original pointer's and current pointer's conditions
+ config.GeneralEvents[i].Rules[j].Conditions[k].RuleID = r.ID
+ c.RuleID = r.ID
+ config.ActionMap[c.GetID()] = actions
+ constraints, err := makeConstraints(config.Devices, c)
+ if err != nil {
+ return config, err
+ }
+ config.ConstraintMap[c.GetID()] = constraints
+ }
+ }
+ }
+ return config, nil
+}
+
+func checkActions(devices map[string]Device, actions []Action) ([]Action, error) {
+ for _, a := range actions {
+ output := a.Message.Output
+ if a.Type == "timer" {
+ instruction, ok := output["instruction"]
+ if !ok {
+ return actions, errors.New("timer should have an instruction defined")
+ }
+ if instruction != "stop" && instruction != "subtract" {
+ return actions, errors.New("timer should have an instruction defined, which is either stop or subtract")
+ }
+ if instruction == "subtract" {
+ value, ok2 := output["value"]
+ if !ok2 {
+ return actions, errors.New("timer with subtract instruction should have value")
+ }
+ if reflect.TypeOf(value).Kind() != reflect.String {
+ return actions, errors.New("timer with subtract instruction should have value in string format")
+ }
+ var rgxPat = regexp.MustCompile(`^[0-9]{2}:[0-9]{2}:[0-9]{2}$`)
+ if !rgxPat.MatchString(value.(string)) {
+ return actions, errors.New(value.(string) + " did not match pattern 'hh:mm:ss'")
+ }
+ }
+ } else if a.Type == "device" {
+ device, ok := devices[a.TypeID]
+ if !ok {
+ return actions, errors.New("device with id " + a.TypeID + " not found in map")
+ }
+ for key, value := range output {
+ expectedType, ok2 := device.Output[key]
+ if !ok2 {
+ return actions, errors.New("component id: " + key + " not found in device input")
+ }
+ // value should be of type specified in device output
+ err := CheckComponentType(expectedType, value)
+ if err != nil {
+ return actions, err
+ }
+ }
+ } else {
+ return actions, errors.New("invalid type of action: " + a.Type)
+ }
+ }
+ return actions, nil
+}
+
+// CheckComponentType checks if value is of componentType and returns error if not.
+func CheckComponentType(componentType interface{}, value interface{}) error {
+ switch componentType {
+ case "string":
+ if reflect.TypeOf(value).Kind() != reflect.String {
+ return errors.New("Value was not of type string: " + fmt.Sprint(value))
+ }
+ case "numeric":
+ if reflect.TypeOf(value).Kind() != reflect.Float64 {
+ return errors.New("Value was not of type numeric: " + fmt.Sprint(value))
+ }
+ case "boolean":
+ if reflect.TypeOf(value).Kind() != reflect.Bool {
+ return errors.New("Value was not of type boolean: " + fmt.Sprint(value))
+ }
+ case "num-array":
+ if reflect.TypeOf(value).Kind() != reflect.Slice {
+ return errors.New("Value was not of type array: " + fmt.Sprint(value))
+ }
+ array := value.([]interface{})
+ for i := range array {
+ if reflect.TypeOf(array[i]).Kind() != reflect.Float64 {
+ return errors.New("Value was not of type num-array: " + fmt.Sprint(value))
+ }
+ }
+ case "string-array":
+ if reflect.TypeOf(value).Kind() != reflect.Slice {
+ return errors.New("Value was not of type array: " + fmt.Sprint(value))
+ }
+ array := value.([]interface{})
+ for i := range array {
+ if reflect.TypeOf(array[i]).Kind() != reflect.String {
+ return errors.New("Value was not of type string-array: " + fmt.Sprint(value))
+ }
+ }
+ case "bool-array":
+ if reflect.TypeOf(value).Kind() != reflect.Slice {
+ return errors.New("Value was not of type array: " + fmt.Sprint(value))
+ }
+ array := value.([]interface{})
+ for i := range array {
+ if reflect.TypeOf(array[i]).Kind() != reflect.Bool {
+ return errors.New("Value was not of type bool-array: " + fmt.Sprint(value))
+ }
+ }
+ default:
+ // Value is of custom type, must be checked at client computer level
+ return nil
+ }
+ return nil
+}
+
+//TODO handling multiple OR/AND - new issue
+//TODO front-end - new issue
+//TODO multiple errors?
+//TODO check device present
+//TODO check component present
+//TODO check output types
+//TODO catch non-existing keys in json
diff --git a/back-end/src/sciler/config/configTypes.go b/back-end/src/sciler/config/configTypes.go
new file mode 100644
index 00000000..3445bcca
--- /dev/null
+++ b/back-end/src/sciler/config/configTypes.go
@@ -0,0 +1,88 @@
+package config
+
+import (
+ "crypto/sha512"
+ "encoding/base64"
+ "fmt"
+)
+
+// ReadConfig specifies all configuration elements of an escape room.
+type ReadConfig struct {
+ General General `json:"general"`
+ Devices []Device `json:"devices"`
+ Puzzles []Puzzle `json:"puzzles"`
+ GeneralEvents []GeneralEvent `json:"general_events"`
+}
+
+// General is a struct that describes the configurations of an escape room.
+type General struct {
+ Name string `json:"name"`
+ Duration string `json:"duration"`
+ Host string `json:"host"`
+ Port int `json:"port"`
+}
+
+// Device is a struct that describes the configurations of a device in the room.
+type Device struct {
+ ID string `json:"id"`
+ Description string `json:"description"`
+ Input map[string]string `json:"input"`
+ Output OutputObject `json:"output"`
+}
+
+// Puzzle is a struct that describes contents of a puzzle.
+type Puzzle struct {
+ Name string `json:"name"`
+ Rules []Rule `json:"rules"`
+ Hints []string `json:"hints"`
+}
+
+// GeneralEvent defines a general event, like start.
+type GeneralEvent struct {
+ Name string `json:"name"`
+ Rules []Rule `json:"rules"`
+}
+
+// Rule is a struct that describes how action flow is handled in the escape room.
+type Rule struct {
+ ID string `json:"id"`
+ Description string `json:"description"`
+ Limit int `json:"limit"`
+ Conditions []Condition `json:"conditions"`
+ Actions []Action `json:"actions"`
+}
+
+// Condition is a struct that determines when rules are fired.
+type Condition struct {
+ Type string `json:"type"`
+ TypeID string `json:"type_id"`
+ Constraints []ConstraintInfo `json:"constraints"`
+ RuleID string
+}
+
+// ConstraintInfo is a general map allowing to read input constraints, which are later parsed to real constraint objects.
+type ConstraintInfo map[string]interface{}
+
+// GetID returns hash of condition, limited to the first 24 characters
+func (condition *Condition) GetID() string {
+ hasher := sha512.New()
+ toHash := condition.RuleID + condition.TypeID + fmt.Sprint(condition.Constraints)
+ hasher.Write([]byte(toHash))
+ hash := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
+ return hash[0:24]
+}
+
+// Action is a struct that determines what happens when a rule is fired.
+type Action struct {
+ Type string `json:"type"`
+ TypeID string `json:"type_id"`
+ Message ActionMessage `json:"message"`
+}
+
+// ActionMessage can be sent across clients of the brokers.
+type ActionMessage struct {
+ Output OutputObject `json:"output"`
+}
+
+// OutputObject contains a map defining either input or output.
+type OutputObject map[string]interface{}
diff --git a/back-end/src/sciler/config/configTypes_test.go b/back-end/src/sciler/config/configTypes_test.go
new file mode 100644
index 00000000..4b5b3b22
--- /dev/null
+++ b/back-end/src/sciler/config/configTypes_test.go
@@ -0,0 +1,13 @@
+package config
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestCondition_GetID(t *testing.T) {
+ constraint := ConstraintInfo{"comp": "eq", "value": 4, "component_id": "turn"}
+ cond := Condition{"device", "telephone", []ConstraintInfo{constraint}, "ruleID"}
+ cond2 := Condition{"device", "telephone", []ConstraintInfo{constraint}, "ruleID"}
+ assert.Equal(t, cond.GetID(), cond2.GetID(), "IDs of conditions with same values should be equal")
+}
diff --git a/back-end/src/sciler/config/constraintHandler.go b/back-end/src/sciler/config/constraintHandler.go
new file mode 100644
index 00000000..b4ee903a
--- /dev/null
+++ b/back-end/src/sciler/config/constraintHandler.go
@@ -0,0 +1,228 @@
+package config
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+ "regexp"
+)
+
+// Forms constraint to processed constraint, which includes type. Value is also checked to be of that type.
+func makeConstraints(devices map[string]Device, condition Condition) (map[string][]interface{}, error) {
+ var constraints = make(map[string][]interface{})
+ for _, cons := range condition.Constraints {
+ // If the cons is enforced on a device, then the cons value type must match the input type.
+ if condition.Type == "device" {
+ generalConstraint, constraintError := createConstraint(cons, true)
+ if constraintError != nil {
+ return constraints, constraintError
+ }
+ device, ok := devices[condition.TypeID]
+ if !ok {
+ return constraints, errors.New("device with id " + condition.TypeID + " not found in map")
+ }
+ newConstraint, constraintType, err := checkDeviceType(cons, generalConstraint, device)
+ if err != nil {
+ return constraints, err
+ }
+ constraints[constraintType] = append(constraints[constraintType], newConstraint)
+ } else
+ // If the cons is enforced on a timer, then the cons value type must be in format "hh:mm:ss".
+ if condition.Type == "timer" {
+ generalConstraint, constraintError := createConstraint(cons, false)
+ if constraintError != nil {
+ return constraints, constraintError
+ }
+ if reflect.TypeOf(cons["value"]).Kind() != reflect.String {
+ return constraints, errors.New(fmt.Sprint(cons["value"]) + " cannot be cast to string, for time constraint")
+ }
+ var rgxPat = regexp.MustCompile(`^[0-9]{2}:[0-9]{2}:[0-9]{2}$`)
+ if !rgxPat.MatchString(cons["value"].(string)) {
+ return constraints, errors.New(fmt.Sprint(cons["value"]) + " did not match pattern 'hh:mm:ss'")
+ }
+ constraints["timer"] = append(constraints["timer"],
+ ConstraintTimer{generalConstraint, cons["value"].(string)})
+ } else {
+ return constraints, errors.New("invalid type of condition: " + condition.Type)
+ }
+ }
+ return constraints, nil
+}
+
+// Checks constraint value to be of type, retrieved from device input.
+func checkDeviceType(constraint ConstraintInfo, generalConstraint Constraint, device Device) (interface{}, string, error) {
+ componentType, ok := device.Input[constraint["component_id"].(string)]
+ if !ok {
+ return Constraint{}, "", errors.New("component id: " + constraint["component_id"].(string) + " not found in device input")
+ }
+ err := CheckComponentType(componentType, constraint["value"])
+ if err != nil {
+ return Constraint{}, "", err
+ }
+ switch componentType {
+ case "string":
+ return ConstraintString{generalConstraint, constraint["value"].(string)}, "string", nil
+ case "numeric":
+ return ConstraintNumeric{generalConstraint, constraint["value"].(float64)}, "numeric", nil
+ case "boolean":
+ return ConstraintBool{generalConstraint, constraint["value"].(bool)}, "boolean", nil
+ case "num-array":
+ array := constraint["value"].([]interface{})
+ var newArray []float64
+ for i := range array {
+ newArray = append(newArray, array[i].(float64))
+ }
+ return ConstraintNumericArray{generalConstraint, newArray}, "num-array", nil
+ case "string-array":
+ array := constraint["value"].([]interface{})
+ var newArray []string
+ for i := range array {
+ newArray = append(newArray, array[i].(string))
+ }
+ return ConstraintStringArray{generalConstraint, newArray}, "string-array", nil
+ case "bool-array":
+ array := constraint["value"].([]interface{})
+ var newArray []bool
+ for i := range array {
+ newArray = append(newArray, array[i].(bool))
+ }
+ return ConstraintBoolArray{generalConstraint, newArray}, "bool-array", nil
+ default:
+ // Value is of custom type, must be checked at client computer level
+ return ConstraintCustomType{generalConstraint, constraint["value"]}, "custom", nil
+ }
+}
+
+// Check if constraint can be constructed, with string comparison and string component id.
+// includeCompo specifies whether to include the component id in the constraint, which is not the case for timer type.
+func createConstraint(constraint ConstraintInfo, includeComponentID bool) (Constraint, error) {
+ var err error
+ if reflect.TypeOf(constraint["comp"]).Kind() != reflect.String {
+ return Constraint{}, errors.New("comparison '" + fmt.Sprint(constraint["comp"]) + "' cannot be cast to string")
+ }
+ if !checkComparison(constraint["comp"].(string)) {
+ return Constraint{}, errors.New("comparison should be 'eq', 'lt', 'lte', 'gt', 'gte' or 'contains'")
+ }
+ if includeComponentID {
+ if reflect.TypeOf(constraint["component_id"]).Kind() != reflect.String {
+ return Constraint{}, errors.New("component_id " + fmt.Sprint(constraint["component_id"]) + " cannot be cast to string")
+ }
+ return Constraint{constraint["comp"].(string), constraint["component_id"].(string)}, err
+ }
+ return Constraint{constraint["comp"].(string), ""}, err
+}
+
+// Check if comparison is valid
+func checkComparison(comparison string) bool {
+ possibleComparisons := []string{"eq", "lt", "lte", "gt", "gte", "contains"}
+ for _, ex := range possibleComparisons {
+ if ex == comparison {
+ return true
+ }
+ }
+ return false
+}
+
+// GetConstraintTimer struct object from map
+func GetConstraintTimer(config WorkingConfig, conditionID string) []ConstraintTimer {
+ var constraintArray []ConstraintTimer
+ if config.ConstraintMap[conditionID] == nil {
+ panic(errors.New("Condition ID: " + conditionID + " not in constraint map").Error())
+ }
+ var resultArray []interface{} = config.ConstraintMap[conditionID]["timer"]
+ for i := range resultArray {
+ constraintArray = append(constraintArray, resultArray[i].(ConstraintTimer))
+ }
+ return constraintArray
+}
+
+// GetConstraintBool struct object from map
+func GetConstraintBool(config WorkingConfig, conditionID string) []ConstraintBool {
+ var constraintArray []ConstraintBool
+ if config.ConstraintMap[conditionID] == nil {
+ panic(errors.New("Condition ID: " + conditionID + " not in constraint map").Error())
+ }
+ var resultArray []interface{} = config.ConstraintMap[conditionID]["boolean"]
+ for i := range resultArray {
+ constraintArray = append(constraintArray, resultArray[i].(ConstraintBool))
+ }
+ return constraintArray
+}
+
+// GetConstraintString struct object from map
+func GetConstraintString(config WorkingConfig, conditionID string) []ConstraintString {
+ var constraintArray []ConstraintString
+ if config.ConstraintMap[conditionID] == nil {
+ panic(errors.New("Condition ID: " + conditionID + " not in constraint map").Error())
+ }
+ var resultArray []interface{} = config.ConstraintMap[conditionID]["string"]
+ for i := range resultArray {
+ constraintArray = append(constraintArray, resultArray[i].(ConstraintString))
+ }
+ return constraintArray
+}
+
+// GetConstraintNumeric struct object from map
+func GetConstraintNumeric(config WorkingConfig, conditionID string) []ConstraintNumeric {
+ var constraintArray []ConstraintNumeric
+ if config.ConstraintMap[conditionID] == nil {
+ panic(errors.New("Condition ID: " + conditionID + " not in constraint map").Error())
+ }
+ var resultArray []interface{} = config.ConstraintMap[conditionID]["numeric"]
+ for i := range resultArray {
+ constraintArray = append(constraintArray, resultArray[i].(ConstraintNumeric))
+ }
+ return constraintArray
+}
+
+// GetConstraintStringArray struct object from map
+func GetConstraintStringArray(config WorkingConfig, conditionID string) []ConstraintStringArray {
+ var constraintArray []ConstraintStringArray
+ if config.ConstraintMap[conditionID] == nil {
+ panic(errors.New("Condition ID: " + conditionID + " not in constraint map").Error())
+ }
+ var resultArray []interface{} = config.ConstraintMap[conditionID]["string-array"]
+ for i := range resultArray {
+ constraintArray = append(constraintArray, resultArray[i].(ConstraintStringArray))
+ }
+ return constraintArray
+}
+
+// GetConstraintBoolArray struct object from map
+func GetConstraintBoolArray(config WorkingConfig, conditionID string) []ConstraintBoolArray {
+ var constraintArray []ConstraintBoolArray
+ if config.ConstraintMap[conditionID] == nil {
+ panic(errors.New("Condition ID: " + conditionID + " not in constraint map").Error())
+ }
+ var resultArray []interface{} = config.ConstraintMap[conditionID]["bool-array"]
+ for i := range resultArray {
+ constraintArray = append(constraintArray, resultArray[i].(ConstraintBoolArray))
+ }
+ return constraintArray
+}
+
+// GetConstraintNumArray struct object from map
+func GetConstraintNumArray(config WorkingConfig, conditionID string) []ConstraintNumericArray {
+ var constraintArray []ConstraintNumericArray
+ if config.ConstraintMap[conditionID] == nil {
+ panic(errors.New("Condition ID: " + conditionID + " not in constraint map").Error())
+ }
+ var resultArray []interface{} = config.ConstraintMap[conditionID]["num-array"]
+ for i := range resultArray {
+ constraintArray = append(constraintArray, resultArray[i].(ConstraintNumericArray))
+ }
+ return constraintArray
+}
+
+// GetConstraintCustomType struct object from map
+func GetConstraintCustomType(config WorkingConfig, conditionID string) []ConstraintCustomType {
+ var constraintArray []ConstraintCustomType
+ if config.ConstraintMap[conditionID] == nil {
+ panic(errors.New("Condition ID: " + conditionID + " not in constraint map").Error())
+ }
+ var resultArray []interface{} = config.ConstraintMap[conditionID]["custom"]
+ for i := range resultArray {
+ constraintArray = append(constraintArray, resultArray[i].(ConstraintCustomType))
+ }
+ return constraintArray
+}
diff --git a/back-end/src/sciler/config/errorConfig_test.go b/back-end/src/sciler/config/errorConfig_test.go
new file mode 100644
index 00000000..0859f473
--- /dev/null
+++ b/back-end/src/sciler/config/errorConfig_test.go
@@ -0,0 +1,289 @@
+package config
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+/////////////////////////////////// Test errors
+func TestDurationError(t *testing.T) {
+ filename := "../../../resources/testing/testDurationError.json"
+ assert.PanicsWithValue(t,
+ "json: cannot unmarshal number into Go struct field General.general.duration of type string",
+ func() { ReadFile(filename) },
+ "Incorrect json (duration in int) should panic")
+}
+
+func TestFileError(t *testing.T) {
+ filename := "missing.json"
+ assert.PanicsWithValue(t,
+ "Could not read file missing.json",
+ func() { ReadFile(filename) },
+ "Could not find json file")
+}
+
+/////////////////////////////////// Wrong reference to device
+func TestDeviceInputWrongTypeError(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testDeviceInputWrongTypeError.json"
+ assert.PanicsWithValue(t,
+ "json: cannot unmarshal number into Go struct field Device.devices.input of type string",
+ func() { ReadFile(filename) },
+ "Incorrect json (no input type in string format) should panic")
+}
+
+// Currently not relevant as device output is allowed to take in a map of type/instruction
+//func TestDeviceOutputWrongTypeError(t *testing.T) {
+// filename := "../../../resources/testing/wrong-types/testDeviceOutputWrongTypeError.json"
+// assert.PanicsWithValue(t,
+// "json: cannot unmarshal number into Go struct field Device.devices.output of type string",
+// func() { ReadFile(filename) },
+// "Incorrect json (no output type in string format) should panic")
+//}
+
+func TestDeviceActionNotPresent(t *testing.T) {
+ filename := "../../../resources/testing/testDeviceActionNotPresent.json"
+ assert.PanicsWithValue(t,
+ "device with id telephone not found in map",
+ func() { ReadFile(filename) },
+ "Device used in action should be present in device list")
+}
+
+func TestComponentActionNotPresent(t *testing.T) {
+ filename := "../../../resources/testing/testComponentActionNotPresent.json"
+ assert.PanicsWithValue(t,
+ "component id: light not found in device input",
+ func() { ReadFile(filename) },
+ "Component used in action should be present in device list")
+}
+
+func TestDeviceConstraintNotPresent(t *testing.T) {
+ filename := "../../../resources/testing/testDeviceConstraintNotPresent.json"
+ assert.PanicsWithValue(t,
+ "device with id telephone not found in map",
+ func() { ReadFile(filename) },
+ "Device used in constraint should be present in device list")
+}
+
+func TestComponentConstraintNotPresent(t *testing.T) {
+ filename := "../../../resources/testing/testComponentConstraintNotPresent.json"
+ assert.PanicsWithValue(t,
+ "component id: color not found in device input",
+ func() { ReadFile(filename) },
+ "Component used in constraint should be present in device list")
+}
+
+func TestIncorrectTypeCondition(t *testing.T) {
+ filename := "../../../resources/testing/testIncorrectTypeCondition.json"
+ assert.PanicsWithValue(t,
+ "invalid type of condition: mytype",
+ func() { ReadFile(filename) },
+ "Condition type should be 'device' or 'timer'")
+}
+
+func TestIncorrectTypeAction(t *testing.T) {
+ filename := "../../../resources/testing/testIncorrectTypeAction.json"
+ assert.PanicsWithValue(t,
+ "invalid type of action: my-type",
+ func() { ReadFile(filename) },
+ "Condition type should be 'device' or 'timer'")
+}
+
+func TestCheckActionComponentType(t *testing.T) {
+ filename := "../../../resources/testing/testCheckActionComponentType.json"
+ assert.PanicsWithValue(t,
+ "Value was not of type string: 0",
+ func() { ReadFile(filename) },
+ "Condition type should be 'device' or 'timer'")
+}
+
+/////////////////////////////////// Test timer actions
+func TestTimerNoInstruction(t *testing.T) {
+ filename := "../../../resources/testing/timer/testTimerNoInstruction.json"
+ assert.PanicsWithValue(t,
+ "timer should have an instruction defined",
+ func() { ReadFile(filename) },
+ "Timer should have an instruction defined")
+}
+
+func TestTimerIncorrectInstruction(t *testing.T) {
+ filename := "../../../resources/testing/timer/testTimerIncorrectInstruction.json"
+ assert.PanicsWithValue(t,
+ "timer should have an instruction defined, which is either stop or subtract",
+ func() { ReadFile(filename) },
+ "Timer should have a correct instruction defined: stop or subtract")
+}
+
+func TestTimerNoSubtractValue(t *testing.T) {
+ filename := "../../../resources/testing/timer/testTimerNoSubtractValue.json"
+ assert.PanicsWithValue(t,
+ "timer with subtract instruction should have value",
+ func() { ReadFile(filename) },
+ "Timer should have a value defined for subtract instruction")
+}
+
+func TestTimerIncorrectSubtractValue(t *testing.T) {
+ filename := "../../../resources/testing/timer/testTimerIncorrectSubtractValue.json"
+ assert.PanicsWithValue(t,
+ "timer with subtract instruction should have value in string format",
+ func() { ReadFile(filename) },
+ "Timer should have a string value defined for subtract instruction")
+}
+
+func TestTimerInvalidSubtractValue(t *testing.T) {
+ filename := "../../../resources/testing/timer/testTimerInvalidSubtractValue.json"
+ assert.PanicsWithValue(t,
+ "test did not match pattern 'hh:mm:ss'",
+ func() { ReadFile(filename) },
+ "Timer should have a value defined for subtract instruction in format 'hh:mm:ss'")
+}
+
+/////////////////////////////////// Wrong inputs
+func TestTimerIncorrectPattern(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testTimerIncorrectPattern.json"
+ assert.PanicsWithValue(t,
+ "30:00 did not match pattern 'hh:mm:ss'",
+ func() { ReadFile(filename) },
+ "Time should be entered in correct pattern")
+}
+
+func TestTimerIncorrectType(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testTimerIncorrectType.json"
+ assert.PanicsWithValue(t,
+ "30 cannot be cast to string, for time constraint",
+ func() { ReadFile(filename) },
+ "Timer should take in string value")
+}
+
+func TestStringIncorrectType(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testStringIncorrectType.json"
+ assert.PanicsWithValue(t,
+ "Value was not of type string: 30",
+ func() { ReadFile(filename) },
+ "If device input specifies string type, constraint value should be a string")
+}
+
+func TestNumericIncorrectType(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testNumericIncorrectType.json"
+ assert.PanicsWithValue(t,
+ "Value was not of type numeric: 30",
+ func() { ReadFile(filename) },
+ "If device input specifies numeric type, constraint value should be a numeric")
+}
+
+func TestBooleanIncorrectType(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testBooleanIncorrectType.json"
+ assert.PanicsWithValue(t,
+ "Value was not of type boolean: 30",
+ func() { ReadFile(filename) },
+ "If device input specifies boolean type, constraint value should be a boolean")
+}
+
+func TestNumArrayIncorrectType(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testNumArrayIncorrectType.json"
+ assert.PanicsWithValue(t,
+ "Value was not of type num-array: [true]",
+ func() { ReadFile(filename) },
+ "If device input specifies num array type, constraint value should be a num array")
+}
+
+func TestNumArrayIncorrectType2(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testNumArrayIncorrectType2.json"
+ assert.PanicsWithValue(t,
+ "Value was not of type array: test",
+ func() { ReadFile(filename) },
+ "If device input specifies num array type, constraint value should be a num array")
+}
+
+func TestStringArrayIncorrectType(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testStringArrayIncorrectType.json"
+ assert.PanicsWithValue(t,
+ "Value was not of type string-array: [30]",
+ func() { ReadFile(filename) },
+ "If device input specifies string array type, constraint value should be a string array")
+}
+
+func TestStringArrayIncorrectType2(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testStringArrayIncorrectType2.json"
+ assert.PanicsWithValue(t,
+ "Value was not of type array: 30",
+ func() { ReadFile(filename) },
+ "If device input specifies string array type, constraint value should be a string array")
+}
+
+func TestBoolArrayIncorrectType(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testBoolArrayIncorrectType.json"
+ assert.PanicsWithValue(t,
+ "Value was not of type bool-array: [30]",
+ func() { ReadFile(filename) },
+ "If device input specifies bool array type, constraint value should be a bool array")
+}
+
+func TestBoolArrayIncorrectType2(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testBoolArrayIncorrectType2.json"
+ assert.PanicsWithValue(t,
+ "Value was not of type array: 30",
+ func() { ReadFile(filename) },
+ "If device input specifies bool array type, constraint value should be a bool array")
+}
+
+func TestIncorrectComparisonType(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testIncorrectTypeComparison.json"
+ assert.PanicsWithValue(t,
+ "comparison '30' cannot be cast to string",
+ func() { ReadFile(filename) },
+ "Comparison should be entered in string format")
+}
+
+func TestInvalidComparisonType(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testInvalidTypeComparison.json"
+ assert.PanicsWithValue(t,
+ "comparison should be 'eq', 'lt', 'lte', 'gt', 'gte' or 'contains'",
+ func() { ReadFile(filename) },
+ "Comparison should be a correct type")
+}
+
+func TestIncorrectComponentIDType(t *testing.T) {
+ filename := "../../../resources/testing/wrong-types/testIncorrectTypeComponentID.json"
+ assert.PanicsWithValue(t,
+ "component_id 30 cannot be cast to string",
+ func() { ReadFile(filename) },
+ "Component ID should be entered in string format")
+}
+
+/////////////////////////////////// Missing condition keys
+func TestNoComponentIDForDeviceConstraintBool(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ assert.PanicsWithValue(t,
+ "Condition ID: ID not in constraint map",
+ func() { GetConstraintBool(config, "ID") },
+ "Panic thrown when ID not found for bool")
+ assert.PanicsWithValue(t,
+ "Condition ID: ID not in constraint map",
+ func() { GetConstraintString(config, "ID") },
+ "Panic thrown when ID not found for string")
+ assert.PanicsWithValue(t,
+ "Condition ID: ID not in constraint map",
+ func() { GetConstraintNumeric(config, "ID") },
+ "Panic thrown when ID not found for numeric")
+ assert.PanicsWithValue(t,
+ "Condition ID: ID not in constraint map",
+ func() { GetConstraintStringArray(config, "ID") },
+ "Panic thrown when ID not found for string array")
+ assert.PanicsWithValue(t,
+ "Condition ID: ID not in constraint map",
+ func() { GetConstraintNumArray(config, "ID") },
+ "Panic thrown when ID not found for num array")
+ assert.PanicsWithValue(t,
+ "Condition ID: ID not in constraint map",
+ func() { GetConstraintBoolArray(config, "ID") },
+ "Panic thrown when ID not found for bool array")
+ assert.PanicsWithValue(t,
+ "Condition ID: ID not in constraint map",
+ func() { GetConstraintCustomType(config, "ID") },
+ "Panic thrown when ID not found for custom type")
+ assert.PanicsWithValue(t,
+ "Condition ID: ID not in constraint map",
+ func() { GetConstraintTimer(config, "ID") },
+ "Panic thrown when ID not found for timer")
+}
diff --git a/back-end/src/sciler/config/helperConfigTypes.go b/back-end/src/sciler/config/helperConfigTypes.go
new file mode 100644
index 00000000..4aa59aa6
--- /dev/null
+++ b/back-end/src/sciler/config/helperConfigTypes.go
@@ -0,0 +1,70 @@
+package config
+
+// WorkingConfig has additional fields to ReadConfig, with lists of conditions, constraints and actions.
+type WorkingConfig struct {
+ General General
+ Puzzles []Puzzle
+ GeneralEvents []GeneralEvent
+ //TODO maps and concurrency
+ Devices map[string]Device
+ Rules map[string]Rule
+ ActionMap map[string][]Action
+ // first key is condition pointer, second is type of constraint:
+ // string, bool, numeric, string-array, bool-array, num-array, custom, timer
+ // ex: ConstraintMap[conditionID][boolean] = []ConstraintBool
+ ConstraintMap map[string]map[string][]interface{}
+}
+
+// Constraint is main constraint type with comparison and componentID (latter only for device condition).
+type Constraint struct {
+ Comparison string
+ ComponentID string
+}
+
+// ConstraintTimer is struct for constraint with timer value: "hh:mm:ss".
+type ConstraintTimer struct {
+ Constraint
+ Value string
+}
+
+// ConstraintNumeric is struct for constraint with numeric value.
+type ConstraintNumeric struct {
+ Constraint
+ Value float64
+}
+
+// ConstraintString is struct for constraint with string value.
+type ConstraintString struct {
+ Constraint
+ Value string
+}
+
+// ConstraintBool is struct for constraint with boolean value.
+type ConstraintBool struct {
+ Constraint
+ Value bool
+}
+
+// ConstraintStringArray is struct for constraint with string array value.
+type ConstraintStringArray struct {
+ Constraint
+ Value []string
+}
+
+// ConstraintNumericArray is struct for constraint with numeric array value.
+type ConstraintNumericArray struct {
+ Constraint
+ Value []float64
+}
+
+// ConstraintBoolArray is struct for constraint with bool array value.
+type ConstraintBoolArray struct {
+ Constraint
+ Value []bool
+}
+
+// ConstraintCustomType is struct for constraint with custom type value.
+type ConstraintCustomType struct {
+ Constraint
+ Value interface{}
+}
diff --git a/back-end/src/sciler/config/testConfig_test.go b/back-end/src/sciler/config/testConfig_test.go
new file mode 100644
index 00000000..6bb17226
--- /dev/null
+++ b/back-end/src/sciler/config/testConfig_test.go
@@ -0,0 +1,186 @@
+package config
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+/////////////////////////////////// Test correct test_config
+func TestGeneralInformation(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ general := General{"Escape X", "00:30:00", "192.0.0.84", 1883}
+ assert.Equal(t, general, config.General,
+ "General information should be correct")
+}
+
+func TestPuzzleSize(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ assert.Equal(t, 3, len(config.Puzzles), "Should have read three puzzles")
+}
+
+func TestDeviceInput(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ input := map[string]string{
+ "redSwitch": "boolean",
+ "orangeSwitch": "boolean",
+ "greenSwitch": "boolean",
+ "slider1": "numeric",
+ "slider2": "numeric",
+ "slider3": "numeric",
+ "mainSwitch": "boolean",
+ }
+ assert.Equal(t, input, config.Devices["controlBoard"].Input,
+ "Input of device should be retrieved correctly from the devices map")
+}
+
+func TestActionOutput(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ output := OutputObject{
+ "greenLight1": false,
+ "greenLight2": true,
+ "greenLight3": false,
+ "redLight1": false,
+ "redLight2": false,
+ "redLight3": false,
+ }
+ assert.Equal(t, output, config.Puzzles[1].Rules[0].Actions[1].Message.Output,
+ "Message from action should be of OutputObject type, retrieved through puzzles in config")
+}
+
+func TestRulesMap(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ assert.Equal(t, "De juiste volgorde van cijfers moet gedraaid worden.",
+ config.Rules["correctSequence"].Description,
+ "Description from rule should retrieved correctly through rules map")
+}
+
+func TestGeneralEvents(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ assert.Equal(t, "Start", config.GeneralEvents[0].Name,
+ "Name of general event should be retrieved correctly")
+}
+
+func TestComponentIDForDeviceConstraintNum(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ list := GetConstraintNumeric(config, config.Puzzles[1].Rules[0].Conditions[0].GetID())
+ assert.Equal(t, 1, len(list), "There should be one numeric constraint for this condition")
+ assert.Equal(t, "slider2", list[0].ComponentID,
+ "ComponentID should be retrieved correctly through puzzle, rule, condition, constraint")
+}
+
+func TestComponentIDForDeviceConstraintBool(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ list := GetConstraintBool(config, config.Puzzles[1].Rules[0].Conditions[0].GetID())
+ assert.Equal(t, 3, len(list), "There should be three boolean constraints for this condition")
+ assert.Equal(t, "greenSwitch", list[1].ComponentID,
+ "ComponentID should be retrieved correctly through puzzle, rule, condition, constraint")
+}
+
+func TestComponentIDForDeviceConstraintDouble(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ list := GetConstraintNumeric(config, config.Puzzles[2].Rules[0].Conditions[0].GetID())
+ assert.Equal(t, 1, len(list), "There should be one numeric constraint for this condition")
+ assert.Equal(t, "numeric", list[0].ComponentID,
+ "ComponentID should be retrieved correctly through puzzle, rule, condition, constraint")
+ assert.Equal(t, 2.5, list[0].Value,
+ "Custom type constraint should have correctly retrieved value")
+}
+
+func TestComponentIDForDeviceConstraintString(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ list := GetConstraintString(config, config.Puzzles[2].Rules[0].Conditions[0].GetID())
+ assert.Equal(t, 1, len(list), "There should be one string constraint for this condition")
+ assert.Equal(t, "string", list[0].ComponentID,
+ "ComponentID should be retrieved correctly through puzzle, rule, condition, constraint")
+ assert.Equal(t, "mystring", list[0].Value,
+ "Custom type constraint should have correctly retrieved value")
+}
+
+func TestComponentIDForDeviceConstraintNumArray(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ list := GetConstraintNumArray(config, config.Puzzles[2].Rules[0].Conditions[0].GetID())
+ assert.Equal(t, 1, len(list), "There should be one num-array constraint for this condition")
+ assert.Equal(t, "num-array", list[0].ComponentID,
+ "ComponentID should be retrieved correctly through puzzle, rule, condition, constraint")
+ assert.Equal(t, []float64{0, -1, 0.5, 25, 9.12}, list[0].Value,
+ "Custom type constraint should have correctly retrieved value")
+}
+
+func TestComponentIDForDeviceConstraintStringArray(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ list := GetConstraintStringArray(config, config.Puzzles[2].Rules[0].Conditions[0].GetID())
+ assert.Equal(t, 1, len(list), "There should be one num-array constraint for this condition")
+ assert.Equal(t, "string-array", list[0].ComponentID,
+ "ComponentID should be retrieved correctly through puzzle, rule, condition, constraint")
+ assert.Equal(t, []string{"mystring1", "mystring2"}, list[0].Value,
+ "Custom type constraint should have correctly retrieved value")
+}
+
+func TestComponentIDForDeviceConstraintBoolArray(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ list := GetConstraintBoolArray(config, config.Puzzles[2].Rules[0].Conditions[0].GetID())
+ assert.Equal(t, 1, len(list), "There should be one bool-array constraint for this condition")
+ assert.Equal(t, "bool-array", list[0].ComponentID,
+ "ComponentID should be retrieved correctly through puzzle, rule, condition, constraint")
+ assert.Equal(t, []bool{true, false, true}, list[0].Value,
+ "Custom type constraint should have correctly retrieved value")
+}
+
+func TestComponentIDForDeviceConstraintCustom(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ list := GetConstraintCustomType(config, config.Puzzles[2].Rules[0].Conditions[0].GetID())
+ assert.Equal(t, 1, len(list), "There should be one custom type constraint for this condition")
+ assert.Equal(t, "custom", list[0].ComponentID,
+ "ComponentID should be retrieved correctly through puzzle, rule, condition, constraint")
+ output := map[string]interface{}{"instruction": "test"}
+ assert.Equal(t, output, list[0].Value,
+ "Custom type constraint should have correctly retrieved value")
+}
+
+func TestNoComponentForTimeConstraint(t *testing.T) {
+ filename := "../../../resources/testing/test_config.json"
+ config := ReadFile(filename)
+ list := GetConstraintTimer(config, config.GeneralEvents[0].Rules[0].Conditions[0].GetID())
+ assert.Equal(t, 1, len(list), "There should be one timer constraint for this condition")
+ assert.Equal(t, "", list[0].ComponentID,
+ "There should not be a component id for a timer constraint, it should be ''")
+ assert.Equal(t, "00:01:00", list[0].Value,
+ "Timer constraint should have correctly retrieved value")
+}
+
+/////////////////////////////////// Edge case behavior
+func TestDeviceInputCustomType(t *testing.T) {
+ filename := "../../../resources/testing/testDeviceCustomType.json"
+ config := ReadFile(filename)
+ for key, value := range config.Devices["telephone"].Input {
+ assert.Equal(t, "turningWheel", key,
+ "Id of component should be key in input map")
+ assert.Equal(t, "my-type", value,
+ "Custom type of component should be value in input map")
+ }
+}
+
+func TestDeviceOutputCustomType(t *testing.T) {
+ filename := "../../../resources/testing/testDeviceCustomType.json"
+ config := ReadFile(filename)
+ for key, value := range config.Devices["telephone"].Output {
+ assert.Equal(t, "audio", key,
+ "Id of component should be key in input map")
+ assert.Equal(t, "string", value,
+ "Custom type of component should be value in output map")
+ }
+}
diff --git a/back-end/src/sciler/main.go b/back-end/src/sciler/main.go
new file mode 100644
index 00000000..df25f032
--- /dev/null
+++ b/back-end/src/sciler/main.go
@@ -0,0 +1,106 @@
+package main
+
+import (
+ "encoding/json"
+ mqtt "github.com/eclipse/paho.mqtt.golang"
+ "github.com/sirupsen/logrus"
+ "os"
+ "sciler/communication"
+ "sciler/config"
+ "time"
+)
+
+var topics = []string{"back-end", "status", "hint", "connect"}
+
+// Message is a type that follows the structure all messages have, described in resources/message_manual.md
+type Message struct {
+ DeviceID string `json:"device_id"`
+ TimeSent string `json:"time_sent"`
+ Type string `json:"type"`
+ Contents map[string]interface{} `json:"contents"`
+}
+
+func main() {
+ dir, err := os.Getwd()
+ if err != nil {
+ logrus.Fatal(err)
+ }
+ filename := dir + "/back-end/resources/room_config.json"
+ configurations := config.ReadFile(filename)
+ logrus.Info("configurations read from: " + filename)
+ host := configurations.General.Host
+ port := configurations.General.Port
+
+ communicator := communication.NewCommunicator(host, port, topics)
+ go communicator.Start(func(client mqtt.Client, message mqtt.Message) {
+ // TODO: Make advanced message handler which acts according to the events / configuration
+ var raw Message
+ if err := json.Unmarshal(message.Payload(), &raw); err != nil {
+ logrus.Errorf("Invalid JSON received: %v", err)
+ }
+ switch raw.Type {
+ case "instruction":
+ logrus.Info("instruction message received")
+ {
+ if raw.Contents["instruction"] == "test all" && raw.DeviceID == "front-end" { // TODO maybe switch again
+ message := Message{
+ DeviceID: "back-end",
+ TimeSent: time.Now().Format("02-01-2006 15:04:05"),
+ Type: "instruction",
+ Contents: map[string]interface{}{
+ "instruction": "test",
+ },
+ }
+ jsonMessage, err := json.Marshal(&message)
+ if err != nil {
+ logrus.Errorf("Error occurred while constructing message to publish: %v", err)
+ } else {
+ communicator.Publish("test", string(jsonMessage), 3)
+ }
+ }
+ }
+ case "status":
+ {
+ logrus.Info("status message received")
+ if raw.DeviceID == "controlBoard1" {
+ var instruction string
+ if raw.Contents["switch1"] == "1" {
+ instruction = "turn off"
+ } else if raw.Contents["switch1"] == "0" {
+ instruction = "turn on"
+ }
+ if instruction != "" {
+ message := Message{
+ DeviceID: "back-end",
+ TimeSent: time.Now().Format("02-01-2006 15:04:05"),
+ Type: "instruction",
+ Contents: map[string]interface{}{
+ "instruction": instruction,
+ },
+ }
+ jsonMessage, err := json.Marshal(&message)
+ if err != nil {
+ logrus.Errorf("Error occurred while constructing message to publish: %v", err)
+ } else {
+ communicator.Publish("test", string(jsonMessage), 3)
+ }
+ }
+ }
+
+ }
+ case "confirmation":
+ {
+ logrus.Info("confirmation message received")
+ }
+ case "connection":
+ {
+ logrus.Info("connection message received")
+ }
+ }
+ })
+
+ // loop for now preventing app to exit
+ for {
+ time.Sleep(time.Microsecond * time.Duration(250))
+ }
+}
diff --git a/broker/README.md b/broker/README.md
index 4fc6e1c4..85367665 100644
--- a/broker/README.md
+++ b/broker/README.md
@@ -1,5 +1,7 @@
# BEP_1920_Q2 - broker
+
+### Set-up
When using Mosquitto as a broker, mosquitto.conf should be used to setup a broker with an extra listener for websockets.
The bind-address should be changed to local-ip when communication outside a one machine develepment setup is required.
@@ -14,3 +16,9 @@ mosquitto -c
```
When there is a mosquitto broker already running, this will not work. Then, first run `net stop mosquitto`.
+
+### Running
+```
+mosquitto -t
+mosquitto -h
+```
\ No newline at end of file
diff --git a/broker/mosquitto.conf b/broker/mosquitto.conf
index 6e1b9eac..a80e88f1 100644
--- a/broker/mosquitto.conf
+++ b/broker/mosquitto.conf
@@ -9,7 +9,7 @@
# given, the default listener will not be bound to a specific
# address and so will be accessible to all network interfaces.
# bind_address ip-address/host name
-bind_address localhost
+bind_address 192.168.178.82
# Port to use for the default listener.
port 1883
diff --git a/cc_library/README.md b/cc_library/README.md
index 67bf9136..b6273a4f 100644
--- a/cc_library/README.md
+++ b/cc_library/README.md
@@ -7,3 +7,11 @@ To make sure all dependencies are loaded, run `pip install -r cc_library/require
- To check unittests, run `python -m unittest discover`.
- To update formatting, run `black cc_library`.
- To check the codestyle, run `flake8 cc_library`.
+
+### Raspberry Pi Set up
+- Code specific methods of the device into `