Compare commits

...

55 Commits

Author SHA1 Message Date
4e7fa44301 Add delay for writing files 2021-03-23 18:27:05 -06:00
Elijah Lucian
104d39b4c6 🐛🔨 2021-03-24 00:04:04 -07:00
Elijah Lucian
88e62bdf48 Merge branch 'master' of https://git.tannercollin.com/tanner/3Dshock 2021-03-23 23:40:56 -07:00
Elijah Lucian
b9c8b6c1e6 💎 2021-03-23 23:40:53 -07:00
66f86827ff Allow adjusting LED time and add time info 2021-03-23 17:30:47 -06:00
Elijah Lucian
8ca4d55045 🔧 2021-03-17 10:33:11 -06:00
E
2b495779d3 adding dank transition 2021-03-11 13:11:18 -07:00
Tanner
ce46f006d5 Add random delay to fake download 2021-03-10 22:41:37 -07:00
E
5f8aad7355 Merge branch 'master' of https://git.tannercollin.com/tanner/3Dshock 2021-03-10 22:33:22 -07:00
E
c975432b73 🌫🎲🎰🎇📟🎏 2021-03-10 22:33:06 -07:00
be140b4c22 Add number to fake downloaded photos 2021-03-10 22:24:18 -07:00
E
3c5855ecd3 Merge branch 'master' of https://git.tannercollin.com/tanner/3Dshock 2021-03-10 20:04:21 -07:00
E
1499ee5736 🌠 2021-03-10 20:04:18 -07:00
Tanner
a07d4469c0 Add screenshot of network settings 2021-03-10 20:00:23 -07:00
Tanner
5e565f84c2 Add test for capture 2021-03-10 20:00:08 -07:00
E
b2cc6f4723 Merge branch 'master' of https://git.tannercollin.com/tanner/3Dshock 2021-03-10 19:51:34 -07:00
E
6720816a97 🗯 2021-03-10 19:51:31 -07:00
Tanner
5c15c8fcf5 Freeze requirements 2021-03-10 19:35:13 -07:00
738e688c51 Adjust capture and add logging 2021-03-10 19:22:41 -07:00
19352bf409 Add license 2021-03-10 19:22:41 -07:00
a632034751 Change statuses to ints 2021-03-10 19:22:41 -07:00
E
fa6809b507 Merge branch 'master' of https://git.tannercollin.com/tanner/3Dshock 2021-03-10 19:17:35 -07:00
E
c29a64b1cc 💅 2021-03-10 19:17:23 -07:00
d1486b61df Remove a zero 2021-03-10 17:57:31 -07:00
ecb8c1f815 Detect when the system isn't connected 2021-03-10 17:57:31 -07:00
ec8128d96a Add debug mode to simulate a capture 2021-03-10 17:57:31 -07:00
E
15023b0e24 🏁 2021-03-07 22:53:30 -07:00
E
534a3fe632 Merge branch 'master' of https://git.tannercollin.com/tanner/3Dshock 2021-03-07 22:51:36 -07:00
E
f338e14577 🧹 2021-03-07 22:51:31 -07:00
d5cfbafbde Add status API route 2021-03-07 22:50:02 -07:00
b213f4cb18 Rename partially downloaded images 2021-03-07 22:50:02 -07:00
612f47d551 Re-enable file removal 2021-03-07 22:50:02 -07:00
E
14395bdbdd 🚀 2021-03-07 22:48:48 -07:00
E
be284b9acb v0.4.2 2021-03-07 22:00:33 -07:00
E
9fe790ed9c dank swag pop and lolz 2021-03-07 21:58:36 -07:00
E
91e26fe9fc Merge branch 'master' of https://git.tannercollin.com/tanner/3Dshock 2021-03-07 21:46:43 -07:00
E
0760c93ce4 MVP 2021-03-07 21:46:40 -07:00
b1aaffa4db Add session API routes 2021-03-07 21:43:57 -07:00
0f0f6b1f7f Ignore output directory 2021-03-07 21:43:57 -07:00
e8822a8d3a Add route to create client 2021-03-07 21:43:57 -07:00
E
b0aec2cfd1 🍊 2021-03-07 20:56:20 -07:00
E
1e8d655d1d 🧼 2021-03-07 20:45:57 -07:00
E
181a2bbb74 2021-03-07 20:08:18 -07:00
E
963939d511 🏗️ 2021-03-07 19:21:31 -07:00
E
d08a6e77f4 Merge branch 'master' of https://git.tannercollin.com/tanner/3Dshock 2021-03-07 19:17:05 -07:00
E
d1e5b0310b 🥳 2021-03-07 19:17:00 -07:00
026913013a Install Flask and serve index.html 2021-03-07 19:16:59 -07:00
ba63fcaf01 Add module to trigger a capture 2021-03-07 19:16:59 -07:00
d1f527c93b Add module to download photos 2021-03-07 19:16:59 -07:00
23f15aed34 Ditch logging module 2021-03-07 19:16:59 -07:00
2c0c838bdb Add driver for grid projectors 2021-03-07 19:16:59 -07:00
c3d83be3c4 Add driver for lights 2021-03-07 19:16:59 -07:00
7cc29f6b9f Add gitignore 2021-03-07 19:16:59 -07:00
bb9e9e6a11 Move main.py 2021-03-07 19:16:59 -07:00
a436ff0ad7 Install requests 2021-03-07 19:16:59 -07:00
60 changed files with 31193 additions and 9042 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021 Tanner and Elijah
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

4
client/.gitignore vendored
View File

@@ -9,7 +9,9 @@
/coverage
# production
/build
# /build
client/settings.ts
# misc
.DS_Store

View File

@@ -2,6 +2,30 @@
# Requirements (pages)
# Routes
Client Datatype
```ts
type Client = {
name: string
email: string
phone: number
photos: string[]
}
```
post /api/clients -> create new client
get /api/clients/:id -> get client
post /api/clients/:id/session -> begin capture
delete /api/clients/:id/session -> delete all current photos (for new capture)
### Note Needed
get /api/clients -> get client list
get /api/clients/:id/session -> get active sesion (list of preview photo locations)
## Create Session
Information gathering

