initial commit

This commit is contained in:
Tobias Hopp 2023-05-21 18:46:48 +02:00
commit 19772073ae
27 changed files with 17663 additions and 0 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

5
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

7
.idea/jsLibraryMappings.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{chart.js}" />
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

7
.idea/misc.xml generated Normal file
View 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
View 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/pm25.iml" filepath="$PROJECT_DIR$/.idea/pm25.iml" />
</modules>
</component>
</project>

13
.idea/pm25.iml generated Normal file
View 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$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="chart.js" level="application" />
</component>
</module>

11
.idea/runConfigurations/bin_www.xml generated Normal file
View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="bin/www" type="NodeJSConfigurationType" path-to-js-file="bin/www" working-dir="$PROJECT_DIR$">
<envs>
<env name="DEBUG" value="pm25:*" />
</envs>
<EXTENSION ID="com.jetbrains.nodejs.run.NodeStartBrowserRunConfigurationExtension">
<browser url="http://localhost:3000/" />
</EXTENSION>
<method v="2" />
</configuration>
</component>

19
Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM node:18
# Create app directory
WORKDIR /app/
VOLUME /app/
RUN npm install -g npm@latest
COPY ./package.json ./
RUN yarn
EXPOSE 8081
COPY . .
ENV MONGODB_URL="mongodb://db:27017/pm25"
ENV PORT=8081
CMD DEBUG=pm25:* node main.js

39
app.js Normal file
View File

@ -0,0 +1,39 @@
const createError = require( "http-errors" );
const express = require( "express" );
const path = require( "path" );
const cookieParser = require( "cookie-parser" );
const logger = require( "morgan" );
const indexRouter = require( "./routes/index" );
const app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;

21
bin/dbHandler.js Normal file
View File

@ -0,0 +1,21 @@
const mongoose = require("mongoose");
function connect( )
{
return new Promise( async (resolve, reject) => {
console.log("Connecting to DB...")
let url = process.env.MONGODB_URL || "mongodb://localhost:27017/pm25";
try {
mongoose.connect(url, {
useNewUrlParser: true,
useUnifiedTopology: true
})
resolve();
console.log("Connected!");
} catch( e )
{
reject(e);
}
} )
}
module.exports = {connect}

90
bin/www Executable file
View File

@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
const app = require('../app');
const debug = require('debug')('pm25:server');
const http = require('http');
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || '8081');
app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

16
log.txt Normal file
View File

@ -0,0 +1,16 @@
yarn run v1.22.19
$ node ./bin/www
GET / 304 454.863 ms - -
GET /stylesheets/style.css 304 4.045 ms - -
GET /javascripts/main.js 304 1.168 ms - -
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
yarn run v1.22.19
$ node main.js
Starting PM2.5 Sensor Server
Connecting to DB...
Connected!
yarn run v1.22.19
$ node main.js
Starting PM2.5 Sensor Server
Connecting to DB...
Connected!

14
main.js Normal file
View File

@ -0,0 +1,14 @@
const dbHandler = require("./bin/dbHandler");
console.log("Starting PM2.5 Sensor Server");
async function main()
{
await dbHandler.connect();
require("./bin/www"); // Starting Webserver
}
main();

13
models/PMEntry.js Normal file
View File

@ -0,0 +1,13 @@
const mongoose = require( "mongoose" );
// PM2.5 = 2.10, PM10 = 5.30
const schema = new mongoose.Schema( {
pm25: { type: Number, required: true },
pm10:{ type: Number, required: true },
sensor_name: {type: String}
}, { timestamps: true } );
const Model = mongoose.model(
"PMEntry",
schema,
);
module.exports = Model;

14
models/Status.js Normal file
View File

@ -0,0 +1,14 @@
const mongoose = require( "mongoose" );
// PM2.5 = 2.10, PM10 = 5.30
const schema = new mongoose.Schema( {
error: { type: Boolean, required: true },
sleeping: { type: Boolean, required: true },
ip: { type: String, required: true },
}, { timestamps: true } );
const Model = mongoose.model(
"Status",
schema,
);
module.exports = Model;

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "pm25",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node main.js"
},
"dependencies": {
"chart.js": "^4.3.0",
"chartjs-plugin-annotation": "^3.0.0",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"mongoose": "^7.1.1",
"morgan": "~1.9.1",
"pug": "2.0.0-beta11"
},
"devDependencies": {
"webpack": "^5.82.1",
"webpack-cli": "^5.1.1"
}
}

