Commits (24)
#include <Arduino.h>
#define tiltEnterPin 2
#define tiltLeavePin 3
#define greenPin 4
#define redPin 5
#define switchLockPin 7
#define initCatsInside 1
bool tiltOneHigh, tiltTwoHigh;
bool locked;
int catsInside;
long lastEnteredAt, lastLeftAt, lastSwitchedAt;
void setup() {
Serial.begin(9600);
pinMode(switchLockPin, INPUT );
pinMode(tiltEnterPin, INPUT);
pinMode(tiltLeavePin, INPUT);
pinMode(greenPin, OUTPUT);
pinMode(redPin, OUTPUT);
digitalWrite(greenPin, HIGH);
digitalWrite(redPin, LOW);
attachInterrupt(digitalPinToInterrupt( tiltEnterPin ), catEntered, HIGH );
attachInterrupt(digitalPinToInterrupt( tiltLeavePin ), catLeft, HIGH );
catsInside = initCatsInside;
}
volatile bool clicked = false;
void loop() {
if(digitalRead(switchLockPin) == HIGH) lockUnlock();
}
void printStatus() {
String status = "{\"locked\": " + (locked ? String("true") : String("false")) + ", \"catsInside\": " + String(catsInside) + "}";
Serial.println(status);
}
void catEntered() {
long now = millis();
if(now - lastEnteredAt < 1000) return;
++catsInside;
lastEnteredAt = now;
printStatus();
while(digitalRead(tiltEnterPin) == HIGH) {
lastEnteredAt = millis();
}
}
void catLeft() {
long now = millis();
if(now - lastLeftAt < 1000) return;
if(catsInside > 0) --catsInside;
lastLeftAt = now;
printStatus();
while(digitalRead(tiltLeavePin) == HIGH) {
lastLeftAt = millis();
}
}
void lockUnlock() {
long now = millis();
if(now - lastSwitchedAt < 50) return;
locked = !locked;
digitalWrite(greenPin, !locked);
digitalWrite(redPin, locked);
printStatus();
while(digitalRead(switchLockPin) == HIGH) {
lastSwitchedAt = millis();
}
}
/*
SerialEvent occurs whenever a new data comes in the hardware serial RX. This
routine is run between each time loop() runs, so using delay inside loop can
delay response. Multiple bytes of data may be available.
*/
String message = "";
void serialEvent() {
while (Serial.available()) {
char inChar = (char)Serial.read();
message += inChar;
if(inChar == '\n') {
handleMessage(message);
message = "";
}
}
}
void handleMessage(String message) {
message.trim();
if(message == "lock") {
locked = true;
digitalWrite(greenPin, LOW);
digitalWrite(redPin, HIGH);
printStatus();
}else if(message == "unlock") {
locked = false;
digitalWrite(redPin, LOW);
digitalWrite(greenPin, HIGH);
printStatus();
}else if(message == "catEntered") {
++catsInside;
printStatus();
}else if(message == "catLeft") {
if(catsInside > 0) --catsInside;
printStatus();
}else if(message == "status") {
printStatus();
}else {
Serial.println("Unknown event");
}
}
\ No newline at end of file
node_modules/
\ No newline at end of file
SERIAL_PORT=/dev/cu.usbmodem12401
SERIAL_BAUDRATE=9600
API_PORT=3000
WS_PORT=3001
\ No newline at end of file
node_modules/
\ No newline at end of file
FROM node:latest
WORKDIR /pina-maomen-back
COPY . .
RUN npm install
CMD ["npm", "start"]
EXPOSE 3000
EXPOSE 3001
\ No newline at end of file
import mariadb from 'mariadb'
import dotenv from 'dotenv'
dotenv.config();
const pool = mariadb.createPool({
host: process.env.MARIA_DB_HOST,
user: process.env.MARIA_DB_USER,
password: process.env.MARIA_DB_PASSWORD,
connectionLimit: 5,
database: process.env.MARIA_DB_DATABASE
});
const connection = await pool.getConnection();
export default connection;
\ No newline at end of file
import connection from './db.js'
import { parse } from 'path'
import wss from './websocket.js'
import {port, parser} from './port.js'
let status = null
let history = null
// If port is already open, fetch status from arduino, otherwise wait for port to open
// the delay is to make sure the arduino is ready to receive data
if(port.isOpen) {
setTimeout(getStatusFromArduino, 3000)
}else {
port.on('open', () => {setTimeout(getStatusFromArduino, 3000)})
}
// Fetch history from database on startup
await upadteHistory()
// Listen for data from arduino
parser.on('data', function returnStatus(data) {
const status = JSON.parse(data.toString())
updateStatus(status)
wss.clients.forEach(client => {
client.send(JSON.stringify({status: status}))
})
})
// Listen for websocket connections
const allowedMessages = ['lock', 'unlock', 'catEntered', 'catLeft', 'status']
wss.on('connection', async function connection(ws) {
if(!status) await getStatusFromArduino()
await upadteHistory()
ws.send(JSON.stringify({status, history}))
ws.on('message', async function incoming(buffer) {
const message = buffer.toString('utf8').trim()
console.log(`received: ${message}`)
if(allowedMessages.includes(message)) {
port.write(`${message}\n`)
port.flush()
}else if(message === 'history') {
if(!history) await upadteHistory()
ws.write(JSON.stringify(history))
}
});
})
function getStatusFromArduino() {
port.write('status\n')
port.flush()
}
async function upadteHistory() {
const [lockLog, catLog] = await Promise.all([
connection.query(`SELECT * FROM lock_log ORDER BY date DESC LIMIT 10`),
connection.query(`SELECT * FROM cat_log ORDER BY date DESC LIMIT 10`)
])
const lockHistoryForWebsocket = lockLog.map(log => {
return {
date: new Date(log.date),
event: log.action
}
})
const catHistoryForWebsocket = catLog.map(log => {
return {
date: new Date(log.date),
event: log.action,
catsInside: log.cats_inside
}
})
history = {
lockLog: lockHistoryForWebsocket,
catLog: catHistoryForWebsocket
}
}
async function updateStatus(newStatus) {
if(!status) return status = newStatus
if(newStatus.locked === status.locked && newStatus.catsInside === status.catsInside) return
if(newStatus.locked !== status.locked) {
await connection.query(`INSERT INTO lock_log (date, action) VALUES ('${(new Date()).toISOString()}', '${newStatus.locked ? 'locked' : 'unlocked'}')`)
history.lockLog.unshift({event: newStatus.locked ? 'locked' : 'unlocked', date: new Date()})
}else if(newStatus.catsInside < status.catsInside) {
await connection.query(`INSERT INTO cat_log (date, action, cats_inside) VALUES ('${(new Date()).toISOString()}', 'left', ${newStatus.catsInside})`)
history.catLog.unshift({event: 'left', date: new Date(), catsInside: newStatus.catsInside})
}else {
await connection.query(`INSERT INTO cat_log (date, action, cats_inside) VALUES ('${(new Date()).toISOString()}', 'entered', ${newStatus.catsInside})`)
history.catLog.unshift({event: 'entered', date: new Date(), catsInside: newStatus.catsInside})
}
status = newStatus
}
\ No newline at end of file
{
"name": "pina-maomen-back",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pina-maomen-back",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@serialport/parser-readline": "^10.5.0",
"dotenv": "^16.0.3",
"mariadb": "^3.0.2",
"serialport": "^10.5.0",
"ws": "^8.12.0"
}
},
"node_modules/@serialport/binding-mock": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
"integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==",
"dependencies": {
"@serialport/bindings-interface": "^1.2.1",
"debug": "^4.3.3"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@serialport/bindings-cpp": {
"version": "10.8.0",
"resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-10.8.0.tgz",
"integrity": "sha512-OMQNJz5kJblbmZN5UgJXLwi2XNtVLxSKmq5VyWuXQVsUIJD4l9UGHnLPqM5LD9u3HPZgDI5w7iYN7gxkQNZJUw==",
"hasInstallScript": true,
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"@serialport/parser-readline": "^10.2.1",
"debug": "^4.3.2",
"node-addon-api": "^5.0.0",
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=12.17.0 <13.0 || >=14.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-interface": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz",
"integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==",
"engines": {
"node": "^12.22 || ^14.13 || >=16"
}
},
"node_modules/@serialport/parser-byte-length": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-10.5.0.tgz",
"integrity": "sha512-eHhr4lHKboq1OagyaXAqkemQ1XyoqbLQC8XJbvccm95o476TmEdW5d7AElwZV28kWprPW68ZXdGF2VXCkJgS2w==",
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-cctalk": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-10.5.0.tgz",
"integrity": "sha512-Iwsdr03xmCKAiibLSr7b3w6ZUTBNiS+PwbDQXdKU/clutXjuoex83XvsOtYVcNZmwJlVNhAUbkG+FJzWwIa4DA==",
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-delimiter": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-10.5.0.tgz",
"integrity": "sha512-/uR/yT3jmrcwnl2FJU/2ySvwgo5+XpksDUR4NF/nwTS5i3CcuKS+FKi/tLzy1k8F+rCx5JzpiK+koqPqOUWArA==",
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-inter-byte-timeout": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-10.5.0.tgz",
"integrity": "sha512-WPvVlSx98HmmUF9jjK6y9mMp3Wnv6JQA0cUxLeZBgS74TibOuYG3fuUxUWGJALgAXotOYMxfXSezJ/vSnQrkhQ==",
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-packet-length": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-10.5.0.tgz",
"integrity": "sha512-jkpC/8w4/gUBRa2Teyn7URv1D7T//0lGj27/4u9AojpDVXsR6dtdcTG7b7dNirXDlOrSLvvN7aS5/GNaRlEByw==",
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/@serialport/parser-readline": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-10.5.0.tgz",
"integrity": "sha512-0aXJknodcl94W9zSjvU+sLdXiyEG2rqjQmvBWZCr8wJZjWEtv3RgrnYiWq4i2OTOyC8C/oPK8ZjpBjQptRsoJQ==",
"dependencies": {
"@serialport/parser-delimiter": "10.5.0"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-ready": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-10.5.0.tgz",
"integrity": "sha512-QIf65LTvUoxqWWHBpgYOL+soldLIIyD1bwuWelukem2yDZVWwEjR288cLQ558BgYxH4U+jLAQahhqoyN1I7BaA==",
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-regex": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-10.5.0.tgz",
"integrity": "sha512-9jnr9+PCxRoLjtGs7uxwsFqvho+rxuJlW6ZWSB7oqfzshEZWXtTJgJRgac/RuLft4hRlrmRz5XU40i3uoL4HKw==",
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-slip-encoder": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-10.5.0.tgz",
"integrity": "sha512-wP8m+uXQdkWSa//3n+VvfjLthlabwd9NiG6kegf0fYweLWio8j4pJRL7t9eTh2Lbc7zdxuO0r8ducFzO0m8CQw==",
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-spacepacket": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-10.5.0.tgz",
"integrity": "sha512-BEZ/HAEMwOd8xfuJSeI/823IR/jtnThovh7ils90rXD4DPL1ZmrP4abAIEktwe42RobZjIPfA4PaVfyO0Fjfhg==",
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/stream": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-10.5.0.tgz",
"integrity": "sha512-gbcUdvq9Kyv2HsnywS7QjnEB28g+6OGB5Z8TLP7X+UPpoMIWoUsoQIq5Kt0ZTgMoWn3JGM2lqwTsSHF+1qhniA==",
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"debug": "^4.3.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@types/geojson": {
"version": "7946.0.10",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
},
"node_modules/@types/node": {
"version": "17.0.45",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/lru-cache": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz",
"integrity": "sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==",
"engines": {
"node": ">=12"
}
},
"node_modules/mariadb": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.0.2.tgz",
"integrity": "sha512-dVjiQZ6RW0IXFnX+T/ZEmnqs724DgkQsXqfCyInXn0XxVfO2Px6KbS4M3Ny6UiBg0zJ93SHHvfVBgYO4ZnFvvw==",
"dependencies": {
"@types/geojson": "^7946.0.10",
"@types/node": "^17.0.45",
"denque": "^2.1.0",
"iconv-lite": "^0.6.3",
"lru-cache": "^7.14.0",
"moment-timezone": "^0.5.38"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
},
"node_modules/moment-timezone": {
"version": "0.5.40",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.40.tgz",
"integrity": "sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg==",
"dependencies": {
"moment": ">= 2.9.0"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
"node_modules/node-gyp-build": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz",
"integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/serialport": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/serialport/-/serialport-10.5.0.tgz",
"integrity": "sha512-7OYLDsu5i6bbv3lU81pGy076xe0JwpK6b49G6RjNvGibstUqQkI+I3/X491yBGtf4gaqUdOgoU1/5KZ/XxL4dw==",
"dependencies": {
"@serialport/binding-mock": "10.2.2",
"@serialport/bindings-cpp": "10.8.0",
"@serialport/parser-byte-length": "10.5.0",
"@serialport/parser-cctalk": "10.5.0",
"@serialport/parser-delimiter": "10.5.0",
"@serialport/parser-inter-byte-timeout": "10.5.0",
"@serialport/parser-packet-length": "10.5.0",
"@serialport/parser-readline": "10.5.0",
"@serialport/parser-ready": "10.5.0",
"@serialport/parser-regex": "10.5.0",
"@serialport/parser-slip-encoder": "10.5.0",
"@serialport/parser-spacepacket": "10.5.0",
"@serialport/stream": "10.5.0",
"debug": "^4.3.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/ws": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz",
"integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}
{
"name": "pina-maomen-back",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@serialport/parser-readline": "^10.5.0",
"dotenv": "^16.0.3",
"mariadb": "^3.0.2",
"serialport": "^10.5.0",
"ws": "^8.12.0"
},
"type": "module"
}
import { SerialPort } from 'serialport'
import { ReadlineParser } from '@serialport/parser-readline'
import * as dotenv from 'dotenv'
dotenv.config()
const port = new SerialPort({
path: process.env.SERIAL_PORT,
baudRate: parseInt(process.env.SERIAL_BAUDRATE),
dataBits: 8,
stopBits: 1,
parity: 'none',
});
const parser = port.pipe(new ReadlineParser({ delimiter: '\n' }))
port.on('open', () => {
console.log('serial port open')
})
export {port}
export {parser}
\ No newline at end of file
import { WebSocketServer } from 'ws';
import * as dotenv from 'dotenv'
dotenv.config();
// Creating a new websocket server
const wss = new WebSocketServer({ port: process.env.WS_PORT})
// Creating connection using websocket
wss.on("connection", ws => {
ws.id = crypto.randomUUID()
console.log(`New client connected (${ws.id})`)
// handling what to do when clients disconnects from server
ws.on("close", () => {
console.log(`${ws.id} disconnected`)
});
// handling client connection error
ws.onerror = function () {
console.log("Some WS Error occurred")
}
});
console.log(`The WebSocket server is running on port ${process.env.WS_PORT}`);
export default wss
\ No newline at end of file
FROM node:latest
WORKDIR /pina-meomen-web
WORKDIR /pina-maomen-web
COPY . .
RUN npm install
CMD ["npm", "run", "dev"]
......@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>Pina maomen</title>
</head>
<body>
<div id="root"></div>
......
......@@ -8,8 +8,11 @@
"name": "pina-maomen-web",
"version": "0.0.0",
"dependencies": {
"date-fns": "^2.29.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-toastify": "^9.1.1",
"react-use-websocket": "^4.2.0"
},
"devDependencies": {
"@types/react": "^18.0.22",
......@@ -637,6 +640,14 @@
"node": ">=4"
}
},
"node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
......@@ -664,6 +675,18 @@
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
"dev": true
},
"node_modules/date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
......@@ -1286,6 +1309,27 @@
"node": ">=0.10.0"
}
},
"node_modules/react-toastify": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz",
"integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==",
"dependencies": {
"clsx": "^1.1.1"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-use-websocket": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.2.0.tgz",
"integrity": "sha512-ZovaTlc/tWX6a590fi3kMWImhyoWj46BWJWvO5oucZJzRnVVhYtes2D9g+5MKXjSdR7Es3456hB89v4/1pcBKg==",
"peerDependencies": {
"react": ">= 18.0.0",
"react-dom": ">= 18.0.0"
}
},
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
......@@ -1920,6 +1964,11 @@
"supports-color": "^5.3.0"
}
},
"clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
......@@ -1947,6 +1996,11 @@
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
"dev": true
},
"date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
......@@ -2300,6 +2354,20 @@
"integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==",
"dev": true
},
"react-toastify": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz",
"integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==",
"requires": {
"clsx": "^1.1.1"
}
},
"react-use-websocket": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.2.0.tgz",
"integrity": "sha512-ZovaTlc/tWX6a590fi3kMWImhyoWj46BWJWvO5oucZJzRnVVhYtes2D9g+5MKXjSdR7Es3456hB89v4/1pcBKg==",
"requires": {}
},
"resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
......
......@@ -9,8 +9,11 @@
"preview": "vite preview"
},
"dependencies": {
"date-fns": "^2.29.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-toastify": "^9.1.1",
"react-use-websocket": "^4.2.0"
},
"devDependencies": {
"@types/react": "^18.0.22",
......@@ -19,4 +22,4 @@
"typescript": "^4.6.4",
"vite": "^3.2.0"
}
}
\ No newline at end of file
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
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;
min-height: 100vh;
background-color: #282c34;
}
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
color: white;
margin: 0;
}
\ No newline at end of file
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import { useEffect, useState } from 'react';
import './App.css'
import Bouton from "./interface/Bouton";
import CatHistory from "./interface/CatHistory";
import Chats from "./interface/Chats";
import useWebSocket, { ReadyState } from 'react-use-websocket';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
function App() {
const [count, setCount] = useState(0)
const [status, setStatus] = useState({})
const [history, setHistory] = useState({})
const [ignoreNextToast, setIgnoreNextToast] = useState(false)
const { sendMessage, lastMessage, readyState } = useWebSocket('ws://localhost:3001');
useEffect(() => {
if (readyState === ReadyState.CLOSED) {
toast.error('Connection lost, please refresh the page', {
autoClose: false,
});
}
}, [readyState]);
useEffect(() => {
if (lastMessage === null) return;
const data = JSON.parse(lastMessage.data);
console.log(data);
if(data.status) setStatus(data.status);
if(data.history) setHistory(data.history);
else updateHistory(data.status)
}, [lastMessage]);
return (
<div className="App">
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" className="logo" alt="Vite logo" />
</a>
<a href="https://reactjs.org" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
<div>
<h1>La chatière de Pina !</h1>
</div>
<Bouton locked={status.locked} sendMessage={sendMessage} setIgnoreNextToast={setIgnoreNextToast} />
<Chats catsInside={status.catsInside} sendMessage={sendMessage} setIgnoreNextToast={setIgnoreNextToast} />
<CatHistory history={history} />
<ToastContainer />
</div>
)
function updateHistory(newStatus) {
const date = new Date();
if(newStatus.catsInside < status.catsInside) {
const log = {date: date, event: 'left'};
if(!ignoreNextToast) toast('🐾 A pina just left the house');
setHistory((prevHistory) => ({...prevHistory, catLog: [log, ...prevHistory.catLog]}));
}else if(newStatus.catsInside > status.catsInside) {
const log = {date: date, event: 'entered'};
if(!ignoreNextToast) toast('🐈 A pina just entered the house');
setHistory((prevHistory) => ({...prevHistory, catLog: [log, ...prevHistory.catLog]}));
}else if(newStatus.locked !== status.locked) {
const log = newStatus.locked ? {date: date, event: 'locked'} : {date: date, event: 'unlocked'};
if(!ignoreNextToast) toast(newStatus.locked ? '🔴 Cat door locked 🔴' : '🟢 Cat door unlocked 🟢');
setHistory((prevHistory) => ({...prevHistory, lockLog: [log, ...prevHistory.lockLog]}));
}
setIgnoreNextToast(false);
}
}
export default App
export type LockHistoryRecord = {
event: string,
date: Date
}
export type CatHistoryRecord = {
event: string,
date: Date
}
export type History = {
lockLog: LockHistoryRecord[],
catLog: CatHistoryRecord[]
}
\ No newline at end of file