BIN
client/build/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
client/build/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
client/build/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
client/build/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,25 @@
{
"files": {
"main.css": "/static/css/main.895ab0f5.chunk.css",
"main.js": "/static/js/main.4388b0cf.chunk.js",
"main.js.map": "/static/js/main.4388b0cf.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.8d7962eb.js",
"runtime-main.js.map": "/static/js/runtime-main.8d7962eb.js.map",
"static/css/2.015dc0ab.chunk.css": "/static/css/2.015dc0ab.chunk.css",
"static/js/2.6d12aa16.chunk.js": "/static/js/2.6d12aa16.chunk.js",
"static/js/2.6d12aa16.chunk.js.map": "/static/js/2.6d12aa16.chunk.js.map",
"static/js/3.8e9312c3.chunk.js": "/static/js/3.8e9312c3.chunk.js",
"static/js/3.8e9312c3.chunk.js.map": "/static/js/3.8e9312c3.chunk.js.map",
"index.html": "/index.html",
"static/css/2.015dc0ab.chunk.css.map": "/static/css/2.015dc0ab.chunk.css.map",
"static/css/main.895ab0f5.chunk.css.map": "/static/css/main.895ab0f5.chunk.css.map",
"static/js/2.6d12aa16.chunk.js.LICENSE.txt": "/static/js/2.6d12aa16.chunk.js.LICENSE.txt"
},
"entrypoints": [
"static/js/runtime-main.8d7962eb.js",
"static/css/2.015dc0ab.chunk.css",
"static/js/2.6d12aa16.chunk.js",
"static/css/main.895ab0f5.chunk.css",
"static/js/main.4388b0cf.chunk.js"
]
}

BIN
client/build/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

5
client/build/favicon.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://w3.org/2000/svg" viewBox="0 0 100 100">
<text y=".9em" font-size="90">
💩
</text>
</svg>

After

Width:  |  Height:  |  Size: 110 B

