Author SHA1 Message Date
Kostas Drakontidis 7e1a7d0eca verify faces from camera 2026-05-29 23:46:15 +03:00
Kostas Drakontidis 1ac5ddbf33 cross compile files for extract embeddings exec 2026-05-29 23:45:50 +03:00
Kostas Drakontidis 8dffdc9bf9 added go gitignore 2026-05-29 23:45:10 +03:00
Kostas Drakontidis 23116f6a19 move to subdir 2026-05-29 23:18:45 +03:00
Kostas Drakontidis 2badcb92e2 extract from jpeg 2026-05-29 02:02:48 +03:00
Kostas DrakontidisandGitHub 8abae7b9ba Update progress.md with new completed tasks 2026-05-25 06:04:28 +03:00
Kostas DrakontidisandGitHub 1bce98e083 Merge pull request #17 from dkwstas/nodered_config
nodered flow upload
2026-05-25 06:02:13 +03:00
Kostas Drakontidis 92b3c72e3e nodered flow upload 2026-05-25 06:01:47 +03:00
Kostas DrakontidisandGitHub 41cf0ad7ca Merge pull request #15 from dkwstas/public_api
Public api
2026-05-25 05:58:36 +03:00
Kostas Drakontidis 80ff81b1e0 room occupancy impl & race cond fix & local user roomID and psuedoID storage for cleanup and indexing 2026-05-25 05:57:35 +03:00
Kostas Drakontidis 46016d966d ngsi-ld public api code 2026-05-25 05:55:43 +03:00
Kostas Drakontidis c2b947a197 flush redis on startup and init subredis 2026-05-25 05:55:04 +03:00
Kostas Drakontidis 66ab3fa6ca added debug prefix 2026-05-25 05:54:37 +03:00
Kostas Drakontidis c8da529ce3 whitespace 2026-05-25 05:54:25 +03:00
Kostas Drakontidis 93c65a948b added routes 2026-05-25 05:54:17 +03:00
Kostas Drakontidis 124ef542b0 moved redis & added subredis for expire notifications 2026-05-25 05:53:53 +03:00
Kostas DrakontidisandGitHub 31f8dc2f98 Update progress.md with Week 4 details 2026-05-24 00:25:38 +03:00
Kostas DrakontidisandGitHub 77f53435fe Merge pull request #14 from dkwstas/core_dev
Core dev
2026-05-24 00:21:14 +03:00
Kostas Drakontidis 565900c9d4 init fully working core code 2026-05-24 00:19:09 +03:00
Kostas Drakontidis a3d28e8cd5 prisma migrations 2026-05-24 00:18:31 +03:00
Kostas Drakontidis 9d024de5a6 get data from tag table instead of user 2026-05-24 00:04:43 +03:00
Kostas Drakontidis 18932a3f4d add room entity 2026-05-24 00:03:59 +03:00
Kostas Drakontidis e4868f6fdc install mqtt & redis packages 2026-05-24 00:03:51 +03:00
Kostas Drakontidis d4cf7f5b48 mqtt topic and msg small change 2026-05-23 23:43:28 +03:00
Kostas Drakontidis c8f07e22d9 Add reset command scanner 2026-05-22 13:03:47 +03:00
Kostas DrakontidisandGitHub 22f4d777a4 Merge pull request #13 from dkwstas/persistent_config
Closes #11
2026-05-20 17:46:57 +03:00
31 changed files with 2150 additions and 17 deletions
+13
View File
@@ -45,3 +45,16 @@
### Next Steps ### Next Steps
- WiFi self-healing and MQTT reconnection & testing - 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)
- NGSI-LD public api
- public dashboard (Node-RED)
### In Progress
- Admin API with CRUD functions
### Next Steps
- Add verification with camera in core script
+352
View File
@@ -0,0 +1,352 @@
[
{
"id": "f1398f7c64b1d0ea",
"type": "tab",
"label": "OfficeSense Flow",
"disabled": false,
"info": "",
"env": []
},
{
"id": "20bb112ec6e856de",
"type": "ui-table",
"z": "f1398f7c64b1d0ea",
"group": "05cf402c93866d4e",
"name": "",
"label": "",
"order": 1,
"width": 0,
"height": 0,
"maxrows": 0,
"passthru": false,
"autocols": true,
"showSearch": true,
"deselect": true,
"selectionType": "none",
"columns": [],
"mobileBreakpoint": "sm",
"mobileBreakpointType": "defaults",
"action": "replace",
"className": "",
"x": 690,
"y": 360,
"wires": [
[]
]
},
{
"id": "0a5259b54388cdfc",
"type": "inject",
"z": "f1398f7c64b1d0ea",
"name": "",
"props": [
{
"p": "payload"
}
],
"repeat": "3",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 70,
"y": 400,
"wires": [
[
"4030b9e7bcab5435",
"f37401387ca7a595"
]
]
},
{
"id": "4030b9e7bcab5435",
"type": "http request",
"z": "f1398f7c64b1d0ea",
"name": "",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "http://192.168.1.2/livedata/entities",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 270,
"y": 360,
"wires": [
[
"4aee360198b34403"
]
]
},
{
"id": "f37401387ca7a595",
"type": "http request",
"z": "f1398f7c64b1d0ea",
"name": "",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "http://192.168.1.2/livedata/entities?type=room",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 270,
"y": 440,
"wires": [
[
"448ba65b518d534d"
]
]
},
{
"id": "448ba65b518d534d",
"type": "function",
"z": "f1398f7c64b1d0ea",
"name": "ParseNGSI",
"func": "msg.payload = msg.payload.map(r => {\n return {\n Room: r.name?.value,\n Occupancy: r.occupancy?.value\n };\n});\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 490,
"y": 440,
"wires": [
[
"586c57be87bf8bc6",
"49042a0c5086d4b8"
]
]
},
{
"id": "586c57be87bf8bc6",
"type": "ui-table",
"z": "f1398f7c64b1d0ea",
"group": "a98ec11a60f808be",
"name": "",
"label": "",
"order": 1,
"width": 0,
"height": 0,
"maxrows": 0,
"passthru": false,
"autocols": true,
"showSearch": true,
"deselect": true,
"selectionType": "none",
"columns": [],
"mobileBreakpoint": "sm",
"mobileBreakpointType": "defaults",
"action": "replace",
"className": "",
"x": 690,
"y": 440,
"wires": [
[]
]
},
{
"id": "4aee360198b34403",
"type": "function",
"z": "f1398f7c64b1d0ea",
"name": "ParseNGSI",
"func": "const entities = msg.payload;\n\nconst users = entities.filter(e => e.type === \"User\");\nconst rooms = entities.filter(e => e.type === \"Room\");\n\nconst roomMap = {};\nfor (const r of rooms) {\n roomMap[r.id.replace(\"urn:ngsi-ld:Room:\", \"\")] = r;\n}\n\nmsg.payload = users.map(u => {\n\n const roomId = u.locatedIn?.object;\n const room = roomMap[roomId];\n\n return {\n Name: u.name?.value,\n Room: room ? room.name?.value : \"Unknown\",\n RSSI: u.rssi?.value,\n Auth: u.authenticationStatus?.value,\n Time: new Date(u.observedAt?.value)\n .toLocaleString(\"el-GR\", { timeZone: \"Europe/Athens\" })\n };\n});\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 490,
"y": 360,
"wires": [
[
"20bb112ec6e856de"
]
]
},
{
"id": "49042a0c5086d4b8",
"type": "ui-chart",
"z": "f1398f7c64b1d0ea",
"group": "a98ec11a60f808be",
"name": "",
"label": "Room Occupancy Chart",
"order": 2,
"chartType": "bar",
"category": "",
"categoryType": "none",
"xAxisLabel": "Room",
"xAxisProperty": "Room",
"xAxisPropertyType": "property",
"xAxisType": "category",
"xAxisFormat": "",
"xAxisFormatType": "auto",
"xmin": "",
"xmax": "",
"yAxisLabel": "Occupancy",
"yAxisProperty": "Occupancy",
"yAxisPropertyType": "property",
"ymin": "0",
"ymax": "10",
"bins": 10,
"action": "replace",
"stackSeries": false,
"pointShape": "circle",
"pointRadius": 4,
"showLegend": true,
"removeOlder": 1,
"removeOlderUnit": "3600",
"removeOlderPoints": "",
"colors": [
"#0095ff",
"#ff0000",
"#ff7f0e",
"#2ca02c",
"#a347e1",
"#d62728",
"#ff9896",
"#9467bd",
"#c5b0d5"
],
"textColor": [
"#666666"
],
"textColorDefault": true,
"gridColor": [
"#e5e5e5"
],
"gridColorDefault": true,
"width": 6,
"height": 8,
"className": "",
"interpolation": "linear",
"x": 750,
"y": 520,
"wires": [
[]
]
},
{
"id": "05cf402c93866d4e",
"type": "ui-group",
"name": "Users",
"page": "fc203945d260d5ec",
"width": 6,
"height": 1,
"order": 1,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "a98ec11a60f808be",
"type": "ui-group",
"name": "Rooms",
"page": "fc203945d260d5ec",
"width": 6,
"height": 1,
"order": 2,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "fc203945d260d5ec",
"type": "ui-page",
"name": "OfficeSense",
"ui": "7d0f4d8165214cca",
"path": "/officesense",
"icon": "home",
"layout": "grid",
"theme": "f49a05b173b6cac3",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "3"
},
{
"name": "Tablet",
"px": "576",
"cols": "6"
},
{
"name": "Small Desktop",
"px": "768",
"cols": "9"
},
{
"name": "Desktop",
"px": "1024",
"cols": "12"
}
],
"order": 1,
"className": "",
"visible": "true",
"disabled": "false"
},
{
"id": "7d0f4d8165214cca",
"type": "ui-base",
"name": "My Dashboard",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": false,
"headerContent": "page",
"navigationStyle": "default",
"titleBarStyle": "default",
"showReconnectNotification": true,
"notificationDisplayTime": 1,
"showDisconnectNotification": true,
"allowInstall": false
},
{
"id": "f49a05b173b6cac3",
"type": "ui-theme",
"name": "Default Theme",
"colors": {
"surface": "#ffffff",
"primary": "#0094ce",
"bgPage": "#eeeeee",
"groupBg": "#ffffff",
"groupOutline": "#cccccc"
},
"sizes": {
"density": "default",
"pagePadding": "12px",
"groupGap": "12px",
"groupBorderRadius": "4px",
"widgetGap": "12px"
}
},
{
"id": "2372b8eb214a55ee",
"type": "global-config",
"env": [],
"modules": {
"@flowfuse/node-red-dashboard": "1.30.2"
}
}
]
+519 -3
View File
@@ -12,7 +12,9 @@
"@prisma/adapter-better-sqlite3": "^7.8.0", "@prisma/adapter-better-sqlite3": "^7.8.0",
"@prisma/client": "^7.8.0", "@prisma/client": "^7.8.0",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^5.2.1" "express": "^5.2.1",
"mqtt": "^5.15.1",
"redis": "^5.12.1"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",
@@ -23,6 +25,15 @@
"typescript": "^6.0.3" "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": { "node_modules/@electric-sql/pglite": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", "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": { "node_modules/@standard-schema/spec": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -944,7 +1027,6 @@
"version": "25.7.0", "version": "25.7.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz",
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.21.0" "undici-types": "~7.21.0"
@@ -975,6 +1057,15 @@
"csstype": "^3.2.2" "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": { "node_modules/@types/send": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
@@ -996,6 +1087,27 @@
"@types/node": "*" "@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": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -1121,6 +1233,18 @@
"url": "https://opencollective.com/express" "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": { "node_modules/buffer": {
"version": "5.7.1", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@@ -1145,6 +1269,12 @@
"ieee754": "^1.1.13" "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": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -1247,6 +1377,36 @@
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC" "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": { "node_modules/confbox": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
@@ -1581,6 +1741,24 @@
"node": ">= 0.6" "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": { "node_modules/expand-template": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -1670,6 +1848,19 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "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": { "node_modules/fast-uri": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
@@ -1919,6 +2110,12 @@
"node": ">= 0.4" "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": { "node_modules/hono": {
"version": "4.12.18", "version": "4.12.18",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz",
@@ -2004,6 +2201,15 @@
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC" "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": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -2043,6 +2249,16 @@
"jiti": "lib/jiti-cli.mjs" "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": { "node_modules/json-schema-traverse": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -2057,6 +2273,12 @@
"devOptional": true, "devOptional": true,
"license": "Apache-2.0" "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": { "node_modules/lru.min": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
@@ -2155,6 +2377,141 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT" "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": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2222,6 +2579,16 @@
"node": ">=10" "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": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "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": { "node_modules/proper-lockfile": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
@@ -2569,6 +2951,22 @@
"url": "https://paulmillr.com/funding/" "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": { "node_modules/remeda": {
"version": "2.33.4", "version": "2.33.4",
"resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz",
@@ -2609,6 +3007,12 @@
"node": ">= 4" "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": { "node_modules/router": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -2881,6 +3285,39 @@
"simple-concat": "^1.0.0" "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": { "node_modules/sqlstring": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
@@ -2962,6 +3399,12 @@
"node": ">=0.6" "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": { "node_modules/tsx": {
"version": "4.21.0", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
@@ -3025,6 +3468,12 @@
"url": "https://opencollective.com/express" "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": { "node_modules/typescript": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
@@ -3043,7 +3492,6 @@
"version": "7.21.0", "version": "7.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz",
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unpipe": { "node_modules/unpipe": {
@@ -3101,12 +3549,80 @@
"node": ">= 8" "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": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "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": { "node_modules/zeptomatch": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz",
+3 -1
View File
@@ -23,6 +23,8 @@
"@prisma/adapter-better-sqlite3": "^7.8.0", "@prisma/adapter-better-sqlite3": "^7.8.0",
"@prisma/client": "^7.8.0", "@prisma/client": "^7.8.0",
"dotenv": "^17.4.2", "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;
+6 -1
View File
@@ -19,4 +19,9 @@ model Tag {
id String @id @default(uuid()) id String @id @default(uuid())
userId String @unique userId String @unique
user User @relation(fields: [userId], references: [id]) 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"; import { prisma } from "../../lib/prisma.js";
export async function existsByUUID(uuid: string): Promise<boolean> { export async function existsByUUID(uuid: string): Promise<boolean> {
const user = await prisma.user.findUnique({ const tag = await prisma.tag.findUnique({
where: { id: uuid } where: { id: uuid }
}); });
return !!user; return !!tag;
} }
+57
View File
@@ -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;
+173
View File
@@ -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);
}
}
+27
View File
@@ -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);
}
}
+48
View File
@@ -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);
}
}
+94
View File
@@ -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;
}
}
+14 -7
View File
@@ -1,12 +1,19 @@
import express from "express"; import express from "express";
import lookupRoutes from "./api/lookup/lookup.routes.js"; 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(); (async () => {
const PORT = 80; const app = express();
const ADDRESS = "0.0.0.0";
app.use("/", lookupRoutes); app.use("/lookup", lookupRoutes);
app.use("/livedata", livedataRoutes);
app.listen(PORT, ADDRESS, () => { app.listen(config.api.port, config.api.address, () => {
console.log(`Server listening on ${ADDRESS}:${PORT}`); console.log(`Server listening on ${config.api.address}:${config.api.port}`);
}); });
bootstrap();
})()
+48
View File
@@ -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;
}
+11 -2
View File
@@ -128,7 +128,7 @@ void mqttTask(void *)
{ {
while (!mqtt.connected()) 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())) if (mqtt.connect(config.dev_name.c_str(), config.mqtt.username.c_str(), config.mqtt.password.c_str()))
Serial.println("MQTT connected."); Serial.println("MQTT connected.");
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));
@@ -139,7 +139,7 @@ void mqttTask(void *)
char payload[128]; char payload[128];
snprintf(payload, sizeof(payload), snprintf(payload, sizeof(payload),
"{\"uuid\":\"%s\",\"rssi\":%.2f}", "{\"tagID\":\"%s\",\"rssi\":%.2f}",
ev.uuid, ev.uuid,
ev.ema); ev.ema);
@@ -369,6 +369,14 @@ void handleCommand(String line)
else if (key == TAG_NAME_CONFIG_KEY) else if (key == TAG_NAME_CONFIG_KEY)
Serial.println(config.tag_name); Serial.println(config.tag_name);
} }
else if (line == "reset")
{
prefs.begin("config", false);
prefs.clear();
prefs.end();
esp_restart();
}
else if (line == "reboot") else if (line == "reboot")
{ {
Serial.println("Rebooting..."); Serial.println("Rebooting...");
@@ -381,6 +389,7 @@ void handleCommand(String line)
Serial.println(" set <key> <value> - Set a configuration value"); Serial.println(" set <key> <value> - Set a configuration value");
Serial.println(" show <key> - Show a configuration value"); Serial.println(" show <key> - Show a configuration value");
Serial.println(" save - Save configuration to non-volatile storage"); Serial.println(" save - Save configuration to non-volatile storage");
Serial.println(" reset - Reset configuration to defaults");
Serial.println(" reboot - Reboot the device"); Serial.println(" reboot - Reboot the device");
Serial.println(" help - Show this help message"); Serial.println(" help - Show this help message");
} }
+1 -1
View File
@@ -35,7 +35,7 @@
#define MQTT_TOPIC "scanners/" #define MQTT_TOPIC "scanners/"
#define WIFI_SSID "COSMOTE-489882" #define WIFI_SSID "COSMOTE-489882"
#define WIFI_PASS "x32hbh54673ngccdsfa9" #define WIFI_PASS "x32hbh54673ngccdsfa9"
#define DEV_NAME "Scanner_Room_A" #define DEV_NAME "572b29cb-6a93-480e-adf0-c5c44e9a58df"
struct Config struct Config
{ {
+8
View File
@@ -0,0 +1,8 @@
# dlib compile cache
.docker-cache/
# face recognition models
models/
# executable
output/
@@ -0,0 +1,55 @@
ARG BUILDPLATFORM=linux/amd64
FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS builder
# Install dlib deps
RUN dpkg --add-architecture arm64 && \
apt-get update && apt-get install -y \
g++-aarch64-linux-gnu \
libdlib-dev:arm64 \
libjpeg-dev:arm64 \
libblas-dev:arm64 \
libopenblas-dev:arm64 \
libatlas-base-dev:arm64 \
liblapack-dev:arm64 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Compiler wrapper for arm64
RUN printf '#!/bin/bash\nexec aarch64-linux-gnu-gcc "${@//-march=native/-march=armv8-a}"\n' \
> /usr/local/bin/arm64-gcc && \
printf '#!/bin/bash\nexec aarch64-linux-gnu-g++ "${@//-march=native/-march=armv8-a}"\n' \
> /usr/local/bin/arm64-g++ && \
chmod +x /usr/local/bin/arm64-gcc /usr/local/bin/arm64-g++
# Compiler variables for cross-compile to arm64
ENV CGO_ENABLED=1 \
GOOS=linux \
GOARCH=arm64 \
CC=/usr/local/bin/arm64-gcc \
CXX=/usr/local/bin/arm64-g++ \
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig
COPY go.mod go.sum ./
RUN go mod download
# Pre-compile all CGO deps into cache
RUN go build -v ./... 2>/dev/null || \
go list -m all && \
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \
go build -gcflags="-trimpath" -v \
$(go list -f '{{if .CgoFiles}}{{.ImportPath}}{{end}}' ./...) 2>/dev/null; exit 0
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
go mod download
COPY . .
# Build - reuse CGO cache
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
go build -o cameraSession
FROM scratch AS export
COPY --from=builder /app/cameraSession /cameraSession
@@ -0,0 +1,10 @@
all:
docker buildx build --platform linux/amd64 --cache-from type=local,src=./.docker-cache --cache-to type=local,dest=./.docker-cache --target export --output type=local,dest=./output .
models:
wget https://github.com/Kagami/go-face-testdata/raw/master/models/shape_predictor_5_face_landmarks.dat
wget https://github.com/Kagami/go-face-testdata/raw/master/models/dlib_face_recognition_resnet_model_v1.dat
wget https://github.com/Kagami/go-face-testdata/raw/master/models/mmod_human_face_detector.dat
clean:
rm -r output/
@@ -0,0 +1,163 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
"sync"
"github.com/Kagami/go-face"
"github.com/blackjack/webcam"
)
type InputPacket struct {
UUID string `json:"uuid"`
Descriptor face.Descriptor `json:"descriptor"`
}
type ResultPacket struct {
UUID string `json:"uuid"`
Verified bool `json:"verified"`
}
var (
rec *face.Recognizer
mtx sync.Mutex
results = make([]ResultPacket, 0)
)
func captureCameraFrame() ([]byte, error) {
cam, err := webcam.Open("/dev/video0")
if err != nil {
return nil, err
}
defer cam.Close()
format := webcam.PixelFormat(uint32(0x47504a4d)) // MJPG
_, _, _, err = cam.SetImageFormat(format, 1280, 720)
if err != nil {
return nil, fmt.Errorf("camera format error: %v", err)
}
err = cam.StartStreaming()
if err != nil {
return nil, err
}
defer cam.StopStreaming()
for {
err = cam.WaitForFrame(5)
if err != nil {
continue
}
frame, err := cam.ReadFrame()
if err != nil {
return nil, err
}
if len(frame) != 0 {
result := make([]byte, len(frame))
copy(result, frame)
return result, nil
}
}
}
// verfication thread
func verifyFace(packet InputPacket, descriptors []face.Descriptor, wg *sync.WaitGroup) {
defer wg.Done()
const threshold = 0.35
verified := false
for _, descriptor := range descriptors {
distance := face.SquaredEuclideanDistance(packet.Descriptor, descriptor)
if distance < threshold {
verified = true
break
}
}
mtx.Lock()
results = append(results, ResultPacket{
UUID: packet.UUID,
Verified: verified,
})
mtx.Unlock()
}
func main() {
var err error
// initialize recognizer
rec, err = face.NewRecognizer("./models")
if err != nil {
log.Fatalf("failed to initialize recognizer: %v", err)
}
defer rec.Close()
// capture image
cameraImage, err := captureCameraFrame()
if err != nil {
log.Fatalf("failed to read camera: %v", err)
}
// recognize faces from camera input
faces, err := rec.RecognizeCNN(cameraImage)
if err != nil {
log.Fatalf("recognition error: %v", err)
}
if len(faces) == 0 {
fmt.Println("no face found.")
return
}
var descriptors []face.Descriptor
// extract descriptors to array
for _, f := range faces {
descriptors = append(descriptors, f.Descriptor)
}
// read stdin
scanner := bufio.NewScanner(os.Stdin)
var wg sync.WaitGroup
for scanner.Scan() {
line := scanner.Bytes()
var packet InputPacket
err := json.Unmarshal(line, &packet)
if err != nil {
log.Printf("invalid json packet: %v", err)
continue
}
wg.Add(1)
go verifyFace(packet, descriptors, &wg)
}
if err := scanner.Err(); err != nil {
log.Printf("stdin error: %v", err)
}
// wait threads to finish
wg.Wait()
// print results to stdout
output, err := json.Marshal(results)
if err != nil {
log.Fatalf("failed to marshal results: %v", err)
}
fmt.Println(string(output))
}
@@ -0,0 +1,10 @@
module OfficeSense/cameraSession
go 1.26.3
require (
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e
github.com/blackjack/webcam v0.6.1
)
require golang.org/x/sys v0.14.0 // indirect
@@ -0,0 +1,6 @@
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e h1:lqIUFzxaqyYqUn4MhzAvSAh4wIte/iLNcIEWxpT/qbc=
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e/go.mod h1:9wdDJkRgo3SGTcFwbQ7elVIQhIr2bbBjecuY7VoqmPU=
github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE=
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -0,0 +1,55 @@
ARG BUILDPLATFORM=linux/amd64
FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS builder
# Install dlib deps
RUN dpkg --add-architecture arm64 && \
apt-get update && apt-get install -y \
g++-aarch64-linux-gnu \
libdlib-dev:arm64 \
libjpeg-dev:arm64 \
libblas-dev:arm64 \
libopenblas-dev:arm64 \
libatlas-base-dev:arm64 \
liblapack-dev:arm64 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Compiler wrapper for arm64
RUN printf '#!/bin/bash\nexec aarch64-linux-gnu-gcc "${@//-march=native/-march=armv8-a}"\n' \
> /usr/local/bin/arm64-gcc && \
printf '#!/bin/bash\nexec aarch64-linux-gnu-g++ "${@//-march=native/-march=armv8-a}"\n' \
> /usr/local/bin/arm64-g++ && \
chmod +x /usr/local/bin/arm64-gcc /usr/local/bin/arm64-g++
# Compiler variables for cross-compile to arm64
ENV CGO_ENABLED=1 \
GOOS=linux \
GOARCH=arm64 \
CC=/usr/local/bin/arm64-gcc \
CXX=/usr/local/bin/arm64-g++ \
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig
COPY go.mod go.sum ./
RUN go mod download
# Pre-compile all CGO deps into cache
RUN go build -v ./... 2>/dev/null || \
go list -m all && \
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \
go build -gcflags="-trimpath" -v \
$(go list -f '{{if .CgoFiles}}{{.ImportPath}}{{end}}' ./...) 2>/dev/null; exit 0
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
go mod download
COPY . .
# Build - reuse CGO cache
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
go build -o extractEmbeddings
FROM scratch AS export
COPY --from=builder /app/extractEmbeddings /extractEmbeddings
@@ -0,0 +1,10 @@
all:
docker buildx build --platform linux/amd64 --cache-from type=local,src=./.docker-cache --cache-to type=local,dest=./.docker-cache --target export --output type=local,dest=./output .
models:
wget https://github.com/Kagami/go-face-testdata/raw/master/models/shape_predictor_5_face_landmarks.dat
wget https://github.com/Kagami/go-face-testdata/raw/master/models/dlib_face_recognition_resnet_model_v1.dat
wget https://github.com/Kagami/go-face-testdata/raw/master/models/mmod_human_face_detector.dat
clean:
rm -r output/
@@ -0,0 +1,126 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"image"
"image/jpeg"
"io"
"os"
"github.com/Kagami/go-face"
"golang.org/x/image/draw"
)
type Result struct {
Success bool `json:"success"`
Descriptor face.Descriptor `json:"descriptor"`
Error string `json:"error"`
}
var res Result
func atExit() {
resString, err := json.Marshal(res)
if err != nil {
os.Exit(1)
}
fmt.Println(string(resString))
os.Exit(0)
}
func resizeIfNeeded(img image.Image, max int) image.Image {
b := img.Bounds()
w := b.Dx()
h := b.Dy()
if w <= max && h <= max {
return img
}
var scale float64
if w > h {
scale = float64(max) / float64(w)
} else {
scale = float64(max) / float64(h)
}
newW := int(float64(w) * scale)
newH := int(float64(h) * scale)
dst := image.NewRGBA(image.Rect(0, 0, newW, newH))
draw.BiLinear.Scale(dst, dst.Bounds(), img, b, draw.Over, nil)
return dst
}
func main() {
defer atExit()
// Read input image from stdin in Base64 encoding
input, err := io.ReadAll(os.Stdin)
if err != nil {
res.Success = false
res.Error = "[STDIN]: " + err.Error()
return
}
// Decode Base64
image, err := base64.StdEncoding.DecodeString(string(input))
if err != nil {
res.Success = false
res.Error = "[BASE64]: " + err.Error()
return
}
imageJPEG, err := jpeg.Decode(bytes.NewReader(image))
if err != nil {
res.Success = false
res.Error = "[JPEG][DECODE]: " + err.Error()
return
}
imageJPEG = resizeIfNeeded(imageJPEG, 600)
var buf bytes.Buffer
err = jpeg.Encode(&buf, imageJPEG, &jpeg.Options{Quality: 90})
if err != nil {
res.Success = false
res.Error = "[JPEG][ENCODE]: " + err.Error()
return
}
image = buf.Bytes()
// Create recognizer - provide ./models dir
rec, err := face.NewRecognizer("models")
if err != nil {
res.Success = false
res.Error = "[DLIB][INIT]: " + err.Error()
return
}
defer rec.Close()
faces, err := rec.RecognizeCNN(image)
if err != nil {
res.Success = false
res.Error = "[DLIB][CNN]: " + err.Error()
return
} else if len(faces) == 0 {
res.Success = false
res.Error = "[DLIB][CNN]: No face found in image."
return
} else if len(faces) != 1 {
res.Success = false
res.Error = "[DLIB][CNN]: Multiple faces found in image."
return
}
res.Success = true
res.Descriptor = faces[0].Descriptor
}
@@ -0,0 +1,8 @@
module OfficeSense/extractEmbeddings
go 1.26.3
require (
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e
golang.org/x/image v0.41.0
)
@@ -0,0 +1,4 @@
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e h1:lqIUFzxaqyYqUn4MhzAvSAh4wIte/iLNcIEWxpT/qbc=
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e/go.mod h1:9wdDJkRgo3SGTcFwbQ7elVIQhIr2bbBjecuY7VoqmPU=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=