diff --git a/README.md b/README.md index ad19ec2..d1a4336 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,41 @@ ## TODO update with latest + +- [ ] Create clean table structure and files structure + +``` +events +status - optimized what we save every interval? Start with saving it all. +debug - save everything evey tick. Can be blown away at any time. +``` + +- [ ] Make observable grill status a dir, and break out individual observables which can be tested with marbles. +- [ ] Subsribers very leightweight, connect to logging or to datastore events. +- [ ] Add a unit test to demo a hex / message into an expected grill status, so we can make sure we are storing the right data to do this in future. + + +Then + +- [ ] Update chart to visualize events (mark series) and status (line charts) + + +--------- + + +- [x] knowing each socket connection is different we need to refactor that a bit. Create a socket for pinging status, and in sendCommand create a new socket each time and listen for its response. +- [ ] Use sendGrillCOmmandOnce for polling. Make async. Create grill status still in service. + - Seems to have other errors with this - not sure if its better to open one long polling or to keep opening many shot polls. + + +- [ ] Capture other grill errors? See https://github.com/Aenima4six2/gmg/commit/614589cf775422e394f4529b4befbe8ac33bbdf0 + +‭ + +## Structure + +This project is based on structure in this article: https://softwareontheroad.com/ideal-nodejs-project-structure/. A quick read over will help you better navigate this project. + + ## Command API Command api allows for remote control of your GMG. A few implementation notes @@ -123,3 +159,24 @@ rvictl -x ## GET /cook // returns all cooks + + +## OLD database notes + +- `brew install rethinkdb` +- `rethinkdb` +- in browser go to http://localhost:8080/ + +https://marmelab.com/blog/2019/09/25/couchdb_pouchdb_serious_firebase_alternative.html + + +Good article on optimization +https://www.mongodb.com/blog/post/time-series-data-and-mongodb-part-2-schema-design-best-practices + + +Filtering changes +https://pouchdb.com/api.html#filtered-changes + + +Better doc IDS that have our sensor prop names in them? +https://pouchdb.com/2014/06/17/12-pro-tips-for-better-code-with-pouchdb.html \ No newline at end of file diff --git a/README_database.md b/README_database.md deleted file mode 100644 index 5264af0..0000000 --- a/README_database.md +++ /dev/null @@ -1,22 +0,0 @@ - -# To get running - -- `brew install rethinkdb` -- `rethinkdb` -- in browser go to http://localhost:8080/ - - - -https://marmelab.com/blog/2019/09/25/couchdb_pouchdb_serious_firebase_alternative.html - - -Good article on optimization -https://www.mongodb.com/blog/post/time-series-data-and-mongodb-part-2-schema-design-best-practices - - -Filtering changes -https://pouchdb.com/api.html#filtered-changes - - -Better doc IDS that have our sensor prop names in them? -https://pouchdb.com/2014/06/17/12-pro-tips-for-better-code-with-pouchdb.html \ No newline at end of file diff --git a/__mocks__/dgram.js b/__mocks__/dgram.js index 376737c..ad4c2e7 100644 --- a/__mocks__/dgram.js +++ b/__mocks__/dgram.js @@ -1 +1,4 @@ -module.exports = { createSocket: jest.fn() }; +import { EventEmitter } from 'events'; + + +module.exports = { createSocket: new EventEmitter() }; diff --git a/__mocks__/pouchdb.js b/__mocks__/pouchdb.js new file mode 100644 index 0000000..672e1b1 --- /dev/null +++ b/__mocks__/pouchdb.js @@ -0,0 +1,8 @@ +module.exports = jest.fn(() => ({ + put: () => { + return new Promise(() => true) + }, + allDocs: () => { + return new Promise(() => true) + }, +})); diff --git a/routes/index.js b/api/index.js similarity index 100% rename from routes/index.js rename to api/index.js diff --git a/routes/power.js b/api/power.js similarity index 100% rename from routes/power.js rename to api/power.js diff --git a/routes/settings.js b/api/settings.js similarity index 66% rename from routes/settings.js rename to api/settings.js index 254787f..d322d1e 100644 --- a/routes/settings.js +++ b/api/settings.js @@ -3,14 +3,17 @@ import requireCode from "../middleware/requireCode"; import sendCommand from "../middleware/sendCommand"; import jsonCommandSuccessMsg from "../middleware/jsonCommandSuccessMsg"; import { HEX_COMMANDS } from "../constants"; -import { latestStatus } from "../grill-polling"; +import observableGrillStatus from "./../services/observableGrillStatus"; const router = express.Router(); router.get( "/pizza", requireCode, - sendCommand(() => HEX_COMMANDS.setPizzaMode(latestStatus().settings), "hex"), + sendCommand( + () => HEX_COMMANDS.setPizzaMode(observableGrillStatus.value.settings), + "hex" + ), jsonCommandSuccessMsg("pizza mode") ); @@ -18,7 +21,7 @@ router.get( "/regular", requireCode, sendCommand( - () => HEX_COMMANDS.setRegularMode(latestStatus().settings), + () => HEX_COMMANDS.setRegularMode(observableGrillStatus.value.settings), "hex" ), jsonCommandSuccessMsg("regular mode") diff --git a/routes/temp.js b/api/temp.js similarity index 100% rename from routes/temp.js rename to api/temp.js diff --git a/app.js b/app.js index 1f96736..d47368a 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,6 @@ import express from "express"; -import { temp, power, settings } from "./routes"; +import { temp, power, settings } from "./api"; const app = express(); diff --git a/config.js b/config.js deleted file mode 100644 index 63561d5..0000000 --- a/config.js +++ /dev/null @@ -1,4 +0,0 @@ -export const FILE = "./data/brisket2.csv"; -export const IP = "192.168.86.134"; -export const PORT = 8080; -export const INTERVAL = 1000; \ No newline at end of file diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..1970424 --- /dev/null +++ b/config/index.js @@ -0,0 +1,3 @@ +export const IP = "192.168.86.140"; +export const PORT = 8080; +export const INTERVAL = 2000; \ No newline at end of file diff --git a/constants/csv.js b/constants/csv.js new file mode 100644 index 0000000..0318770 --- /dev/null +++ b/constants/csv.js @@ -0,0 +1,13 @@ +export const HEADERS = [ + "timestamp", + "state", + "isOn", + "currentGrillTemp", + "desiredGrillTemp", + "currentProbe1Temp", + "desiredProbe1Temp", + "currentProbe2Temp", + "desiredProbe2Temp", + "fanModeActive", + "lowPelletAlarmActive", +]; diff --git a/constants/emitter-events.js b/constants/emitter-events.js new file mode 100644 index 0000000..5aeb819 --- /dev/null +++ b/constants/emitter-events.js @@ -0,0 +1,3 @@ +// @todo remove, not needed? +export const NEW_GRILL_EVENT = 'NEW_GRILL_EVENT'; +export const NEW_GRILL_STATUS = 'NEW_GRILL_STATUS'; \ No newline at end of file diff --git a/constants.js b/constants/grill-commands.js similarity index 59% rename from constants.js rename to constants/grill-commands.js index 461a9b7..52c6eea 100644 --- a/constants.js +++ b/constants/grill-commands.js @@ -1,16 +1,10 @@ -export const HEADERS = [ - "timestamp", - "state", - "isOn", - "currentGrillTemp", - "desiredGrillTemp", - "currentProbe1Temp", - "desiredProbe1Temp", - "currentProbe2Temp", - "desiredProbe2Temp", - "fanModeActive", - "lowPelletAlarmActive", -]; +String.prototype.replaceAt = function (index, replacement) { + return ( + this.substr(0, index) + + replacement + + this.substr(index + replacement.length) + ); +}; export const COMMANDS = Object.freeze({ powerOn: "UK001!", @@ -25,30 +19,13 @@ export const COMMANDS = Object.freeze({ // setRegularMode: 'UC.+ 9 !' }); -String.prototype.replaceAt = function (index, replacement) { - return ( - this.substr(0, index) + - replacement + - this.substr(index + replacement.length) - ); -}; - export const HEX_COMMANDS = Object.freeze({ setPizzaMode: (settings) => { - const mode = `55430${settings.replaceAt(1, '2')}21`; + const mode = `55430${settings.replaceAt(1, "2")}21`; return mode; }, setRegularMode: (settings) => { - const mode = `55430${settings.replaceAt(1, '0')}21`; + const mode = `55430${settings.replaceAt(1, "0")}21`; return mode; }, }); - -// UN - what does this do? -// UF150 -// Uf150 - - -// used to work -// 5543052b023220202020 -// 52b14321919191921 \ No newline at end of file diff --git a/constants/grill-events.js b/constants/grill-events.js new file mode 100644 index 0000000..e07c36c --- /dev/null +++ b/constants/grill-events.js @@ -0,0 +1,5 @@ +export const POWER_ON = "POWER_ON"; +export const POWER_ON_COLDSMOKE = "POWER_ON_COLDSMOKE"; +export const FAN_MODE = "FAN_MODE"; +export const POWER_OFF = "POWER_OFF"; +export const PROBE_DESIRED_CHANGE = "PROBE_DESIRED_CHANGE"; diff --git a/constants/grill.js b/constants/grill.js new file mode 100644 index 0000000..f80fa43 --- /dev/null +++ b/constants/grill.js @@ -0,0 +1,10 @@ +export const GRILL_NOT_SURE_MAP = { + 0: "Climate ICY", + 4: "Climate COLD", + 8: "Climate AVERAGE", + 12: "Climate WARM", + 1: "Climate HOT", + 10: "Lock temp display OFF", + 9: "Auto Revert Wifi OFF", + 11: "Auto Revert Wifi ON", +}; diff --git a/constants/index.js b/constants/index.js new file mode 100644 index 0000000..7c0697d --- /dev/null +++ b/constants/index.js @@ -0,0 +1,12 @@ +import { COMMANDS, HEX_COMMANDS } from "./grill-commands"; +import * as GRILL_EVENTS from "./grill-events"; +import * as EMITTER_EVENTS from "./emitter-events"; +import { GRILL_NOT_SURE_MAP } from "./grill"; + +export { + COMMANDS, + HEX_COMMANDS, + GRILL_EVENTS, + GRILL_NOT_SURE_MAP, + EMITTER_EVENTS, +}; diff --git a/data/brisket2.csv b/data/brisket2.csv deleted file mode 100644 index c3d9ce7..0000000 --- a/data/brisket2.csv +++ /dev/null @@ -1,9 +0,0 @@ -timestamp,state,isOn,currentGrillTemp,desiredGrillTemp,currentProbe1Temp,desiredProbe1Temp,currentProbe2Temp,desiredProbe2Temp,fanModeActive,lowPelletAlarmActive -1586196504422,off,false,47,0,48,0,49,0,false,false -1586196506410,off,false,47,0,48,0,49,0,false,false -1586196508408,off,false,47,0,48,0,49,0,false,false -1586196510407,off,false,47,0,48,0,49,0,false,false -1586196512415,off,false,47,0,48,0,49,0,false,false -1586196514416,off,false,47,0,48,0,49,0,false,false -1586217994172,off,false,57,0,60,0,60,0,false,false -1586217996162,off,false,57,0,60,0,60,0,false,false diff --git a/datastore-pouch.js b/datastore/pouch.js similarity index 67% rename from datastore-pouch.js rename to datastore/pouch.js index 63f1186..4fc212b 100644 --- a/datastore-pouch.js +++ b/datastore/pouch.js @@ -2,8 +2,7 @@ var PouchDB = require("pouchdb"); const uuid = require("uuid/v4"); const db = new PouchDB("http://localhost:5984/api_test_1"); - -export async function setup() {} +const eventsDb = new PouchDB("http://localhost:5984/events_test_1"); export async function add(item) { return await db @@ -11,7 +10,11 @@ export async function add(item) { .then(null, (err) => console.error(err)); } -export async function last() {} +export async function lastKnownStatus() { + return await db + .allDocs({ include_docs: true, limit: 1, descending: true }) + .then((data) => data.rows.length ? data.rows[0] : null); +} export async function all() { return await db @@ -19,6 +22,13 @@ export async function all() { .then((data) => data.rows.map(({ doc }) => doc).reverse()); } +export async function addEvent(item) { + console.log('addEvent', item); + const _id = `${item.timestamp || new Date().getTime()}-${item.type}` + return await eventsDb + .put({ _id, ...item }) + .then(null, (err) => console.error(err)); +} /** * const PouchDB = require("pouchdb"); @@ -44,4 +54,4 @@ export async function all() { .catch(err => console.error(err)); } - */ \ No newline at end of file + */ diff --git a/grill-polling.js b/grill-polling.js deleted file mode 100644 index edb0fb1..0000000 --- a/grill-polling.js +++ /dev/null @@ -1,68 +0,0 @@ -const dgram = require("dgram"); -const GrillStatus = require("./GrillStatus"); -const asciichart = require("asciichart"); -import { PORT, IP, INTERVAL } from "./config"; -import { COMMANDS } from "./constants"; -import * as store from "./datastore-pouch"; - -const socket = dgram.createSocket("udp4"); - -let status = new GrillStatus(Buffer.from('')); - -const renderChart = async () => { - const history = await store.all(); - const data = history.map(({ currentProbe1Temp }) => currentProbe1Temp); - console.clear(); - console.log(asciichart.plot(data, { height: 6 })); -}; - -socket.on("error", (err) => { - console.error("error", err); -}); - -socket.on("message", async (msg, info) => { - // console.log('message ===='); - // console.log(msg); - // console.log(Buffer.from(msg).toString()); - // console.log(info); - // console.log('end message ===='); - // @todo check for non grill status messages - status = new GrillStatus(Buffer.from(msg)); - // console.log('hex ', status.hex); - // console.log('grillOptions', ' ', status.settings); - // console.log(' '); - // console.log('grilOptions decode', status.grillOptions); - if (status.currentGrillTemp !== NaN) { - // assume valid status - const dataToStore = { timestamp: new Date().getTime(), ...status }; - await store.add({ timestamp: new Date().getTime(), ...status }); - console.clear(); - console.log(dataToStore); - } -}); - -const latestStatus = () => { - return status; -}; - -const sendOnce = (message, mode = "ascii") => { - const data = Buffer.from(message, mode); - socket.send(data, 0, data.byteLength, PORT, IP, (error) => {}); -}; - -const pollStatus = () => { - const data = Buffer.from(COMMANDS.getGrillStatus, "ascii"); - socket.send(data, 0, data.byteLength, PORT, IP, (error) => {}); - setTimeout(() => { - pollStatus(); - }, INTERVAL); -}; - -store.setup(); - -// @todo change to exports -module.exports = { - pollStatus, - sendOnce, - latestStatus -}; diff --git a/index.js b/index.js index 8b76000..b9dbd90 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,11 @@ -import { pollStatus } from "./grill-polling"; +import './subscribers/grillStatus'; +// import './subscribers/process'; +import { grillPollStatus } from "./services/grillPollStatus"; const app = require("./app.js"); -pollStatus(); +// @TODO start polling here +grillPollStatus(); app.listen(3000, function () { console.log("Example app listening on port 3000!"); diff --git a/index.spec.js b/index.spec.js index eeb2816..215c6f1 100644 --- a/index.spec.js +++ b/index.spec.js @@ -1,17 +1,14 @@ import { COMMANDS, HEX_COMMANDS } from "./constants"; import * as codeGen from "./helpers/code-gen"; import url from "url"; +import { grillSendCommandOnce } from "./services/grillSendCommandOnce"; +import observableGrillStatus from "./services/observableGrillStatus"; const request = require("supertest"); const app = require("./app"); -const grillPolling = require("./grill-polling"); -jest.mock("./grill-polling", () => ({ - pollStatus: jest.fn(), - sendOnce: jest.fn(), - latestStatus: jest.fn(() => ({ - settings: "xxxxxxxxxxxx", - })), +jest.mock("./services/grillSendCommandOnce", () => ({ + grillSendCommandOnce: jest.fn(), })); jest.spyOn(codeGen, "newCode"); @@ -21,7 +18,7 @@ describe("COMMANDS", () => { codeGen.newCode.mockClear(); }); afterEach(() => { - grillPolling.sendOnce.mockClear(); + grillSendCommandOnce.mockClear(); }); describe("Command API", () => { describe("Power commands", () => { @@ -51,7 +48,10 @@ describe("COMMANDS", () => { expect(link).toEqual(expectedLink); const res2 = await request(app).get(`/command/power/off?code=${code}`); expect(res2.statusCode).toEqual(200); - expect(grillPolling.sendOnce).toHaveBeenCalledWith(COMMANDS.powerOff, undefined); + expect(grillSendCommandOnce).toHaveBeenCalledWith( + COMMANDS.powerOff, + undefined + ); expect(codeGen.newCode).toHaveBeenCalledTimes(2); done(); }); @@ -60,7 +60,7 @@ describe("COMMANDS", () => { expect(res.statusCode).toEqual(403); expect(res.body.message).toEqual("invalid code"); expect(codeGen.newCode).toHaveBeenCalledTimes(1); - expect(grillPolling.sendOnce).not.toHaveBeenCalled(); + expect(grillSendCommandOnce).not.toHaveBeenCalled(); done(); }); describe("quick check for each", () => { @@ -101,10 +101,12 @@ describe("COMMANDS", () => { }, }); expect(link).toEqual(expectedLink); - const res2 = await request(app).get(`/command/temp/grill?temp=150&code=${code}`); + const res2 = await request(app).get( + `/command/temp/grill?temp=150&code=${code}` + ); expect(res2.statusCode).toEqual(200); expect(res2.body.message).toEqual("Sent grill grill temp 150 command"); - expect(grillPolling.sendOnce).toHaveBeenCalledWith( + expect(grillSendCommandOnce).toHaveBeenCalledWith( COMMANDS.setGrillTempF(150), undefined ); @@ -142,11 +144,13 @@ describe("COMMANDS", () => { }, }); expect(link).toEqual(expectedLink); - const res2 = await request(app).get(`/command/settings/pizza?code=${code}`); + const res2 = await request(app).get( + `/command/settings/pizza?code=${code}` + ); expect(res2.statusCode).toEqual(200); expect(res2.body.message).toEqual("Sent grill pizza mode command"); - expect(grillPolling.sendOnce).toHaveBeenCalledWith( - HEX_COMMANDS.setPizzaMode(grillPolling.latestStatus().settings), + expect(grillSendCommandOnce).toHaveBeenCalledWith( + HEX_COMMANDS.setPizzaMode(observableGrillStatus.value.settings), "hex" ); expect(codeGen.newCode).toHaveBeenCalledTimes(2); @@ -171,7 +175,7 @@ describe("COMMANDS", () => { // it("on", async (done) => { // const res = await request(app).get("/command/on"); // expect(res.statusCode).toEqual(200); - // expect(grillPolling.sendOnce).toHaveBeenCalledWith(COMMANDS.powerOn); + // expect(grillSendCommandOnce).toHaveBeenCalledWith(COMMANDS.powerOn); // done(); // }); }); diff --git a/kittens/000005.ldb b/kittens/000005.ldb deleted file mode 100644 index 1da02f8..0000000 Binary files a/kittens/000005.ldb and /dev/null differ diff --git a/kittens/000008.ldb b/kittens/000008.ldb deleted file mode 100644 index 1f41adf..0000000 Binary files a/kittens/000008.ldb and /dev/null differ diff --git a/kittens/000011.ldb b/kittens/000011.ldb deleted file mode 100644 index fc5e883..0000000 Binary files a/kittens/000011.ldb and /dev/null differ diff --git a/kittens/000012.log b/kittens/000012.log deleted file mode 100644 index c1e8c50..0000000 Binary files a/kittens/000012.log and /dev/null differ diff --git a/kittens/CURRENT b/kittens/CURRENT deleted file mode 100644 index 3051f81..0000000 --- a/kittens/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000010 diff --git a/kittens/LOCK b/kittens/LOCK deleted file mode 100644 index e69de29..0000000 diff --git a/kittens/LOG b/kittens/LOG deleted file mode 100644 index 5ad124c..0000000 --- a/kittens/LOG +++ /dev/null @@ -1,5 +0,0 @@ -2020/04/06-12:14:43.178332 70000af6a000 Recovering log #9 -2020/04/06-12:14:43.178976 70000af6a000 Level-0 table #11: started -2020/04/06-12:14:43.180203 70000af6a000 Level-0 table #11: 287 bytes OK -2020/04/06-12:14:43.181677 70000af6a000 Delete type=3 #7 -2020/04/06-12:14:43.181938 70000af6a000 Delete type=0 #9 diff --git a/kittens/LOG.old b/kittens/LOG.old deleted file mode 100644 index 07ef927..0000000 --- a/kittens/LOG.old +++ /dev/null @@ -1,5 +0,0 @@ -2020/04/06-12:14:29.629341 700009e4a000 Recovering log #6 -2020/04/06-12:14:29.629967 700009e4a000 Level-0 table #8: started -2020/04/06-12:14:29.631150 700009e4a000 Level-0 table #8: 645 bytes OK -2020/04/06-12:14:29.632380 700009e4a000 Delete type=0 #6 -2020/04/06-12:14:29.632532 700009e4a000 Delete type=3 #4 diff --git a/kittens/MANIFEST-000010 b/kittens/MANIFEST-000010 deleted file mode 100644 index e7b260b..0000000 Binary files a/kittens/MANIFEST-000010 and /dev/null differ diff --git a/middleware/sendCommand.js b/middleware/sendCommand.js index 9cdc01b..c86e3d0 100644 --- a/middleware/sendCommand.js +++ b/middleware/sendCommand.js @@ -1,12 +1,12 @@ import { isFunction } from "lodash"; -import { sendOnce } from "../grill-polling"; +import { grillSendCommandOnce } from "../services/grillSendCommandOnce"; export default function (command, mode) { return function (req, res, next) { if (isFunction(command)) { - sendOnce(command(req, res), mode); + grillSendCommandOnce(command(req, res), mode); } else { - sendOnce(command, mode); + grillSendCommandOnce(command, mode); } next(); }; diff --git a/GrillStatus.js b/models/GrillStatus.js similarity index 89% rename from GrillStatus.js rename to models/GrillStatus.js index 6f5bbe0..81716d9 100644 --- a/GrillStatus.js +++ b/models/GrillStatus.js @@ -1,3 +1,9 @@ +// @TODO unit test and simplify using `test` function +// @TODO use constants for power states, modes +// @TODO settings should be default string + +import { GRILL_NOT_SURE_MAP } from "../constants"; + const getRawValue = (hex, position) => { const value = hex.substr(position, 2); const parsed = parseInt(value, 16); @@ -11,6 +17,7 @@ const getGrillState = (hex) => { else if (status === 1) status = "on"; else if (status === 2) status = "fan mode"; else if (status === 3) status = "cold smoke mode"; + // @todo set percent and not temperature when in cold smoke mode else status = "unknown"; return status; }; @@ -63,19 +70,9 @@ const parseHex = (str) => { return parseInt(str, 16); }; -const someMap = { - 0: "Climate ICY", - 4: "Climate COLD", - 8: "Climate AVERAGE", - 12: "Climate WARM", - 1: "Climate HOT", - 10: "Lock temp display OFF", - 9: "Auto Revert Wifi OFF", - 11: "Auto Revert Wifi ON", -}; - class GrillStatus { constructor(bytes) { + this.timestamp = new Date().getTime(); const hex = Buffer.from(bytes).toString("hex"); this.state = getGrillState(hex); this.settings = hex.substring(17, 32); @@ -83,7 +80,8 @@ class GrillStatus { who: this.settings.substring(0, 1), pizzaMode: this.settings.substring(1, 2) === "2", manySettingsValue: parseHex(this.settings.substring(2, 3)), - lastManySettings: someMap[parseHex(this.settings.substring(2, 3))], + lastManySettings: + GRILL_NOT_SURE_MAP[parseHex(this.settings.substring(2, 3))], grillAdjustA: parseHex(this.settings.substring(3, 5)), // -20 to 20 range grillAdjustB: parseHex(this.settings.substring(5, 7)), probe1AdjustA: parseHex(this.settings.substring(7, 9)), // -25 to 25 deg range @@ -97,7 +95,7 @@ class GrillStatus { parseHex(this.settings.substring(2, 3)) === 0 ) { this.grillOptions.pizzaMode = false; - this.grillOptions.lastManySettings = someMap[1]; + this.grillOptions.lastManySettings = GRILL_NOT_SURE_MAP[1]; } this.hex = hex; this.isOn = this.state === "on" || this.state === "cold smoke mode"; diff --git a/package-lock.json b/package-lock.json index 8bee40f..f00bcaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2386,6 +2386,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, + "optional": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -2404,6 +2405,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -2415,6 +2417,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, + "optional": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -2427,6 +2430,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -2454,7 +2458,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2475,12 +2480,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2495,17 +2502,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2622,7 +2632,8 @@ "inherits": { "version": "2.0.4", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2634,6 +2645,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2648,6 +2660,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2655,12 +2668,14 @@ "minimist": { "version": "1.2.5", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.9.0", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2679,6 +2694,7 @@ "version": "0.5.3", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "^1.2.5" } @@ -2740,7 +2756,8 @@ "npm-normalize-package-bin": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "npm-packlist": { "version": "1.4.8", @@ -2768,7 +2785,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2780,6 +2798,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2857,7 +2876,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2893,6 +2913,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2912,6 +2933,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2955,12 +2977,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -2969,6 +2993,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2" }, @@ -2978,6 +3003,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -3011,6 +3037,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, + "optional": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -3952,6 +3979,12 @@ "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", "dev": true }, + "fast-equals": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.0.tgz", + "integrity": "sha512-u6RBd8cSiLLxAiC04wVsLV6GBFDOXcTCgWkd3wEoFXgidPSoAJENqC9m7Jb2vewSvjBIfXV6icKeh3GTKfIaXA==", + "dev": true + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -7506,6 +7539,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2" }, @@ -7515,6 +7549,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -7830,6 +7865,23 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "dev": true }, + "rxjs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "rxjs-marbles": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rxjs-marbles/-/rxjs-marbles-6.0.0.tgz", + "integrity": "sha512-d9EzhUc0VWeY8OaF/qpZNmunelzE6tml+dOPlyJ7bUWFnmUrQuHAvIOP9C5eJil36dkANNqna1Ev/aPtAd5joA==", + "dev": true, + "requires": { + "fast-equals": "^2.0.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -8747,6 +8799,11 @@ "punycode": "^2.1.0" } }, + "tslib": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 9677a28..ea93ddb 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "pouchdb-find": "^7.2.1", "random-words": "^1.1.1", "rethinkdb": "^2.4.2", + "rxjs": "^6.5.5", "short-uuid": "^3.1.1", "uuid": "^7.0.3" }, @@ -36,6 +37,7 @@ "babel-jest": "^25.3.0", "cross-env": "^7.0.2", "jest": "^25.3.0", + "rxjs-marbles": "^6.0.0", "supertest": "^4.0.2" }, "jest": { diff --git a/sandbox/basic.js b/sandbox/basic.js deleted file mode 100644 index a1878c5..0000000 --- a/sandbox/basic.js +++ /dev/null @@ -1,46 +0,0 @@ -import express from 'express'; -import dgram from 'dgram'; - -import GrillStatus from '../GrillStatus'; - -const app = express(); -const socket = dgram.createSocket("udp4"); - -const IP = "192.168.86.134"; -const PORT = 8080; - -const commands = Object.freeze({ - powerOn: 'UK001!', - // powerOff: 'UK004!', - getGrillStatus: 'UR001!', - getGrillId: 'UL!', - // setGrillTempF: (temp) => `UT${temp}!`, - setFoodTempF: (temp) => `UF${temp}!` -}); - -// socket.setBroadcast(true); - -socket.on("message", (msg, info) => { - console.log('message', msg, info); - const buff = Buffer.from(msg); - // console.log('message', buff.toString()); - const status = new GrillStatus(buff); - console.log(status);. -}); - -const doSend = (message) => { - const data = Buffer.from(message, 'ascii') - socket.send(data, 0, data.byteLength, PORT, IP, (error) => { - console.error(error); - }); -} - -// doSend(commands.getGrillId); -doSend(commands.getGrillStatus); - -app.get("/", function (req, res) { - res.send("Hello World!"); -}); -app.listen(3000, function () { - console.log("Example app listening on port 3000!"); -}); \ No newline at end of file diff --git a/services/eventEmitter.js b/services/eventEmitter.js new file mode 100644 index 0000000..e7be1cf --- /dev/null +++ b/services/eventEmitter.js @@ -0,0 +1,2 @@ +import EventEmitter from 'events'; +export default new EventEmitter(); diff --git a/services/grillPollStatus.js b/services/grillPollStatus.js new file mode 100644 index 0000000..bb3bbb7 --- /dev/null +++ b/services/grillPollStatus.js @@ -0,0 +1,57 @@ +import dgram from "dgram"; +import { PORT, IP, INTERVAL } from "../config"; +import observableGrillStatus from "../services/observableGrillStatus"; +import GrillStatus from "../models/GrillStatus"; +import { COMMANDS } from "../constants"; +import eventEmitter from "./eventEmitter"; +import { grillSendCommandOnce } from "./grillSendCommandOnce"; + +// export const grillPollStatus = () => { +// const socket = dgram.createSocket("udp4"); +// const data = Buffer.from(COMMANDS.getGrillStatus, "ascii"); + +// socket.on("message", async (msg, info) => { +// console.log(msg.toString()); +// console.log(info); +// observableGrillStatus.next(new GrillStatus(Buffer.from(msg))); +// }); + +// socket.on("error", (msg, info) => { +// console.error("POLL error"); +// }); + +// // @TODO catch error +// const doSendNow = () => { +// socket.send(data, 0, data.byteLength, PORT, IP, (err) => {}); +// }; + +// setInterval(() => { +// doSendNow(); +// }, INTERVAL); + +// doSendNow(); +// }; + +export const grillPollStatus = async () => { + const msg = await grillSendCommandOnce(COMMANDS.getGrillStatus, "ascii"); + observableGrillStatus.next(new GrillStatus(Buffer.from(msg))); + + setTimeout(() => { + grillPollStatus(); + }, INTERVAL); +}; + + +/** + * export const grillPollStatus = () => { + setInterval(() => { + _grillPollStatus(); + }, INTERVAL); +}; + +const _grillPollStatus = async () => { + const msg = await grillSendCommandOnce(COMMANDS.getGrillStatus, "ascii"); + observableGrillStatus.next(new GrillStatus(Buffer.from(msg))); +} + + */ \ No newline at end of file diff --git a/services/grillSendCommandOnce.js b/services/grillSendCommandOnce.js new file mode 100644 index 0000000..5f7403d --- /dev/null +++ b/services/grillSendCommandOnce.js @@ -0,0 +1,27 @@ +import dgram from "dgram"; +import { PORT, IP } from "../config"; + +export const grillSendCommandOnce = async (message, mode = "ascii") => { + return new Promise((resolve, reject) => { + const socket = dgram.createSocket("udp4"); + const data = Buffer.from(message, mode); + // @todo catch error + // @todo check info for address / port make sure its not originating from us + // I have not actually seen this happen... + socket.on("message", (msg, info) => { + console.log(msg, info); + socket.close(); + resolve(msg); + }); + socket.on("error", (msg, info) => { + console.error(message, msg.toString()); + socket.close(); + reject(msg.toString()); + }); + socket.send(data, 0, data.byteLength, PORT, IP, (err) => { + if (err) { + console.error(err); + } + }); + }); +}; diff --git a/services/observableGrillStatus.js b/services/observableGrillStatus.js new file mode 100644 index 0000000..d383419 --- /dev/null +++ b/services/observableGrillStatus.js @@ -0,0 +1,12 @@ +import { BehaviorSubject } from "rxjs"; +import GrillStatus from "../models/GrillStatus"; +import { lastKnownStatus } from "../datastore/pouch"; + +// (async () => { +// return await lastKnownStatus(); +// })(); + +// console.warn(status); + +// @todo model should have a `fromJson` method? +export default new BehaviorSubject(""); diff --git a/services/observableGrillStatus.spec.js b/services/observableGrillStatus.spec.js new file mode 100644 index 0000000..667c15a --- /dev/null +++ b/services/observableGrillStatus.spec.js @@ -0,0 +1,27 @@ +import observableGrillStatus from "./observableGrillStatus"; +import eventEmitter from "./eventEmitter"; +import { GRILL_EVENTS } from "../constants"; + +jest.spyOn(eventEmitter, "emit"); + +describe("grill polling", () => { + beforeEach(() => { + eventEmitter.emit.mockClear(); + }); + it("testing rxjs behaviorsubject", async (done) => { + observableGrillStatus.next({ + state: "on", + }); + expect(observableGrillStatus.value.state).toEqual("on"); + expect(eventEmitter.emit).toHaveBeenCalledWith(GRILL_EVENTS.POWER_ON); + done(); + }); + it("testing rxjs behaviorsubject off", async (done) => { + observableGrillStatus.next({ + state: "off", + }); + expect(observableGrillStatus.value.state).toEqual("off"); + expect(eventEmitter.emit).toHaveBeenCalledWith(GRILL_EVENTS.POWER_OFF); + done(); + }); +}); diff --git a/services/renderDebugChart.js b/services/renderDebugChart.js new file mode 100644 index 0000000..2716b0a --- /dev/null +++ b/services/renderDebugChart.js @@ -0,0 +1,9 @@ +import * as store from "./datastore/pouch"; +import asciichart from 'asciichart'; + +export const renderChart = async () => { + const history = await store.all(); + const data = history.map(({ currentProbe1Temp }) => currentProbe1Temp); + console.clear(); + console.log(asciichart.plot(data, { height: 6 })); +}; diff --git a/subscribers/grillStatus.js b/subscribers/grillStatus.js new file mode 100644 index 0000000..29cc6e2 --- /dev/null +++ b/subscribers/grillStatus.js @@ -0,0 +1,75 @@ +import { filter, distinctUntilChanged, skip, pluck, tap } from "rxjs/operators"; +import { GRILL_EVENTS } from "../constants"; +import { addEvent } from "../datastore/pouch"; +import observableGrillStatus from "../services/observableGrillStatus"; + +observableGrillStatus.pipe(skip(1)).subscribe((status) => { + // console.log(" "); + // console.log(" "); + console.clear(); + console.log(">>", status); + // console.log(" "); + // console.log(" "); +}); + +const stateChanges = observableGrillStatus.pipe( + pluck("state"), + distinctUntilChanged(), + skip(2) +); + +export const probe1Changes = (subject) => + subject.pipe( + pluck("desiredProbe1Temp"), + distinctUntilChanged(), + skip(2) + ); + +const probe2Changes = observableGrillStatus.pipe( + pluck("desiredProbe2Temp"), + distinctUntilChanged(), + skip(2) +); + +probe1Changes(observableGrillStatus).subscribe((temp) => { + addEvent({ + type: GRILL_EVENTS.PROBE_DESIRED_CHANGE, + probe: 1, + temp, + }); +}); + +probe2Changes.subscribe((temp) => { + addEvent({ + type: GRILL_EVENTS.PROBE_DESIRED_CHANGE, + probe: 2, + temp, + }); +}); + +stateChanges.pipe(filter((state) => state === "on")).subscribe(() => { + addEvent({ + type: GRILL_EVENTS.POWER_ON, + }); +}); + +stateChanges.pipe(filter((state) => state === "off")).subscribe(() => { + addEvent({ + type: GRILL_EVENTS.POWER_OFF, + }); +}); + +stateChanges.pipe(filter((state) => state === "fan mode")).subscribe(() => { + addEvent({ + type: GRILL_EVENTS.FAN_MODE, + }); +}); + +stateChanges + .pipe(filter((state) => state === "cold smoke mode")) + .subscribe(() => { + addEvent({ + type: GRILL_EVENTS.POWER_ON_COLDSMOKE, + // @todo add percent? + }); + }); diff --git a/subscribers/index.js b/subscribers/index.js new file mode 100644 index 0000000..8fc1040 --- /dev/null +++ b/subscribers/index.js @@ -0,0 +1,24 @@ +import eventEmitter from "../services/eventEmitter"; +import { addEvent } from "../datastore/pouch"; +import { GRILL_EVENTS, EMITTER_EVENTS } from "../constants"; + +eventEmitter.on(EMITTER_EVENTS.NEW_GRILL_STATUS, (status) => { + console.log(" "); + console.log(" "); + console.log(">>", status); + console.log(" "); + console.log(" "); +}); + +// @todo refactor this to use EMITTER_EVENTS +eventEmitter.on(GRILL_EVENTS.POWER_ON, () => { + addEvent({ + type: GRILL_EVENTS.POWER_ON, + }); +}); + +eventEmitter.on(GRILL_EVENTS.POWER_OFF, () => { + addEvent({ + type: GRILL_EVENTS.POWER_OFF, + }); +}); diff --git a/subscribers/marbles.spec.js b/subscribers/marbles.spec.js new file mode 100644 index 0000000..16b9048 --- /dev/null +++ b/subscribers/marbles.spec.js @@ -0,0 +1,168 @@ +import { marbles } from "rxjs-marbles/jest"; +import { + pluck, + map, + concat, + distinctUntilChanged, + skip, + tap, +} from "rxjs/operators"; +import { probe1Changes } from "./grillStatus"; +import { BehaviorSubject } from "rxjs"; +// import observableGrillStatus from "../services/observableGrillStatus"; + +// jest.mock("../services/observableGrillStatus", () => jest.fn()); + +describe("learn", () => { + it( + "probe1Changes", + marbles((m) => { + const sValues = { + a: { probe1Changes: undefined }, + b: { probe1Changes: 100 }, + c: { probe1Changes: 100 }, + d: { probe1Changes: 150 }, + }; + const eValues = { d: 150 }; + const source = m.hot(" ^-a-b-c-d-d-|", sValues); + const expected = m.cold(" --------d---|", eValues); + const dest = source.pipe( + pluck("probe1Changes"), + distinctUntilChanged(), + skip(2), + tap((v) => console.log(v)) + ); + m.expect(dest).toBeObservable(expected); + }) + ); + it( + "probe1Changes with sharedGrillState", + marbles((m) => { + const sValues = { + a: { desiredProbe1Temp: "1" }, + b: { desiredProbe1Temp: 100 }, + c: { desiredProbe1Temp: 100 }, + d: { desiredProbe1Temp: 150 }, + }; + const eValues = { d: 150 }; + const source = m.hot(" ^-a-b-c-d-d-|", sValues); + const expected = m.cold(" --------d---|", eValues); + const dest = probe1Changes(source); + m.expect(dest).toBeObservable(expected); + }) + ); + it( + "parses", + marbles((m) => { + const source = m.cold("---a---b---|"); + const expected = " ---a---b---|"; + m.expect(source).toBeObservable(expected); + }) + ); + it( + "distinctUntilChanged", + marbles((m) => { + const source = m.hot("--a---a-^-a---a---|"); + const expected = " --a-------|"; + const dest = source.pipe(distinctUntilChanged()); + m.expect(dest).toBeObservable(expected); + }) + ); + it( + "distinctUntilChanged but skips them all", + marbles((m) => { + const source = m.hot("-^-a---b---a---a---|"); + const expected = " ------b---a-------|"; + const dest = source.pipe(distinctUntilChanged(), skip(1)); + m.expect(dest).toBeObservable(expected); + }) + ); + it( + "hot example", + marbles((m) => { + const obs1 = m.hot("---a-^-b---|"); + const obs2 = m.hot("----c^------d--|"); + const expected = " --b----d--|"; + const dest = obs1.pipe(concat(obs2)); + m.expect(dest).toBeObservable(expected); + }) + ); +}); + +describe("basic", () => { + it( + "should support marble tests without values", + marbles((m) => { + const source = m.hot(" --^-a-b-c-|"); + const subs = " ^-------!"; + const expected = m.cold(" --b-c-d-|"); + + const destination = source.pipe( + map((value) => String.fromCharCode(value.charCodeAt(0) + 1)) + ); + m.expect(destination).toBeObservable(expected); + m.expect(source).toHaveSubscriptions(subs); + }) + ); + + it( + "should support marble tests with values", + marbles((m) => { + const inputs = { + a: 1, + b: 2, + c: 3, + }; + const outputs = { + x: 2, + y: 3, + z: 4, + }; + + const source = m.hot(" --^-a-b-c-|", inputs); + const subs = " ^-------!"; + const expected = m.cold(" --x-y-z-|", outputs); + + const destination = source.pipe(map((value) => value + 1)); + m.expect(destination).toBeObservable(expected); + m.expect(source).toHaveSubscriptions(subs); + }) + ); + + it( + "should support marble tests with errors", + marbles((m) => { + const source = m.hot(" --^-a-b-c-#"); + const subs = " ^-------!"; + const expected = m.cold(" --a-b-c-#"); + + const destination = source; + m.expect(destination).toBeObservable(expected); + m.expect(source).toHaveSubscriptions(subs); + }) + ); + + it( + "should support marble tests with explicit errors", + marbles((m) => { + const inputs = { + a: 1, + b: 2, + c: 3, + }; + const outputs = { + x: 2, + y: 3, + z: 4, + }; + + const source = m.hot(" --^-a-b-c-#", inputs, new Error("Boom!")); + const subs = " ^-------!"; + const expected = m.cold(" --x-y-z-#", outputs, new Error("Boom!")); + + const destination = source.pipe(map((value) => value + 1)); + m.expect(destination).toBeObservable(expected); + m.expect(source).toHaveSubscriptions(subs); + }) + ); +}); diff --git a/subscribers/process.js b/subscribers/process.js new file mode 100644 index 0000000..a9160c3 --- /dev/null +++ b/subscribers/process.js @@ -0,0 +1,25 @@ +import process from "process"; +import { lastKnownStatus } from "../datastore/pouch"; + +process.stdin.resume(); //so the program will not close instantly + +async function exitHandler(options, exitCode) { + const status = await lastKnownStatus(); + console.log('status', status); + if (options.cleanup) console.log("clean"); + if (exitCode || exitCode === 0) console.log(exitCode); + if (options.exit) process.exit(); +} + +//do something when app is closing +process.on("exit", exitHandler.bind(null, { cleanup: true })); + +//catches ctrl+c event +process.on("SIGINT", exitHandler.bind(null, { exit: true })); + +// catches "kill pid" (for example: nodemon restart) +process.on("SIGUSR1", exitHandler.bind(null, { exit: true })); +process.on("SIGUSR2", exitHandler.bind(null, { exit: true })); + +//catches uncaught exceptions +process.on("uncaughtException", exitHandler.bind(null, { exit: true }));