200
public/javascripts/main.js Normal file
View File

@ -0,0 +1,200 @@
const lastStatus = document.getElementById("lastStatus");
const pm10 = document.getElementById("pm10");
const pm25 = document.getElementById("pm25");
// Define color ranges for the different value ranges
const colorRanges = {
good: 'rgba(154, 204, 91, 0.5)',
moderate: 'rgba(255, 235, 59, 0.5)',
unhealthySensitive: 'rgba(255, 152, 0, 0.5)',
unhealthy: 'rgba(255, 59, 59, 0.5)',
veryUnhealthy: 'rgba(143, 63, 151, 0.5)',
hazardous: 'rgba(126, 10, 2, 0.5)',
};
// Define the annotations
const annotations = {
annotations:
[
{
type: 'box',
yMin: 0,
yMax: 12,
backgroundColor: colorRanges.good,
},
{
type: 'box',
yMin: 12.1,
yMax: 35.4,
backgroundColor: colorRanges.moderate,
},
{
type: 'box',
yMin: 35.5,
yMax: 55.4,
backgroundColor: colorRanges.unhealthySensitive,
},
{
type: 'box',
yMin: 55.5,
yMax: 150.4,
backgroundColor: colorRanges.unhealthy,
},
{
type: 'box',
yMin: 150.5,
yMax: 250.4,
backgroundColor: colorRanges.veryUnhealthy,
},
{
type: 'box',
yMin: 250.5,
yMax: 500.4,
backgroundColor: colorRanges.hazardous,
},
]
};
// Initialisiere das Chart-Objekt
var ctx = document.getElementById('chart').getContext('2d');
var chart = new Chart(ctx, {
// Art des Diagramms
type: 'line',
// Daten
data: {
labels: [],
datasets: [{
label: 'PM2.5',
data: [],
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255,99,132,1)',
borderWidth: 2,
fill: false
}, {
label: 'PM10',
data: [],
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 2,
fill: false
}]
},
// Konfigurationsoptionen
options: {
maintainAspectRatio: false,
responsive: true,
title: {
display: true,
text: 'Feinstaubwerte'
},
scales: {
x: {
/*type: 'time',
time: {
unit: 'hour',
displayFormats: {
hour: 'MMM D, hA'
}
},*/
display: true,
},
y: {
display: true,
scaleLabel: {
display: true,
labelString: 'µg/m³'
},
beginAtZero: true,
suggestedMin: 0,
suggestedMax: 10
}
},
plugins: {
annotation: annotations
},
},
});
// Funktion zum Hinzufügen neuer Datenpunkte
function addData(time, pm25, pm10) {
// Füge die neuen Datenpunkte hinzu
chart.data.labels.push(time);
chart.data.datasets[0].data.push(pm25);
chart.data.datasets[1].data.push(pm10);
// Begrenze die Anzahl der Datenpunkte auf 100 oder die letzten 12 Stunden
var maxPoints = 60;
var maxTime = new Date();
maxTime.setHours(maxTime.getHours() - 12);
while (chart.data.labels.length > maxPoints || (chart.data.labels.length > 0 && new Date(chart.data.labels[0]) < maxTime)) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
chart.data.datasets[1].data.shift();
}
// Aktualisiere das Chart
chart.update();
}
setInterval(( ) => {
//addData(new Date().toLocaleTimeString(), 1+Math.floor(Math.random() *2), 3+Math.floor(Math.random() * 2));
loadValues();
loadStatus();
}, 2000);
function httpGetAsync(theUrl)
{
return new Promise( (resolve) => {
const xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState === 4 && xmlHttp.status === 200)
resolve(JSON.parse(xmlHttp.responseText));
}
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.send(null);
})
}
let last_request = new Date();
last_request.setDate(last_request.getDate()-1);
function loadValues()
{
return new Promise( async () => {
let data = await httpGetAsync("/data/" + last_request.getTime() );
for( let entry of data )
{
addData(new Date(entry.createdAt).toLocaleTimeString(), entry.pm25, entry.pm10)
pm25.innerText = "" + (Math.round(entry.pm25*100)/100);
pm10.innerText = "" + (Math.round(entry.pm10*100)/100);
}
last_request = new Date();
})
}
function loadStatus()
{
return new Promise( async () => {
let data = await httpGetAsync("/status" );
if( data.sleeping )
lastStatus.innerHTML = "Letzter Status:<br>Schläft | " + (new Date(data.createdAt).toLocaleTimeString()) + "<br>Status: " + (data["error"] ? "Fehler!" : "OK");
else
lastStatus.innerHTML = "Letzter Status:<br>Misst | " + (new Date(data.createdAt).toLocaleTimeString()) + "<br>Status: " + (data["error"] ? "Fehler!" : "OK");
})
}
loadStatus();
loadValues();