1
client/build/index.html Normal file
View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="3D Shock by Tanner and Elijah enterprises"/><link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡</text></svg>"><link rel="manifest" href="/manifest.json"/><title>3D Shock!</title><link href="/static/css/2.015dc0ab.chunk.css" rel="stylesheet"><link href="/static/css/main.895ab0f5.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function t(t){for(var n,i,a=t[0],c=t[1],l=t[2],s=0,p=[];s<a.length;s++)i=a[s],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&p.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(t);p.length;)p.shift()();return u.push.apply(u,l||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,a=1;a<r.length;a++){var c=r[a];0!==o[c]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.e=function(e){var t=[],r=o[e];if(0!==r)if(r)t.push(r[2]);else{var n=new Promise((function(t,n){r=o[e]=[t,n]}));t.push(r[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+"static/js/"+({}[e]||e)+"."+{3:"8e9312c3"}[e]+".chunk.js"}(e);var c=new Error;u=function(t){a.onerror=a.onload=null,clearTimeout(l);var r=o[e];if(0!==r){if(r){var n=t&&("load"===t.type?"missing":t.type),u=t&&t.target&&t.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,r[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(t)},i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/",i.oe=function(e){throw console.error(e),e};var a=this.webpackJsonpclient=this.webpackJsonpclient||[],c=a.push.bind(a);a.push=t,a=a.slice();for(var l=0;l<a.length;l++)t(a[l]);var f=c;r()}([])</script><script src="/static/js/2.6d12aa16.chunk.js"></script><script src="/static/js/main.4388b0cf.chunk.js"></script></body></html>

BIN
client/build/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
client/build/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
client/build/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,"Courier New",monospace}form{max-width:500px;-webkit-flex-direction:column;flex-direction:column}form,form label{display:-webkit-flex;display:flex;margin:auto}form label{width:100%;-webkit-flex-direction:row;flex-direction:row;-webkit-justify-content:space-between;justify-content:space-between}:root{--color-primary:#282c34}.App-header{background-color:#282c34;background-color:var(--color-primary);min-height:100vh;display:-webkit-flex;display:flex;-webkit-flex-direction:column;flex-direction:column;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center;font-size:calc(10px + 2vmin);color:#fff}.App-link{color:#61dafb}@-webkit-keyframes App-logo-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes App-logo-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.error{color:red}.photo-wall{display:-webkit-flex;display:flex;-webkit-justify-content:center;justify-content:center;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:2rem}.dashboard-form{margin:auto}.ant-card{margin:1rem}.ant-card img{max-width:250px}.loading-bar-container{background:#282c34;background:var(--color-primary);width:100%;height:30px;min-height:30px}.loading-bar{transition:all .5s linear;background:#6f6;height:100%}.page-head{background:#282c34;background:var(--color-primary);color:#fff!important;margin-top:0;padding:1rem}.session-toolbar button{margin:0 .5rem;padding:.5rem 1rem;height:100%;font-weight:700;text-transform:uppercase;letter-spacing:.05rem}.ant-modal-content{border-radius:1rem;padding:0!important}.ant-modal-body img{border-radius:.5rem}.ant-modal-close{font-weight:700}.ant-card{border:1px solid #282c34;border:1px solid var(--color-primary);border-radius:.2rem}.ant-card-head{background:#282c34;background:var(--color-primary);color:#fff;font-weight:700;text-transform:uppercase;letter-spacing:.2rem}.session-toolbar{-webkit-justify-content:center;justify-content:center;width:60%;margin:1rem auto}.session-toolbar h3{font-weight:700;color:#fff;margin-right:1rem}.slider{width:400px;margin:auto 1rem}.toolbar{padding:.5rem;background:#282c34;background:var(--color-primary)}.client-info{-webkit-justify-content:center;justify-content:center;width:60%;margin:1rem auto}.client-info span{margin:auto 1rem}
/*# sourceMappingURL=main.895ab0f5.chunk.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["webpack://src/index.css","webpack://src/App.css"],"names":[],"mappings":"AAAA,KACE,QAAS,CACT,mJAEY,CACZ,kCAAmC,CACnC,iCACF,CAEA,KACE,yEAEF,CAEA,KAEE,eAAgB,CAEhB,6BAAsB,CAAtB,qBACF,CAEA,gBANE,oBAAa,CAAb,YAAa,CAEb,WAUF,CANA,WACE,UAAW,CAEX,0BAAmB,CAAnB,kBAAmB,CACnB,qCAA8B,CAA9B,6BAEF,CC3BA,MACE,uBACF,CAEA,YACE,wBAAsC,CAAtC,qCAAsC,CACtC,gBAAiB,CACjB,oBAAa,CAAb,YAAa,CACb,6BAAsB,CAAtB,qBAAsB,CACtB,0BAAmB,CAAnB,kBAAmB,CACnB,8BAAuB,CAAvB,sBAAuB,CACvB,4BAA6B,CAC7B,UACF,CAEA,UACE,aACF,CAEA,iCACE,GACE,8BAAuB,CAAvB,sBACF,CACA,GACE,+BAAyB,CAAzB,uBACF,CACF,CAPA,yBACE,GACE,8BAAuB,CAAvB,sBACF,CACA,GACE,+BAAyB,CAAzB,uBACF,CACF,CAEA,OACE,SACF,CAEA,YACE,oBAAa,CAAb,YAAa,CACb,8BAAuB,CAAvB,sBAAuB,CACvB,sBAAe,CAAf,cAAe,CACf,WACF,CAEA,gBACE,WACF,CAEA,UACE,WACF,CAEA,cACE,eACF,CAEA,uBACE,kBAAgC,CAAhC,+BAAgC,CAChC,UAAW,CACX,WAAY,CACZ,eACF,CAEA,aACE,yBAA2B,CAC3B,eAAmB,CACnB,WACF,CAEA,WACE,kBAAgC,CAAhC,+BAAgC,CAChC,oBAAuB,CACvB,YAAa,CACb,YACF,CAEA,wBACE,cAAgB,CAChB,kBAAoB,CACpB,WAAY,CACZ,eAAiB,CACjB,wBAAyB,CACzB,qBACF,CAEA,mBACE,kBAAmB,CACnB,mBACF,CAEA,oBACE,mBACF,CAEA,iBACE,eACF,CAEA,UACE,wBAAsC,CAAtC,qCAAsC,CACtC,mBACF,CAEA,eACE,kBAAgC,CAAhC,+BAAgC,CAChC,UAAY,CACZ,eAAiB,CACjB,wBAAyB,CACzB,oBACF,CAEA,iBACE,8BAAuB,CAAvB,sBAAuB,CACvB,SAAU,CACV,gBACF,CAEA,oBACE,eAAiB,CACjB,UAAY,CACZ,iBACF,CAEA,QACE,WAAY,CACZ,gBACF,CAEA,SACE,aAAe,CACf,kBAAgC,CAAhC,+BACF,CAEA,aACE,8BAAuB,CAAvB,sBAAuB,CACvB,SAAU,CACV,gBACF,CAEA,kBACE,gBACF","file":"main.895ab0f5.chunk.css","sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\",\n \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, \"Courier New\",\n monospace;\n}\n\nform {\n display: flex;\n max-width: 500px;\n margin: auto;\n flex-direction: column;\n}\n\nform label {\n width: 100%; \n display: flex;\n flex-direction: row;\n justify-content: space-between;\n margin: auto;\n}\n\n",":root {\n --color-primary: #282c34;\n}\n\n.App-header {\n background-color: var(--color-primary);\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n font-size: calc(10px + 2vmin);\n color: white;\n}\n\n.App-link {\n color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\n.error {\n color: red;\n}\n\n.photo-wall {\n display: flex;\n justify-content: center;\n flex-wrap: wrap;\n margin: 2rem;\n}\n\n.dashboard-form {\n margin: auto;\n}\n\n.ant-card {\n margin: 1rem;\n}\n\n.ant-card img {\n max-width: 250px;\n}\n\n.loading-bar-container {\n background: var(--color-primary);\n width: 100%;\n height: 30px;\n min-height: 30px;\n}\n\n.loading-bar {\n transition: all 0.5s linear;\n background: #66ff66;\n height: 100%;\n}\n\n.page-head {\n background: var(--color-primary);\n color: white !important;\n margin-top: 0;\n padding: 1rem;\n}\n\n.session-toolbar button {\n margin: 0 0.5rem;\n padding: 0.5rem 1rem;\n height: 100%;\n font-weight: bold;\n text-transform: uppercase;\n letter-spacing: 0.05rem;\n}\n\n.ant-modal-content {\n border-radius: 1rem;\n padding: 0 !important;\n}\n\n.ant-modal-body img {\n border-radius: 0.5rem;\n}\n\n.ant-modal-close {\n font-weight: bold;\n}\n\n.ant-card {\n border: 1px solid var(--color-primary);\n border-radius: 0.2rem;\n}\n\n.ant-card-head {\n background: var(--color-primary);\n color: white;\n font-weight: bold;\n text-transform: uppercase;\n letter-spacing: 0.2rem;\n}\n\n.session-toolbar {\n justify-content: center;\n width: 60%;\n margin: 1rem auto;\n}\n\n.session-toolbar h3 {\n font-weight: bold;\n color: white;\n margin-right: 1rem;\n}\n\n.slider {\n width: 400px;\n margin: auto 1rem;\n}\n\n.toolbar {\n padding: 0.5rem;\n background: var(--color-primary);\n}\n\n.client-info {\n justify-content: center;\n width: 60%;\n margin: 1rem auto;\n}\n\n.client-info span {\n margin: auto 1rem;\n}\n"]}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
Copyright (c) 2017 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/** @license React v0.20.1
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.1
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.1
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.1
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
(this.webpackJsonpclient=this.webpackJsonpclient||[]).push([[3],{399:function(t,n,e){"use strict";e.r(n),e.d(n,"getCLS",(function(){return v})),e.d(n,"getFCP",(function(){return y})),e.d(n,"getFID",(function(){return k})),e.d(n,"getLCP",(function(){return C})),e.d(n,"getTTFB",(function(){return P}));var i,a,r,o,c=function(t,n){return{name:t,value:void 0===n?-1:n,delta:0,entries:[],id:"v1-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},u=function(t,n){try{if(PerformanceObserver.supportedEntryTypes.includes(t)){var e=new PerformanceObserver((function(t){return t.getEntries().map(n)}));return e.observe({type:t,buffered:!0}),e}}catch(t){}},f=!1,s=function(t,n){f||"undefined"!=typeof InstallTrigger||(addEventListener("beforeunload",(function(){})),f=!0),addEventListener("visibilitychange",(function e(i){"hidden"===document.visibilityState&&(t(i),n&&removeEventListener("visibilitychange",e,!0))}),!0)},d=function(t){addEventListener("pageshow",(function(n){n.persisted&&t(n)}),!0)},m="function"==typeof WeakSet?new WeakSet:new Set,p=function(t,n,e){var i;return function(){n.value>=0&&(e||m.has(n)||"hidden"===document.visibilityState)&&(n.delta=n.value-(i||0),(n.delta||void 0===i)&&(i=n.value,t(n)))}},v=function(t,n){var e,i=c("CLS",0),a=function(t){t.hadRecentInput||(i.value+=t.value,i.entries.push(t),e())},r=u("layout-shift",a);r&&(e=p(t,i,n),s((function(){r.takeRecords().map(a),e()})),d((function(){i=c("CLS",0),e=p(t,i,n)})))},l=-1,h=function(){return"hidden"===document.visibilityState?0:1/0},S=function(){s((function(t){var n=t.timeStamp;l=n}),!0)},g=function(){return l<0&&(l=h(),S(),d((function(){setTimeout((function(){l=h(),S()}),0)}))),{get timeStamp(){return l}}},y=function(t,n){var e,i=g(),a=c("FCP"),r=u("paint",(function(t){"first-contentful-paint"===t.name&&(r&&r.disconnect(),t.startTime<i.timeStamp&&(a.value=t.startTime,a.entries.push(t),m.add(a),e()))}));r&&(e=p(t,a,n),d((function(i){a=c("FCP"),e=p(t,a,n),requestAnimationFrame((function(){requestAnimationFrame((function(){a.value=performance.now()-i.timeStamp,m.add(a),e()}))}))})))},w={passive:!0,capture:!0},E=new Date,L=function(t,n){i||(i=n,a=t,r=new Date,F(removeEventListener),T())},T=function(){if(a>=0&&a<r-E){var t={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+a};o.forEach((function(n){n(t)})),o=[]}},b=function(t){if(t.cancelable){var n=(t.timeStamp>1e12?new Date:performance.now())-t.timeStamp;"pointerdown"==t.type?function(t,n){var e=function(){L(t,n),a()},i=function(){a()},a=function(){removeEventListener("pointerup",e,w),removeEventListener("pointercancel",i,w)};addEventListener("pointerup",e,w),addEventListener("pointercancel",i,w)}(n,t):L(n,t)}},F=function(t){["mousedown","keydown","touchstart","pointerdown"].forEach((function(n){return t(n,b,w)}))},k=function(t,n){var e,r=g(),f=c("FID"),v=function(t){t.startTime<r.timeStamp&&(f.value=t.processingStart-t.startTime,f.entries.push(t),m.add(f),e())},l=u("first-input",v);e=p(t,f,n),l&&s((function(){l.takeRecords().map(v),l.disconnect()}),!0),l&&d((function(){var r;f=c("FID"),e=p(t,f,n),o=[],a=-1,i=null,F(addEventListener),r=v,o.push(r),T()}))},C=function(t,n){var e,i=g(),a=c("LCP"),r=function(t){var n=t.startTime;n<i.timeStamp&&(a.value=n,a.entries.push(t)),e()},o=u("largest-contentful-paint",r);if(o){e=p(t,a,n);var f=function(){m.has(a)||(o.takeRecords().map(r),o.disconnect(),m.add(a),e())};["keydown","click"].forEach((function(t){addEventListener(t,f,{once:!0,capture:!0})})),s(f,!0),d((function(i){a=c("LCP"),e=p(t,a,n),requestAnimationFrame((function(){requestAnimationFrame((function(){a.value=performance.now()-i.timeStamp,m.add(a),e()}))}))}))}},P=function(t){var n,e=c("TTFB");n=function(){try{var n=performance.getEntriesByType("navigation")[0]||function(){var t=performance.timing,n={entryType:"navigation",startTime:0};for(var e in t)"navigationStart"!==e&&"toJSON"!==e&&(n[e]=Math.max(t[e]-t.navigationStart,0));return n}();e.value=e.delta=n.responseStart,e.entries=[n],t(e)}catch(t){}},"complete"===document.readyState?setTimeout(n,0):addEventListener("pageshow",n)}}}]);
//# sourceMappingURL=3.8e9312c3.chunk.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
!function(e){function t(t){for(var n,i,a=t[0],c=t[1],l=t[2],s=0,p=[];s<a.length;s++)i=a[s],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&p.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(t);p.length;)p.shift()();return u.push.apply(u,l||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,a=1;a<r.length;a++){var c=r[a];0!==o[c]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.e=function(e){var t=[],r=o[e];if(0!==r)if(r)t.push(r[2]);else{var n=new Promise((function(t,n){r=o[e]=[t,n]}));t.push(r[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+"static/js/"+({}[e]||e)+"."+{3:"8e9312c3"}[e]+".chunk.js"}(e);var c=new Error;u=function(t){a.onerror=a.onload=null,clearTimeout(l);var r=o[e];if(0!==r){if(r){var n=t&&("load"===t.type?"missing":t.type),u=t&&t.target&&t.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,r[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(t)},i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/",i.oe=function(e){throw console.error(e),e};var a=this.webpackJsonpclient=this.webpackJsonpclient||[],c=a.push.bind(a);a.push=t,a=a.slice();for(var l=0;l<a.length;l++)t(a[l]);var f=c;r()}([]);
//# sourceMappingURL=runtime-main.8d7962eb.js.map

File diff suppressed because one or more lines are too long

19831
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
"@types/node": "^12.0.0",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.0",
"antd": "^4.13.1",
"axios": "^0.21.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",

BIN
client/public/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
client/public/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
client/public/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
client/public/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://w3.org/2000/svg" viewBox="0 0 100 100">
<text y=".9em" font-size="90">
💩
</text>
</svg>

After

Width:  |  Height:  |  Size: 110 B

View File

@@ -2,14 +2,13 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
content="3D Shock by Tanner and Elijah enterprises"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡</text></svg>">
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -24,7 +23,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>3D Shock!</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,20 +1,9 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
:root {
--color-primary: #282c34;
}
.App-header {
background-color: #282c34;
background-color: var(--color-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
@@ -36,3 +25,113 @@
transform: rotate(360deg);
}
}
.error {
color: red;
}
.photo-wall {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin: 2rem;
}
.dashboard-form {
margin: auto;
}
.ant-card {
margin: 1rem;
}
.ant-card img {
max-width: 250px;
}
.loading-bar-container {
background: var(--color-primary);
width: 100%;
height: 30px;
min-height: 30px;
}
.loading-bar {
transition: all 0.5s linear;
background: #66ff66;
height: 100%;
}
.page-head {
background: var(--color-primary);
color: white !important;
margin-top: 0;
padding: 1rem;
}
.session-toolbar button {
margin: 0 0.5rem;
padding: 0.5rem 1rem;
height: 100%;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.05rem;
}
.ant-modal-content {
border-radius: 1rem;
padding: 0 !important;
}
.ant-modal-body img {
border-radius: 0.5rem;
}
.ant-modal-close {
font-weight: bold;
}
.ant-card {
border: 1px solid var(--color-primary);
border-radius: 0.2rem;
}
.ant-card-head {
background: var(--color-primary);
color: white;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.2rem;
}
.session-toolbar {
justify-content: center;
width: 60%;
margin: 1rem auto;
}
.session-toolbar h3 {
font-weight: bold;
color: white;
margin-right: 1rem;
}
.slider {
width: 400px;
margin: auto 1rem;
}
.toolbar {
padding: 0.5rem;
background: var(--color-primary);
}
.client-info {
justify-content: center;
width: 60%;
margin: 1rem auto;
}
.client-info span {
margin: auto 1rem;
}

View File

@@ -5,15 +5,15 @@ import { BrowserRouter, Switch, Route } from 'react-router-dom'
import { Dashboard } from './pages/Dashboard'
import { Session } from './pages/Session'
console.log('ENV', process.env.NODE_ENV)
function App() {
return (
<BrowserRouter>
<div className="App">
<Switch>
<Route path="/" component={Dashboard} />
<Route path="/sessions/:clientId" component={Session} />
<p>landing page</p>
<p>session</p>
<Route exact path="/" component={Dashboard} />
</Switch>
</div>
</BrowserRouter>

View File

@@ -1,33 +1,72 @@
import settings from '../settings'
import { Client } from '../types'
import { Client, Timings } from '../types'
import axios from 'axios'
import { message } from 'antd'
import { Status } from '../components/StatusChip'
const { apiUrl } = settings
const dev = process.env.NODE_ENV === 'development'
export const createClient = async (body: Client) => {
await axios.post(`${apiUrl}/clients`, body)
if (dev) {
const host = 'http://192.168.1.114:5000'
axios.defaults.baseURL = host
}
const mock = false
export const createClient = async (body: Omit<Client, 'has_photos'>) => {
if (mock) return 'test'
const res = await axios.post<{ client_id: string }>(`/api/clients`, body)
return res.data.client_id
}
export const getClient = async (id: string): Promise<Client> => {
if (mock)
return {
name: 'Test Client',
has_photos: false,
email: 'test@test.test',
phone: 1234567890,
}
const res = await axios.get<Client>(`/api/clients/${id}`)
return res.data
}
export const startSession = async (clientId: string, timings: Timings) => {
try {
const res = await axios.post(`/api/clients/${clientId}/session`, timings)
return res.data
} catch (err) {
message.error('Something went wrong, check connection with the machine')
return err
}
}
export const getSession = async (clientId: string) => {
const res = await axios.get<{ photos: string[] }>(
`/api/clients/${clientId}/session`,
)
return res.data // session data
}
export const killSession = async (clientId: string) => {
await axios.delete(`/api/clients/${clientId}/session`)
}
export const restartSession = async (clientId: string, timings: Timings) => {
await killSession(clientId)
await startSession(clientId, timings)
}
// TOOD: Get status
export const getStatus = async (): Promise<Status> => {
const res = await axios.get<{ status: Status }>('/api/status')
return res.data.status
}
// Someday
export const getClients = async (): Promise<Client[]> => {
const res = await axios.post<Client[]>(`${apiUrl}/clients`)
return res.data as Client[]
}
export const beginCapture = async (clientId: string) => {
const res = await axios.post(`${apiUrl}/clients/${clientId}/session`)
return res.data as string // capture id
}
export const getCapture = async (
clientId: string,
): Promise<null | string[]> => {
const res = await axios.get(`${apiUrl}/clients/${clientId}/session`)
return res.data as null | string[]
}
export const retryCapture = async (clientId: string) => {
await axios.delete(`${apiUrl}/clients/${clientId}/session`)
await axios.post(`${apiUrl}/clients/${clientId}/session`)
const res = await axios.get<Client[]>(`/api/clients`)
return res.data
}
export const cleanup = () => {

View File

@@ -0,0 +1,23 @@
import { Button } from 'antd'
import React from 'react'
export const ScrollToTop = () => {
const handleClick = () => {
window.scrollTo(0, 0)
console.log('')
}
return (
<Button
type="link"
style={{
position: 'fixed',
right: 20,
bottom: 20,
}}
onClick={handleClick}
>
Scroll To Top
</Button>
)
}

View File

@@ -0,0 +1,131 @@
import React, { useEffect, useState } from 'react'
import { Card, Modal, Row, Select, Typography } from 'antd'
import { getSession } from '../api'
import { ScrollToTop } from './ScrollToTop'
type Props = {
clientId: string
}
export const SessionPictures = ({ clientId }: Props) => {
const [urls, setUrls] = useState<string[] | null>(null)
const [activeUrl, setActiveUrl] = useState<string | null>(null)
const [focusPhotos, setFocusPhotos] = useState<string[]>(
JSON.parse(window.localStorage.getItem('focusPhotos') || '[]'),
)
useEffect(() => {
const get = async () => {
if (urls && urls.length >= 89 * 1) {
return
}
const { photos } = await getSession(clientId)
if (photos.length) setUrls(photos)
}
const interval = setInterval(get, 250)
return () => clearInterval(interval)
}, [clientId, urls])
const closeModal = () => setActiveUrl(null)
const host =
process.env.NODE_ENV === 'development' ? 'http://192.168.1.107:5000' : ''
if (!urls?.length) return null
const photos = urls.sort((a, b) =>
a.split('_')[0].localeCompare(b.split('_')[0]),
)
const u = urls.length / 89
const handleSelect = (v: string[]) => {
console.log('SEelcted', v)
window.localStorage.setItem('focusPhotos', JSON.stringify(v))
setFocusPhotos(v)
}
const filteredPhotos = photos.filter((name) => {
const num = name.split('_')[0]
return focusPhotos.includes(num)
})
return (
<>
<Modal
visible={!!activeUrl}
onOk={closeModal}
footer={null}
onCancel={closeModal}
width="50%"
>
<img
width="100%"
onClick={closeModal}
src={`${host}/output/${clientId}/${activeUrl}`}
alt="large modal"
></img>
</Modal>
<Row
align="middle"
justify="space-around"
style={{ display: 'flex', width: '100%' }}
>
<Typography.Title style={{ margin: '0.5rem 1rem 0.7rem' }} level={3}>
Session Pictures
</Typography.Title>
<Typography.Text>{urls.length}/ 89 loaded</Typography.Text>
<Typography.Text>Select Featured Photos:</Typography.Text>
<Select
mode="multiple"
allowClear
placeholder="Please select featured"
style={{ width: '35%' }}
defaultValue={focusPhotos}
value={focusPhotos}
onChange={handleSelect}
>
{photos.map((name) => {
const val = name.split('_')[0]
return <Select.Option value={val}>{val}</Select.Option>
})}
</Select>
</Row>
<div className="loading-bar-container">
<div
className="loading-bar"
style={{
width: `${u * 100}%`,
background: `hsl(${Math.floor(u * 120)}, 90%, 70%)`,
}}
></div>
</div>
<div className="featured-photos" style={{ marginTop: '2rem' }}>
{filteredPhotos.map((src) => (
<img
onClick={() => setActiveUrl(src)}
src={`${host}/output/${clientId}/${src}`}
alt="lol"
/>
))}
</div>
<div className="photo-wall">
{photos.map((src) => (
<Card key={src} className="photo" title={src.split('_')[0]}>
<img
onClick={() => setActiveUrl(src)}
src={`${host}/output/${clientId}/${src}`}
alt="lol"
/>
</Card>
))}
</div>
<ScrollToTop />
</>
)
}

View File

@@ -0,0 +1,47 @@
import { Tag } from 'antd'
import { PresetColorType } from 'antd/lib/_util/colors'
import { useEffect, useState } from 'react'
import { getStatus } from '../api'
export enum Status {
'Standing By...',
'Warming Up...',
'Capturing Photo',
'Capturing Grid',
'Writing To Disk',
'Downloading!',
}
const colors: Partial<PresetColorType>[] = [
'lime',
'gold',
'volcano',
'magenta',
'geekblue',
]
type Props = {
poll: boolean
}
export const StatusChip = ({ poll }: Props) => {
const [status, setStatus] = useState<Status>(Status['Standing By...'])
useEffect(() => {
const get = async () => {
if (!poll) return
const status = await getStatus()
setStatus(status)
}
const interval = setInterval(get, 1000 / 4)
return () => clearInterval(interval)
}, [poll])
return (
<Tag color={colors[status]} style={{ display: 'flex' }}>
<span style={{ margin: 'auto' }}>{Status[status]}</span>
</Tag>
)
}

21
client/src/data/index.ts Normal file
View File

@@ -0,0 +1,21 @@
import { Client } from '../types'
export const clients: Client[] = [
{
name: 'Elijah',
email: 'elijah@elijah.com',
phone: 4039876543,
has_photos: false,
},
{
name: 'Tanner',
email: 'tanner@tanner.com',
phone: 4031234567,
has_photos: true,
},
]
export const client: Client = clients[0]
export const session: { photos: string[] } = {
photos: ['/images/1.jpg', '/images/2.jpg', '/images/3.jpg', '/images/4.jpg'],
}

View File

@@ -11,3 +11,19 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
form {
display: flex;
max-width: 500px;
margin: auto;
flex-direction: column;
}
form label {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
margin: auto;
}

View File

@@ -1,17 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import 'antd/dist/antd.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
ReactDOM.render(<App />, document.getElementById('root'))
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
reportWebVitals()

View File

@@ -1,45 +1,78 @@
import React from 'react'
import { Button, Divider, Form, Input, message, Row, Typography } from 'antd'
import FormItem from 'antd/lib/form/FormItem'
import { Content } from 'antd/lib/layout/layout'
import { useState } from 'react'
import { useHistory } from 'react-router-dom'
import { createClient } from '../api'
type FormData = {
name: string
email: string
phone: string
}
export const Dashboard = () => {
const history = useHistory()
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [phone, setPhone] = useState('')
const [error, setError] = useState<string | null>(null)
const [form] = Form.useForm<FormData>()
const handleReset = () => {
//
form.resetFields()
}
const handleSubmit = async () => {
// phone number is stripped for numbers
const handleSubmit = async (values: FormData) => {
if (values.phone.length < 10) {
// helpful message
message.error('Check all fields!')
setError('Phone number needs to be a length of at least 10')
return
}
await createClient({ name, email, phone })
history.push(`/sessions/${phone}`)
const client_id = await createClient({
name: values.name,
email: values.email,
phone: parseInt(values.phone.replace(/\D/g, '')),
})
history.push(`/sessions/${client_id}`)
}
return (
<div>
<h1>Dashboard</h1>
<form onSubmit={handleSubmit}>
<label htmlFor="name">
Name:
<input type="text" name="name" />
</label>
<label htmlFor="email">
Email:
<input type="email" name="email" />
</label>
<label htmlFor="phone">
Phone:
<input type="phone" name="phone" />
</label>
<button type="submit">Start Session</button>
<button onClick={handleReset}>Reset</button>
</form>
<div>TODO: List of past sessions for review?</div>
</div>
<Content>
<Typography.Title className="page-head" level={3}>
Dashboard
</Typography.Title>
<Divider />
<Form
form={form}
className="dashboard-form"
onFinish={handleSubmit}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
>
<Typography.Paragraph style={{ textAlign: 'center' }}>
Enter the name, email and phone number of the subject
</Typography.Paragraph>
<FormItem label="name" name="name">
<Input minLength={3} />
</FormItem>
<FormItem label="email" name="email">
<Input type="email" />
</FormItem>
<FormItem label="phone" name="phone">
<Input type="tel" minLength={10} />
</FormItem>
<Row justify="space-between">
<Button danger onClick={handleReset}>
Reset
</Button>
<Button htmlType="submit" type="primary">
Start Session
</Button>
</Row>
{error && <p className="error">{error}</p>}
</Form>
</Content>
)
}

View File

@@ -1,15 +1,142 @@
import React from 'react'
import { useEffect } from 'react'
import { RouteComponentProps } from 'react-router-dom'
import React, { useEffect, useState } from 'react'
import { RouteComponentProps, useHistory } from 'react-router-dom'
import { getClient, killSession, restartSession, startSession } from '../api'
import { SessionPictures } from '../components/SessionPictures'
import { StatusChip } from '../components/StatusChip'
import {
Button,
Input,
InputNumber,
message,
Popconfirm,
Row,
Typography,
} from 'antd'
import { Content } from 'antd/lib/layout/layout'
import { Client } from '../types'
type Props = RouteComponentProps<{ clientId: string }>
export const Session = (props: Props) => {
const history = useHistory()
const { clientId } = props.match.params
const [client, setClient] = useState<Client | null>(null)
const [active, setActive] = useState(false)
const [lightTime, setLightTime] = useState(
parseInt(window.localStorage.getItem('lightTime') || '5000'),
)
const [submitted, setSubmitted] = useState(false)
const handleTimingUpdate = (n: number) => {
window.localStorage.setItem('lightTime', n.toString())
setLightTime(n)
}
useEffect(() => {})
const handleStartSession = async () => {
message.loading('Photo sequence starting! Stand by...')
await startSession(clientId, { light_time: lightTime })
setActive(true)
}
return <div>Session {clientId}</div>
const handleRestartSession = async () => {
setActive(false)
message.loading(
'Deleting photos & restarting capture sequence! Stand by...',
)
await restartSession(clientId, { light_time: lightTime })
setActive(true)
}
const handleExit = async () => {
history.push('/')
}
const handleNuke = async () => {
await killSession(clientId)
message.success('Photos Deleted! Going back to dashboard')
history.push('/')
}
useEffect(() => {
const get = async () => {
const client = await getClient(clientId)
setClient(client)
if (client.has_photos) setActive(true)
}
get()
}, [clientId])
return (
<Content>
<Typography.Title className="page-head" level={3}>
Session View
</Typography.Title>
<Row className="client-info">
<Typography.Text>
<strong>Name:</strong> {client?.name}
</Typography.Text>
<Typography.Text>
<strong>Email:</strong> {client?.email}
</Typography.Text>
<Typography.Text>
<strong>Phone:</strong> {client?.phone}
</Typography.Text>
</Row>
<div className="toolbar">
<Row justify="center" className="session-toolbar">
<Button key="finish" onClick={handleExit}>
Back To Dashboard
</Button>
<Button
key="startsession"
disabled={active}
type="primary"
onClick={handleStartSession}
>
Capture
</Button>
<Popconfirm
disabled={!active}
key="retry"
title="Re-capture set?"
onConfirm={handleRestartSession}
>
<Button type="default" disabled={!active}>
Retry Capture
</Button>
</Popconfirm>
<Popconfirm
key="nuke"
disabled={!active}
title="Delete all photos and return to dashboard?"
onConfirm={handleNuke}
>
<Button danger disabled={!active}>
Abort Session
</Button>
</Popconfirm>
<StatusChip poll={true} />
</Row>
<Row className="session-toolbar">
<h3>Light Duration (ms)</h3>
<InputNumber value={lightTime} onChange={handleTimingUpdate} />
<Input
className="slider"
type="range"
onChange={(e) => handleTimingUpdate(parseInt(e.target.value))}
value={lightTime}
min={500}
max={10000}
step={500}
/>
</Row>
</div>
<Row className="controls">
{active && <SessionPictures clientId={clientId} />}
</Row>
</Content>
)
}

View File

@@ -1,4 +0,0 @@
export default {
apiUrl: "http://localhost/api",
port: 4442,
};

View File

@@ -1,9 +1,14 @@
export type Client = {
name: string;
email: string;
phone: number;
};
name: string
email: string
phone: number
has_photos: boolean
}
export type Session = {
timestamp: number;
};
timestamp: number
}
export type Timings = {
light_time: number // 5000
}

File diff suppressed because it is too large Load Diff

BIN
network-settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

106
server/.gitignore vendored Normal file
View File

@@ -0,0 +1,106 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# Editor
*.swp
*.swo
paramiko.log
output/

34
server/capture.py Normal file
View File

@@ -0,0 +1,34 @@
import socket
import sys
import time
# make sure multicast is being routed to the right interface ie.
# sudo route add -net 224.0.0.0 netmask 240.0.0.0 dev enx00249b649e67
def trigger_capture():
charid = 1
unitid = 1
groupid = 1
gtdate = time.gmtime()
now = str(gtdate.tm_year) + str(gtdate.tm_mon) + str(gtdate.tm_mday) + str(gtdate.tm_hour) + str(gtdate.tm_min) + str(gtdate.tm_sec)
SDATA = str(now)
print('Sending: ' + SDATA)
MCAST_GRP = '224.1.1.1'
MCAST_PORT = 5007
SCMD = chr(charid)
SUNIT = chr(unitid)
SGROUP = chr(groupid)
SEND = SCMD+SUNIT+SGROUP+SDATA
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
dev = 'eth0' + '\0'
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
sock.sendto(SEND.encode('utf-8'), (MCAST_GRP, MCAST_PORT))
sock.close()
print('Sent.')
if __name__ == '__main__':
print('Triggering test capture...')
trigger_capture()

56
server/download.py Normal file
View File

@@ -0,0 +1,56 @@
import os
import settings
import threading
import paramiko
import time
paramiko.util.log_to_file('paramiko.log')
def fake_download(ip, dest):
import requests, random
num = ip.split('.')[3]
r = requests.get('https://picsum.photos/400/300?random=' + num)
time.sleep(random.uniform(2, 10))
with open(dest / (num + '_420.jpg'), 'wb') as f:
f.write(r.content)
def download(ip, dest):
if settings.DEBUG:
fake_download(ip, dest)
print('Downloading from', ip)
port = 22
transport = paramiko.Transport((ip, port))
transport.connect(None, settings.RASPBERRY_USER, settings.RASPBERRY_PASS)
sftp = paramiko.SFTPClient.from_transport(transport)
files = sftp.listdir('/3dscan/')
for f in files:
source_file = '/3dscan/' + f
dest_file = dest / (f + '.tmp')
print('Grabbing file', source_file)
sftp.get(source_file, dest_file)
sftp.remove(source_file)
done_file = dest / f
dest_file.rename(done_file)
if sftp: sftp.close()
if transport: transport.close()
print('Finished downloading from', ip)
def download_all_photos(dest):
if not dest.exists():
raise Exception('Destination does not exist')
print('Downloading all photos to', dest)
for ip in settings.RASPBERRY_IPS:
t = threading.Thread(target=download, args=(ip, dest))
t.start()
if __name__ == '__main__':
from pathlib import Path
download_all_photos(Path('test/'))

168
server/main.py Normal file
View File

@@ -0,0 +1,168 @@
import os
import json
import time
from datetime import datetime
from pathlib import Path
from flask import Flask, request, abort, send_from_directory
from flask_cors import CORS
import power, capture, download, settings
import logging
log = logging.getLogger('werkzeug')
if not settings.DEBUG:
log.setLevel(logging.ERROR)
build_folder = Path('../client/build')
output_folder = Path('./output')
app = Flask(__name__, static_folder=str(build_folder), static_url_path='')
CORS(app)
STANDBY = 0
WARMUP = 1
CAPTURING_PHOTO = 2
CAPTURING_GRID = 3
WRITING = 4
DOWNLOADING = 5
status = STANDBY
@app.route('/api/status', methods=['GET'])
def status_get():
return {'status': status}
@app.route('/api/clients', methods=['POST'])
def clients_post():
content = request.json
phone = str(content['phone'])
for i in range(1, 100):
suffix = str(i).zfill(2)
folder = phone + '_' + suffix
path = output_folder / folder
if not path.exists():
break
content['date'] = datetime.now().strftime('%Y-%m-%d')
content['time'] = datetime.now().strftime('%H:%M:%S')
path.mkdir()
info_file = path / 'info.txt'
info_file.touch()
info_file.write_text(json.dumps(content, indent=4))
client_id = folder
print('POST client:', content, 'cid:', client_id)
return {'client_id': client_id}
@app.route('/api/clients/<cid>', methods=['GET'])
def clients_get(cid):
folder = cid
path = output_folder / cid
if not path.exists():
abort(404)
info_file = path / 'info.txt'
info_text = info_file.read_text()
res = json.loads(info_text)
photo_glob = path.glob('*.jpg')
res['has_photos'] = bool(list(photo_glob))
print('GET client:', cid, 'res:', res)
return res
@app.route('/api/clients/<cid>/session', methods=['GET'])
def session_get(cid):
folder = cid
path = output_folder / cid
if not path.exists():
abort(404)
photo_glob = path.glob('*.jpg')
res = {}
res['photos'] = sorted([x.name for x in photo_glob])
return res
@app.route('/api/clients/<cid>/session', methods=['DELETE'])
def session_delete(cid):
folder = cid
path = output_folder / cid
if not path.exists():
abort(404)
photo_glob = path.glob('*.jpg')
for p in photo_glob:
p.unlink()
print('DELETE session:', cid)
return ''
@app.route('/api/clients/<cid>/session', methods=['POST'])
def session_post(cid):
content = request.json
light_time = content.get('light_time', 5000)
global status
print('POST session:', cid)
folder = cid
path = output_folder / cid
if not path.exists():
abort(404)
# go through the photo taking process
try:
# warmup
status = WARMUP
power.lights_on()
time.sleep(2)
power.lights_off()
time.sleep(1)
except BaseException as e:
print('Problem with lights: {} - {}'.format(e.__class__.__name__, str(e)))
print()
print('Are you sure the system is connected?')
print()
abort(500)
# capture
status = CAPTURING_PHOTO
power.lights_on()
time.sleep(0.1)
capture.trigger_capture()
time.sleep(light_time / 1000)
power.lights_off()
status = WRITING
time.sleep(max(5 - light_time / 1000, 1))
status = DOWNLOADING
download.download_all_photos(path)
time.sleep(3)
status = STANDBY
print('Finished.')
return ''
@app.route('/')
def index():
return app.send_static_file('index.html')
@app.errorhandler(404)
def not_found(e):
return app.send_static_file('index.html')
@app.route('/output/<path:filename>')
def output(filename):
return send_from_directory('output/', filename)
app.run(host='0.0.0.0')

0
server/output/.gitkeep Normal file
View File

53
server/power.py Normal file
View File

@@ -0,0 +1,53 @@
import requests
import settings
import time
def np_02B_api(ip, username, password, is_on):
if settings.DEBUG: return
if is_on:
endpoint = '/cmd.cgi?grp=0'
else:
endpoint = '/cmd.cgi?grp=30'
url = 'http://' + ip + endpoint
r = requests.get(url, auth=(username, password), timeout=4)
r.raise_for_status()
def lights_on():
np_02B_api(settings.LIGHT_IP, settings.LIGHT_USER, settings.LIGHT_PASS, True)
def lights_off():
np_02B_api(settings.LIGHT_IP, settings.LIGHT_USER, settings.LIGHT_PASS, False)
def grid_on():
np_02B_api(settings.GRID_IP, settings.GRID_USER, settings.GRID_PASS, True)
def grid_off():
np_02B_api(settings.GRID_IP, settings.GRID_USER, settings.GRID_PASS, False)
if __name__ == '__main__':
try:
print('Turning lights on...')
lights_on()
print('Waiting three seconds...')
time.sleep(3)
print('Turning lights off...')
lights_off()
except BaseException as e:
print('Problem with lights: {} - {}'.format(e.__class__.__name__, str(e)))
try:
print('Turning grid on...')
grid_on()
print('Waiting three seconds...')
time.sleep(3)
print('Turning grid off...')
grid_off()
except BaseException as e:
print('Problem with grid: {} - {}'.format(e.__class__.__name__, str(e)))

36
server/requirements.txt Normal file
View File

@@ -0,0 +1,36 @@
appdirs==1.4.3
bcrypt==3.2.0
CacheControl==0.12.6
certifi==2019.11.28
cffi==1.14.5
chardet==3.0.4
click==7.1.2
colorama==0.4.3
contextlib2==0.6.0
cryptography==3.4.6
distlib==0.3.0
distro==1.4.0
Flask==1.1.2
Flask-Cors==3.0.10
html5lib==1.0.1
idna==2.8
ipaddr==2.2.0
itsdangerous==1.1.0
Jinja2==2.11.3
lockfile==0.12.2
MarkupSafe==1.1.1
msgpack==0.6.2
packaging==20.3
paramiko==2.7.2
pep517==0.8.2
progress==1.5
pycparser==2.20
PyNaCl==1.4.0
pyparsing==2.4.6
pytoml==0.1.21
requests==2.22.0
retrying==1.3.3
six==1.14.0
urllib3==1.25.8
webencodings==0.5.1
Werkzeug==1.0.1

104
server/settings.py Normal file
View File

@@ -0,0 +1,104 @@
import os
DEBUG = os.environ.get('FLASK_ENV', None) == 'development'
LIGHT_IP = '192.168.99.25'
LIGHT_USER = 'admin'
LIGHT_PASS = 'admin'
GRID_IP = '192.168.99.20'
GRID_USER = 'admin'
GRID_PASS = 'admin'
RASPBERRY_USER = 'root'
RASPBERRY_PASS = '3dscan'
RASPBERRY_IPS = [
'192.168.99.101',
'192.168.99.103',
'192.168.99.104',
'192.168.99.105',
'192.168.99.106',
'192.168.99.107',
'192.168.99.108',
'192.168.99.109',
'192.168.99.110',
'192.168.99.111',
'192.168.99.112',
'192.168.99.113',
'192.168.99.114',
'192.168.99.115',
'192.168.99.116',
'192.168.99.117',
'192.168.99.118',
'192.168.99.119',
'192.168.99.120',
'192.168.99.121',
'192.168.99.122',
'192.168.99.123',
'192.168.99.124',
'192.168.99.125',
'192.168.99.126',
'192.168.99.127',
'192.168.99.128',
'192.168.99.129',
'192.168.99.130',
'192.168.99.131',
'192.168.99.132',
'192.168.99.133',
'192.168.99.134',
'192.168.99.135',
'192.168.99.136',
'192.168.99.137',
'192.168.99.138',
'192.168.99.139',
'192.168.99.140',
'192.168.99.141',
'192.168.99.142',
'192.168.99.143',
'192.168.99.144',
'192.168.99.145',
'192.168.99.146',
'192.168.99.147',
'192.168.99.148',
'192.168.99.149',
'192.168.99.150',
'192.168.99.151',
'192.168.99.152',
'192.168.99.153',
'192.168.99.154',
'192.168.99.155',
'192.168.99.156',
'192.168.99.157',
'192.168.99.158',
'192.168.99.159',
'192.168.99.160',
'192.168.99.161',
'192.168.99.162',
'192.168.99.163',
'192.168.99.164',
'192.168.99.165',
'192.168.99.166',
'192.168.99.167',
'192.168.99.168',
'192.168.99.169',
'192.168.99.170',
'192.168.99.171',
'192.168.99.172',
'192.168.99.173',
'192.168.99.174',
'192.168.99.175',
'192.168.99.176',
'192.168.99.177',
'192.168.99.178',
'192.168.99.179',
'192.168.99.180',
'192.168.99.181',
'192.168.99.182',
'192.168.99.183',
'192.168.99.184',
'192.168.99.185',
'192.168.99.191',
'192.168.99.192',
'192.168.99.193',
'192.168.99.194',
'192.168.99.195',
]

View File

@@ -1,3 +0,0 @@
import socket
import sys
import time