Initial commit
This commit is contained in:
commit
7f52850b2e
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
images/cap*
|
||||
package-lock.json
|
||||
temp
|
||||
dist
|
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
7
.idea/jsLibraryMappings.xml
generated
Normal file
7
.idea/jsLibraryMappings.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<file url="PROJECT" libraries="{Roboto}" />
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="ASK" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/photobox.iml" filepath="$PROJECT_DIR$/.idea/photobox.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
13
.idea/photobox.iml
generated
Normal file
13
.idea/photobox.iml
generated
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Roboto" level="application" />
|
||||
</component>
|
||||
</module>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
25
capture.py
Executable file
25
capture.py
Executable file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import cv2
|
||||
import sys
|
||||
|
||||
cap = cv2.VideoCapture(0)
|
||||
cap.set(3, 1280)
|
||||
cap.set(4, 720)
|
||||
cap.set(cv2.CAP_PROP_FPS, 30) # Setze die Framerate auf 30 fps
|
||||
|
||||
print("Starting")
|
||||
|
||||
doSave = 0
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
#cv2.imshow('frame', frame)
|
||||
|
||||
# Speichern des Frames als JPEG-Bild
|
||||
file_name = f'./temp/picture.jpg'
|
||||
cv2.imwrite(file_name, frame)
|
||||
print("wrote_frame")
|
||||
sys.stdout.flush() # Leere den Ausgabepuffer
|
||||
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
44
html/index.html
Normal file
44
html/index.html
Normal file
@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Photobox</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<script src="../src/web/socket.io.min.js"></script>
|
||||
<script src="../dist/photobox.js"></script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="preload" as="image" href="../images/broken.png">
|
||||
<link rel="preload" as="image" href="../images/lock.png">
|
||||
<link rel="preload" as="image" href="../images/buzzer_blue.png">
|
||||
<link rel="preload" as="image" href="../images/buzzer_red.png">
|
||||
<link rel="stylesheet" href="https://fonts.gaminggeneration.de/Roboto.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- The Modal -->
|
||||
<div id="myModal" class="modal">
|
||||
<!-- Modal content -->
|
||||
<div class="modal-content">
|
||||
<span id="modalClose" class="close">×</span>
|
||||
<h1 id="modalTitle"></h1>
|
||||
<p id="modalText"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="flash"></div>
|
||||
<div id="titleBox">
|
||||
<h1>Fotobox</h1>
|
||||
</div>
|
||||
|
||||
<div id="imageBox"><img id="imageElement"></div>
|
||||
<p id="countdown"></p>
|
||||
<div id="buttonBox">
|
||||
<h2>Foto machen</h2>
|
||||
<img id="capture" src="../images/buzzer_red.png" class="button">
|
||||
<br><br>
|
||||
<h2>Foto löschen</h2>
|
||||
<img id="delete" src="../images/buzzer_blue.png" class="button">
|
||||
</div>
|
||||
<div id="lastImages">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
176
html/style.css
Normal file
176
html/style.css
Normal file
@ -0,0 +1,176 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #2c2d42;
|
||||
font-size: 1.1em;
|
||||
font-family: Roboto, sans-serif;
|
||||
}
|
||||
|
||||
#flash {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top:0;
|
||||
left:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
background-color: #FFFFFF;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
#titleBox {
|
||||
text-align: center;
|
||||
font-size: 2.2em;
|
||||
margin-top: -2.8%;
|
||||
color: #FFFFFF;
|
||||
font-family: Rachana, serif;
|
||||
}
|
||||
|
||||
#imageBox {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top:10%;
|
||||
left:1%;
|
||||
right:20%;
|
||||
bottom: 10%;
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
#buttonBox {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 8%;
|
||||
right: 1%;
|
||||
left: 79%;
|
||||
width: 20%;
|
||||
height: 85%;
|
||||
text-align: center;
|
||||
}
|
||||
#buttonBox h2 {
|
||||
margin-left: -5%;
|
||||
color: white;
|
||||
margin-bottom: -2%;
|
||||
}
|
||||
|
||||
#imageElement {
|
||||
width: 90%;
|
||||
height: 85%;
|
||||
box-shadow: 8px 8px #1B1B23;
|
||||
margin-top: -0.5%;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#lastImages {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 2%;
|
||||
width: 98%;
|
||||
overflow: hidden;
|
||||
height: 17%;
|
||||
display: grid;
|
||||
text-align: center;
|
||||
grid-template-columns: repeat(7, calc(100% / 7));
|
||||
grid-template-rows: 100%;
|
||||
grid-column-gap: 2%;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.lastImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slowFadeInAnimation {
|
||||
animation: fadeIn 1s linear forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.lastImageImg {
|
||||
width: 100%;
|
||||
height: 79%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.lastImageLock {
|
||||
position: absolute;
|
||||
width: 10%;
|
||||
top: 65%;
|
||||
left: 92%;
|
||||
}
|
||||
|
||||
#countdown {
|
||||
display: inline;
|
||||
position: fixed;
|
||||
top:10%;
|
||||
left:1%;
|
||||
right:20%;
|
||||
bottom: 10%;
|
||||
text-align: center;
|
||||
font-size: 10em;
|
||||
z-index: 99;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
|
||||
/* Modal */
|
||||
|
||||
.modal {
|
||||
display: none; /* Hidden by default */
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1; /* Sit on top */
|
||||
padding-top: 100px; /* Location of the box */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgb(0,0,0); /* Fallback color */
|
||||
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
|
||||
}
|
||||
|
||||
/* Modal Content */
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 70%;
|
||||
text-align: center;
|
||||
font-size: 1.3em;
|
||||
font-family: "Roboto Light", sans-serif;;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
/* The Close Button */
|
||||
.close {
|
||||
color: #aaaaaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
BIN
images/broken.png
Normal file
BIN
images/broken.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
images/buzzer_blue.png
Normal file
BIN
images/buzzer_blue.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
images/buzzer_red.png
Normal file
BIN
images/buzzer_red.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
images/lock.png
Normal file
BIN
images/lock.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
4
mount.sh
Normal file
4
mount.sh
Normal file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
echo "Add this script to autostart"
|
||||
sudo mount -t tmpfs -o size=2m tmpfs ./temp/
|
||||
echo "Mounted!"
|
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "photobox",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"author": "Tobias Hopp <tobi@gaminggeneration.de>",
|
||||
"dependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^20.2.1",
|
||||
"@types/node-webcam": "^0.8.0",
|
||||
"@types/socket.io": "^3.0.2",
|
||||
"canvas": "^2.11.2",
|
||||
"express": "^4.18.2",
|
||||
"node-webcam": "^0.8.1",
|
||||
"socket.io": "^4.6.1",
|
||||
"socket.io-client": "^4.6.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc; webpack",
|
||||
"watch": "tsc -w",
|
||||
"start": "npm run build && electron ./dist/main.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/electron": "^1.6.10",
|
||||
"electron": "^24.3.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^5.0.4",
|
||||
"webpack": "^5.83.1",
|
||||
"webpack-cli": "^5.1.1"
|
||||
}
|
||||
}
|
85
src/file_handler.ts
Normal file
85
src/file_handler.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
export class FileHandler {
|
||||
public static saveBase64Photo(data: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Entferne das Präfix "data:image/jpeg;base64,"
|
||||
const base64Data = data.replace(/^data:image\/jpeg;base64,/, '');
|
||||
|
||||
// Konvertiere den Base64-String in ein Buffer-Objekt
|
||||
const imageBuffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
const date = new Date(); // cap_2022-06-07T23_02_23.jpeg
|
||||
const filename = "cap_" + date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() + "T" + date.getHours() + "_" + date.getMinutes() + "_" + date.getSeconds() + ".jpeg";
|
||||
|
||||
try {
|
||||
fs.mkdirSync("./images/");
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
// Speichere den Buffer als JPEG-Datei
|
||||
fs.writeFile("./images/" + filename, imageBuffer, (err) => {
|
||||
if (err) {
|
||||
console.error('Error on saving image', err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('Image saved as: ', filename);
|
||||
resolve(filename);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public static getLastImages(): Promise<{ image: string, locked: boolean }[]> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let files = await this.getSortedFiles("./images/");
|
||||
console.log(files);
|
||||
let i = 0;
|
||||
let data: { image: string, locked: boolean }[] = [];
|
||||
for (let file of files) {
|
||||
if (i == 7) break;
|
||||
|
||||
let d = await fs.promises.readFile("./images/" + file);
|
||||
let locked = ((await fs.promises.stat("./images/" + file)).birthtime.getTime() + 30000 < Date.now());
|
||||
data.push({image: "data:image/jpeg;base64," + d.toString("base64"), locked: locked});
|
||||
i++;
|
||||
}
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
}
|
||||
|
||||
public static getTempPicture() {
|
||||
return "data:image/jpeg;base64," + (fs.readFileSync('./temp/picture.jpg').toString("base64"));
|
||||
}
|
||||
|
||||
public static deleteLastImage() {
|
||||
return new Promise(async resolve => {
|
||||
let files = await this.getSortedFiles("./images/");
|
||||
await fs.promises.unlink("./images/" + files[0]);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private static getSortedFiles(dir: string): Promise<string[]> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const files = await fs.promises.readdir(dir);
|
||||
|
||||
resolve(files
|
||||
.map(fileName => ({
|
||||
name: fileName,
|
||||
time: fs.statSync(`${dir}/${fileName}`).mtime.getTime(),
|
||||
}))
|
||||
.sort((a, b) => a.time - b.time)
|
||||
.reverse()
|
||||
.map(file => file.name)
|
||||
.filter((element) => element.startsWith("cap_") )
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
129
src/gpio.ts
Normal file
129
src/gpio.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
export class GPIO {
|
||||
|
||||
private static openPorts: GPIO[] = [];
|
||||
private readonly gpioPath: string;
|
||||
private readonly gpioPin: number;
|
||||
|
||||
public constructor(pin: number, direction: 'in' | 'out') {
|
||||
const gpioPath = `/sys/class/gpio/gpio${pin}`;
|
||||
this.gpioPath = gpioPath;
|
||||
this.gpioPin = pin;
|
||||
|
||||
try {
|
||||
fs.accessSync(gpioPath);
|
||||
} catch( e )
|
||||
{
|
||||
throw new GPIOError("GPIO not accessible");
|
||||
}
|
||||
|
||||
// Überprüfe, ob der GPIO-Pin bereits exportiert ist
|
||||
if (!fs.existsSync(gpioPath)) {
|
||||
this.export();
|
||||
}
|
||||
|
||||
// Setze den Richtungsmodus auf Ausgang
|
||||
fs.writeFileSync(`${gpioPath}/direction`, direction);
|
||||
GPIO.openPorts.push(this);
|
||||
}
|
||||
|
||||
public static getPin(pin: number, direction: 'in' | 'out'): GPIO {
|
||||
let port = new GPIO(pin, direction);
|
||||
this.openPorts.push(port);
|
||||
return port;
|
||||
}
|
||||
|
||||
public static watchForButton(pin: number, onPress: (pin: number) => void) {
|
||||
const button = new GPIO(pin, 'in');
|
||||
this.openPorts.push(button);
|
||||
|
||||
|
||||
button.watch((err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
onPress(pin);
|
||||
}, 100, 500);
|
||||
}
|
||||
|
||||
public static unexportAll() {
|
||||
for (let p of this.openPorts) {
|
||||
try {
|
||||
p.unexport();
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public getValue(): number {
|
||||
// Überprüfe, ob der GPIO-Pin bereits exportiert ist
|
||||
if (!fs.existsSync(this.gpioPath)) {
|
||||
throw new GPIOError('Pin not exported');
|
||||
}
|
||||
|
||||
// Lese den Wert des GPIO-Pins
|
||||
const value = fs.readFileSync(`${this.gpioPath}/value`, 'utf8');
|
||||
|
||||
return Number.parseInt(value.trim());
|
||||
}
|
||||
|
||||
public setValue(value: number) {
|
||||
// Setze den Wert auf X
|
||||
fs.writeFileSync(`${(this.gpioPath)}/value`, String(value));
|
||||
}
|
||||
|
||||
public setHigh() {
|
||||
this.setValue(1);
|
||||
}
|
||||
|
||||
public setLow() {
|
||||
this.setValue(0);
|
||||
}
|
||||
|
||||
public changeDirection(direction: 'in' | 'out') {
|
||||
// Setze den Richtungsmodus auf Ausgang
|
||||
fs.writeFileSync(`${this.gpioPath}/direction`, direction);
|
||||
}
|
||||
|
||||
public watch(callback: (value: number) => void, interval: number = 100, bounceWait = 500) {
|
||||
let lastState = this.getValue();
|
||||
let lastBounce = Date.now();
|
||||
setInterval(() => {
|
||||
if (lastBounce + bounceWait > Date.now())
|
||||
return;
|
||||
|
||||
let val = this.getValue();
|
||||
if (lastState != val) {
|
||||
callback(val);
|
||||
lastState = val;
|
||||
}
|
||||
|
||||
}, interval);
|
||||
}
|
||||
|
||||
private export() {
|
||||
// Exportiere den GPIO-Pin
|
||||
fs.writeFileSync('/sys/class/gpio/export', this.gpioPin.toString());
|
||||
}
|
||||
|
||||
private unexport() {
|
||||
// Exportiere den GPIO-Pin
|
||||
fs.writeFileSync('/sys/class/gpio/unexport', this.gpioPin.toString());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export class GPIOError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(msg);
|
||||
|
||||
// Set the prototype explicitly.
|
||||
Object.setPrototypeOf(this, GPIOError.prototype);
|
||||
}
|
||||
}
|
110
src/main.ts
Normal file
110
src/main.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import {app, BrowserWindow} from "electron";
|
||||
import * as path from "path";
|
||||
import {Server} from "./server";
|
||||
import * as process from "process";
|
||||
import {GPIO} from "./gpio";
|
||||
import {PythonWebcam} from "./python_webcam";
|
||||
import {FileHandler} from "./file_handler";
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
height: 1080,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
width: 1920,
|
||||
fullscreenable: true,
|
||||
fullscreen: true,
|
||||
closable: true,
|
||||
autoHideMenuBar: true,
|
||||
});
|
||||
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadFile(path.join(__dirname, "../html/index.html"));
|
||||
|
||||
// Open the DevTools.
|
||||
//mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on("window-all-closed", () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// In this file you can include the rest of your app"s specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
|
||||
Server.initServer().then();
|
||||
PythonWebcam.startPythonService();
|
||||
|
||||
/*setInterval(() => {
|
||||
capture()
|
||||
}, 1000*10);*/
|
||||
|
||||
|
||||
|
||||
try {
|
||||
// Capture button
|
||||
GPIO.watchForButton(10, (pin) => {
|
||||
Main.capture()
|
||||
});
|
||||
|
||||
// Delete button
|
||||
GPIO.watchForButton(11, (pin) => {
|
||||
console.log("Delete fired!");
|
||||
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Could not bind gpio ports");
|
||||
}
|
||||
|
||||
process.on("SIGINT", _ => {
|
||||
GPIO.unexportAll();
|
||||
});
|
||||
|
||||
|
||||
export class Main {
|
||||
|
||||
private static port = GPIO.getPin(20, 'out')
|
||||
static async delete_last() {
|
||||
console.log("Delete fired!");
|
||||
|
||||
let last_shots = await FileHandler.getLastImages();
|
||||
let last = last_shots[0];
|
||||
|
||||
if(last.locked) {
|
||||
Server.io.emit("deleting", false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Server.io.emit("deleting", true);
|
||||
await FileHandler.deleteLastImage();
|
||||
await Server.sendRecentPhotos();
|
||||
}
|
||||
}
|
||||
|
||||
static capture() {
|
||||
console.log("Capture fired!");
|
||||
Server.io.emit("capturing", 3);
|
||||
|
||||
setTimeout( async () => {
|
||||
this.port.setHigh();
|
||||
}, 2300 );
|
||||
setTimeout(async () => {
|
||||
let base64_string = await PythonWebcam.captureImage();
|
||||
Server.io.emit("captured", base64_string);
|
||||
this.port.setLow();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
}
|
0
src/preload.ts
Normal file
0
src/preload.ts
Normal file
73
src/python_webcam.ts
Normal file
73
src/python_webcam.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import * as child_process from "child_process";
|
||||
import {FileHandler} from "./file_handler";
|
||||
import {Server} from "./server";
|
||||
|
||||
const {spawn} = require('node:child_process');
|
||||
|
||||
|
||||
export class PythonWebcam {
|
||||
private static filename = __dirname + "/../capture.py";
|
||||
private static waitFor = "wrote_frame";
|
||||
private static calls: Function[] = [];
|
||||
private static noCamera = true;
|
||||
|
||||
private static process: child_process.ChildProcess;
|
||||
|
||||
public static startPythonService() {
|
||||
this.process = spawn("python", [this.filename], {stdio: 'pipe'});
|
||||
this.process.stdout.setEncoding('utf-8');
|
||||
console.log("spawned")
|
||||
|
||||
this.process.stdout.on('data', (data) => {
|
||||
this.noCamera = false;
|
||||
const line = data.trim().split('\n')[0].replace(/\r?\n|\r/gm, "").replace("\n", '');
|
||||
if (line.startsWith(this.waitFor.trim())) {
|
||||
for (let c of this.calls) {
|
||||
c();
|
||||
}
|
||||
this.calls = [];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
this.process.on("exit", (code) => {
|
||||
console.log("Subprocess exited! " + code)
|
||||
if (code != 0)
|
||||
{
|
||||
this.noCamera = true;
|
||||
throw new Error("Could not open camera device/script - Code: " + code.toString());
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public static getImage(): Promise<String | Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if( this.calls.length > 1 )
|
||||
{
|
||||
return reject("Already capturing!");
|
||||
}
|
||||
this.waitForScriptFrame(() => {
|
||||
resolve(FileHandler.getTempPicture());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static captureImage(): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let img = (await this.getImage()).toString();
|
||||
await FileHandler.saveBase64Photo(img);
|
||||
resolve(img);
|
||||
await Server.sendRecentPhotos();
|
||||
});
|
||||
}
|
||||
|
||||
private static waitForScriptFrame(callback: () => void) {
|
||||
if(this.noCamera)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.calls.push(callback);
|
||||
}
|
||||
}
|
0
src/renderer.ts
Normal file
0
src/renderer.ts
Normal file
64
src/server.ts
Normal file
64
src/server.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import {Socket} from "socket.io";
|
||||
import {FileHandler} from "./file_handler";
|
||||
import {PythonWebcam} from "./python_webcam";
|
||||
import {Main} from "./main";
|
||||
|
||||
export class Server {
|
||||
private static app = require("express")();
|
||||
private static server = require('http').createServer(this.app);
|
||||
public static io = require('socket.io')(this.server);
|
||||
private static interval : NodeJS.Timer;
|
||||
|
||||
public static initServer(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Socket.IO-Verbindung herstellen
|
||||
this.io.on('connection', async (socket: Socket) => {
|
||||
console.log('Client connected');
|
||||
await this.sendRecentPhotos();
|
||||
|
||||
socket.on("do_shot", () => {
|
||||
console.log("Capture fired!");
|
||||
Server.io.emit("capturing", "");
|
||||
|
||||
setTimeout(async () => {
|
||||
let base64_string = await PythonWebcam.captureImage();
|
||||
Server.io.emit("captured", base64_string);
|
||||
}, 3000);
|
||||
});
|
||||
socket.on("do_delete", () => {
|
||||
Main.delete_last();
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
//Webcam.startCameraStream();
|
||||
this.server.listen(3000);
|
||||
|
||||
this.startInterval();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public static async sendRecentPhotos()
|
||||
{
|
||||
let lastImages = await FileHandler.getLastImages();
|
||||
console.log("Sending last shots... " );
|
||||
this.io.emit('last_shots', lastImages);
|
||||
}
|
||||
|
||||
|
||||
public static startInterval(){
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
this.io.emit("stream", (await PythonWebcam.getImage()));
|
||||
} catch( e )
|
||||
{
|
||||
}
|
||||
this.startInterval();
|
||||
}, 1000/30);
|
||||
}
|
||||
|
||||
|
||||
}
|
126
src/web/main.ts
Normal file
126
src/web/main.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import {io} from "socket.io-client";
|
||||
|
||||
|
||||
let isShowingCaptured = false;
|
||||
|
||||
|
||||
function openModal(title: string, text: string, closeTimeout = 4000 )
|
||||
{
|
||||
let modal = document.getElementById("myModal");
|
||||
let modalTitle = document.getElementById("modalTitle");
|
||||
let modalText = document.getElementById("modalText");
|
||||
modalTitle.innerText = title;
|
||||
modalText.innerHTML = text;
|
||||
modal.style.display = "block";
|
||||
let t = setTimeout( () => {
|
||||
modal.style.display = "none";
|
||||
}, closeTimeout );
|
||||
|
||||
|
||||
let modalClose = document.getElementById("modalClose");
|
||||
modalClose.onclick = () => {
|
||||
clearTimeout(t);
|
||||
modal.style.display = "none";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const imageElement = document.getElementById('imageElement') as HTMLImageElement;
|
||||
const lastImages = document.getElementById('lastImages') as HTMLImageElement;
|
||||
const countdown = document.getElementById("countdown") as HTMLParagraphElement;
|
||||
const flash = document.getElementById("flash");
|
||||
const capture_btn = document.getElementById("capture");
|
||||
const del_btn = document.getElementById("delete");
|
||||
|
||||
let socket = io("http://localhost:3000");
|
||||
|
||||
socket.on("stream", (data) => {
|
||||
//console.log(data);
|
||||
if (isShowingCaptured) return;
|
||||
|
||||
imageElement.src = data;
|
||||
});
|
||||
|
||||
socket.on("last_shots", (data: { image: string, locked: boolean }[]) => {
|
||||
lastImages.innerHTML = "";
|
||||
for (let last of data) {
|
||||
let img = document.createElement("img");
|
||||
img.src = last.image;
|
||||
img.classList.add("lastImageImg");
|
||||
img.alt = "Bild wird geladen...";
|
||||
|
||||
let lock = document.createElement("img");
|
||||
lock.src = last.locked ? "../images/lock.png" : "../images/broken.png";
|
||||
lock.classList.add("lastImageLock");
|
||||
lock.alt = "[Lock]";
|
||||
|
||||
let div = document.createElement("div");
|
||||
div.classList.add("lastImage");
|
||||
div.append(img);
|
||||
div.onclick = () => {
|
||||
openModal("Foto Ansicht", `<img src="${last.image}" style="width: 90%">`, 1000 * 15 );
|
||||
}
|
||||
|
||||
div.append(lock);
|
||||
|
||||
if (data[0] == last) {
|
||||
div.classList.add("slowFadeInAnimation");
|
||||
setTimeout(() => lastImages.prepend(div), 500);
|
||||
} else
|
||||
lastImages.append(div);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
socket.on('capturing', (data) => {
|
||||
countdown.innerText = "3";
|
||||
setTimeout(() => {
|
||||
countdown.innerText = "2";
|
||||
setTimeout(() => {
|
||||
countdown.innerText = "1";
|
||||
setTimeout(() => {
|
||||
countdown.innerText = "CHEESE!";
|
||||
setTimeout(() => {
|
||||
flash.style.display = "block";
|
||||
}, 150);
|
||||
}, 800);
|
||||
}, 950);
|
||||
}, 1000)
|
||||
});
|
||||
|
||||
socket.on('captured', (data) => {
|
||||
isShowingCaptured = true;
|
||||
flash.style.display = "none";
|
||||
|
||||
imageElement.src = data;
|
||||
setTimeout(() => {
|
||||
countdown.innerText = "";
|
||||
}, 250);
|
||||
|
||||
setTimeout(() => isShowingCaptured = false, 3000);
|
||||
});
|
||||
|
||||
socket.on('deleting', (data: boolean) => {
|
||||
if(!data)
|
||||
{
|
||||
openModal("Löschen nicht möglich!", "Foto kann nicht entfernt werden, da es bereits zu alt ist.<br>Wende dich bei Fragen an Tobi");
|
||||
}
|
||||
else
|
||||
{
|
||||
openModal("Löschen erfolgreich!", "Foto wurde unwiderruflich entfernt.");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
capture_btn.onclick = () => {
|
||||
socket.emit("do_shot");
|
||||
}
|
||||
del_btn.onclick = () => {
|
||||
socket.emit("do_delete");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
7
src/web/socket.io.min.js
vendored
Normal file
7
src/web/socket.io.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
56
src/webcam.ts
Normal file
56
src/webcam.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import {FSWebcam, ImageSnapWebcam, WebcamOptions, WindowsWebcam} from "node-webcam";
|
||||
import * as fs from "fs";
|
||||
import {FileHandler} from "./file_handler";
|
||||
import {Server} from "./server";
|
||||
|
||||
const NodeWebcam = require("node-webcam");
|
||||
|
||||
export class Webcam {
|
||||
// Konfiguriere Kamera
|
||||
private static videoDevice = '/dev/video0';
|
||||
private static camera: ImageSnapWebcam | FSWebcam | WindowsWebcam = null;
|
||||
private static width = 1280;
|
||||
private static height = 720;
|
||||
|
||||
|
||||
public static startCameraStream() {
|
||||
const webcamOptions = {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
quality: 100,
|
||||
output: "jpeg",
|
||||
callbackReturn: "base64",
|
||||
verbose: false,
|
||||
device: this.videoDevice
|
||||
} as WebcamOptions;
|
||||
this.camera = NodeWebcam.create(webcamOptions);
|
||||
}
|
||||
|
||||
public static getImage(): Promise<String | Buffer> {
|
||||
return new Promise(resolve => {
|
||||
if( !fs.existsSync("./temp/") )
|
||||
fs.mkdirSync("./temp/");
|
||||
|
||||
this.camera.capture("./temp/picture.jpg", function (err, data) {
|
||||
if (!err) {
|
||||
resolve(data);
|
||||
} else {
|
||||
console.error("Error on webcam capture: ", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static captureImage(): Promise<string>
|
||||
{
|
||||
return new Promise(async (resolve,reject) => {
|
||||
let img = (await this.getImage() ).toString();
|
||||
await FileHandler.saveBase64Photo(img);
|
||||
resolve(img);
|
||||
await Server.sendRecentPhotos();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": [
|
||||
"node_modules/*"
|
||||
]
|
||||
},
|
||||
"removeComments": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
22
webpack.config.js
Normal file
22
webpack.config.js
Normal file
@ -0,0 +1,22 @@
|
||||
// webpack.config.js
|
||||
module.exports = [
|
||||
{
|
||||
mode: 'development',
|
||||
entry: './src/web/main.ts',
|
||||
devtool: "inline-source-map",
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.ts$/,
|
||||
include: /src/,
|
||||
use: [{ loader: 'ts-loader' }]
|
||||
}]
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js"],
|
||||
},
|
||||
output: {
|
||||
path: __dirname + '/dist',
|
||||
filename: 'photobox.js'
|
||||
}
|
||||
}
|
||||
];
|
2468
yarn-error.log
Normal file
2468
yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user