Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80ff81b1e0 | ||
|
|
46016d966d | ||
|
|
c2b947a197 | ||
|
|
66ab3fa6ca | ||
|
|
c8da529ce3 | ||
|
|
93c65a948b | ||
|
|
124ef542b0 | ||
|
|
31f8dc2f98 | ||
|
|
77f53435fe | ||
|
|
565900c9d4 | ||
|
|
a3d28e8cd5 | ||
|
|
9d024de5a6 | ||
|
|
18932a3f4d | ||
|
|
e4868f6fdc | ||
|
|
d4cf7f5b48 | ||
|
|
c8f07e22d9 | ||
|
|
22f4d777a4 |
+12
@@ -45,3 +45,15 @@
|
||||
|
||||
### Next Steps
|
||||
- WiFi self-healing and MQTT reconnection & testing
|
||||
|
||||
## Week 4
|
||||
### Completed
|
||||
- Tag & Scanner CLI for configuration & persistent config
|
||||
- Core script (MQTT, location estimation, Store in Redis)
|
||||
|
||||
### In Progress
|
||||
- NGSI-LD data for public API
|
||||
- Admin API with CRUD functions
|
||||
|
||||
### Next Steps
|
||||
- Add verification with camera in core script
|
||||
|
||||
Generated
+519
-3
@@ -12,7 +12,9 @@
|
||||
"@prisma/adapter-better-sqlite3": "^7.8.0",
|
||||
"@prisma/client": "^7.8.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1"
|
||||
"express": "^5.2.1",
|
||||
"mqtt": "^5.15.1",
|
||||
"redis": "^5.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
@@ -23,6 +25,15 @@
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electric-sql/pglite": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz",
|
||||
@@ -870,6 +881,78 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/bloom": {
|
||||
"version": "5.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.12.1.tgz",
|
||||
"integrity": "sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client": {
|
||||
"version": "5.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz",
|
||||
"integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@node-rs/xxhash": "^1.1.0",
|
||||
"@opentelemetry/api": ">=1 <2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@node-rs/xxhash": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/json": {
|
||||
"version": "5.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.12.1.tgz",
|
||||
"integrity": "sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/search": {
|
||||
"version": "5.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.12.1.tgz",
|
||||
"integrity": "sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/time-series": {
|
||||
"version": "5.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.12.1.tgz",
|
||||
"integrity": "sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
@@ -944,7 +1027,6 @@
|
||||
"version": "25.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz",
|
||||
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.21.0"
|
||||
@@ -975,6 +1057,15 @@
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/readable-stream": {
|
||||
"version": "4.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz",
|
||||
"integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||
@@ -996,6 +1087,27 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
@@ -1121,6 +1233,18 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/broker-factory": {
|
||||
"version": "3.1.14",
|
||||
"resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.14.tgz",
|
||||
"integrity": "sha512-L45k5HMbPIrMid0nTOZ/UPXG/c0aRuQKVrSDFIb1zOkvfiyHgYmIjc3cSiN1KwQIvRDOtKE0tfb3I9EZ3CmpQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"fast-unique-numbers": "^9.0.27",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-factory": "^7.0.49"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
@@ -1145,6 +1269,12 @@
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -1247,6 +1377,36 @@
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/commist": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz",
|
||||
"integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||
"engines": [
|
||||
"node >= 6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.0.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||
@@ -1581,6 +1741,24 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
@@ -1670,6 +1848,19 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-unique-numbers": {
|
||||
"version": "9.0.27",
|
||||
"resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz",
|
||||
"integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||
@@ -1919,6 +2110,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/help-me": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.18",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz",
|
||||
@@ -2004,6 +2201,15 @@
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -2043,6 +2249,16 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/js-sdsl": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
||||
"integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
@@ -2057,6 +2273,12 @@
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/lru.min": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
|
||||
@@ -2155,6 +2377,141 @@
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mqtt": {
|
||||
"version": "5.15.1",
|
||||
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.1.tgz",
|
||||
"integrity": "sha512-V1WnkGuJh3ec9QXzy5Iylw8OOBK+Xu1WhxcQ9mMpLThG+/JZIMV1PgLNRgIiqXhZnvnVLsuyxHl5A/3bHHbcAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/readable-stream": "^4.0.21",
|
||||
"@types/ws": "^8.18.1",
|
||||
"commist": "^3.2.0",
|
||||
"concat-stream": "^2.0.0",
|
||||
"debug": "^4.4.1",
|
||||
"help-me": "^5.0.0",
|
||||
"lru-cache": "^10.4.3",
|
||||
"minimist": "^1.2.8",
|
||||
"mqtt-packet": "^9.0.2",
|
||||
"number-allocator": "^1.0.14",
|
||||
"readable-stream": "^4.7.0",
|
||||
"rfdc": "^1.4.1",
|
||||
"socks": "^2.8.6",
|
||||
"split2": "^4.2.0",
|
||||
"worker-timers": "^8.0.23",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"bin": {
|
||||
"mqtt": "build/bin/mqtt.js",
|
||||
"mqtt_pub": "build/bin/pub.js",
|
||||
"mqtt_sub": "build/bin/sub.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt-packet": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
|
||||
"integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^6.0.8",
|
||||
"debug": "^4.3.4",
|
||||
"process-nextick-args": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt-packet/node_modules/bl": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
|
||||
"integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/readable-stream": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt-packet/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt-packet/node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mqtt/node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -2222,6 +2579,16 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/number-allocator": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz",
|
||||
"integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.1",
|
||||
"js-sdsl": "4.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@@ -2392,6 +2759,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proper-lockfile": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||
@@ -2569,6 +2951,22 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "5.12.1",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-5.12.1.tgz",
|
||||
"integrity": "sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@redis/bloom": "5.12.1",
|
||||
"@redis/client": "5.12.1",
|
||||
"@redis/json": "5.12.1",
|
||||
"@redis/search": "5.12.1",
|
||||
"@redis/time-series": "5.12.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/remeda": {
|
||||
"version": "2.33.4",
|
||||
"resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz",
|
||||
@@ -2609,6 +3007,12 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
@@ -2881,6 +3285,39 @@
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
|
||||
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.1.1",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sqlstring": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||
@@ -2962,6 +3399,12 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
@@ -3025,6 +3468,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||
@@ -3043,7 +3492,6 @@
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz",
|
||||
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
@@ -3101,12 +3549,80 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/worker-factory": {
|
||||
"version": "7.0.49",
|
||||
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.49.tgz",
|
||||
"integrity": "sha512-lW7tpgy6aUv2dFsQhv1yv+XFzdkCf/leoKRTGMPVK5/die6RrUjqgJHJf556qO+ZfytNG6wPXc17E8zzsOLUDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"fast-unique-numbers": "^9.0.27",
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/worker-timers": {
|
||||
"version": "8.0.31",
|
||||
"resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.31.tgz",
|
||||
"integrity": "sha512-ngkq5S6JuZyztom8tDgBzorLo9byhBMko/sXfgiUD945AuzKGg1GCgDMCC3NaYkicLpGKXutONM36wEX8UbBCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-timers-broker": "^8.0.16",
|
||||
"worker-timers-worker": "^9.0.14"
|
||||
}
|
||||
},
|
||||
"node_modules/worker-timers-broker": {
|
||||
"version": "8.0.16",
|
||||
"resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.16.tgz",
|
||||
"integrity": "sha512-JyP3AvUGyPGbBGW7XiUewm2+0pN/aYo1QpVf5kdXAfkDZcN3p7NbWrG6XnyDEpDIvfHk/+LCnOW/NsuiU9riYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"broker-factory": "^3.1.14",
|
||||
"fast-unique-numbers": "^9.0.27",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-timers-worker": "^9.0.14"
|
||||
}
|
||||
},
|
||||
"node_modules/worker-timers-worker": {
|
||||
"version": "9.0.14",
|
||||
"resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.14.tgz",
|
||||
"integrity": "sha512-/qF06C60sXmSLfUl7WglvrDIbspmPOM8UrG63Dnn4bi2x4/DfqHS/+dxF5B+MdHnYO5tVuZYLHdAodrKdabTIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"tslib": "^2.8.1",
|
||||
"worker-factory": "^7.0.49"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/zeptomatch": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz",
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"@prisma/adapter-better-sqlite3": "^7.8.0",
|
||||
"@prisma/client": "^7.8.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1"
|
||||
"express": "^5.2.1",
|
||||
"mqtt": "^5.15.1",
|
||||
"redis": "^5.12.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Room" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"nameame" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"firstName" TEXT NOT NULL,
|
||||
"lastName" TEXT NOT NULL,
|
||||
"faceEmbedding" BLOB
|
||||
);
|
||||
INSERT INTO "new_User" ("faceEmbedding", "firstName", "id", "lastName") SELECT "faceEmbedding", "firstName", "id", "lastName" FROM "User";
|
||||
DROP TABLE "User";
|
||||
ALTER TABLE "new_User" RENAME TO "User";
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `nameame` on the `Room` table. All the data in the column will be lost.
|
||||
- Added the required column `name` to the `Room` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Room" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO "new_Room" ("id") SELECT "id" FROM "Room";
|
||||
DROP TABLE "Room";
|
||||
ALTER TABLE "new_Room" RENAME TO "Room";
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -20,3 +20,8 @@ model Tag {
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
model Room {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import express from "express";
|
||||
import { getEntities, getEntitiesOfType, getEntity } from "./livedata.controller.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/entities", getEntities);
|
||||
router.get("/entities", getEntitiesOfType);
|
||||
router.get("/entities/:urn", getEntity);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,172 @@
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import { getActiveUserData, getRedisRoomData, type RedisRoomData, type RedisUserData } from "./livedata.repository.js";
|
||||
import { prisma } from "../../lib/prisma.js";
|
||||
|
||||
interface Params {
|
||||
urn: string;
|
||||
}
|
||||
|
||||
type Relationship = {
|
||||
type: "Relationship";
|
||||
object: string;
|
||||
};
|
||||
|
||||
type Property<T = any> = {
|
||||
type: "Property";
|
||||
value: T;
|
||||
};
|
||||
|
||||
interface Entity {
|
||||
id: string;
|
||||
type: string;
|
||||
name: {
|
||||
type: "Property";
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface User extends Entity {
|
||||
type: "User";
|
||||
locatedIn: Relationship;
|
||||
rssi: Property<number>;
|
||||
authenticationStatus: Property<string>;
|
||||
observedAt: Property<string>;
|
||||
}
|
||||
|
||||
interface Room extends Entity {
|
||||
type: "Room";
|
||||
occupancy: Property<number>;
|
||||
}
|
||||
|
||||
function convertToNGSIUser(user: RedisUserData): User {
|
||||
return {
|
||||
id: `urn:ngsi-ld:User:${user.userID}`,
|
||||
type: "User",
|
||||
name: {
|
||||
type: "Property",
|
||||
value: user.name
|
||||
},
|
||||
locatedIn: {
|
||||
type: "Relationship",
|
||||
object: user.room
|
||||
},
|
||||
rssi: {
|
||||
type: "Property",
|
||||
value: user.rssi,
|
||||
},
|
||||
authenticationStatus: {
|
||||
type: "Property",
|
||||
value: user.verified ? "verified" : "unverified"
|
||||
},
|
||||
observedAt: {
|
||||
type: "Property",
|
||||
value: (new Date(user.timestamp)).toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function convertToNGSIRoom(room: RedisRoomData): Promise<Room | null> {
|
||||
const roomName = await prisma.room.findUnique({
|
||||
where: { id: room.roomID },
|
||||
select: { name: true }
|
||||
})
|
||||
|
||||
if (!roomName?.name) return null;
|
||||
|
||||
return {
|
||||
id: `urn:ngsi-ld:Room:${room.roomID}`,
|
||||
type: "Room",
|
||||
name: {
|
||||
type: "Property",
|
||||
value: roomName.name
|
||||
},
|
||||
occupancy: {
|
||||
type: "Property",
|
||||
value: room.occupancy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getNGSIUsers(): Promise<User[]> {
|
||||
let users: User[] = [];
|
||||
|
||||
const result: RedisUserData[] = await getActiveUserData();
|
||||
|
||||
for (const r of result) {
|
||||
const user = convertToNGSIUser(r);
|
||||
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
async function getNGSIRooms(): Promise<Room[]> {
|
||||
let rooms: Room[] = [];
|
||||
|
||||
const result: RedisRoomData[] = await getRedisRoomData();
|
||||
|
||||
for (const r of result) {
|
||||
const room = await convertToNGSIRoom(r);
|
||||
if (room == null) continue;
|
||||
|
||||
rooms.push(room);
|
||||
}
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
export async function getEntities(req: Request, res: Response, next: NextFunction) {
|
||||
if (Object.keys(req.query).length > 0) return next();
|
||||
|
||||
let entities: Entity[] = [];
|
||||
|
||||
entities.push(...await getNGSIUsers());
|
||||
entities.push(...await getNGSIRooms());
|
||||
|
||||
return res.status(200).send(entities);
|
||||
}
|
||||
|
||||
export async function getEntitiesOfType(req: Request, res: Response) {
|
||||
const { type } = req.query;
|
||||
|
||||
switch (type) {
|
||||
case "user": return res.status(200).send(await getNGSIUsers());
|
||||
case "room": return res.status(200).send(await getNGSIRooms());
|
||||
default:
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEntity(req: Request<Params>, res: Response) {
|
||||
const { urn } = req.params;
|
||||
|
||||
if (!urn || !urn.startsWith("urn:ngsi-ld:")) return res.sendStatus(400);
|
||||
|
||||
const entity = urn.slice(12);
|
||||
|
||||
if (!entity) return res.sendStatus(400);
|
||||
|
||||
if (entity.toLowerCase().startsWith("room:")) {
|
||||
const roomID = entity.slice(5);
|
||||
|
||||
if (!roomID) return res.sendStatus(400);
|
||||
|
||||
const data = await getRedisRoomData(roomID);
|
||||
|
||||
if (!data.length) return res.sendStatus(404);
|
||||
|
||||
return res.status(200).send(await convertToNGSIRoom(data[0]!));
|
||||
} else if (entity.toLowerCase().startsWith("user:")) {
|
||||
const userID = entity.slice(5);
|
||||
|
||||
if (!userID) return res.sendStatus(400);
|
||||
|
||||
const data = await getActiveUserData(userID);
|
||||
|
||||
if (!data.length) return res.sendStatus(404);
|
||||
|
||||
return res.status(200).send(convertToNGSIUser(data[0]!));
|
||||
} else
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { prisma } from "../../lib/prisma.js";
|
||||
import { getRedis } from "../../redis/redis.js";
|
||||
|
||||
export interface RedisUserData {
|
||||
userID: string;
|
||||
name: string;
|
||||
rssi: number;
|
||||
room: string;
|
||||
verified: boolean;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface RedisRoomData {
|
||||
roomID: string;
|
||||
occupancy: number;
|
||||
}
|
||||
|
||||
export async function getActiveUserData(userID?: string): Promise<RedisUserData[]> {
|
||||
const redis = getRedis();
|
||||
|
||||
if (!userID) {
|
||||
let cursor = "0";
|
||||
let result: RedisUserData[] = [];
|
||||
|
||||
do {
|
||||
const res = await redis.scan(cursor, {
|
||||
MATCH: "user:*",
|
||||
COUNT: 100,
|
||||
});
|
||||
|
||||
cursor = res.cursor;
|
||||
|
||||
const values = await Promise.all(
|
||||
res.keys.map((key) => redis.get(key))
|
||||
);
|
||||
|
||||
result.push(...values
|
||||
.filter((v): v is string => v !== null)
|
||||
.map((v) => JSON.parse(v))
|
||||
);
|
||||
|
||||
} while (cursor !== "0");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const _userID = await redis.get(`_user:${userID}`);
|
||||
|
||||
if (!_userID) return [];
|
||||
|
||||
const res = await redis.get(`user:${_userID}`);
|
||||
|
||||
if (!res) return [];
|
||||
|
||||
const resObj: RedisUserData = JSON.parse(res);
|
||||
|
||||
return [resObj];
|
||||
}
|
||||
|
||||
export async function getRedisRoomData(roomID?: string): Promise<RedisRoomData[]> {
|
||||
const redis = getRedis();
|
||||
|
||||
if (!roomID) {
|
||||
let cursor = "0";
|
||||
let result: RedisRoomData[] = [];
|
||||
|
||||
do {
|
||||
const res = await redis.scan(cursor, {
|
||||
MATCH: "room:*",
|
||||
COUNT: 100,
|
||||
});
|
||||
|
||||
cursor = res.cursor;
|
||||
|
||||
const values = await Promise.all(
|
||||
res.keys.map(async (key) => ({
|
||||
roomID: key,
|
||||
occupancy: await redis.get(key),
|
||||
}))
|
||||
);
|
||||
|
||||
result.push(...values
|
||||
.filter((v) => v.occupancy !== null)
|
||||
.map((v) => ({
|
||||
roomID: v.roomID.slice(5),
|
||||
occupancy: Number(v.occupancy),
|
||||
}))
|
||||
);
|
||||
|
||||
} while (cursor !== "0");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const res = await redis.get(`room:${roomID}`);
|
||||
|
||||
if (!res) return [];
|
||||
|
||||
return [{ roomID: roomID, occupancy: Number(res) }];
|
||||
}
|
||||
|
||||
export async function getRoomIDs(): Promise<{ id: string; }[]> {
|
||||
return await prisma.room.findMany({
|
||||
select: { id: true }
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { prisma } from "../../lib/prisma.js";
|
||||
|
||||
export async function existsByUUID(uuid: string): Promise<boolean> {
|
||||
const user = await prisma.user.findUnique({
|
||||
const tag = await prisma.tag.findUnique({
|
||||
where: { id: uuid }
|
||||
});
|
||||
|
||||
return !!user;
|
||||
return !!tag;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
interface Config {
|
||||
api: {
|
||||
address: string;
|
||||
port: number;
|
||||
};
|
||||
mqtt: {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
topic: string;
|
||||
};
|
||||
redis: {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
core: {
|
||||
hysteresis: number;
|
||||
candidateHysteresis: number;
|
||||
debounceMS: number;
|
||||
minSamples: number;
|
||||
transitionTTL: number;
|
||||
transitionCleanupInterval: number;
|
||||
lossThreshold: number;
|
||||
userTTL: number;
|
||||
};
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
api: {
|
||||
address: "0.0.0.0",
|
||||
port: 80,
|
||||
},
|
||||
mqtt: {
|
||||
host: "192.168.1.2",
|
||||
port: 1883,
|
||||
username: "user",
|
||||
password: "pass",
|
||||
topic: "scanners/+",
|
||||
},
|
||||
redis: {
|
||||
host: "192.168.1.2",
|
||||
port: 6379,
|
||||
},
|
||||
core: {
|
||||
hysteresis: 6,
|
||||
candidateHysteresis: 3,
|
||||
debounceMS: 3000,
|
||||
minSamples: 4,
|
||||
transitionTTL: 5 * 60 * 1000,
|
||||
transitionCleanupInterval: 60 * 1000,
|
||||
lossThreshold: 5000,
|
||||
userTTL: 3 * 60 * 1000
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,173 @@
|
||||
import { getRedis } from "../redis/redis.js";
|
||||
import { prisma } from "../lib/prisma.js";
|
||||
import { RoomTransition, transitions } from "./transition.js";
|
||||
import config from "../config/config.js";
|
||||
import { type RedisUserData } from "../api/livedata/livedata.repository.js";
|
||||
|
||||
const adjectives = [
|
||||
"Crazy",
|
||||
"Silent",
|
||||
"Dark",
|
||||
"Fast",
|
||||
"Lucky",
|
||||
"Wild",
|
||||
"Epic"
|
||||
];
|
||||
|
||||
const nouns = [
|
||||
"Tiger",
|
||||
"Wolf",
|
||||
"Falcon",
|
||||
"Shadow",
|
||||
"Ninja",
|
||||
"Dragon",
|
||||
"Phoenix"
|
||||
];
|
||||
|
||||
const running = new Set<string>();
|
||||
const userRooms = new Map<string, string>();
|
||||
const userPseudo = new Map<string, string>();
|
||||
|
||||
export async function updateRoomOccupancyListener(message: string, channel: string) {
|
||||
console.log(`[Redis] Key ${message} expired.`);
|
||||
|
||||
const id = message.startsWith("user:") ? message.slice(5) : message;
|
||||
const lastRoomID = userRooms.get(id);
|
||||
const userPseudoID = userPseudo.get(id);
|
||||
|
||||
const redis = getRedis();
|
||||
|
||||
if (!lastRoomID) {
|
||||
console.log("[!] Key not found in local mapι")
|
||||
} else {
|
||||
await redis.decr(`room:${lastRoomID}`);
|
||||
}
|
||||
|
||||
await redis.del(`_user:${userPseudoID}`);
|
||||
|
||||
userRooms.delete(id);
|
||||
userPseudo.delete(id);
|
||||
}
|
||||
|
||||
function generateNickname() {
|
||||
const adjective =
|
||||
adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||
|
||||
const noun =
|
||||
nouns[Math.floor(Math.random() * nouns.length)];
|
||||
|
||||
const number = Math.floor(Math.random() * 1000);
|
||||
|
||||
return `${adjective}${noun}${number}`;
|
||||
}
|
||||
|
||||
function newUser(): { pseudoID: string, psuedoName: string } {
|
||||
return {
|
||||
pseudoID: crypto.randomUUID(),
|
||||
psuedoName: generateNickname()
|
||||
};
|
||||
}
|
||||
|
||||
export async function analyzeData(roomID: string, metrics: {
|
||||
tagID: string,
|
||||
rssi: number
|
||||
}) {
|
||||
if (running.has(metrics.tagID)) {
|
||||
console.log(`[!] Skipping TAG_ID: ${metrics.tagID} already in process.`);
|
||||
return;
|
||||
}
|
||||
|
||||
running.add(metrics.tagID);
|
||||
|
||||
try {
|
||||
console.log(`\n=======================================================\n` +
|
||||
`ROOM_ID: ${roomID}\nTAG_ID: ${metrics.tagID}\nRSSI: ${metrics.rssi}\n` +
|
||||
`=======================================================\n`);
|
||||
|
||||
const userID = (
|
||||
await prisma.tag.findUnique({
|
||||
where: { id: metrics.tagID },
|
||||
select: { userId: true },
|
||||
})
|
||||
)?.userId;
|
||||
|
||||
console.log(`[SQLite] Resolved USER_ID: ${userID}`);
|
||||
|
||||
if (!userID)
|
||||
return;
|
||||
|
||||
const redis = getRedis();
|
||||
const res = await redis.get(`user:${userID}`);
|
||||
|
||||
if (!res) {
|
||||
const user = newUser();
|
||||
console.log(`[Redis] Created USER: ${user.psuedoName} USER_ID: ${user.pseudoID}`);
|
||||
|
||||
// trigger camera
|
||||
|
||||
await redis.set(`user:${userID}`, JSON.stringify({
|
||||
userID: user.pseudoID,
|
||||
name: user.psuedoName,
|
||||
rssi: metrics.rssi,
|
||||
room: roomID,
|
||||
verified: false,
|
||||
timestamp: Date.now()
|
||||
}), { PX: config.core.userTTL });
|
||||
|
||||
await redis.set(`_user:${user.pseudoID}`, userID);
|
||||
|
||||
await redis.incr(`room:${roomID}`);
|
||||
|
||||
userRooms.set(userID, roomID);
|
||||
userPseudo.set(userID, user.pseudoID);
|
||||
|
||||
console.log("[Redis] Stored new user in redis.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let resObj: RedisUserData = JSON.parse(res);
|
||||
console.log(`[Redis] User exists in redis in ROOM_ID: ${resObj["room"]}`);
|
||||
|
||||
if (roomID == resObj["room"]) {
|
||||
console.log("[Redis] Same room, updating redis...");
|
||||
resObj["rssi"] = metrics.rssi;
|
||||
resObj["timestamp"] = Date.now()
|
||||
|
||||
await redis.set(`user:${userID}`, JSON.stringify(resObj), { PX: config.core.userTTL });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let transition = transitions.get(userID);
|
||||
|
||||
// update room occupancy
|
||||
|
||||
if (!transition) {
|
||||
console.log("[!] Starting new transition");
|
||||
transition = new RoomTransition();
|
||||
transitions.set(userID, transition);
|
||||
}
|
||||
|
||||
if (transition.shouldTransitionTo(roomID, metrics.rssi, resObj["rssi"], resObj["timestamp"])) {
|
||||
// trigger camera
|
||||
|
||||
console.log(`[!] Transition done ${resObj["room"]} -> ${roomID}`);
|
||||
|
||||
await redis.decr(`room:${resObj["room"]}`);
|
||||
await redis.incr(`room:${roomID}`);
|
||||
|
||||
userRooms.set(userID, roomID);
|
||||
|
||||
resObj["rssi"] = metrics.rssi;
|
||||
resObj["room"] = roomID;
|
||||
resObj["timestamp"] = Date.now()
|
||||
|
||||
|
||||
await redis.set(`user:${userID}`, JSON.stringify(resObj), { PX: config.core.userTTL });
|
||||
} else
|
||||
console.log("[!] Transition declined");
|
||||
} finally {
|
||||
running.delete(metrics.tagID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getRoomIDs } from "../api/livedata/livedata.repository.js";
|
||||
import { initRedis, initSubRedis } from "../redis/redis.js";
|
||||
import * as mqtt from "./mqtt.js";
|
||||
import { cleanupWorker } from "./transition.js";
|
||||
|
||||
export async function bootstrap() {
|
||||
try {
|
||||
const redis = await initRedis();
|
||||
|
||||
await redis.flushDb();
|
||||
console.log(`[!] Flushed Redis.`);
|
||||
|
||||
const rooms = await getRoomIDs();
|
||||
for (const { id } of rooms)
|
||||
await redis.set(`room:${id}`, 0);
|
||||
|
||||
await initSubRedis();
|
||||
await mqtt.start();
|
||||
|
||||
cleanupWorker();
|
||||
|
||||
console.log("[!] Core started.");
|
||||
} catch (err: any) {
|
||||
console.log("[!] Bootstrap failed:", err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import mqtt, { MqttClient } from "mqtt";
|
||||
import config from "../config/config.js";
|
||||
import { analyzeData } from "./analyze.js";
|
||||
|
||||
function initMqtt(): Promise<MqttClient> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = mqtt.connect(
|
||||
`mqtt://${config.mqtt.host}:${config.mqtt.port}`,
|
||||
{
|
||||
username: config.mqtt.username,
|
||||
password: config.mqtt.password
|
||||
}
|
||||
);
|
||||
|
||||
client.once("connect", () => {
|
||||
console.log("[MQTT] MQTT connected.");
|
||||
|
||||
client.subscribe(config.mqtt.topic, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
console.log("[MQTT] MQTT subscribed.");
|
||||
resolve(client);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
client.once("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function start() {
|
||||
try {
|
||||
const mqttClient = await initMqtt();
|
||||
|
||||
mqttClient.on("message", (topic, message) => {
|
||||
analyzeData(topic.split("/").pop()!, JSON.parse(message.toString()));
|
||||
});
|
||||
|
||||
mqttClient.on("error", (err) => {
|
||||
console.log(err.message);
|
||||
})
|
||||
} catch (err: any) {
|
||||
console.log("[MQTT] MQTT connection failed:", err.message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import config from "../config/config.js";
|
||||
|
||||
export const transitions = new Map<string, RoomTransition>();
|
||||
|
||||
export function cleanupWorker() {
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
|
||||
for (const [userID, transition] of transitions) {
|
||||
const inactiveFor = now - transition.lastSeen;
|
||||
|
||||
if (inactiveFor > 5 * config.core.transitionTTL)
|
||||
transitions.delete(userID);
|
||||
}
|
||||
}, config.core.transitionCleanupInterval);
|
||||
}
|
||||
|
||||
type Candidate = {
|
||||
roomID: string | null;
|
||||
since: number | null;
|
||||
samples: number;
|
||||
rssi: number | null;
|
||||
};
|
||||
|
||||
export class RoomTransition {
|
||||
candidate: Candidate;
|
||||
lastSeen: number;
|
||||
|
||||
constructor() {
|
||||
this.lastSeen = Date.now();
|
||||
this.candidate = { roomID: null, since: null, samples: 0, rssi: null };
|
||||
}
|
||||
|
||||
shouldTransitionTo(roomID: string, candidateRSSI: number, currRSSI: number, lastCurrentSeen: number) {
|
||||
const now = Date.now();
|
||||
this.lastSeen = now;
|
||||
|
||||
const stronger = candidateRSSI > currRSSI + config.core.hysteresis;
|
||||
|
||||
const signalLost = now - lastCurrentSeen > config.core.lossThreshold;
|
||||
|
||||
console.log(`[TRANSITION] stronger = ${stronger}`);
|
||||
console.log(`[TRANSITION] signalLost = ${signalLost}`);
|
||||
|
||||
if (!stronger && !signalLost) {
|
||||
this.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
const noCandidate = this.candidate.roomID == null;
|
||||
const betterCandidate = this.candidate.roomID != roomID &&
|
||||
this.candidate.rssi !== null &&
|
||||
candidateRSSI > this.candidate.rssi + config.core.candidateHysteresis;
|
||||
|
||||
console.log(`[TRANSITION] noCandidate = ${noCandidate}`);
|
||||
console.log(`[TRANSITION] betterCandidate = ${betterCandidate}`);
|
||||
|
||||
if (noCandidate || betterCandidate) {
|
||||
this.candidate.roomID = roomID;
|
||||
this.candidate.since = now;
|
||||
this.candidate.samples = 1;
|
||||
this.candidate.rssi = candidateRSSI;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.candidate.samples++;
|
||||
this.candidate.rssi = candidateRSSI;
|
||||
|
||||
console.log(`[TRANSITION] samples = ${this.candidate.samples}`);
|
||||
console.log(`[TRANSITION] candidateRSSI = ${this.candidate.rssi}`);
|
||||
|
||||
const enoughTime = now - (this.candidate.since ?? now) >= config.core.debounceMS;
|
||||
const enoughConfirmations = this.candidate.samples >= config.core.minSamples;
|
||||
|
||||
console.log(`[TRANSITION] enoughTime = ${enoughTime}`);
|
||||
console.log(`[TRANSITION] enoughConfirmations = ${enoughConfirmations}`);
|
||||
console.log(`[TRANSITION] signalLost = ${signalLost}`);
|
||||
|
||||
if ((enoughTime && enoughConfirmations) || signalLost) {
|
||||
this.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.candidate.roomID = null;
|
||||
this.candidate.since = null;
|
||||
this.candidate.samples = 0;
|
||||
this.candidate.rssi = null;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
import express from "express";
|
||||
import lookupRoutes from "./api/lookup/lookup.routes.js";
|
||||
import livedataRoutes from "./api/livedata/livadata.routes.js"
|
||||
import config from "./config/config.js";
|
||||
import { bootstrap } from "./core/bootstrap.js";
|
||||
|
||||
const app = express();
|
||||
const PORT = 80;
|
||||
const ADDRESS = "0.0.0.0";
|
||||
(async () => {
|
||||
const app = express();
|
||||
|
||||
app.use("/", lookupRoutes);
|
||||
app.use("/lookup", lookupRoutes);
|
||||
app.use("/livedata", livedataRoutes);
|
||||
|
||||
app.listen(PORT, ADDRESS, () => {
|
||||
console.log(`Server listening on ${ADDRESS}:${PORT}`);
|
||||
});
|
||||
app.listen(config.api.port, config.api.address, () => {
|
||||
console.log(`Server listening on ${config.api.address}:${config.api.port}`);
|
||||
});
|
||||
|
||||
bootstrap();
|
||||
|
||||
})()
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createClient, type RedisClientType } from "redis";
|
||||
import config from "../config/config.js";
|
||||
import { updateRoomOccupancyListener } from "../core/analyze.js";
|
||||
|
||||
let redisClient: RedisClientType;
|
||||
let subRedisClient: RedisClientType;
|
||||
|
||||
export async function initSubRedis(): Promise<void> {
|
||||
subRedisClient = createClient({
|
||||
url: `redis://${config.redis.host}:${config.redis.port}`
|
||||
});
|
||||
|
||||
subRedisClient.on("error", (err) => {
|
||||
console.log("[Redis] SubRedis error:", err.message);
|
||||
});
|
||||
|
||||
await subRedisClient.connect();
|
||||
|
||||
console.log("[Redis] SubRedis connected.");
|
||||
|
||||
subRedisClient.pSubscribe("__keyevent@0__:expired", updateRoomOccupancyListener);
|
||||
|
||||
console.log("[Redis] SubRedis listener set.");
|
||||
}
|
||||
|
||||
export async function initRedis(): Promise<RedisClientType> {
|
||||
redisClient = createClient({
|
||||
url: `redis://${config.redis.host}:${config.redis.port}`
|
||||
});
|
||||
|
||||
redisClient.on("error", (err) => {
|
||||
console.log("[Redis] Redis error:", err.message);
|
||||
});
|
||||
|
||||
await redisClient.connect();
|
||||
|
||||
console.log("[Redis] Redis connected.");
|
||||
|
||||
return redisClient;
|
||||
}
|
||||
|
||||
export function getRedis(): RedisClientType {
|
||||
if (!redisClient) {
|
||||
throw new Error("[Redis] Redis not initialized");
|
||||
}
|
||||
|
||||
return redisClient;
|
||||
}
|
||||
@@ -128,7 +128,7 @@ void mqttTask(void *)
|
||||
{
|
||||
while (!mqtt.connected())
|
||||
{
|
||||
Serial.printf("MQTT Disconnected. Connecting to %s:%d\n", config.mqtt.host.c_str(), config.mqtt.port);
|
||||
// Serial.printf("MQTT Disconnected. Connecting to %s:%d\n", config.mqtt.host.c_str(), config.mqtt.port);
|
||||
if (mqtt.connect(config.dev_name.c_str(), config.mqtt.username.c_str(), config.mqtt.password.c_str()))
|
||||
Serial.println("MQTT connected.");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
@@ -139,7 +139,7 @@ void mqttTask(void *)
|
||||
char payload[128];
|
||||
|
||||
snprintf(payload, sizeof(payload),
|
||||
"{\"uuid\":\"%s\",\"rssi\":%.2f}",
|
||||
"{\"tagID\":\"%s\",\"rssi\":%.2f}",
|
||||
ev.uuid,
|
||||
ev.ema);
|
||||
|
||||
@@ -369,6 +369,14 @@ void handleCommand(String line)
|
||||
else if (key == TAG_NAME_CONFIG_KEY)
|
||||
Serial.println(config.tag_name);
|
||||
}
|
||||
else if (line == "reset")
|
||||
{
|
||||
prefs.begin("config", false);
|
||||
prefs.clear();
|
||||
prefs.end();
|
||||
|
||||
esp_restart();
|
||||
}
|
||||
else if (line == "reboot")
|
||||
{
|
||||
Serial.println("Rebooting...");
|
||||
@@ -381,6 +389,7 @@ void handleCommand(String line)
|
||||
Serial.println(" set <key> <value> - Set a configuration value");
|
||||
Serial.println(" show <key> - Show a configuration value");
|
||||
Serial.println(" save - Save configuration to non-volatile storage");
|
||||
Serial.println(" reset - Reset configuration to defaults");
|
||||
Serial.println(" reboot - Reboot the device");
|
||||
Serial.println(" help - Show this help message");
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
#define MQTT_TOPIC "scanners/"
|
||||
#define WIFI_SSID "COSMOTE-489882"
|
||||
#define WIFI_PASS "x32hbh54673ngccdsfa9"
|
||||
#define DEV_NAME "Scanner_Room_A"
|
||||
#define DEV_NAME "572b29cb-6a93-480e-adf0-c5c44e9a58df"
|
||||
|
||||
struct Config
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user