This commit is contained in:
2023-08-14 13:49:06 -04:00
commit aa0f98e742
84 changed files with 19695 additions and 0 deletions

3
server/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
.DS_Store

3881
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
server/package.json Normal file
View 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"
}
}

View 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();
}

View 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)
}
}

View 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();
}

View 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
View 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}`));

View 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;
}
}

View 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);
}
}

View 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');
}
}

View 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
View 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/**/*"]
}