diff --git a/.gitignore b/.gitignore index aa0926a..c6b9373 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules/ dist/ .env *.log +logs/ +public/snapshots/ \ No newline at end of file diff --git a/data/settings/callibration_cam0.npz b/data/settings/callibration_cam0.npz new file mode 100755 index 0000000..040851c Binary files /dev/null and b/data/settings/callibration_cam0.npz differ diff --git a/data/settings/callibration_cam0_720.npz b/data/settings/callibration_cam0_720.npz new file mode 100755 index 0000000..8d20672 Binary files /dev/null and b/data/settings/callibration_cam0_720.npz differ diff --git a/data/settings/callibration_cam1.npz b/data/settings/callibration_cam1.npz new file mode 100755 index 0000000..867ef58 Binary files /dev/null and b/data/settings/callibration_cam1.npz differ diff --git a/data/settings/callibration_cam1_720.npz b/data/settings/callibration_cam1_720.npz new file mode 100755 index 0000000..a85a40b Binary files /dev/null and b/data/settings/callibration_cam1_720.npz differ diff --git a/data/settings/camera_intrinsics_v0.npz b/data/settings/camera_intrinsics_v0.npz new file mode 100755 index 0000000..f894cc2 Binary files /dev/null and b/data/settings/camera_intrinsics_v0.npz differ diff --git a/data/settings/camera_intrinsics_v1.npz b/data/settings/camera_intrinsics_v1.npz new file mode 100755 index 0000000..9681556 Binary files /dev/null and b/data/settings/camera_intrinsics_v1.npz differ diff --git a/data/settings/settings.json b/data/settings/settings.json new file mode 100755 index 0000000..ada9ccc --- /dev/null +++ b/data/settings/settings.json @@ -0,0 +1,10 @@ +{ "coordinateSystem":{ + "MarkersUsed":"DICT_4X4_250", + "KnownMarkers": + { + "50": [0.0, 0.0, 0.0], + "71": [0.140, 0.0, 0.0], + "101": [0.0, -0.080, 0.0] + } + } +} diff --git a/data/settings/settingsBoard.json b/data/settings/settingsBoard.json new file mode 100755 index 0000000..89ea18b --- /dev/null +++ b/data/settings/settingsBoard.json @@ -0,0 +1,10 @@ +{ "coordinateSystem":{ + "MarkersUsed":"DICT_4X4_250", + "KnownMarkers": + { + "50": [0.0, 0.0, 0.0], + "71": [0.140, 0.0, 0.0], + "101": [0.0, -0.080, 0.0] + } + } +} diff --git a/data/settings/settingsMachine.json b/data/settings/settingsMachine.json new file mode 100755 index 0000000..46d24c7 --- /dev/null +++ b/data/settings/settingsMachine.json @@ -0,0 +1,10 @@ +{ "coordinateSystem":{ + "MarkersUsed":"DICT_4X4_250", + "KnownMarkers": + { + "58": [0.0, 0.0, 0.0], + "65": [0.161, 0.0, 0.0], + "75": [0.0, -0.070, 0.0] + } + } +} diff --git a/gitPush.sh b/gitPush.sh new file mode 100755 index 0000000..d7d60f4 --- /dev/null +++ b/gitPush.sh @@ -0,0 +1,4 @@ + +git add -A +git commit -m "Committed and Pushed by ButtonClick" +git push \ No newline at end of file diff --git a/https/server.crt b/https/server.crt new file mode 100755 index 0000000..0fb61c6 --- /dev/null +++ b/https/server.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUfD0V6IOHq6iL+tCtV3CMQ9w6uqQwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTExNTA4NDMzOFoXDTI2MTEx +NTA4NDMzOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEArK1NqQEZBpXDgfOTNFrIcTf0qmsE8yfpqahytAdD/wL3 +CE7YKPs5Bsq77YiPPC1Svc8Q8APETRjb3uPet+xfHtyCZX+cu8Dc45AA8sV1bAco +JDFH5x4oi9fnUBEMoxJewIrR6iGvPLN3OFzvq0QG65LTB7HEozYOeTBx2LNEzYWT ++iIu3Tj/iya4EWqsUKWv4LAdHxNfIyTScjYq98/thy9jbesVq2e62gV+q3Km3bqJ +Q+7NWwXM3sHvSXVU/+yqWxwMxiMO6t8QXFVP71ti6IdOdWlSOds5tTa1X0O6wdje +VKN+JXfI3M+Cq7fMLnOKyrm+olcQ9RHcrNHrN6LYrK/yXwW0XTdt0RDsbFE5VH8x +X887zS1Xj0q4IPXutm/Z5uHAGCchg5BDW+w08fxo3pdCIPVy6CVrpabR5lJ4cNPE +lzOAhCOAZYvFyATtJXOPw0CW5mVUDl2BTjPrijf/2YA0Vh+9j4SvwdPR7W2gXdPL +zsr04LwQmFLIEUKgpPlW28K2gun6H7vDshue+jJ9iuCF3BcC9MZI1hFi1omHaJ8T +ehX00Q5HTOhVZFdxzkGGCUbX7B7umKgbLXbItGU3Cnw2fdgi3+1zZbVSWAm/ixiQ +do/Iaw3CrO9pAgmy813hT95qnBKxmuXBeZ+pf1XlhvOAgJw+uhdWOdjGjyJPjIMC +AwEAAaNTMFEwHQYDVR0OBBYEFH0WYvzhiYjoWDpkmMdx0uk3I/NAMB8GA1UdIwQY +MBaAFH0WYvzhiYjoWDpkmMdx0uk3I/NAMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAHC96KCq7u7WR2t6crbg+MjFuvq8lOGvYdxea4pw8aCK4T31 +KPSVU/4NzeIGHfjAvmghY8/J47wt15rI9nxGFyP0JBD+waSKo+Ruq1hOCSR1Qf3C +4GgUS64Fyj6uXrusszYsa8q50ZXitAYM6o4Nkd5PqQIouvwoHcAABSWBAdxxzXNO +6t+GGElYaajZ8Zv+pVKLdiRJny0A9PSSe3pPd/loqYecr8nX3Be2i+C6lgPFpYY4 +wpswmbGjt7oQgt2UCSr4Tz1tsfeZ94as3HYUDL5W1eIAQKUdtltBRNdC2DT53MS6 +9b2lgCDUNLXKtBe6naIpDRb/jWFXzPTUwEDyXJN6ORJFdAkYIBIoQKzGg+l9NEsO +lYyQB4bMoLwZ4a1B5R8PtUtWxtDcHAegdSIK/9fVz1/QcjYYr3/42NGF6Nr7kpGx +2QCj0z84hJbw/QEGBsg/yFvlOLRE83LTIkjoA4hPo3HRHPuy9/frc3bFk99LnX8+ +BKWbVb7J2eyCXX3LAcUb9RU8x4UvYqpjqjA6BKpryyEsRyhgZh/sVHvjwoW/ifFG +JAqQoYe+TCAPf7s/rsxjB6Y5NKDesFYqwT4qu02+xCmy6LjZc1lyyrzJJCAZ/bjN +ATomtrbmCgMho7FvIT7D2c+VRUBb2NSO64egUvxgXBYho8wbQOW6xkAuf/Ft +-----END CERTIFICATE----- diff --git a/https/server.key b/https/server.key new file mode 100755 index 0000000..1e18749 --- /dev/null +++ b/https/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCsrU2pARkGlcOB +85M0WshxN/SqawTzJ+mpqHK0B0P/AvcITtgo+zkGyrvtiI88LVK9zxDwA8RNGNve +49637F8e3IJlf5y7wNzjkADyxXVsBygkMUfnHiiL1+dQEQyjEl7AitHqIa88s3c4 +XO+rRAbrktMHscSjNg55MHHYs0TNhZP6Ii7dOP+LJrgRaqxQpa/gsB0fE18jJNJy +Nir3z+2HL2Nt6xWrZ7raBX6rcqbduolD7s1bBczewe9JdVT/7KpbHAzGIw7q3xBc +VU/vW2Loh051aVI52zm1NrVfQ7rB2N5Uo34ld8jcz4Krt8wuc4rKub6iVxD1Edys +0es3otisr/JfBbRdN23REOxsUTlUfzFfzzvNLVePSrgg9e62b9nm4cAYJyGDkENb +7DTx/Gjel0Ig9XLoJWulptHmUnhw08SXM4CEI4Bli8XIBO0lc4/DQJbmZVQOXYFO +M+uKN//ZgDRWH72PhK/B09HtbaBd08vOyvTgvBCYUsgRQqCk+VbbwraC6fofu8Oy +G576Mn2K4IXcFwL0xkjWEWLWiYdonxN6FfTRDkdM6FVkV3HOQYYJRtfsHu6YqBst +dsi0ZTcKfDZ92CLf7XNltVJYCb+LGJB2j8hrDcKs72kCCbLzXeFP3mqcErGa5cF5 +n6l/VeWG84CAnD66F1Y52MaPIk+MgwIDAQABAoICACrjqMRvh13BWRwv+cIpQlwM +v/KMPmB+62S+eC8LFvKCNAWWP85+B74OIPtwzdLulvyaL+TNqrZTlFkaVDlutnmd +362CMtXXo4XKQNIMBYxdgrTqoKdhMET7zXJvqanfaDV+xYDX+QSkttoDC2yIqwdc +IppopyS6yoGDbOOcM1yw7v5T+zvral2PsmxxCHfHj5XJaJJNZ3X9MWA44BdJSAAp +3xJwL6OxUhHRB5NiGNw99tYuvEb9e9NgbCTcxc4DZJTYtLuJ/ayM1y136zDSjBCh +evViwY+pSf4eppIQ6pQ3X5G6QhzgUb22tQgd0NNA05pi5FzCL24Pj7ZFNZ1OD278 +mjozzNirn8plbgQ+OGCs5NHprjdwQNW+Q6sE34Yh8FBlF0JfNmk3uaKTetIwSsJF +laMHXR5OtDoX2cWqzjHijTDiUC5mk5ph8/lhOdY3Z1Ax1w52qKEFNOIC9gZrTgFE +2oyp/Nv0tdNUBuhMKOLvKF0xwCDP/2iWeYow0j7iclbpotK5oRKWjbIQqstKAAs7 +vRW6n5CTPUIyzE1oLglp7DbYy++xAUpLBjPYzZn5aKb680FSmJtbBF3CZDupfKus +b3bUWeVmY6TIakSRYMNXK9BWz1fXQinCoJx/ASv0yxpMsrEr/9NFI2f+gkWlD4hu +HX2DwIzGcpghYnUvngXxAoIBAQDTn5Ci652mxBEBkKgfX7QXp9oBdebmisJWeYca +qxVEiXfqoyfEKW+59Y+IZIANKA3e5R7HJofshKKrXJCLRTi7wSm+4oo4Oo/6TDDa +5Qmsxx9WkTJkBm0E/9nDI3e8q53LwiLOvXel/WZAYWXrIKsQJMgnDjFqdes5efG9 +k8jnZ8tf7TtaL7+B+ku5fLagWaLUltg3WS9GjdrS7ZMhkZDJ1ZqqVEQQYYgg756r +ksIim7jQL2dcW8wayB0g9SV7koIoIy/06lp7RX9HrMfhIH5Xorek44ZH2nCDPwtk +s2prjYIzneCt2+fibki/rj2riPlBXTTkf3z2SNe2tBlpx3etAoIBAQDQ4wIAKTVy +BSddk6oVqHNf3tfjub2pp28UXij+K0TMeZHDIDf3oy2AQ+LEatsA0h8g2m93TLIR +KxCBi/Us5DJzrvPNHEhB/XHWEcXinPSI0+guehOIqXKz6TtzA5+XuZE+r+NqhEL/ +2iVSK3va7WweeD/CC1G+HYN0zSdAaW6RzfQcDS6UmykoEu2wyRgO5TNBXj45eVh/ +/BFKv527oqetpchxhzEJ1ZzT+J1lJBtkKoE5AXb8Wsf3O5ZYwG8F8hQfLybzeIh2 +CBskuJO37xZxPSRgcNn5nd0rPPGWKEiX4cRsSTob8Pse5ifBNzxkS8v6s8yIIBt1 +S26WyWp1lFrvAoIBABoF5ihSpvlJ5Pl3S2VIRIIgLuu9Dt7Ms2ck3JtH7H6YFPny +hEJYAhgw/Sx9h02W3lXJgQZmU5KfIM3HvTKTGY3lC/ggLXUKpofV9LAGODFZ7x2b +D0JDlAZoW+PmKaQ2ylmzDsqze9IangdOstS+GKsMitxan3MC+yD/QN3aHXtvRRAP +wRuvAXK/T66IioCfZSmVPxNXUTvw17bWZiBboR1gufs2D4SgKbg7HxzkGCFfWtOm +8KPn2ep1LzfNTYWrl3vOD+ijJOtBuYwb0Bx7/W1TYhfRrsKJNwq8pu8ELRL6vMcS +I/3dK9+pRiLkD0tXtab3CjkLAFfcz0H1VaavU6UCggEABty+TxULfXBv56IXP1jm +WWrvurp6YZ1vh8LEI/116CXCRR/E1uzUbNdOFtfP1AoTHbgvW0L4wpmglDDt4Air +I6PGvKFGOmzCFZ9F2fkAC5KymPxHsgCnFQP/gPrIfmqJO/75QKGRtegLu9RT4FBW +cfXPWmeWyuEbVXX76SDNkhqq/1Trh9RFGNzuVBV3Jd4fvfEDqE21O5cjVkpPOz/P +tGOy3w/q64DKAyiyuwThpXvD/QRwTUAKO7QIb2f6/b4DLTcWV39JNF91zNIHgE5p +dVTl4gkzEAFAp8/7u8wc/mhbVJdfQlW7WjuDaNSQtlbLH7RSbtJnNIZC3s9FIRG7 +nwKCAQBEJnM0+kFJurE8wRWcHN/oHHyQCrMuFptV+9o1+Ncy3B5+kdo2Pqy0AXDY ++OZucTdkVqIxb0goM1FluP0os3oVq9w5wxZH2tksIEehWPPj3BaWkMVFQMy8vV+O +tn4xeOPK4V89voj8BuLsn2s5t6tEa9vvKuiOTPrnTalfoBYKxYANrOo24MLrCzlS +H++cDZD7ug/cBCSNENQsZDKNMekPRJFd3SYAe8VfM8UCfRMZbX9jQZn7dsSdLf+W +KX28dhZ8WnSBpJE4IVBzAPfiya9wN5+kKxh/AxD3cq3O6UXNA9FTbTxrDTYvHgAM +hwXVuAeFYYyuRFN8xJ9SkIPdpr1Y +-----END PRIVATE KEY----- diff --git a/package-lock.json b/package-lock.json new file mode 100755 index 0000000..53f363e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1192 @@ +{ + "name": "video-streamer", + "version": "1.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "video-streamer", + "version": "1.1.0", + "dependencies": { + "compression": "^1.7.4", + "express": "^4.21.1", + "helmet": "^7.1.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "nodemon": "^3.1.7" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..a4116ed --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "video-streamer", + "version": "1.1.0", + "description": "HTTPS + WSS dual-camera MJPEG streamer (modular)", + "main": "server.js", + "type": "commonjs", + "scripts": { + "start": "node server.js", + "dev": "PORT=8443 nodemon server.js" + }, + "dependencies": { + "compression": "^1.7.4", + "express": "^4.21.1", + "helmet": "^7.1.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "nodemon": "^3.1.7" + } +} \ No newline at end of file diff --git a/programs/driver.js b/programs/driver.js new file mode 100755 index 0000000..0ffdbed --- /dev/null +++ b/programs/driver.js @@ -0,0 +1,71 @@ +const WebSocket = require("ws"); + +/** + * Forwards WebSocket messages between browser clients (/robot) + * and a target WebSocket server (behind a firewall). + * + * @param {WebSocket.Server} wssInput - Local WebSocket server for browser clients + * @param {string} targetUrl - URL of target WebSocket server, e.g. "wss://internal.local:8080" + */ +function setupCommandForwarding(wssInput, targetUrl) { + let targetSocket; + const clients = new Set(); + + function connectTarget() { + console.log(`🔌 Connecting to target server: ${targetUrl}`); + targetSocket = new WebSocket(targetUrl); + + targetSocket.on("open", () => { + console.log("✅ Connected to target server"); + }); + + targetSocket.on("message", (msg) => { + const data = msg.toString(); + console.log("⬅️ Message from target:", data); + // Broadcast to all connected browsers + for (const client of clients) { + if (client.readyState === WebSocket.OPEN) { + client.send(data); + } + } + }); + + targetSocket.on("close", () => { + console.warn("⚠️ Target connection closed. Reconnecting in 5s..."); + setTimeout(connectTarget, 5000); + }); + + targetSocket.on("error", (err) => { + console.error("❌ Target connection error:", err.message); + }); + } + + connectTarget(); + + // When a browser connects to /robot + wssInput.on("connection", (ws, req) => { + console.log("🤖 Browser connected:", req.socket.remoteAddress); + clients.add(ws); + + ws.on("message", (msg) => { + const data = msg.toString(); + console.log("➡️ From browser → target:", data); + if (targetSocket?.readyState === WebSocket.OPEN) { + targetSocket.send(data); + } else { + console.warn("⚠️ Target not connected. Message dropped."); + } + }); + + ws.on("close", () => { + clients.delete(ws); + console.log("🔌 Browser disconnected"); + }); + + ws.on("error", (err) => { + console.error("❌ Browser socket error:", err.message); + }); + }); +} + +module.exports = { setupCommandForwarding }; \ No newline at end of file diff --git a/programs/input.js b/programs/input.js new file mode 100755 index 0000000..a8a3254 --- /dev/null +++ b/programs/input.js @@ -0,0 +1,49 @@ +// programs/input.js +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +function byIdCaptureCandidates() { + const dir = '/dev/v4l/by-id'; + try { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir) + .filter(n => n.endsWith('-index0')) + .map(n => fs.realpathSync(path.join(dir, n))); + } catch { + return []; + } +} + +function naiveVideoNodes() { + try { + return fs.readdirSync('/dev') + .filter(n => /^video\d+$/.test(n)) + .map(n => path.join('/dev', n)) + .sort((a, b) => Number(a.replace(/\D/g, '')) - Number(b.replace(/\D/g, ''))); + } catch { + return ['/dev/video0', '/dev/video2']; + } +} + +function pickDevices(env = process.env) { + const DEV0 = env.DEV0 || null; + const DEV1 = env.DEV1 || null; + + if (DEV0 && DEV1) return [DEV0, DEV1]; + + const byId = byIdCaptureCandidates(); + if (DEV0 || DEV1) { + const pool = byId.length ? byId : naiveVideoNodes(); + const d0 = DEV0 || pool[0]; + const d1 = DEV1 || pool.find(d => d !== d0) || pool[1]; + return [d0, d1]; + } + + if (byId.length >= 2) return [byId[0], byId[1]]; + const naive = naiveVideoNodes(); + return [naive[0], naive[1]]; +} + +module.exports = { pickDevices }; \ No newline at end of file diff --git a/programs/log.js b/programs/log.js new file mode 100755 index 0000000..bde9330 --- /dev/null +++ b/programs/log.js @@ -0,0 +1,165 @@ +// programs/log.js +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +// --- configuration --- +const LOG_DIR = path.join(__dirname, '..', 'logs'); +fs.mkdirSync(LOG_DIR, { recursive: true }); + +function getLogFilePath(d = new Date()) { + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + return path.join(LOG_DIR, `${yyyy}_${mm}_${dd}.txt`); +} + +function write(obj) { + const line = JSON.stringify(obj) + '\n'; + fs.appendFile(getLogFilePath(), line, (err) => { + if (err) console.error('[log] write error:', err); + }); +} + +// --- common extractors --- +function commonFromReq(req) { + try { + const xff = req?.headers?.['x-forwarded-for']; + const xRealIp = req?.headers?.['x-real-ip']; + const ipFromXff = xff ? xff.split(',')[0].trim() : null; + const ip = + ipFromXff || + xRealIp || + req?.ip || + req?.socket?.remoteAddress || + null; + + const tls = + req?.socket?.encrypted + ? { + protocol: + typeof req.socket.getProtocol === 'function' + ? req.socket.getProtocol() + : null, + cipher: + typeof req.socket.getCipher === 'function' + ? (req.socket.getCipher() || {}).name + : null, + } + : null; + + // MAC is not available across routed networks + const mac = null; + + return { + ip, + ips: Array.isArray(req?.ips) ? req.ips : [], + xff: xff || null, + remoteAddress: req?.socket?.remoteAddress || null, + remoteFamily: req?.socket?.remoteFamily || null, + userAgent: req?.headers?.['user-agent'] || null, + acceptLanguage: req?.headers?.['accept-language'] || null, + secChUa: req?.headers?.['sec-ch-ua'] || null, + secChUaPlatform: req?.headers?.['sec-ch-ua-platform'] || null, + secChUaMobile: req?.headers?.['sec-ch-ua-mobile'] || null, + referer: req?.headers?.['referer'] || null, + tls, + mac, + }; + } catch { + return {}; + } +} + +function commonFromSocket(socket) { + return { + remoteAddress: socket?.remoteAddress || null, + remoteFamily: socket?.remoteFamily || null, + }; +} + +// --- specific log functions --- +function logHttpRequest(req) { + write({ + ts: new Date().toISOString(), + type: 'http', + method: req?.method || null, + url: (req?.originalUrl ?? req?.url) || null, + ...commonFromReq(req), + }); +} + +function logTcpConnection(socket) { + write({ + ts: new Date().toISOString(), + type: 'tcp', + ...commonFromSocket(socket), + }); +} + +function logHttpUpgrade(req) { + write({ + ts: new Date().toISOString(), + type: 'http-upgrade', + url: req?.url || null, + ...commonFromReq(req), + }); +} + +function logWssConnected(req) { + write({ + ts: new Date().toISOString(), + type: 'wss', + url: req?.url || null, + ...commonFromReq(req), + }); +} + +function logWssClosed(req, code, reason) { + write({ + ts: new Date().toISOString(), + type: 'wss-close', + url: req?.url || null, + code: typeof code === 'number' ? code : null, + reason: reason ? reason.toString() : null, + ...commonFromReq(req), + }); +} + +function logSnapshot(python, response){ + write({ + ts: new Date().toISOString(), + type: 'snapshot', + command: python.toString(), + wsResponse: response.toString() + }) +} + +// --- generic hooks you requested --- +function connected(context = {}) { + write({ + ts: new Date().toISOString(), + type: 'connected', + ...context, + }); +} + +function connectionLost(context = {}) { + write({ + ts: new Date().toISOString(), + type: 'connection-lost', + ...context, + }); +} + +module.exports = { + logHttpRequest, + logTcpConnection, + logHttpUpgrade, + logWssConnected, + logSnapshot, + logWssClosed, + connected, + connectionLost, +}; diff --git a/programs/readCamPos.py b/programs/readCamPos.py new file mode 100755 index 0000000..1cb1371 --- /dev/null +++ b/programs/readCamPos.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python3 +""" +ArUco detection with multi-marker machine-frame fit + camera pose output (OpenCV >= 4.8). + +- Reads: webCam_1.jpg +- Detects DICT_4X4_250 markers (ids expected: 0, 5, 10, 15) +- Uses multiple reference markers with known machine coordinates to fit camera->machine transform +- Reports positions/orientations of all markers **and the camera** in machine coordinates +- Draws detected markers, per-marker axes, and the machine axes +- Saves: webCam_1a.jpg (annotated) and marker_poses_machine.csv (poses incl. camera) + + +Usage: + python3 readCamPos.py -i snapshot_video1_1764493534200.jpg -npz camera_intrinsics_v0.npz -setting settings.json + + +""" +import faulthandler +faulthandler.enable() + +import argparse +import os +import sys +import csv +import json +import time +from typing import Tuple, Dict, List +import numpy as np +import cv2 + + + +# ----------------------- Configuration Defaults ----------------------- +IMAGE_PATH = "default.jpg" +OUTPUT_IMAGE_PATH = "default.jpg" +OUTPUT_CSV_PATH = "default.csv" +OUTPUT_JSON_PATH = "default.json" + +# Marker side length in meters (25 mm) +MARKER_LENGTH_M = 0.025 + +# Axis lengths for visualization (in meters) +AXIS_LENGTH_M = 0.05 # per-marker axis +MACHINE_AXIS_X_M = 0.200 # 200 mm along +X +MACHINE_AXIS_Y_M = -0.100 # -100 mm along Y (towards camera per description) +MACHINE_AXIS_Z_M = 0.100 # +Z visualized as 100 mm + +# Known machine coordinates for reference markers (meters) +cam_anchor_pts = {} + +EXPECTED_IDS = {50, 71, 101} + +# ----------------------- Utilities ----------------------- + +def load_intrinsics_npz(npz_path: str) -> Tuple[np.ndarray, np.ndarray]: + if os.path.exists(npz_path): + + print("NPZ from File:", npz_path) + data = np.load(npz_path) + for k in ('camera_matrix', 'mtx', 'K'): + if k in data: + camera_matrix = data[k].astype(np.float32) + break + else: + raise KeyError("Camera matrix not found.") + for k in ('dist_coeffs', 'dist', 'D'): + if k in data: + dist = data[k].astype(np.float32).reshape(-1, 1) + break + else: + dist = np.zeros((5, 1), dtype=np.float32) + return camera_matrix, dist + + camera_matrix = np.array([[1400, 0, 640], + [0, 1400, 360], + [0, 0, 1]], dtype=np.float32) + + dist_coeffs = np.zeros((5, 1), dtype=np.float32) + + print("[WARN] Using default approximate intrinsics.") + return camera_matrix, dist_coeffs + + +def rvec_to_R(rvec: np.ndarray) -> np.ndarray: + R, _ = cv2.Rodrigues(rvec) + return R + + +def R_to_euler_zyx(R: np.ndarray) -> Tuple[float, float, float]: + yaw = float(np.degrees(np.arctan2(R[1,0], R[0,0]))) + sp = np.sqrt(R[2,1]**2 + R[2,2]**2) + pitch = float(np.degrees(np.arctan2(-R[2,0], sp))) + roll = float(np.degrees(np.arctan2(R[2,1], R[2,2]))) + return roll, pitch, yaw + + +def corners_to_image_points(corners: np.ndarray) -> np.ndarray: + return corners.reshape(4, 2).astype(np.float32) + + +def get_detector(): + dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_250) + try: + params = cv2.aruco.DetectorParameters() + except Exception: + params = cv2.aruco.DetectorParameters_create() + try: + detector = cv2.aruco.ArucoDetector(dictionary, params) + return detector, None + except Exception: + return None, (dictionary, params) + + +def detect_markers(image: np.ndarray, detector_tuple): + detector, fallback = detector_tuple + print(detector) + if detector is not None: + corners, ids, rejected = detector.detectMarkers(image) + else: + dictionary, params = fallback + corners, ids, rejected = cv2.aruco.detectMarkers(image, dictionary, parameters=params) + return corners, ids, rejected + + +def rigid_transform_no_scale(A: np.ndarray, B: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + assert A.shape == B.shape and A.shape[1] == 3, "A and B must be Nx3" + N = A.shape[0] + if N < 2: + raise ValueError("Need at least 2 points; 3+ recommended.") + centroid_A = A.mean(axis=0) + centroid_B = B.mean(axis=0) + AA = A - centroid_A + BB = B - centroid_B + H = AA.T @ BB + U, S, Vt = np.linalg.svd(H) + R = Vt.T @ U.T + if np.linalg.det(R) < 0: + Vt[-1, :] *= -1 + R = Vt.T @ U.T + t = centroid_B - R @ centroid_A + return R.astype(np.float32), t.astype(np.float32) + +def readSettings(fileSetting): + global cam_anchor_pts + print("Read Settings") + + if(fileSetting == None): + cam_anchor_pts = { + 50: np.array([0.0, 0.0, 0.0], dtype=np.float32), + 71: np.array([0.140, 0.0, 0.0], dtype=np.float32), + 101: np.array([-0.0, -0.080, 0.0], dtype=np.float32), + #15: np.array([20,20,20]) # add if known + } + return + + + with open(fileSetting, 'r') as f: + settings = json.load(f) + for marker_id, coords in settings['coordinateSystem']['KnownMarkers'].items(): + cam_anchor_pts[int(marker_id)] = np.array(coords, dtype=np.float32) + + #KNOWN_MACHINE_POS = {int(k): np.array(v, dtype=np.float32) for k, v in settings.items()} + + +# ----------------------- Main ----------------------- + +def main(): + + parser = argparse.ArgumentParser(description="Detect ArUco markers in two images and compute camera poses in machine coordinates.") + parser.add_argument('-i', '--images', action='append', required=False, + help="Path to image. Provide this option twice: once per camera (e.g., -i2 cam1.jpg -i2 cam2.jpg)") + parser.add_argument('-npz', '--npz', action='append', required=False, default=['camera_intrinsics_v1.npz']) + parser.add_argument('--cam-calib', action='append', required=False, + help="Paths to calibration YAMLs for camera 1 and camera 2 (e.g., cam1.npz cam2.npz)") + parser.add_argument('--marker-size-mm', type=float, default=25, + help="Marker side length in millimeters (e.g., 50)") + parser.add_argument('--dict', default='DICT_4X4_250', + help="ArUco dictionary name (default: DICT_4X4_250)") + parser.add_argument('-settings', type=str, default=None, + help="Json File with Machine Settings") + args = parser.parse_args() + + + + print("ABC 0") + + readSettings(args.settings) + + print("ABC 0") + + if(args.images is None): + image = cv2.imread(IMAGE_PATH) + else: + image = cv2.imread(args.images[0]) + OUTPUT_IMAGE_PATH = args.images[0].replace(".jpg","r.jpg").replace(".PNG","r.PNG") + OUTPUT_CSV_PATH = args.images[0].replace(".jpg",".csv").replace(".PNG",".csv") + OUTPUT_JSON_PATH = args.images[0].replace(".jpg",".json").replace(".PNG",".json") + + + if image is None: + print(f"[ERROR] Cannot read image '{IMAGE_PATH}'.") + sys.exit(1) + + + print("ABC 1") + + camera_matrix, dist_coeffs = load_intrinsics_npz(args.npz[0]) + print("ABC 1a") + detector_tuple = get_detector() + print("ABC 1b") + corners_list, ids, rejected = detect_markers(image, detector_tuple) + + + print("ABC 2") + + if ids is None or len(ids) == 0: + print("[ERROR] No markers detected.") + sys.exit(1) + + draw_img = image.copy() + cv2.aruco.drawDetectedMarkers(draw_img, corners_list, ids) + + half = MARKER_LENGTH_M / 2.0 + obj_points = np.array([ + [-half, half, 0.0], + [ half, half, 0.0], + [ half, -half, 0.0], + [-half, -half, 0.0], + ], dtype=np.float32) + + poses_cam: Dict[int, Tuple[np.ndarray, np.ndarray]] = {} + centers_cam: Dict[int, np.ndarray] = {} + for i, marker_id in enumerate(ids.flatten()): + img_pts = corners_to_image_points(corners_list[i]) + success, rvec, tvec = cv2.solvePnP(obj_points, img_pts, camera_matrix, dist_coeffs, flags=cv2.SOLVEPNP_IPPE_SQUARE) + if not success: + success, rvec, tvec = cv2.solvePnP(obj_points, img_pts, camera_matrix, dist_coeffs) + if success: + rvec = rvec.reshape(3,1) + tvec = tvec.reshape(3,1) + poses_cam[int(marker_id)] = (rvec, tvec) + centers_cam[int(marker_id)] = tvec.flatten() + try: + cv2.drawFrameAxes(draw_img, camera_matrix, dist_coeffs, rvec, tvec, AXIS_LENGTH_M) + except Exception: + pass + else: + print(f"[WARN] solvePnP failed for marker {marker_id}") + + common_ids: List[int] = [mid for mid in cam_anchor_pts.keys() if mid in centers_cam] + if len(common_ids) < 2: + print(f"[ERROR] Need at least 2 reference markers; found {len(common_ids)}: {common_ids}") + sys.exit(1) + if len(common_ids) < 3: + print(f"[WARN] Only {len(common_ids)} references ({common_ids}). Fit may be less stable; 3+ recommended.") + + A = np.stack([centers_cam[mid] for mid in common_ids], axis=0) + B = np.stack([cam_anchor_pts[mid] for mid in common_ids], axis=0) + + R_cam_to_machine, t_cam_to_machine = rigid_transform_no_scale(A, B) + + residuals_mm = [] + for i, mid in enumerate(common_ids): + pred = R_cam_to_machine @ A[i] + t_cam_to_machine + err = np.linalg.norm(pred - B[i]) * 1000.0 + residuals_mm.append(err) + rms = float(np.sqrt(np.mean(np.square(residuals_mm)))) if residuals_mm else 0.0 + print("\nReference fit residuals (mm) per marker:") + for mid, e in zip(common_ids, residuals_mm): + print(f" ID {mid}: {e:.2f} mm") + print(f"RMS residual: {rms:.2f} mm") + + # Camera pose in machine coordinates: + # Camera origin (0,0,0 in camera) maps to t_cam_to_machine + cam_pos_machine = t_cam_to_machine + cam_R_machine = R_cam_to_machine # camera basis expressed in machine frame + cam_roll, cam_pitch, cam_yaw = R_to_euler_zyx(cam_R_machine) + + rows = [("id", "x_mm", "y_mm", "z_mm", "roll_deg", "pitch_deg", "yaw_deg")] + marker_list = [] + + print("\nMarker Positions and Orientations in Machine Coordinates:") + print(f"{'ID':>8} {'X(mm)':>10} {'Y(mm)':>10} {'Z(mm)':>10} {'Roll':>10} {'Pitch':>10} {'Yaw':>10}") + + # Add camera first + cx, cy, cz = (cam_pos_machine * 1000.0).tolist() + print(f"{'camera':>8} {cx:10.2f} {cy:10.2f} {cz:10.2f} {cam_roll:10.2f} {cam_pitch:10.2f} {cam_yaw:10.2f}") + rows.append(("camera", f"{cx:.3f}", f"{cy:.3f}", f"{cz:.3f}", f"{cam_roll:.3f}", f"{cam_pitch:.3f}", f"{cam_yaw:.3f}")) + camera_pose = { + "id": "camera", + "position_mm": [float(x) for x in cam_pos_machine * 1000.0], + "orientation_deg": {"roll": cam_roll, "pitch": cam_pitch, "yaw": cam_yaw} +} + + # Then markers + for marker_id in sorted(poses_cam.keys()): + rvec, tvec = poses_cam[marker_id] + R_marker_cam = rvec_to_R(rvec) + pos_machine = R_cam_to_machine @ tvec.flatten() + t_cam_to_machine + R_marker_machine = R_cam_to_machine @ R_marker_cam + roll_deg, pitch_deg, yaw_deg = R_to_euler_zyx(R_marker_machine) + x_mm, y_mm, z_mm = (pos_machine * 1000.0).tolist() + print(f"{marker_id:8d} {x_mm:10.2f} {y_mm:10.2f} {z_mm:10.2f} {roll_deg:10.2f} {pitch_deg:10.2f} {yaw_deg:10.2f}") + rows.append((marker_id, f"{x_mm:.3f}", f"{y_mm:.3f}", f"{z_mm:.3f}", f"{roll_deg:.3f}", f"{pitch_deg:.3f}", f"{yaw_deg:.3f}")) + marker_list.append({"id": marker_id, "position_mm": [x_mm, y_mm, z_mm], "orientation_deg": {"roll": roll_deg, "pitch": pitch_deg, "yaw": yaw_deg}}) + + # Save CSV + try: + with open(OUTPUT_CSV_PATH, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerows(rows) + print(f"\n[INFO] Saved CSV poses to '{OUTPUT_CSV_PATH}'.") + except Exception as e: + print(f"[WARN] Could not save CSV: {e}") + + + # Save JSON + json_data = { + "metadata": { + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + "reference_markers": common_ids, + "rms_residual_mm": rms, + "description": "Multi-marker machine frame fit with camera pose" + }, + "camera": camera_pose, + "markers": marker_list + } + with open(OUTPUT_JSON_PATH, 'w', encoding='utf-8') as f: + json.dump(json_data, f, indent=4) + + # Warn about expected IDs + detected_ids = set(poses_cam.keys()) + missing = EXPECTED_IDS - detected_ids + if missing: + print(f"[WARN] Expected markers not detected: {sorted(missing)}") + + # Draw machine axes using global transform (machine->camera) + R_machine_to_cam = R_cam_to_machine.T + t_machine_to_cam = - R_machine_to_cam @ t_cam_to_machine + try: + machine_axes = np.float32([ + [0.0, 0.0, 0.0], + [MACHINE_AXIS_X_M, 0.0, 0.0], + [0.0, MACHINE_AXIS_Y_M, 0.0], + [0.0, 0.0, MACHINE_AXIS_Z_M], + ]) + rvec_global, _ = cv2.Rodrigues(R_machine_to_cam) + imgpts, _ = cv2.projectPoints(machine_axes, rvec_global, t_machine_to_cam, camera_matrix, dist_coeffs) + origin = tuple(np.round(imgpts[0].ravel()).astype(int)) + x_end = tuple(np.round(imgpts[1].ravel()).astype(int)) + y_end = tuple(np.round(imgpts[2].ravel()).astype(int)) + z_end = tuple(np.round(imgpts[3].ravel()).astype(int)) + cv2.line(draw_img, origin, x_end, (0, 0, 255), 3) + cv2.line(draw_img, origin, y_end, (0, 255, 0), 3) + cv2.line(draw_img, origin, z_end, (255, 0, 0), 3) + cv2.putText(draw_img, "X (200 mm)", x_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2) + cv2.putText(draw_img, "Y (-100 mm)", y_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2) + cv2.putText(draw_img, "+Z (100 mm)", z_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,0), 2) + except Exception as e: + print(f"[WARN] Failed to draw machine axes: {e}") + + ok = cv2.imwrite(OUTPUT_IMAGE_PATH, draw_img) + if ok: + print(f"[INFO] Annotated image saved as '{OUTPUT_IMAGE_PATH}'.") + else: + print(f"[ERROR] Failed to save annotated image '{OUTPUT_IMAGE_PATH}'.") + +if __name__ == '__main__': + main() diff --git a/programs/readTwoImages.py b/programs/readTwoImages.py new file mode 100755 index 0000000..cda3198 --- /dev/null +++ b/programs/readTwoImages.py @@ -0,0 +1,668 @@ +#!/usr/bin/env python3 +""" +readCamPositionTwo.py + +Two-camera ArUco detection with joint optimization of both camera extrinsics +against known machine-frame reference markers, plus triangulation of unknown +marker positions. Outputs camera pose and marker poses in machine coordinates, +with CSV and JSON similar to the single-camera script. + +Dependencies: numpy, opencv-python (cv2) +Optional but NOT required: SciPy (we implement a simple Levenberg–Marquardt). + +Usage example: + python3 readTwoImages.py -i snapshot_video0_1764531874081.jpg -i snapshot_video1_1764531874081.jpg -npz callibration_cam0.npz -npz callibration_cam1.npz -settings settings.json + python3 readTwoImages.py -i snapshot_video0_1764524369655.jpg -i snapshot_video1_1764524369655.jpg -npz callibration_cam0.npz -npz callibration_cam1.npz -settings settings.json + + python3 readTwoImages.py -i snapshot_video0_1765009029764.jpg -i snapshot_video1_1765009029764.jpg -npz callibration_cam0.npz -npz callibration_cam1.npz -settings settings.json + +Settings JSON is expected to contain: +{ + "coordinateSystem": { + "KnownMarkers": { + "50": [0.0, 0.0, 0.0], + "71": [0.140, 0.0, 0.0], + "101": [0.0, -0.080, 0.0] + } + } +} + +Author: M365 Copilot (generated) +""" +import argparse +import os +import sys +import json +import time +from typing import Dict, Tuple, List +import numpy as np +import cv2 + +# ---------------- Configuration defaults ---------------- +AXIS_LENGTH_M = 0.05 + +# ---------------- Utilities ---------------- +def load_intrinsics_npz(npz_path: str) -> Tuple[np.ndarray, np.ndarray]: + print("NPZ reading of file:", npz_path) + if os.path.exists(npz_path): + data = np.load(npz_path) + for k in ('camera_matrix', 'mtx', 'K'): + if k in data: + camera_matrix = data[k].astype(np.float32) + break + else: + raise KeyError("Camera matrix not found in NPZ.") + for k in ('dist_coeffs', 'dist', 'D'): + if k in data: + dist = data[k].astype(np.float32).reshape(-1,1) + break + else: + dist = np.zeros((5,1), dtype=np.float32) + print("NPZ loaded:", npz_path) + return camera_matrix, dist + # Fallback default intrinsics + camera_matrix = np.array([[1400, 0, 640], + [0, 1400, 360], + [0, 0, 1]], dtype=np.float32) + dist_coeffs = np.zeros((5,1), dtype=np.float32) + print("[WARN] Using default approximate intrinsics.") + return camera_matrix, dist_coeffs + + +def get_aruco_detector(dict_name: str): + mapping = { + 'DICT_4X4_250': cv2.aruco.DICT_4X4_250, + 'DICT_5X5_100': cv2.aruco.DICT_5X5_100, + 'DICT_6X6_250': cv2.aruco.DICT_6X6_250, + 'DICT_ARUCO_ORIGINAL': cv2.aruco.DICT_ARUCO_ORIGINAL, + } + if dict_name not in mapping: + dict_id = cv2.aruco.DICT_4X4_250 + else: + dict_id = mapping[dict_name] + dictionary = cv2.aruco.getPredefinedDictionary(dict_id) + try: + params = cv2.aruco.DetectorParameters() + except Exception: + params = cv2.aruco.DetectorParameters_create() + try: + detector = cv2.aruco.ArucoDetector(dictionary, params) + return detector, None + except Exception: + return None, (dictionary, params) + + +def detect_markers(image: np.ndarray, detector_tuple): + detector, fallback = detector_tuple + if detector is not None: + corners, ids, rejected = detector.detectMarkers(image) + else: + dictionary, params = fallback + corners, ids, rejected = cv2.aruco.detectMarkers(image, dictionary, parameters=params) + return corners, ids, rejected + + +def corners_to_image_points(corners: np.ndarray) -> np.ndarray: + return corners.reshape(4,2).astype(np.float32) + + +def marker_center_from_corners(corners: np.ndarray) -> np.ndarray: + pts = corners.reshape(4,2) + return pts.mean(axis=0).astype(np.float32) + + +def rvec_to_R(rvec: np.ndarray) -> np.ndarray: + R, _ = cv2.Rodrigues(rvec) + return R + + +def rigid_transform_no_scale(A: np.ndarray, B: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Find R,t s.t. B ≈ R A + t. A,B: Nx3.""" + assert A.shape == B.shape and A.shape[1] == 3, "A and B must be Nx3" + N = A.shape[0] + if N < 2: + raise ValueError("Need at least 2 points; 3+ recommended.") + centroid_A = A.mean(axis=0) + centroid_B = B.mean(axis=0) + AA = A - centroid_A + BB = B - centroid_B + H = AA.T @ BB + U, S, Vt = np.linalg.svd(H) + R = Vt.T @ U.T + if np.linalg.det(R) < 0: + Vt[-1, :] *= -1 + R = Vt.T @ U.T + t = centroid_B - R @ centroid_A + return R.astype(np.float32), t.astype(np.float32) + + +def undistort_to_normalized(points_px: np.ndarray, K: np.ndarray, D: np.ndarray) -> np.ndarray: + """points_px: Nx2 pixel. Returns Nx2 normalized coords (x,y) where projection is x=Xp/Z, y=Yp/Z. + cv2.undistortPoints with P=None yields normalized coordinates. + """ + pts = points_px.reshape(-1,1,2).astype(np.float32) + und = cv2.undistortPoints(pts, K, D, P=None) # returns Nx1x2 + return und.reshape(-1,2) + + +# ---------------- Joint optimization (LM, numerical Jacobian) ---------------- + +def pack_params(omega1, t1, omega2, t2) -> np.ndarray: + return np.hstack([omega1, t1, omega2, t2]).astype(np.float64) + + +def unpack_params(theta: np.ndarray): + omega1 = theta[0:3] + t1 = theta[3:6] + omega2 = theta[6:9] + t2 = theta[9:12] + return omega1, t1, omega2, t2 + + +def residuals_centers_normalized(theta: np.ndarray, + X_mach: np.ndarray, + obs1_norm: np.ndarray, + obs2_norm: np.ndarray) -> np.ndarray: + """ + Compute residuals in normalized coordinates (no intrinsics, no distortion). + For camera j: X_cam = R_j X_mach + t_j; proj: (x/z, y/z). + Returns stacked residuals [r1; r2] shape (4N,), where N = number of references. + """ + omega1, t1, omega2, t2 = unpack_params(theta) + R1 = cv2.Rodrigues(omega1)[0] + R2 = cv2.Rodrigues(omega2)[0] + # Camera 1 projections + X_cam1 = (R1 @ X_mach.T + t1.reshape(3,1)).T # Nx3 + uv1 = X_cam1[:, :2] / X_cam1[:, 2:3] + r1 = (obs1_norm - uv1).reshape(-1) + # Camera 2 projections + X_cam2 = (R2 @ X_mach.T + t2.reshape(3,1)).T + uv2 = X_cam2[:, :2] / X_cam2[:, 2:3] + r2 = (obs2_norm - uv2).reshape(-1) + return np.hstack([r1, r2]) + + +def numerical_jacobian(f, theta: np.ndarray, eps: float, *args) -> np.ndarray: + """Finite-difference Jacobian: forward differences.""" + r0 = f(theta, *args) + m = r0.size + n = theta.size + J = np.zeros((m, n), dtype=np.float64) + for k in range(n): + th = theta.copy() + th[k] += eps + rk = f(th, *args) + J[:, k] = (rk - r0) / eps + return J, r0 + + +def lm_solve(theta0: np.ndarray, + X_mach: np.ndarray, + obs1_norm: np.ndarray, + obs2_norm: np.ndarray, + max_iter: int = 50, + eps_jac: float = 1e-6, + lambda_init: float = 1e-3) -> Tuple[np.ndarray, Dict]: + """Simple Levenberg–Marquardt on center normalized residuals.""" + lam = lambda_init + theta = theta0.copy() + history = {"iters": [], "rms": []} + for it in range(max_iter): + J, r = numerical_jacobian(residuals_centers_normalized, theta, eps_jac, + X_mach, obs1_norm, obs2_norm) + rms = float(np.sqrt(np.mean(r*r))) if r.size else 0.0 + history["iters"].append(it) + history["rms"].append(rms) + # Normal equations with damping + JTJ = J.T @ J + g = J.T @ r + H = JTJ + lam * np.eye(JTJ.shape[0]) + try: + delta = -np.linalg.solve(H, g) + except np.linalg.LinAlgError: + # Fallback to least squares + delta, *_ = np.linalg.lstsq(H, -g, rcond=None) + theta_trial = theta + delta + r_trial = residuals_centers_normalized(theta_trial, X_mach, obs1_norm, obs2_norm) + rms_trial = float(np.sqrt(np.mean(r_trial*r_trial))) + if rms_trial < rms: # improve + theta = theta_trial + lam *= 0.5 + else: + lam *= 2.0 + # Convergence criteria + if np.linalg.norm(delta) < 1e-9 or abs(rms - rms_trial) < 1e-9: + break + return theta, history + + +# ---------------- Triangulation ---------------- + +def build_projection_matrix(K: np.ndarray, R: np.ndarray, t: np.ndarray) -> np.ndarray: + return K @ np.hstack([R, t.reshape(3,1)]) + + +def triangulate_center(P1: np.ndarray, P2: np.ndarray, u1: np.ndarray, u2: np.ndarray) -> np.ndarray: + # u1,u2: (2,) pixel coordinates + x1 = u1.reshape(2,1) + x2 = u2.reshape(2,1) + X_h = cv2.triangulatePoints(P1, P2, x1, x2) # 4x1 homogeneous in machine frame if P maps machine->pixels + X = (X_h[:3] / X_h[3]).reshape(3) + return X.astype(np.float32) + + +# ---------------- Main ---------------- +def main(): + print("Started") + parser = argparse.ArgumentParser(description="Two-camera ArUco joint optimization and triangulation") + parser.add_argument('-i', '--images', action='append', required=True, + help="Two image paths: first camera then second camera") + parser.add_argument('-npz', '--npz', action='append', required=True, + help="Two NPZ intrinsics paths: cam1 then cam2") + parser.add_argument('-markerSizeMM', '--markerSizeMM', type=float, default=25.0, + help="Marker side length in millimeters") + parser.add_argument('--dict', default='DICT_4X4_250', help="ArUco dictionary name") + parser.add_argument('-settings', type=str, default=None, + help="Json settings file containing machine KnownMarkers") + args = parser.parse_args() + + if len(args.images) != 2 or len(args.npz) != 2: + print("[ERROR] Provide exactly two images and two intrinsics NPZ files.") + sys.exit(1) + + img1 = cv2.imread(args.images[0]) + img2 = cv2.imread(args.images[1]) + draw1 = img1.copy() + draw2 = img2.copy() + h, w = draw1.shape[:2] + #drawPNG1 = np.zeros((h, w, 4), dtype=np.uint8) # fully transparent + drawPNG1 = np.zeros((h, w, 3), dtype=np.uint8) + # Also prepare a matching canvas for camera2 to keep the layout tidy + drawPNG2 = np.zeros((h, w, 3), dtype=np.uint8) + + if img1 is None or img2 is None: + print("[ERROR] Cannot read one of the images.") + sys.exit(1) + + K1, D1 = load_intrinsics_npz(args.npz[0]) + K2, D2 = load_intrinsics_npz(args.npz[1]) + + # Marker 3D local geometry (square corners) + half = (args.markerSizeMM / 1000.0) / 2.0 + obj_points = np.array([ + [-half, half, 0.0], + [ half, half, 0.0], + [ half, -half, 0.0], + [-half, -half, 0.0], + ], dtype=np.float32) + + # Read settings for machine known markers + known_markers: Dict[int, np.ndarray] = {} + if args.settings is not None and os.path.exists(args.settings): + with open(args.settings, 'r') as f: + settings = json.load(f) + for marker_id, coords in settings['coordinateSystem']['KnownMarkers'].items(): + known_markers[int(marker_id)] = np.array(coords, dtype=np.float32) + print("[INFO] Loaded KnownMarkers from settings.") + else: + # Fallback defaults + known_markers = { + 50: np.array([0.0, 0.0, 0.0], dtype=np.float32), + 71: np.array([0.140, 0.0, 0.0], dtype=np.float32), + 101: np.array([0.0, -0.080, 0.0], dtype=np.float32), + } + print("[WARN] Using default KnownMarkers; provide -settings for your machine.") + + # Detect markers in both images + detector_tuple = get_aruco_detector(args.dict) + corners1, ids1, _ = detect_markers(img1, detector_tuple) + corners2, ids2, _ = detect_markers(img2, detector_tuple) + if ids1 is None or ids2 is None: + print("[ERROR] No markers detected in one or both images.") + sys.exit(1) + + ids1 = ids1.flatten().tolist() + ids2 = ids2.flatten().tolist() + + + + # Build dicts: id -> corners, center, rvec/tvec (per-camera PnP) + def build_marker_dict(img, corners_list, ids, K, D, draw = False) -> Tuple[Dict[int,np.ndarray], Dict[int,np.ndarray], Dict[int,Tuple[np.ndarray,np.ndarray]]]: + centers = {} + corners_dict = {} + poses = {} + for i, mid in enumerate(ids): + C = corners_list[i] + corners_dict[int(mid)] = C + centers[int(mid)] = marker_center_from_corners(C) + # Pose from single marker + img_pts = corners_to_image_points(C) + success, rvec, tvec = cv2.solvePnP(obj_points, img_pts, K, D, flags=cv2.SOLVEPNP_IPPE_SQUARE) + if not success: + success, rvec, tvec = cv2.solvePnP(obj_points, img_pts, K, D) + if success: + poses[int(mid)] = (rvec.reshape(3,1), tvec.reshape(3,1)) + if(draw): + cv2.drawFrameAxes(draw1, K, D, rvec, tvec, 0.02) # slim orientation axes + cv2.drawFrameAxes(drawPNG1, K, D, rvec, tvec, 0.02) # slim orientation axes + return centers, corners_dict, poses + + centers1, corners_dict1, poses1 = build_marker_dict(img1, corners1, ids1, K1, D1, draw = True) + centers2, corners_dict2, poses2 = build_marker_dict(img2, corners2, ids2, K2, D2) + + # Common reference markers present in both images and known in machine frame + common_refs = [mid for mid in known_markers.keys() if (mid in centers1 and mid in centers2)] + if len(common_refs) < 3: + print(f"[ERROR] Need ≥3 common reference markers in both cameras; found {len(common_refs)}: {common_refs}") + sys.exit(1) + + # Initial extrinsics from rigid fits per camera using tvec centers of references + # For camera j, A = camera-frame positions of reference markers (from PnP tvec), B = machine positions + def init_extrinsics_from_rigid(poses_cam: Dict[int,Tuple[np.ndarray,np.ndarray]], refs: List[int]) -> Tuple[np.ndarray,np.ndarray]: + A = [] + B = [] + for mid in refs: + if mid in poses_cam: + _, tvec = poses_cam[mid] + A.append(tvec.flatten()) + B.append(known_markers[mid]) + if len(A) < 2: + raise RuntimeError("Insufficient reference poses for rigid transform init.") + A = np.stack(A, axis=0) + B = np.stack(B, axis=0) + R_cm, t_cm = rigid_transform_no_scale(A, B) # camera->machine + # Convert to machine->camera + R_mc = R_cm.T + t_mc = - R_mc @ t_cm + return R_mc.astype(np.float32), t_mc.astype(np.float32) + + R1_init, t1_init = init_extrinsics_from_rigid(poses1, common_refs) + R2_init, t2_init = init_extrinsics_from_rigid(poses2, common_refs) + + # Observations: reference centers (pixels) -> normalized + X_mach_refs = np.stack([known_markers[mid] for mid in common_refs], axis=0).astype(np.float32) + obs1_px = np.stack([centers1[mid] for mid in common_refs], axis=0).astype(np.float32) + obs2_px = np.stack([centers2[mid] for mid in common_refs], axis=0).astype(np.float32) + obs1_norm = undistort_to_normalized(obs1_px, K1, D1) + obs2_norm = undistort_to_normalized(obs2_px, K2, D2) + + # Pack initial params as Rodrigues vectors + omega1_init = cv2.Rodrigues(R1_init)[0].reshape(3) + omega2_init = cv2.Rodrigues(R2_init)[0].reshape(3) + theta0 = pack_params(omega1_init, t1_init.reshape(3), omega2_init, t2_init.reshape(3)) + + theta_opt, hist = lm_solve(theta0, X_mach_refs, obs1_norm, obs2_norm, + max_iter=60, eps_jac=1e-6, lambda_init=1e-3) + + omega1, t1, omega2, t2 = unpack_params(theta_opt) + R1_opt = cv2.Rodrigues(omega1)[0] + R2_opt = cv2.Rodrigues(omega2)[0] + + # Camera pose in machine coordinates (camera->machine): R_cm = R^T, t_cm = -R^T t + R1_cm = R1_opt.T + t1_cm = - R1_cm @ t1 + R2_cm = R2_opt.T + t2_cm = - R2_cm @ t2 + + # Build projection matrices for triangulation (machine -> pixels) + P1 = build_projection_matrix(K1, R1_opt, t1) + P2 = build_projection_matrix(K2, R2_opt, t2) + + # Collect markers seen by at least one camera + all_ids = set(ids1) | set(ids2) + # Output structures + rows = [("id", "x_mm", "y_mm", "z_mm", "roll_deg", "pitch_deg", "yaw_deg")] + marker_list = [] + + # Camera orientations in Euler (ZYX) + def R_to_euler_zyx(R: np.ndarray) -> Tuple[float,float,float]: + yaw = float(np.degrees(np.arctan2(R[1,0], R[0,0]))) + sp = np.sqrt(R[2,1]**2 + R[2,2]**2) + pitch = float(np.degrees(np.arctan2(-R[2,0], sp))) + roll = float(np.degrees(np.arctan2(R[2,1], R[2,2]))) + return roll, pitch, yaw + + cam1_roll, cam1_pitch, cam1_yaw = R_to_euler_zyx(R1_cm) + cam2_roll, cam2_pitch, cam2_yaw = R_to_euler_zyx(R2_cm) + + # Camera rows + c1_mm = (t1_cm * 1000.0).tolist() + rows.append(("camera 0", f"{c1_mm[0]:.2f}", f"{c1_mm[1]:.2f}", f"{c1_mm[2]:.2f}", f"{cam1_roll:.3f}", f"{cam1_pitch:.3f}", f"{cam1_yaw:.3f}")) + c2_mm = (t2_cm * 1000.0).tolist() + rows.append(("camera 1", f"{c2_mm[0]:.2f}", f"{c2_mm[1]:.2f}", f"{c2_mm[2]:.2f}", f"{cam2_roll:.3f}", f"{cam2_pitch:.3f}", f"{cam2_yaw:.3f}")) + + # Triangulate and output markers + def orientation_in_machine(mid: int) -> Tuple[float,float,float]: + # Prefer camera1 orientation, else camera2 + if mid in poses1: + Rm_cam = rvec_to_R(poses1[mid][0]) + Rm_machine = R1_cm @ Rm_cam + elif mid in poses2: + Rm_cam = rvec_to_R(poses2[mid][0]) + Rm_machine = R2_cm @ Rm_cam + else: + Rm_machine = np.eye(3, dtype=np.float32) + return R_to_euler_zyx(Rm_machine) + + # Residual report for references + # Recompute residual RMS in pixels for references (after optimization) + refs_rms_px = [] + for j,(K,R_opt,t_opt,centers_px) in enumerate([(K1,R1_opt,t1,centers1),(K2,R2_opt,t2,centers2)]): + errs = [] + for mid in common_refs: + X = known_markers[mid] + X_cam = R_opt @ X + t_opt + x = K @ X_cam + u_pred = x[0]/x[2] + v_pred = x[1]/x[2] + u_obs, v_obs = centers_px[mid] + errs.append(np.hypot(u_obs-u_pred, v_obs-v_pred)) + refs_rms_px.append(float(np.sqrt(np.mean(np.square(errs))) if errs else 0.0)) + + # Compute per-marker positions + for mid in sorted(all_ids): + # Triangulate if seen in both + if mid in centers1 and mid in centers2: + X_mach = triangulate_center(P1, P2, centers1[mid], centers2[mid]) + elif mid in poses1: + # Fallback single-camera: transform tvec (camera->machine) + X_mach = (R1_cm @ poses1[mid][1].flatten() + t1_cm) + elif mid in poses2: + X_mach = (R2_cm @ poses2[mid][1].flatten() + t2_cm) + else: + continue + roll, pitch, yaw = orientation_in_machine(mid) + x_mm, y_mm, z_mm = (X_mach * 1000.0).tolist() + rows.append((mid, f"{x_mm:.2f}", f"{y_mm:.2f}", f"{z_mm:.2f}", f"{roll:.3f}", f"{pitch:.3f}", f"{yaw:.3f}")) + marker_list.append({ + "id": int(mid), + "position_mm": [float(x_mm), float(y_mm), float(z_mm)], + "orientation_deg": {"roll": float(roll), "pitch": float(pitch), "yaw": float(yaw)} + }) + + # Save CSV & JSON + base1 = os.path.splitext(args.images[0])[0] + base2 = os.path.splitext(args.images[1])[0] + out_csv = f"{base1}_two_cam.csv" + out_json = f"{base1}_two_cam.json" + + try: + import csv + with open(out_csv, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerows(rows) + print(f"[INFO] Saved CSV poses to '{out_csv}'.") + except Exception as e: + print(f"[WARN] Could not save CSV: {e}") + + json_data = { + "metadata": { + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + "reference_markers": common_refs, + "dict": args.dict, + "marker_size_mm": args.markerSizeMM, + "rms_refs_px_cam1": refs_rms_px[0] if refs_rms_px else None, + "rms_refs_px_cam2": refs_rms_px[1] if refs_rms_px else None, + "description": "Two-camera joint optimization with triangulation" + }, + "cameras": [ + { + "id": "camera1", + "position_mm": [float(x) for x in (t1_cm * 1000.0)], + "orientation_deg": {"roll": cam1_roll, "pitch": cam1_pitch, "yaw": cam1_yaw}, + }, + { + "id": "camera2", + "position_mm": [float(x) for x in (t2_cm * 1000.0)], + "orientation_deg": {"roll": cam2_roll, "pitch": cam2_pitch, "yaw": cam2_yaw}, + } + ], + "markers": marker_list + } + + try: + with open(out_json, 'w', encoding='utf-8') as f: + json.dump(json_data, f, indent=4) + print(f"[INFO] Saved JSON poses to '{out_json}'.") + except Exception as e: + print(f"[WARN] Could not save JSON: {e}") + + # Annotate images with machine axes using camera1 extrinsics + try: + R_machine_to_cam1 = R1_opt + t_machine_to_cam1 = t1 + machine_axes = np.float32([ + [0.0, 0.0, 0.0], + [0.200, 0.0, 0.0], + [0.0, -0.100, 0.0], + [0.0, 0.0, 0.100], + ]) + rvec_global, _ = cv2.Rodrigues(R_machine_to_cam1) + imgpts, _ = cv2.projectPoints(machine_axes, rvec_global, t_machine_to_cam1, K1, D1) + origin = tuple(np.round(imgpts[0].ravel()).astype(int)) + x_end = tuple(np.round(imgpts[1].ravel()).astype(int)) + y_end = tuple(np.round(imgpts[2].ravel()).astype(int)) + z_end = tuple(np.round(imgpts[3].ravel()).astype(int)) + + # Draw marker outlines only (omit default small id labels) — we draw larger IDs below + cv2.aruco.drawDetectedMarkers(draw1, corners1) + cv2.aruco.drawDetectedMarkers(drawPNG1, corners1) + # Draw larger blue ID labels (keep default marker outlines as-is) + try: + for i, mid in enumerate(ids1): + try: + pts = corners1[i].reshape((4, 2)) + center = tuple(np.round(pts.mean(axis=0)).astype(int)) + except Exception: + continue + text = str(int(mid)) + # Offset: 5px more to the right and 5px up (y axis is downwards) + pos = (int(center[0]) + 15, int(center[1]) - 15) + cv2.putText(draw1, text, pos, cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,0,0), 3, lineType=cv2.LINE_AA) + cv2.putText(drawPNG1, text, pos, cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,0,0,255), 3, lineType=cv2.LINE_AA) + except Exception: + pass + cv2.line(draw1, origin, x_end, (0,0,255), 3) + cv2.line(draw1, origin, y_end, (0,255,0), 3) + cv2.line(draw1, origin, z_end, (255,0,0), 3) + + # Draw lines (RGBA colors: B,G,R,A). A=255 for fully opaque. + cv2.line(drawPNG1, origin, x_end, (0, 0, 255, 255), 3) # Red X + cv2.line(drawPNG1, origin, y_end, (0, 255, 0, 255), 3) # Green Y + cv2.line(drawPNG1, origin, z_end, (255, 0, 0, 255), 3) # Blue Z + + cv2.putText(draw1, "X (200 mm)", x_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2) + cv2.putText(draw1, "Y (-100 mm)", y_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2) + cv2.putText(draw1, "+Z (100 mm)", z_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,0), 2) + + + # Try to draw text (might be jaggy on transparent BG in older OpenCV) + cv2.putText(drawPNG1, "X (200 mm)", x_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255, 255), 2) + cv2.putText(drawPNG1, "Y (-100 mm)", y_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0, 255), 2) + cv2.putText(drawPNG1, "+Z (100 mm)", z_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0, 255), 2) + + + out_img1 = f"{base1}_two_cam_annotated.jpg" + cv2.imwrite(out_img1, draw1) + print(f"[INFO] Annotated image saved as '{out_img1}'.") + + # Save as transparent PNG + + gray = cv2.cvtColor(drawPNG1, cv2.COLOR_BGR2GRAY) + _, alpha = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY) + + # 5) Merge BGR + alpha → RGBA transparent overlay + drawPNG_1 = cv2.merge([drawPNG1[:, :, 0], drawPNG1[:, :, 1], drawPNG1[:, :, 2], alpha]) + + out_png1 = f"{base1}_two_cam_overlay.png" + cv2.imwrite(out_png1, drawPNG_1) + + except Exception as e: + print(f"[WARN] Failed to draw machine axes: {e}") + + # Also annotate the second camera image and produce a transparent overlay + try: + machine_axes2 = np.float32([ + [0.0, 0.0, 0.0], + [0.200, 0.0, 0.0], + [0.0, -0.100, 0.0], + [0.0, 0.0, 0.100], + ]) + rvec_global2, _ = cv2.Rodrigues(R2_opt) + imgpts2, _ = cv2.projectPoints(machine_axes2, rvec_global2, t2, K2, D2) + origin2 = tuple(np.round(imgpts2[0].ravel()).astype(int)) + x_end2 = tuple(np.round(imgpts2[1].ravel()).astype(int)) + y_end2 = tuple(np.round(imgpts2[2].ravel()).astype(int)) + z_end2 = tuple(np.round(imgpts2[3].ravel()).astype(int)) + + # Draw marker outlines only (omit default small id labels) — we draw larger IDs below + cv2.aruco.drawDetectedMarkers(draw2, corners2) + cv2.aruco.drawDetectedMarkers(drawPNG2, corners2) + # Draw larger blue ID labels (keep default marker outlines as-is) + try: + for i, mid in enumerate(ids2): + try: + pts = corners2[i].reshape((4, 2)) + center = tuple(np.round(pts.mean(axis=0)).astype(int)) + except Exception: + continue + text = str(int(mid)) + # Offset: 5px more to the right and 5px up (y axis is downwards) + pos = (int(center[0]) + 13, int(center[1]) + 3) + cv2.putText(draw2, text, pos, cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,0,0), 3, lineType=cv2.LINE_AA) + cv2.putText(drawPNG2, text, pos, cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255,0,0,255), 3, lineType=cv2.LINE_AA) + except Exception: + pass + + cv2.line(draw2, origin2, x_end2, (0,0,255), 3) + cv2.line(draw2, origin2, y_end2, (0,255,0), 3) + cv2.line(draw2, origin2, z_end2, (255,0,0), 3) + + cv2.line(drawPNG2, origin2, x_end2, (0, 0, 255, 255), 3) + cv2.line(drawPNG2, origin2, y_end2, (0, 255, 0, 255), 3) + cv2.line(drawPNG2, origin2, z_end2, (255, 0, 0, 255), 3) + + cv2.putText(draw2, "X (200 mm)", x_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2) + cv2.putText(draw2, "Y (-100 mm)", y_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2) + cv2.putText(draw2, "+Z (100 mm)", z_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,0), 2) + + cv2.putText(drawPNG2, "X (200 mm)", x_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255, 255), 2) + cv2.putText(drawPNG2, "Y (-100 mm)", y_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0, 255), 2) + cv2.putText(drawPNG2, "+Z (100 mm)", z_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0, 255), 2) + + out_img2 = f"{base2}_two_cam_annotated.jpg" + cv2.imwrite(out_img2, draw2) + print(f"[INFO] Annotated image saved as '{out_img2}'.") + + gray2 = cv2.cvtColor(drawPNG2, cv2.COLOR_BGR2GRAY) + _, alpha2 = cv2.threshold(gray2, 0, 255, cv2.THRESH_BINARY) + drawPNG_2 = cv2.merge([drawPNG2[:, :, 0], drawPNG2[:, :, 1], drawPNG2[:, :, 2], alpha2]) + out_png2 = f"{base2}_two_cam_overlay.png" + cv2.imwrite(out_png2, drawPNG_2) + print(f"[INFO] Overlay PNG saved as '{out_png2}'.") + + except Exception as e: + print(f"[WARN] Failed to draw machine axes for camera2: {e}") + +if __name__ == '__main__': + main() diff --git a/programs/screenShot.js b/programs/screenShot.js new file mode 100755 index 0000000..1edcbd2 --- /dev/null +++ b/programs/screenShot.js @@ -0,0 +1,135 @@ +const fs = require('fs'); +const path = require('path'); +const { exec } = require('child_process'); +const { logSnapshot } = require('./log'); + + +function snapshot(outDir, cam0, cam1, ws){ + + if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); + const picDate = Date.now(); + const name0 = `snapshot_video0_${picDate}.jpg`; + const name1 = `snapshot_video1_${picDate}.jpg`; + cam0.snapshot(path.join(outDir, name0)); + cam1.snapshot(path.join(outDir, name1)); + + + strFile0 = path.join(outDir, name0); + strFile1 = path.join(outDir, name1); + + const relUrl = `/snapshots/${name0}`; + const relUrlApp = `/snapshots/${name0.replace('.jpg','_two_cam_annotated.jpg')}`; + // The Python postprocessor writes an overlay named "_two_cam_overlay.png" and a CSV named "_two_cam.csv" + const relOverlay = `/snapshots/${name0.replace('.jpg','_two_cam_overlay.png')}`; + const relOverlayCSV = `/snapshots/${name0.replace('.jpg','_two_cam.csv')}`; + + const annotatedPath = path.join(outDir, name0.replace('.jpg','_two_cam_annotated.jpg')); + const overlayPath = path.join(outDir, name0.replace('.jpg','_two_cam_overlay.png')); + const csvPath = path.join(outDir, name0.replace('.jpg','_two_cam.csv')); + + + + const command = `python3 /usr/src/app/programs/readTwoImages.py -i ${strFile0} -i ${strFile1} -npz /usr/src/app/data/settings/callibration_cam0.npz -npz /usr/src/app/data/settings/callibration_cam1.npz -settings /usr/src/app/data/settings/settings.json`; + console.log("Executing Python " + command); + + // Run the Python post-processing and send the snapshot response only + // after the annotated files are present to avoid transient 404s in the browser. + exec(command, (error, stdout, stderr) => { + try { + if (error) { + console.error(`Error: ${error.message}`); + // Capture which generated files actually exist for debugging + const files = { + annotated: fs.existsSync(annotatedPath), + overlay: fs.existsSync(overlayPath), + csv: fs.existsSync(csvPath) + }; + // Log full details server-side for diagnosis + const detailed = { + type: 'snapshot', + ok: false, + error: error.message, + stdout: String(stdout).slice(0, 4096), + stderr: String(stderr).slice(0, 4096), + files + }; + logSnapshot(command, JSON.stringify(detailed)); + + // Send a short, user-friendly error to the client (no large stdout/stderr) + const shortError = String(stderr || error.message || '').includes('Corrupt JPEG') + ? 'postprocessor failed: corrupt JPEG input' + : 'postprocessor failed'; + try { ws.send(JSON.stringify({ type: 'snapshot', ok: false, error: shortError })); } catch (e) {} + return; + } + + if (stderr) { + // Log stderr but don't fail outright; sometimes tools output warnings on stderr. + if (String(stderr).trim()) console.error(`Stderr: ${stderr}`); + } + + console.log(`Output:\n${stdout}`); + + // Wait up to ~1s (10 * 100ms) for the generated files to appear on disk. + const waitForFiles = (paths, attempts = 10, delayMs = 100) => new Promise((resolve) => { + let tries = 0; + (function poll() { + const ok = paths.every(p => fs.existsSync(p)); + if (ok || tries >= attempts) return resolve(ok); + tries++; + setTimeout(poll, delayMs); + })(); + }); + + waitForFiles([annotatedPath, overlayPath, csvPath]).then((found) => { + if (!found) { + const files = { + annotated: fs.existsSync(annotatedPath), + overlay: fs.existsSync(overlayPath), + csv: fs.existsSync(csvPath) + }; + // Log details server-side + const detailed = { + type: 'snapshot', + ok: false, + url: relUrl, + urlApp: relUrlApp, + overlay: relOverlay, + overlayCSV: relOverlayCSV, + files + }; + logSnapshot(command, JSON.stringify(detailed)); + // Send a concise error to the client + try { ws.send(JSON.stringify({ type: 'snapshot', ok: false, error: 'postprocessor incomplete (missing outputs)' })); } catch (e) {} + return; + } + + const response = JSON.stringify({ + type: 'snapshot', + ok: found, + url: relUrl, + urlApp: relUrlApp, + overlay: relOverlay, + overlayCSV: relOverlayCSV + }); + logSnapshot(command, response); + try { ws.send(response); } catch (e) {} + }).catch((waitErr) => { + console.error('waitForFiles failed:', waitErr); + const response = JSON.stringify({ type: 'snapshot', ok: false, error: String(waitErr) }); + logSnapshot(command, response); + try { ws.send(response); } catch (e) {} + }); + } catch (handlerErr) { + console.error('snapshot handler error:', handlerErr); + const response = JSON.stringify({ type: 'snapshot', ok: false, error: String(handlerErr) }); + logSnapshot(command, response); + try { ws.send(response); } catch (e) {} + } + }); + + +} + + +module.exports = { snapshot }; \ No newline at end of file diff --git a/programs/videoServer.js b/programs/videoServer.js new file mode 100755 index 0000000..ed3c55d --- /dev/null +++ b/programs/videoServer.js @@ -0,0 +1,234 @@ +// programs/videoServer.js +'use strict'; + +const fs = require('fs'); +const { spawn } = require('child_process'); +const WebSocket = require('ws'); + +class JpegFrameSplitter { + constructor(onFrame) { + this.onFrame = onFrame; + this.buffer = Buffer.alloc(0); + } + push(chunk) { + if (!chunk || !chunk.length) return; + this.buffer = Buffer.concat([this.buffer, chunk]); + let start = this.buffer.indexOf(Buffer.from([0xFF, 0xD8])); + while (start !== -1) { + const end = this.buffer.indexOf(Buffer.from([0xFF, 0xD9]), start + 2); + if (end === -1) break; + const frame = this.buffer.slice(start, end + 2); + try { this.onFrame(frame); } catch {} + this.buffer = this.buffer.slice(end + 2); + start = this.buffer.indexOf(Buffer.from([0xFF, 0xD8])); + } + if (this.buffer.length > 8 * 1024 * 1024) { + const nextSOI = this.buffer.indexOf(Buffer.from([0xFF, 0xD8])); + this.buffer = nextSOI !== -1 ? this.buffer.slice(nextSOI) : Buffer.alloc(0); + } + } +} + +class FFmpegStreamer { + /** + * devicePath: '/dev/videoX' + * options: { + * name, width, height, fps, quality, + * input: { format, fps, size, useWallclock, threadQueueSize, channel }, + * tryFormats: [ 'mjpeg', undefined, 'yuyv422', 'rgb24' ] + * } + */ + constructor(devicePath, options = {}) { + this.devicePath = devicePath; + this.name = options.name || devicePath; + this.opts = { + width: options.width ?? undefined, + height: options.height ?? undefined, + fps: options.fps ?? 20, + quality: options.quality ?? 5, + input: { + format: options.input?.format, + fps: options.input?.fps, + size: options.input?.size, + useWallclock: options.input?.useWallclock ?? true, + threadQueueSize: options.input?.threadQueueSize ?? 64, + channel: options.input?.channel, + }, + tryFormats: (options.tryFormats || [options.input?.format, 'yuyv422', 'mjpeg', 'rgb24']) + .filter((v, i, a) => a.indexOf(v) === i), + }; + + this.proc = null; + this.clients = new Set(); + this.startedAt = null; + this.latestFrame = null; + this.splitter = null; + + this.formatIdx = 0; + this.currentFormat = this.opts.tryFormats[this.formatIdx]; + + this._restarting = false; + this._backoffMs = 500; + this._maxBackoffMs = 8000; + this._stderrBuf = []; + this._stderrMaxLines = 8; + + this._quickFailCount = 0; + this._quickFailLimit = 6; + this._suspendedUntil = 0; + } + + get running() { return !!this.proc; } + _scaling() { return Number(this.opts.width) > 0 && Number(this.opts.height) > 0; } + + _buildFfmpegArgs() { + const outFps = this.opts.fps; + const quality = this.opts.quality; + const scaling = this._scaling(); + + const inFmt = this.currentFormat; + const inFps = this.opts.input.fps; + const inSize = this.opts.input.size; + const useWallclock = this.opts.input.useWallclock; + const tqs = this.opts.input.threadQueueSize; + const inChannel = this.opts.input.channel; + + const args = [ + '-hide_banner', '-loglevel', 'error', '-nostdin', + '-f', 'video4linux2', + ...(tqs ? ['-thread_queue_size', String(tqs)] : []), + ...(inFmt ? ['-input_format', String(inFmt)] : []), + ...(inFps ? ['-framerate', String(inFps)] : []), + ...(inSize ? ['-video_size', String(inSize)] : []), + ...(typeof inChannel === 'number' ? ['-channel', String(inChannel)] : []), + ...(useWallclock ? ['-use_wallclock_as_timestamps', '1'] : []), + '-i', this.devicePath, + '-fflags', 'nobuffer', '-flags', 'low_delay', '-an', '-sn', + ]; + + if (inFmt === 'mjpeg' && !scaling) { + args.push('-vsync', 'passthrough', '-c:v', 'copy', '-f', 'mjpeg', 'pipe:1'); + return args; + } + if (scaling) args.push('-vf', `scale=${Number(this.opts.width)}:${Number(this.opts.height)}`); + if (outFps) args.push('-r', String(outFps)); + args.push('-f', 'mjpeg', '-q:v', String(quality), 'pipe:1'); + return args; + } + + _logStderr(d) { + const s = d.toString().trim(); + if (!s) return; + this._stderrBuf.push(s); + if (this._stderrBuf.length > this._stderrMaxLines) this._stderrBuf.shift(); + } + + start() { + if (this.proc) return; + if (Date.now() < this._suspendedUntil) { + const wait = this._suspendedUntil - Date.now(); + console.warn(`[FFmpeg] ${this.name} suspended for ${wait}ms due to repeated failures`); + return setTimeout(() => this.start(), wait); + } + + const args = this._buildFfmpegArgs(); + console.log(`[FFmpeg] Start ${this.devicePath} (${this.name}) :: ${args.join(' ')}`); + + this._stderrBuf = []; + this.proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'], detached: true }); + this.startedAt = Date.now(); + + this.splitter = new JpegFrameSplitter((frame) => { + this.latestFrame = frame; + this._broadcast(frame); + }); + + this.proc.stdout.on('data', (chunk) => this.splitter?.push(chunk)); + this.proc.stderr.on('data', (d) => this._logStderr(d)); + + this.proc.on('exit', (code, signal) => { + console.warn(`[FFmpeg] ${this.devicePath} exited code=${code} sig=${signal}`); + if (this._stderrBuf.length) console.warn(`[FFmpeg] ${this.name} last errors:\n - ${this._stderrBuf.join('\n - ')}`); + + this.proc = null; + const quick = (Date.now() - (this.startedAt || Date.now())) < 2000; + this.startedAt = null; + + if (quick && !this._restarting) { + this._quickFailCount++; + this.formatIdx = (this.formatIdx + 1) % this.opts.tryFormats.length; + this.currentFormat = this.opts.tryFormats[this.formatIdx]; + console.warn(`[FFmpeg] ${this.name}: quick failure -> trying next format: ${this.currentFormat}`); + } else { + this._quickFailCount = 0; + } + + if (this._quickFailCount >= this._quickFailLimit) { + this._suspendedUntil = Date.now() + 60000; // 60s pause + this._quickFailCount = 0; + console.error(`[FFmpeg] ${this.name}: too many quick failures; suspending restarts for 60s`); + } + + const delay = this._restarting ? 300 : Math.min(this._backoffMs, this._maxBackoffMs); + setTimeout(() => { + if (!this._restarting) this._backoffMs = Math.min(this._backoffMs * 2, this._maxBackoffMs); + else this._backoffMs = 500; + this.start(); + }, delay); + this._restarting = false; + }); + } + + _killProcessGroup(signal = 'SIGTERM') { + if (!this.proc) return; + try { + if (process.platform !== 'win32') process.kill(-this.proc.pid, signal); + else this.proc.kill(signal); + } catch {} + } + + stop() { + if (!this.proc) return; + this._restarting = false; + this._killProcessGroup('SIGTERM'); + } + + restart(newOpts = {}) { + this._restarting = true; + if (newOpts.input) this.opts.input = { ...this.opts.input, ...newOpts.input }; + this.opts = { ...this.opts, ...newOpts, input: this.opts.input }; + if (newOpts.input && Object.prototype.hasOwnProperty.call(newOpts.input, 'format')) { + const idx = this.opts.tryFormats.indexOf(this.opts.input.format); + if (idx >= 0) { + this.formatIdx = idx; + this.currentFormat = this.opts.tryFormats[this.formatIdx]; + } + } + if (this.proc) this._killProcessGroup('SIGTERM'); else { this._restarting = false; this.start(); } + } + + attach(ws) { + this.clients.add(ws); + if (this.latestFrame && ws.readyState === WebSocket.OPEN) { + try { ws.send(this.latestFrame, { binary: true }); } catch {} + } + ws.on('close', () => this.clients.delete(ws)); + } + + snapshot(toFile) { + if (!this.latestFrame) throw new Error('No frame available yet'); + fs.writeFileSync(toFile, this.latestFrame); + return toFile; + } + + _broadcast(frame) { + if (!this.clients.size) return; + for (const ws of this.clients) { + if (ws.readyState !== WebSocket.OPEN) continue; + if (ws.bufferedAmount > 512 * 1024) continue; // drop if back-pressured + try { ws.send(frame, { binary: true }); } catch {} + } + } +} + +module.exports = { FFmpegStreamer }; \ No newline at end of file diff --git a/public/GamePad.js b/public/GamePad.js new file mode 100755 index 0000000..e8d72a6 --- /dev/null +++ b/public/GamePad.js @@ -0,0 +1,110 @@ +var isRunning = false; +var gamePadId = 0; +var gamepad = {}; + +let lastCheckTime = 0; + + + +function checkGamePad() { + if(isRunning == false){return;} + + const stepSize = "0.01"; + const stepSizeXYZ = "0.5"; // 3 ist auch ok + const stepSizeE = "0.01"; + var gp = navigator.getGamepads()[gamePadId] + var buttons = gp.buttons + + var xyzSpeed = 10; // 100 geht auch + + var psi = gp.axes[0]; + var z = gp.axes[1]; + var x = gp.axes[2]; + var y = gp.axes[3]; + + + // Dreieck zum Dreieck-Setzen + if (buttons[3].pressed) { + socketDriver.send(`G90 G1 X0 Y300 Z0 A${Math.PI/2} B${-1.0*Math.PI/2} C0 F${xyzSpeed}`); + } + if (buttons[4].pressed) { + //console.log("x=" + robot.x + " y=" + robot.y + " z=" + robot.z); + } + + + + // X Button setzt eine Marke + if(buttons[0].pressed && (Date.now() - lastCheckTime > 500)){ + lastCheckTime = Date.now() + console.log('FPoint!'); + socketDriver.send('FPoint'); + socketDriver.send('FShow'); + } + + // L1 und R1 Button to forward-backward in Point-List + if(gp.buttons[4].pressed && (Date.now() - lastCheckTime > 500)){ + lastCheckTime = Date.now() + socketDriver.send('FMinus'); + socketDriver.send('FShow'); + } + if(gp.buttons[5].pressed && (Date.now() - lastCheckTime > 500)){ + lastCheckTime = Date.now() + socketDriver.send('FPlus'); + socketDriver.send('FShow'); + } + + if (x < -0.2) { socketDriver.send(`G91 G1 X+${stepSizeXYZ} F${xyzSpeed}`);} + if (x > 0.2) { socketDriver.send(`G91 G1 X-${stepSizeXYZ} F${xyzSpeed}`);} + + if (y < -0.2) { socketDriver.send(`G91 G1 Y${stepSizeXYZ} F${xyzSpeed}`); } + if (y > 0.2) { socketDriver.send(`G91 G1 Y-${stepSizeXYZ} F${xyzSpeed}`);} + + if (z < -0.2) { socketDriver.send(`G91 G1 Z${stepSizeXYZ} F${xyzSpeed}`); } + if (z > 0.2) { socketDriver.send(`G91 G1 Z-${stepSizeXYZ} F${xyzSpeed}`); } + + + // Greif-Richtung + // LeftRight + if(buttons[14].pressed){ socketDriver.send(`G91 G1 A${stepSize} F${xyzSpeed}`);} + if(buttons[15].pressed){ socketDriver.send(`G91 G1 A-${stepSize} F${xyzSpeed}`);} + // Up - Down + if(buttons[12].pressed){ socketDriver.send(`G91 G1 B${stepSize} F${xyzSpeed}`);} + if(buttons[13].pressed){ socketDriver.send(`G91 G1 B-${stepSize} F${xyzSpeed}`);} + // Drehung + if (psi < -0.2) { socketDriver.send(`G91 G1 C${stepSize} F${xyzSpeed}`); } + if (psi > 0.2) { socketDriver.send(`G91 G1 C-${stepSize} F${xyzSpeed}`); } + + // Trigger-Buttons für Öffnen und Schliessen + if(buttons[6].pressed){socketDriver.send(`G91 G1 E-${stepSizeE} F${xyzSpeed}`);} + if(buttons[7].pressed){socketDriver.send(`G91 G1 E${stepSizeE} F${xyzSpeed}`);} + + if (isRunning) { setTimeout(checkGamePad, 15);} +} + +function gamepadHandler(event, connecting) { + + gamepad = event.gamepad; + if (typeof gamepad === `undefined`) { + isRunning = false; + gamePadId = 0; + console.warn("GamePad kann nicht gefunden werden"); + return; + } + + if (connecting) { + console.log("GamePad " + event.gamepad.index + " connected"); + gamePadId = gamepad.index; + isRunning = true; + setTimeout(checkGamePad, 20); + } else { + console.log("GamePad " +gamePadId +" disconnected"); + isRunning = false; + gamePadId = 0; + } +} + + +window.addEventListener("gamepadconnected", function (e) { gamepadHandler(e, true); }, false); +window.addEventListener("gamepaddisconnected", function (e) { gamepadHandler(e, false); }, false); + +document.addEventListener("touchstart", e => { console.log("TouchStart"); }) \ No newline at end of file diff --git a/public/KeyboardInput.js b/public/KeyboardInput.js new file mode 100755 index 0000000..8e26db3 --- /dev/null +++ b/public/KeyboardInput.js @@ -0,0 +1,64 @@ +document.onkeydown = checkKey; + +function checkKey(e) { + + e = e || window.event; + + if(e.key == 'a' || e.key == 'A') + { + console.log("back to A position"); + socketDriver.send(`G90 G1 X0 Y300 Z0 A${Math.PI/2} B-${Math.PI/2} C0 F100`); + } + + // Hand-Winkel (Eulerwinkel) + else if(e.key == 'i' || e.key == 'I'){ + socketDriver.send('G91 G1 B+0.1 F100'); + } + else if(e.key == 'k' || e.key == 'K'){ + socketDriver.send('G91 G1 B-0.1 F100'); + } + else if(e.key == 'l' || e.key == 'L'){ + socketDriver.send('G91 G1 A+0.1 F100'); + } + else if(e.key == 'j' || e.key == 'J'){ + socketDriver.send('G91 G1 A-0.1 F100'); + } + else if(e.key == 'o' || e.key == 'O'){ + socketDriver.send('G91 G1 C+0.1 F100'); + } + else if(e.key == 'u' || e.key == 'U'){ + socketDriver.send('G91 G1 C-0.1 F100'); + } + + // XYZ Koordinaten + else if(e.key == 'e' || e.key == 'E'){ + socketDriver.send('G91 G1 Z+5 F100'); + } + else if(e.key == 'd' || e.key == 'D'){ + socketDriver.send('G91 G1 Z-5 F100'); + } + else if(e.key == 's' || e.key == 'S'){ + socketDriver.send('G91 G1 X5 F100'); + } + else if(e.key == 'f' || e.key == 'S'){ + socketDriver.send('G91 G1 X-5 F100'); + } + else if(e.key == 'r' || e.key == 'R'){ + socketDriver.send('G91 G1 Y5 F100'); + } + else if(e.key == 'w' || e.key == 'W'){ + socketDriver.send('G91 G1 Y-5 F100'); + } + + // File & Log-Operations + else if(e.key == ' '){ + console.log('FPoint!') + socketDriver.send('FPoint'); + } + else if(e.key == 'b'){ + socketDriver.send('FMinus'); + } + else if(e.key == 'n'){ + socketDriver.send('FPlus'); + } +} \ No newline at end of file diff --git a/public/WebSocket.js b/public/WebSocket.js new file mode 100755 index 0000000..e6c1ce4 --- /dev/null +++ b/public/WebSocket.js @@ -0,0 +1,25 @@ +const protocolS = location.protocol === "https:" ? "wss://" : "ws://"; +const robotURL = protocolS + location.host + "/ws/robot"; +console.log("socketDriver try to connect to "+ robotURL); + +const socketDriver = new WebSocket(robotURL); + + + +socketDriver.onopen = () => { console.log("socketDriver WebSocket connected"); }; + +socketDriver.onmessage = (event) => { + if(event.data.toString().includes("position")){ + console.log("Position: " + event.data); + } + else if(event.data.toString().includes("XYZ__FShow__XYZ")){ + const content = event.data.toString().split("XYZ__FShow__XYZ")[1]; + document.querySelectorAll("textarea#GCodeWindow.editor-look")[0].value = content; + } + else{ + console.log('DATA SinceStartup: ' + (Date.now() - startTime).toString() +': ', event.data); + } +} + +socketDriver.onclose = () => {console.log("socketDriver WebSocket is closing");}; +socketDriver.onerror = (err) => { console.error("socketDriver socket error:", err); }; \ No newline at end of file diff --git a/public/app.js b/public/app.js new file mode 100755 index 0000000..b5cdb53 --- /dev/null +++ b/public/app.js @@ -0,0 +1,72 @@ +// /public/app.js +// Small bootstrap moved out of index.html to satisfy CSP (`script-src 'self'`). +(function () { + const isSecure = location.protocol === 'https:'; + const wsProto = isSecure ? 'wss' : 'ws'; + const base = `${wsProto}://${location.host}`; + + // Camera 0 with control channel + window.VideoService.attachStream({ + url: `${base}/ws/video0`, + canvas: document.getElementById('canvas0'), + statusEl: document.getElementById('status0'), + control: { + resSelect: document.getElementById('res0'), + fpsSelect: document.getElementById('fps0'), + qSelect: document.getElementById('q0'), + applyBtn: document.getElementById('apply0'), + snapshotBtn: document.getElementById('snap0'), + startBtn: document.getElementById('start0'), + stopBtn: document.getElementById('stop0'), + snapshotOutEl: document.getElementById('snapshotLink'), + } + }); + + // Camera 1 (no control) + window.VideoService.attachStream({ + url: `${base}/ws/video1`, + canvas: document.getElementById('canvas1'), + statusEl: document.getElementById('status1'), + }); + + // Run an automatic snapshot shortly after the page loads so the client + // receives the freshest annotated image/CSV on startup. This uses a + // synthetic click on the snapshot button; the handler will no-op if the + // WebSocket isn't open yet. + setTimeout(() => { + const snapBtn = document.getElementById('snap0'); + if (snapBtn) snapBtn.click(); + }, 200); + + // Attach handlers for the Point/Up/Down buttons (no inline JS, CSP-safe) + const btnPoint = document.getElementById('btnPoint'); + const btnDown = document.getElementById('btnDown'); + const btnUp = document.getElementById('btnUp'); + if (btnPoint) btnPoint.addEventListener('click', () => { + console.log('FPoint!'); + try { socketDriver.send('FPoint'); socketDriver.send('FShow'); } catch (e) { console.error(e); } + }); + if (btnDown) btnDown.addEventListener('click', () => { + console.log('FPlus'); + try { socketDriver.send('FPlus'); socketDriver.send('FShow'); } catch (e) { console.error(e); } + }); + if (btnUp) btnUp.addEventListener('click', () => { + console.log('FMinus'); + try { socketDriver.send('FMinus'); socketDriver.send('FShow'); } catch (e) { console.error(e); } + }); + + // Other buttons that were previously using inline onclicks. + const btnInfo = document.getElementById('b_M114'); + const btnNull = document.getElementById('b_G28'); + const btnListFile = document.getElementById('btnListFile'); + const btnSendGCode = document.getElementById('btnSendGCode'); + if (btnInfo) btnInfo.addEventListener('click', () => { try { socketDriver.send('M114'); } catch (e) { console.error(e); } }); + if (btnNull) btnNull.addEventListener('click', () => { try { socketDriver.send('G28'); } catch (e) { console.error(e); } }); + if (btnListFile) btnListFile.addEventListener('click', () => { try { socketDriver.send('FShow'); } catch (e) { console.error(e); } }); + if (btnSendGCode) btnSendGCode.addEventListener('click', () => { + try { + const input = document.getElementById('GKarth'); + if (input && input.value) socketDriver.send(input.value); + } catch (e) { console.error(e); } + }); +})(); diff --git a/public/buttonCmd.js b/public/buttonCmd.js new file mode 100755 index 0000000..8fd6c9d --- /dev/null +++ b/public/buttonCmd.js @@ -0,0 +1,23 @@ + + +document.getElementById("b_uparrow").addEventListener('click',() => { socketDriver.send(`G91 G1 Z10 F100`);}); +document.getElementById("b_downarw").addEventListener('click',() => { socketDriver.send(`G90 G1 Z-10 F100`);}); +document.getElementById("b_right").addEventListener('click',() => { socketDriver.send(`G90 G1 X10 F100`);}); +document.getElementById("b_left").addEventListener('click',() => { socketDriver.send(`G90 G1 X-10 F100`);}); +document.getElementById("b_forward").addEventListener('click',() => { socketDriver.send(`G90 G1 Y10 F100`);}); +document.getElementById("b_backward").addEventListener('click',() => { socketDriver.send(`G90 G1 Y-10 F100`);}); + +document.getElementById("b_aPlus").addEventListener('click',() => { socketDriver.send(`G91 G1 A0.10 F100`);}); +document.getElementById("b_aMinus").addEventListener('click',() => { socketDriver.send(`G91 G1 A-0.10 F100`);}); +document.getElementById("b_bPlus").addEventListener('click',() => { socketDriver.send(`G91 G1 B0.10 F100`);}); +document.getElementById("b_bMinus").addEventListener('click',() => { socketDriver.send(`G91 G1 B-0.10 F100`);}); +document.getElementById("b_cPlus").addEventListener('click',() => { socketDriver.send(`G91 G1 C0.10 F100`);}); +document.getElementById("b_cMinus").addEventListener('click',() => { socketDriver.send(`G91 G1 C-0.10 F100`);}); + +document.getElementById("b_ePlus").addEventListener('click',() => { socketDriver.send(`G91 G1 E0.10 F100`);}); +document.getElementById("b_eMinus").addEventListener('click',() => { socketDriver.send(`G91 G1 E-0.10 F100`);}); + + +document.getElementById("b_default").addEventListener('click',() => { socketDriver.send(`G90 G1 X0 Y300 Z0 A${Math.PI/2} B-${Math.PI/2} C0 F100`);}); +document.getElementById("b_G28").addEventListener('click',() => { socketDriver.send(`G28`);}); +document.getElementById("b_M114").addEventListener('click',() => { socketDriver.send(`M114`);}); diff --git a/public/index.html b/public/index.html new file mode 100755 index 0000000..e25c399 --- /dev/null +++ b/public/index.html @@ -0,0 +1,108 @@ + + +
+ +
+