init
This commit is contained in:
3
server/.gitignore
vendored
Normal file
3
server/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store
|
3881
server/package-lock.json
generated
Normal file
3881
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
server/package.json
Normal file
30
server/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "grow-bot",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"serve": "npm run watch | nodemon dist/main.js",
|
||||
"watch": "npm run build && tsc -w"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@types/node": "^14.0.26",
|
||||
"configstore": "^5.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^1.8.2",
|
||||
"express": "^4.16.4",
|
||||
"opencv4nodejs": "^5.6.0",
|
||||
"pg": "^8.3.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"socket.io": "^2.3.0",
|
||||
"typeorm": "^0.2.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.16.1",
|
||||
"nodemon": "^1.19.0",
|
||||
"typescript": "^3.4.5"
|
||||
}
|
||||
}
|
61
server/src/controllers/fanController.ts
Normal file
61
server/src/controllers/fanController.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import {ClimateService} from "../services/climateService";
|
||||
import ConfigStore from 'configstore';
|
||||
import {Express} from "express";
|
||||
|
||||
export function FanController(app: Express, config: ConfigStore, climate: ClimateService) {
|
||||
const ENDPOINT = '/fan'
|
||||
|
||||
app.get(ENDPOINT, get)
|
||||
app.post(ENDPOINT, post);
|
||||
app.put(ENDPOINT, put);
|
||||
|
||||
let onCron, offCron;
|
||||
|
||||
function cron() {
|
||||
if(config.get('climate.autoFan')) {
|
||||
if (onCron != null) {
|
||||
onCron.stop();
|
||||
onCron = null;
|
||||
}
|
||||
} else if (onCron != null || offCron != null) {
|
||||
onCron.stop();
|
||||
onCron = null;
|
||||
offCron.stop();
|
||||
offCron = null;
|
||||
}
|
||||
}
|
||||
|
||||
function get(req, res) {
|
||||
let resp = {
|
||||
on: climate.fanState(),
|
||||
autoFan: config.get('climate.autoFan'),
|
||||
fanMode: config.get('climate.fanMode'),
|
||||
fanOn: config.get('climate.fanOn'),
|
||||
fanOff: config.get('climate.fanOff'),
|
||||
fanTemp: config.get('climate.fanTemp'),
|
||||
fanHumidity: config.get('climate.fanHumidity'),
|
||||
};
|
||||
res.json(resp);
|
||||
}
|
||||
|
||||
function post(req, res) {
|
||||
console.log('Toggling fan');
|
||||
climate.toggleFan();
|
||||
get(req, res);
|
||||
}
|
||||
|
||||
function put(req, res) {
|
||||
console.log('Updating fan config');
|
||||
console.log(req.body);
|
||||
if(req.body['autoFan'] != null) config.set('climate.autoFan', req.body['autoFan']);
|
||||
if(req.body['fanMode'] != null) config.set('climate.fanMode', req.body['fanMode']);
|
||||
if(req.body['fanOn'] != null) config.set('climate.fanOn', req.body['fanOn']);
|
||||
if(req.body['fanOff'] != null) config.set('climate.fanOff', req.body['fanOff']);
|
||||
if(req.body['fanTemp'] != null) config.set('climate.fanTemp', req.body['fanTemp']);
|
||||
if(req.body['fanHumidity'] != null) config.set('climate.fanHumidity', req.body['fanHumidity']);
|
||||
cron();
|
||||
get(req, res)
|
||||
}
|
||||
|
||||
cron();
|
||||
}
|
36
server/src/controllers/lightController.ts
Normal file
36
server/src/controllers/lightController.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import {ClimateService} from "../services/climateService";
|
||||
import ConfigStore from 'configstore';
|
||||
import {Express} from "express";
|
||||
|
||||
export function LightController(app: Express, config: ConfigStore, climate: ClimateService) {
|
||||
const ENDPOINT = '/light'
|
||||
|
||||
app.get(ENDPOINT, get)
|
||||
app.post(ENDPOINT, post);
|
||||
app.put(ENDPOINT, put);
|
||||
|
||||
function get(req, res) {
|
||||
let resp = {
|
||||
on: climate.lightState(),
|
||||
autoLight: config.get('climate.autoLight'),
|
||||
lightOn: config.get('climate.lightOn'),
|
||||
lightOff: config.get('climate.lightOff')
|
||||
};
|
||||
res.json(resp);
|
||||
}
|
||||
|
||||
function post(req, res) {
|
||||
console.log('Toggling light');
|
||||
climate.toggleLight();
|
||||
get(req, res);
|
||||
}
|
||||
|
||||
function put(req, res) {
|
||||
console.log('Updating light config');
|
||||
console.log(req.body);
|
||||
if(req.body['autoLight'] != null) config.set('climate.autoLight', req.body['autoLight']);
|
||||
if(req.body['lightOn'] != null) config.set('climate.lightOn', req.body['lightOn']);
|
||||
if(req.body['lightOff'] != null) config.set('climate.lightOff', req.body['lightOff']);
|
||||
get(req, res)
|
||||
}
|
||||
}
|
77
server/src/controllers/timelapseController.ts
Normal file
77
server/src/controllers/timelapseController.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import express, {Express} from 'express';
|
||||
import ConfigStore from 'configstore';
|
||||
import fs from 'fs';
|
||||
import {environment} from "../environments/environment";
|
||||
import {CameraService} from "../services/cameraService";
|
||||
import {CronJob} from 'cron';
|
||||
|
||||
export function TimelapseController(app: Express, config: ConfigStore, camera: CameraService) {
|
||||
const SAVE_DIR = environment.imageDir;
|
||||
const ENDPOINT = '/timelapse'
|
||||
|
||||
app.use('/images', express.static(SAVE_DIR))
|
||||
app.delete(ENDPOINT + '/:filename', del);
|
||||
app.get(ENDPOINT, get)
|
||||
app.post(ENDPOINT, post);
|
||||
app.put(ENDPOINT, put);
|
||||
|
||||
let timelapseCron: CronJob;
|
||||
|
||||
function cron() {
|
||||
if(config.get('camera.timelapseEnabled') == true) {
|
||||
if(timelapseCron != null) {
|
||||
timelapseCron.stop();
|
||||
timelapseCron = null;
|
||||
}
|
||||
|
||||
timelapseCron = new CronJob(config.get('camera.timelapseFrequency'), () => {
|
||||
console.log('Snapping timelapse picture')
|
||||
let date = new Date();
|
||||
let image = camera.snap();
|
||||
let filename = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}.jpg`;
|
||||
fs.writeFileSync(`${SAVE_DIR}/${filename}`, image, 'base64');
|
||||
});
|
||||
timelapseCron.start();
|
||||
} else if(timelapseCron != null) {
|
||||
timelapseCron.stop();
|
||||
timelapseCron = null;
|
||||
}
|
||||
}
|
||||
|
||||
function del(req, res) {
|
||||
let filename = req.params.filename;
|
||||
console.log(`Deleting ${filename}`);
|
||||
fs.unlinkSync(`${SAVE_DIR}/${filename}`);
|
||||
get(req, res);
|
||||
}
|
||||
|
||||
function get(req, res) {
|
||||
let resp = {
|
||||
timelapseEnabled: config.get('camera.timelapseEnabled'),
|
||||
timelapseFrequency: config.get('camera.timelapseFrequency'),
|
||||
files: fs.readdirSync(SAVE_DIR)
|
||||
};
|
||||
res.json(resp);
|
||||
}
|
||||
|
||||
function post(req, res) {
|
||||
console.log('Snapping picture')
|
||||
let date = new Date();
|
||||
let image = camera.snap();
|
||||
let filename = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}.jpg`;
|
||||
fs.writeFileSync(`${SAVE_DIR}/${filename}`, image, 'base64');
|
||||
get(req, res);
|
||||
}
|
||||
|
||||
function put(req, res) {
|
||||
console.log('Updating timelapse');
|
||||
console.log(req.body);
|
||||
if(req.body['timelapseEnabled'] != null) config.set('camera.timelapseEnabled', req.body['timelapseEnabled']);
|
||||
if(req.body['timelapseFrequency'] != null) config.set('camera.timelapseFrequency', req.body['timelapseFrequency']);
|
||||
cron();
|
||||
get(req, res);
|
||||
}
|
||||
|
||||
if(!fs.existsSync(SAVE_DIR)) fs.mkdirSync(SAVE_DIR);
|
||||
cron();
|
||||
}
|
23
server/src/environments/environment.ts
Normal file
23
server/src/environments/environment.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const environment = {
|
||||
cors: 'http://localhost:4200',
|
||||
imageDir: __dirname + '/../images',
|
||||
port: 5000,
|
||||
defaultConfig: {
|
||||
camera: {
|
||||
timelapseEnabled: true,
|
||||
timelapseFrequency: '0 0 12 * * *' // '0 0 12 * * *'
|
||||
},
|
||||
climate: {
|
||||
autoFan: false,
|
||||
fanMode: 'time',
|
||||
fanOn: null,
|
||||
fanOff: null,
|
||||
fanTemp: null,
|
||||
fanHumidity: null,
|
||||
autoLight: false,
|
||||
lightOn: null,
|
||||
lightOff: null,
|
||||
logRate: '1m',
|
||||
}
|
||||
}
|
||||
}
|
34
server/src/main.ts
Normal file
34
server/src/main.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import ConfigStore from 'configstore';
|
||||
import express from 'express';
|
||||
import {environment} from './environments/environment';
|
||||
import {CameraService} from "./services/cameraService";
|
||||
import SocketIO from 'socket.io';
|
||||
import * as http from 'http';
|
||||
import CORS from 'cors';
|
||||
import {CameraConnectionService} from "./services/cameraConnectionService";
|
||||
import {TimelapseController} from "./controllers/timelapseController";
|
||||
import {ClimateService} from "./services/climateService";
|
||||
import {FanController} from "./controllers/fanController";
|
||||
import {LightController} from "./controllers/lightController";
|
||||
|
||||
// Configuration
|
||||
const app = express()
|
||||
const server = http.createServer(app);
|
||||
const socket = SocketIO(server);
|
||||
app.use(express.json());
|
||||
app.use(CORS({origin: environment.cors, credentials: true}));
|
||||
const config = new ConfigStore('grow-bot', environment.defaultConfig);
|
||||
config.set(environment.defaultConfig);
|
||||
|
||||
// Services
|
||||
const camera = new CameraService();
|
||||
const cameraConnectionService = new CameraConnectionService(socket, camera);
|
||||
const climateService = new ClimateService();
|
||||
|
||||
// Controllers
|
||||
FanController(app, config, climateService);
|
||||
LightController(app, config, climateService);
|
||||
TimelapseController(app, config, camera);
|
||||
|
||||
// Start server
|
||||
server.listen(environment.port, () => console.log(`Starting Server: http://localhost:${environment.port}`));
|
17
server/src/models/image.ts
Normal file
17
server/src/models/image.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
export class Image {
|
||||
constructor(private image) {}
|
||||
|
||||
save(path: string) {
|
||||
return fs.writeFileSync(path, this.image)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.image;
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.image;
|
||||
}
|
||||
}
|
22
server/src/services/cameraConnectionService.ts
Normal file
22
server/src/services/cameraConnectionService.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {CameraService} from "./cameraService";
|
||||
|
||||
export class CameraConnectionService {
|
||||
private readonly FPS = 4;
|
||||
|
||||
private broadcast;
|
||||
|
||||
constructor(private socket, private camera: CameraService) {
|
||||
this.socket.on('connection', (s) => {
|
||||
let address = s.request.connection.remoteAddress;
|
||||
console.log(`Client Connecting: ${address}`)
|
||||
})
|
||||
this.beginBroadcast();
|
||||
}
|
||||
|
||||
beginBroadcast() {
|
||||
this.broadcast = setInterval(() => {
|
||||
let frame = this.camera.snap();
|
||||
this.socket.emit('stream', frame);
|
||||
}, 1000 / this.FPS);
|
||||
}
|
||||
}
|
17
server/src/services/cameraService.ts
Normal file
17
server/src/services/cameraService.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import cv from 'opencv4nodejs';
|
||||
import {Image} from "../models/image";
|
||||
|
||||
export class CameraService {
|
||||
private capture;
|
||||
|
||||
constructor(private resolution: [number, number] = [1920, 1080]) {
|
||||
this.capture = new cv.VideoCapture(0);
|
||||
this.capture.set(cv.CAP_PROP_FRAME_HEIGHT, this.resolution[0]/1); // hack to turn int into double
|
||||
this.capture.set(cv.CAP_PROP_FRAME_WIDTH, this.resolution[1]/1);
|
||||
}
|
||||
|
||||
snap() {
|
||||
let frame = this.capture.read();
|
||||
return cv.imencode('.jpg', frame).toString('base64');
|
||||
}
|
||||
}
|
44
server/src/services/climateService.ts
Normal file
44
server/src/services/climateService.ts
Normal file
@ -0,0 +1,44 @@
|
||||
export class ClimateService {
|
||||
private readonly interval = 3 // Seconds
|
||||
|
||||
private fanStatus = false;
|
||||
private lightStatus = false;
|
||||
private temp = [25.0];
|
||||
private humidity = [0.65];
|
||||
|
||||
constructor() {
|
||||
setInterval(() => {
|
||||
let up = Math.random() > 0.5,
|
||||
lastTemp = this.temp[this.temp.length - 1],
|
||||
lastHumid = this.humidity[this.humidity.length - 1];
|
||||
this.temp.push(lastTemp + (up ? 1 : -1) * Math.random())
|
||||
this.humidity.push(lastHumid + (up ? 1 : -1) * Math.random())
|
||||
}, this.interval * 1000)
|
||||
}
|
||||
|
||||
public fanState() {
|
||||
return this.fanStatus;
|
||||
}
|
||||
|
||||
public lightState() {
|
||||
return this.lightStatus;
|
||||
}
|
||||
|
||||
public getHumidity() {
|
||||
return this.humidity;
|
||||
}
|
||||
|
||||
public getTemp() {
|
||||
return this.temp;
|
||||
}
|
||||
|
||||
public toggleFan() {
|
||||
this.fanStatus = !this.fanStatus;
|
||||
return this.fanState();
|
||||
}
|
||||
|
||||
public toggleLight() {
|
||||
this.lightStatus = !this.lightStatus;
|
||||
return this.lightState();
|
||||
}
|
||||
}
|
20
server/tsconfig.json
Normal file
20
server/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es6",
|
||||
"noImplicitAny": false,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["node_modules/*"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
],
|
||||
"lib": ["es2015"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
Reference in New Issue
Block a user