View File

@ -0,0 +1,28 @@
const options = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderWidth: 1
}]
},
options: {
plugins: {
annotation: {
annotations: {
box1: {
type: "box",
yMin: 5,
yMax: 10,
backgroundColor: "rgba(255, 99, 132, 0.25)"
}
}
}
}
}
}
const ctx = document.getElementById('chart').getContext('2d');
const chart = new Chart(ctx, options);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,28 @@
body {
font: 1.3em "Lucida Grande", Helvetica, Arial, sans-serif;
width: 100%;
overflow-x: hidden;
}
a {
color: #00B7FF;
}
canvas {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
h1 {
margin-bottom: -1%;
}
#chart {
width: 100%;
overflow-x: hidden;
}
#lastStatus {
font-size: 0.8em;
}

64
routes/index.js Normal file
View File

@ -0,0 +1,64 @@
const express = require('express');
const router = express.Router();
const PMEntry = require("../models/PMEntry");
const Status = require("../models/Status");
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'PM2.5 & PM10' });
});
router.get('/data/:timestamp', async (req,res) => {
let date = new Date(req.params.timestamp);
try {
let entries = await PMEntry.find({ updatedAt: {$gt: req.params.timestamp} });
res.json(entries);
} catch( e )
{
console.error(e);
}
});
router.post("/push", async(req, res) => {
console.log(req.body);
res.status(200);
res.send("ok");
res.end();
let entry = new PMEntry();
entry.pm10 = req.body.pm10;
entry.pm25 = req.body.pm25;
if(entry.pm10 > 500 || entry.pm25 > 500 )
return;
await entry.save();
});
router.get("/status", async(req,res) => {
let status = await Status.findOne({}).sort({createdAt: -1});
res.json(status);
});
router.post("/status", async(req,res) => {
console.log(req.body);
let status = new Status();
status.error = req.body.error;
status.ip = req.body.ip;
status.sleeping = req.body.sleeping;
await status.save();
res.status(200);
res.end();
});
module.exports = router;

6
views/error.pug Normal file
View File

@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

7
views/index.pug Normal file
View File

@ -0,0 +1,7 @@
extends layout
block content
h1 PM2.5 [<span id="pm25"></span>] & PM10 [<span id="pm10"></span>]
p#lastStatus
div(style="width:100%; height:75vh;")
canvas#chart

10
views/layout.pug Normal file
View File

@ -0,0 +1,10 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
script(src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.4.0/chart.js")
script(src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-annotation/1.0.2/chartjs-plugin-annotation.js")
body
block content
script(src="/javascripts/main.js")

27
webpack.config.js Normal file
View File

@ -0,0 +1,27 @@
const path = require( "path" );
module.exports = {
mode: "development",
devtool: "inline-source-map",
entry: {
web: "./public/javascripts/main.js",
},
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
//exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
filename: "[name].bundle.js",
path: path.resolve( __dirname, "public/javascripts/" ),
},
};

1773
yarn.lock Normal file

File diff suppressed because it is too large Load Diff