Compare commits
111 Commits
Author | SHA1 | Date | |
---|---|---|---|
e56b5a2a31 | |||
27ff026c85 | |||
bccbe3dad6 | |||
225d464ad6 | |||
743eb6cfcf | |||
3d14c7a60a | |||
8c61879bb4 | |||
131f426b8f | |||
7864721e3e | |||
e5ba2e52e5 | |||
ee70f34179 | |||
caedbda8c2 | |||
b374dd1d6c | |||
b801aec50e | |||
0db9ff73d1 | |||
55bcbb16eb | |||
7cb59cda01 | |||
a263a26c38 | |||
8224161476 | |||
e7cf1d34f2 | |||
e9c54210aa | |||
3f0be2d972 | |||
f2b0f99941 | |||
0a349d31e4 | |||
9a820741c0 | |||
038390061b | |||
91b87d566d | |||
5c278e9dd5 | |||
94af045e0c | |||
2a756065e7 | |||
0a90a93269 | |||
49d855e306 | |||
00e892c181 | |||
ff9dfb1f43 | |||
a7a736bf9c | |||
ae4f3c7507 | |||
a0a0804754 | |||
bdeb3152d2 | |||
7fa0339791 | |||
42b5576be5 | |||
6276a51f05 | |||
236faaefbe | |||
0c1c1483b1 | |||
594d177348 | |||
8a68c00f01 | |||
a0b0d16c73 | |||
4e4bf9f7ca | |||
4f35efcf79 | |||
a987800cd7 | |||
5fa3d5bb9c | |||
ccc84173cb | |||
24470fe6f2 | |||
9e543989b8 | |||
673aa2faf5 | |||
459008d372 | |||
f0687e4c2b | |||
178bc1e115 | |||
fd4092dbd3 | |||
6f4d94c17b | |||
4666d84905 | |||
c41016a055 | |||
f861b1a145 | |||
e7ada41977 | |||
da06ebc49e | |||
2a4393df73 | |||
768e9faae5 | |||
1eb1dc8270 | |||
02b2f77d4d | |||
4d6e058afb | |||
38a110fa8f | |||
2759b0f9c0 | |||
992ae238eb | |||
c3e70ab9c7 | |||
486c3aaa62 | |||
1932913396 | |||
f0ad09fadd | |||
27cc6bafe5 | |||
fee1e0e88e | |||
7afc662ba7 | |||
32893467bb | |||
3f6f9c3e73 | |||
582c05cdc3 | |||
f16ccbeaa6 | |||
f596ea1bda | |||
1413decbe0 | |||
170cbb7792 | |||
09b524d35b | |||
00a52d0bf5 | |||
1ce82270f5 | |||
9d084ee37a | |||
c0fe29a204 | |||
805e5e28ab | |||
bb3f0cfad5 | |||
288d19e95b | |||
c7ffc0c296 | |||
470d00f468 | |||
538775d726 | |||
6fab8e4101 | |||
1e55c058b9 | |||
b30caa1c60 | |||
ca30189c64 | |||
26c2f0a87d | |||
1ce103d39c | |||
8bea9b55e4 | |||
e24a70053d | |||
76e66c8818 | |||
6ba5766a30 | |||
b8038e7847 | |||
63ef0da043 | |||
180723e4f5 | |||
928b1d36da |
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,11 +9,11 @@ lerna-debug.log*
|
||||
.VSCodeCounter
|
||||
.vscode
|
||||
.vscode/*
|
||||
config.json
|
||||
log.txt
|
||||
nohup.out
|
||||
data/config.db
|
||||
config.db
|
||||
package-lock.json
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -11,7 +11,7 @@
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/main.js"
|
||||
"program": "${workspaceFolder}/src/main.js"
|
||||
}
|
||||
]
|
||||
}
|
27
PCB Notes.md
Normal file
27
PCB Notes.md
Normal file
@ -0,0 +1,27 @@
|
||||
# New Relay Layout
|
||||
|
||||
12VDC Power Supply
|
||||
|
|
||||
Exhaust--------SSR-40DA-|-----------------NC 3V Relay--------GPIO
|
||||
|
|
||||
Igniter--------SSR-40DA-|-----------------NO 3V Relay--------GPIO
|
||||
|
|
||||
Auger----------SSR-40DA-|-----------------NO 3V Relay--------GPIO
|
||||
|
|
||||
GND
|
||||
|
||||
# Power Supply
|
||||
|
||||
|
||||
|
||||
Flyback Diode (for DC coils)
|
||||
|
||||
Purpose: Protects other components from the inductive voltage spike generated when the relay’s coil is de-energized. This voltage spike can damage transistors or other switching components.
|
||||
Type: A simple 1N4007 diode or similar can be used.
|
||||
Placement: Connect the diode across the relay coil, with the cathode (striped side) to the positive voltage and the anode to the negative end of the coil.
|
||||
|
||||
Capacitor (for Noise Suppression)
|
||||
|
||||
Purpose: A capacitor can be added across the relay contacts to reduce arcing and noise caused by contact bounce, which can help reduce wear.
|
||||
Type: Use a ceramic or film capacitor, typically around 0.1 µF, rated for the expected voltage.
|
||||
Placement: Place the capacitor across the relay’s switching contacts. For high-voltage AC circuits, ensure the capacitor is rated accordingly (e.g., X2 type for AC line).
|
72
PCB/Dimensions.md
Normal file
72
PCB/Dimensions.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Info
|
||||
Auger on SSR for frequent switching
|
||||
Exhaust on SSR for inductive load
|
||||
Igniter on Songlei
|
||||
|
||||
# SRD-40-DC
|
||||
Solid state relays
|
||||
|
||||
### Overall
|
||||
```
|
||||
W(x) - 45.75 mm
|
||||
L(y) - 63.00 mm
|
||||
D(z) - 23.75 mm
|
||||
```
|
||||
### Holes
|
||||
Location: Centered on `W(x)`
|
||||
|
||||
```
|
||||
Top: 4.25 x 7.25 mm (diameter by length)
|
||||
Bottom: 4.25 mm (diameter)
|
||||
Inside E2E: 42.00 mm <---> 53.50 mm
|
||||
Outside E2E: 4.30 mm <---> 4.30 mm
|
||||
```
|
||||
|
||||
# Relay Mobules
|
||||
Amazon prepackaged baddies.
|
||||
|
||||
### Overall
|
||||
```
|
||||
W(x) - 70.30 mm
|
||||
L(y) - 16.90 mm
|
||||
D(z) - 18.25 mm
|
||||
```
|
||||
|
||||
### Holes
|
||||
Location: Four outside corners of `W(x)`/`L(y)`
|
||||
|
||||
```
|
||||
Diameter: 2.50 mm
|
||||
Outside E2E: 0.60 mm
|
||||
```
|
||||
|
||||
# Terminal Block
|
||||
Home Depot special, temporary until PCB.
|
||||
|
||||
### Overall
|
||||
```
|
||||
W(x) - 34.00 mm
|
||||
L(y) - 135.25 mm
|
||||
D(z) - 16.00 mm
|
||||
```
|
||||
|
||||
### Holes
|
||||
Location: Four outside corners of `W(x)`/`L(y)`
|
||||
|
||||
```
|
||||
Diameter: - 5.40 mm
|
||||
Outside E2E L(y) - 3.10 mm
|
||||
Outside E2C L(y) - 5.80 mm
|
||||
Inside E2E W(x) - 7.45 mm
|
||||
Inside C2C W(x) - 12.85 mm
|
||||
```
|
||||
|
||||
45.08-8.45=36.63
|
||||
```
|
||||
Top of terminal: 135.25 mm
|
||||
L of RM: 16.9 mm
|
||||
1/2: 8.45
|
||||
Middle of top RM: 101.4375 (109.8875)
|
||||
Mid of mid RM: 67.625 (76.075)
|
||||
Mid of bot RM: 33.8125 (42.2625)
|
||||
```
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2
PCB/Hestia 2024/Hestia 2024.kicad_pcb
Normal file
2
PCB/Hestia 2024/Hestia 2024.kicad_pcb
Normal file
@ -0,0 +1,2 @@
|
||||
(kicad_pcb (version 20240108) (generator "pcbnew") (generator_version "8.0")
|
||||
)
|
83
PCB/Hestia 2024/Hestia 2024.kicad_prl
Normal file
83
PCB/Hestia 2024/Hestia 2024.kicad_prl
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"board": {
|
||||
"active_layer": 0,
|
||||
"active_layer_preset": "",
|
||||
"auto_track_width": true,
|
||||
"hidden_netclasses": [],
|
||||
"hidden_nets": [],
|
||||
"high_contrast_mode": 0,
|
||||
"net_color_mode": 1,
|
||||
"opacity": {
|
||||
"images": 0.6,
|
||||
"pads": 1.0,
|
||||
"tracks": 1.0,
|
||||
"vias": 1.0,
|
||||
"zones": 0.6
|
||||
},
|
||||
"selection_filter": {
|
||||
"dimensions": true,
|
||||
"footprints": true,
|
||||
"graphics": true,
|
||||
"keepouts": true,
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pads": true,
|
||||
"text": true,
|
||||
"tracks": true,
|
||||
"vias": true,
|
||||
"zones": true
|
||||
},
|
||||
"visible_items": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
32,
|
||||
33,
|
||||
34,
|
||||
35,
|
||||
36,
|
||||
39,
|
||||
40
|
||||
],
|
||||
"visible_layers": "fffffff_ffffffff",
|
||||
"zone_display_mode": 0
|
||||
},
|
||||
"git": {
|
||||
"repo_password": "",
|
||||
"repo_type": "",
|
||||
"repo_username": "",
|
||||
"ssh_key": ""
|
||||
},
|
||||
"meta": {
|
||||
"filename": "Hestia 2024.kicad_prl",
|
||||
"version": 3
|
||||
},
|
||||
"project": {
|
||||
"files": []
|
||||
}
|
||||
}
|
392
PCB/Hestia 2024/Hestia 2024.kicad_pro
Normal file
392
PCB/Hestia 2024/Hestia 2024.kicad_pro
Normal file
@ -0,0 +1,392 @@
|
||||
{
|
||||
"board": {
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {},
|
||||
"diff_pair_dimensions": [],
|
||||
"drc_exclusions": [],
|
||||
"rules": {},
|
||||
"track_widths": [],
|
||||
"via_dimensions": []
|
||||
},
|
||||
"ipc2581": {
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": ""
|
||||
},
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"boards": [],
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
"erc": {
|
||||
"erc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"pin_map": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
],
|
||||
"rule_severities": {
|
||||
"bus_definition_conflict": "error",
|
||||
"bus_entry_needed": "error",
|
||||
"bus_to_bus_conflict": "error",
|
||||
"bus_to_net_conflict": "error",
|
||||
"conflicting_netclasses": "error",
|
||||
"different_unit_footprint": "error",
|
||||
"different_unit_net": "error",
|
||||
"duplicate_reference": "error",
|
||||
"duplicate_sheet_names": "error",
|
||||
"endpoint_off_grid": "warning",
|
||||
"extra_units": "error",
|
||||
"global_label_dangling": "warning",
|
||||
"hier_label_mismatch": "error",
|
||||
"label_dangling": "error",
|
||||
"lib_symbol_issues": "warning",
|
||||
"missing_bidi_pin": "warning",
|
||||
"missing_input_pin": "warning",
|
||||
"missing_power_pin": "error",
|
||||
"missing_unit": "warning",
|
||||
"multiple_net_names": "warning",
|
||||
"net_not_bus_member": "warning",
|
||||
"no_connect_connected": "warning",
|
||||
"no_connect_dangling": "warning",
|
||||
"pin_not_connected": "error",
|
||||
"pin_not_driven": "error",
|
||||
"pin_to_pin": "warning",
|
||||
"power_pin_not_driven": "error",
|
||||
"similar_labels": "warning",
|
||||
"simulation_model_issue": "ignore",
|
||||
"unannotated": "error",
|
||||
"unit_value_mismatch": "error",
|
||||
"unresolved_variable": "error",
|
||||
"wire_dangling": "error"
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"pinned_footprint_libs": [],
|
||||
"pinned_symbol_libs": []
|
||||
},
|
||||
"meta": {
|
||||
"filename": "Hestia 2024.kicad_pro",
|
||||
"version": 1
|
||||
},
|
||||
"net_settings": {
|
||||
"classes": [
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Default",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.2,
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 3
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
"netclass_patterns": []
|
||||
},
|
||||
"pcbnew": {
|
||||
"last_paths": {
|
||||
"gencad": "",
|
||||
"idf": "",
|
||||
"netlist": "",
|
||||
"plot": "",
|
||||
"pos_files": "",
|
||||
"specctra_dsn": "",
|
||||
"step": "",
|
||||
"svg": "",
|
||||
"vrml": ""
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"bom_export_filename": "",
|
||||
"bom_fmt_presets": [],
|
||||
"bom_fmt_settings": {
|
||||
"field_delimiter": ",",
|
||||
"keep_line_breaks": false,
|
||||
"keep_tabs": false,
|
||||
"name": "CSV",
|
||||
"ref_delimiter": ",",
|
||||
"ref_range_delimiter": "",
|
||||
"string_delimiter": "\""
|
||||
},
|
||||
"bom_presets": [],
|
||||
"bom_settings": {
|
||||
"exclude_dnp": false,
|
||||
"fields_ordered": [
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reference",
|
||||
"name": "Reference",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Value",
|
||||
"name": "Value",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Datasheet",
|
||||
"name": "Datasheet",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Footprint",
|
||||
"name": "Footprint",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Qty",
|
||||
"name": "${QUANTITY}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "DNP",
|
||||
"name": "${DNP}",
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"filter_string": "",
|
||||
"group_symbols": true,
|
||||
"name": "Grouped By Value",
|
||||
"sort_asc": true,
|
||||
"sort_field": "Reference"
|
||||
},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
"dashed_lines_gap_length_ratio": 3.0,
|
||||
"default_line_thickness": 6.0,
|
||||
"default_text_size": 50.0,
|
||||
"field_names": [],
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
"intersheets_ref_show": false,
|
||||
"intersheets_ref_suffix": "",
|
||||
"junction_size_choice": 3,
|
||||
"label_size_ratio": 0.375,
|
||||
"operating_point_overlay_i_precision": 3,
|
||||
"operating_point_overlay_i_range": "~A",
|
||||
"operating_point_overlay_v_precision": 3,
|
||||
"operating_point_overlay_v_range": "~V",
|
||||
"overbar_offset_ratio": 1.23,
|
||||
"pin_symbol_size": 25.0,
|
||||
"text_offset_ratio": 0.15
|
||||
},
|
||||
"legacy_lib_dir": "",
|
||||
"legacy_lib_list": [],
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"net_format_name": "",
|
||||
"page_layout_descr_file": "",
|
||||
"plot_directory": "",
|
||||
"spice_current_sheet_as_root": false,
|
||||
"spice_external_command": "spice \"%I\"",
|
||||
"spice_model_current_sheet_as_root": true,
|
||||
"spice_save_all_currents": false,
|
||||
"spice_save_all_dissipations": false,
|
||||
"spice_save_all_voltages": false,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
"871df96a-ce20-41c9-b8a3-e7b5b9852509",
|
||||
"Root"
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
}
|
3647
PCB/Hestia 2024/Hestia 2024.kicad_sch
Normal file
3647
PCB/Hestia 2024/Hestia 2024.kicad_sch
Normal file
File diff suppressed because it is too large
Load Diff
97966
PCB/Hestia 2024/fp-info-cache
Normal file
97966
PCB/Hestia 2024/fp-info-cache
Normal file
File diff suppressed because it is too large
Load Diff
114
README.md
114
README.md
@ -2,22 +2,114 @@
|
||||
Node.js Raspberry Pi Pellet Stove Controller, named after the Greek virgin goddess of the hearth.
|
||||
|
||||
# About
|
||||
This project seeks to replace the OEM control panel on a Lennox Winslow PS40 pellet stove with a Raspberry Pi. I will be utilizing a Raspberry Pi Zero W, relays, and switches already installed on the pellet stove. I chose a Pi Zero W for its small form factor, the lack of pre-installed headers, its wireless connectivity, and familiarity with the platform. I had previously used an Arduino Nano for a much more rudimentary version of this project and found great difficulty in making adjustments to the code. Additionally the Raspberry Pi platform will allow for expansion in the future to include IoT controls and logging of usage utilizing networked databases. The project will be written using Node.js and the rpi-gpio Node module. I've chosen Node.js for its familiarity as well as ease of implementation of a web server for future expansion.
|
||||
This project seeks to replace the OEM control panel on a Lennox Winslow PS40 pellet stove with a Raspberry Pi. I am utilizing a Raspberry Pi 3 Model B+, relays, and temperature snap switches installed on the pellet stove. I chose a Pi 3 for its wireless connectivity and familiarity with the platform. I had previously used an Arduino Nano for a much more rudimentary version of this project and found great difficulty in making adjustments to the code. Additionally the Raspberry Pi platform will allow for expansion in the future to include IoT controls and logging of usage utilizing networked databases. The project will be written using Node.js to handle high-level logic and communications, calling Python scripts to handle interacting with GPIO. I'm now in the process of rewriting this project to a more modern v2. Previously I had settled on using deprecated versions of Node.js to maintain compatibility with various Pi GPIO modules, now I've decided to split the GPIO controls off to a small Python interface script.
|
||||
|
||||
# Setting Up the Pi
|
||||
```bash
|
||||
# Update and upgrade the system
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
# Install dependencies and other useful utilities
|
||||
sudo apt install nodejs npm python3 python3-pip neofetch btop fish -y
|
||||
# Install PM2 to manage the Node.js process
|
||||
sudo npm install -g pm2
|
||||
# Set up a new user to run the process
|
||||
sudo useradd -m -s /usr/bin/fish hestia
|
||||
# Set hestia home to /srv/hestia
|
||||
sudo usermod -d /home/hestia hestia
|
||||
# Create a runners group
|
||||
sudo groupadd runners
|
||||
# Add the new user to the runners group
|
||||
sudo usermod -aG runners hestia
|
||||
# Add hestia to the gpio group
|
||||
sudo usermod -aG gpio hestia
|
||||
# Give hestia ownership of the home directory
|
||||
sudo chown hestia:runners /home/hestia -R
|
||||
# Set permissions on the home directory
|
||||
sudo chmod 775 /home/hestia -R
|
||||
# Create a directory for the project
|
||||
sudo mkdir /srv/hestia
|
||||
# Change ownership of the directory to the new user
|
||||
sudo chown hestia:runners /srv/hestia -R
|
||||
# Set permissions on the directory
|
||||
sudo chmod 775 /srv/hestia -R
|
||||
# Change to the new user
|
||||
sudo su hestia
|
||||
# Pull the project
|
||||
git clone https://git.vfsh.dev/voidf1sh/hestia.git /srv/hestia
|
||||
# Change to the project directory
|
||||
cd /srv/hestia
|
||||
# Checkout the backend branch
|
||||
git checkout v2-back
|
||||
# Install the project dependencies
|
||||
npm install
|
||||
# Test run
|
||||
node src/main.js
|
||||
# Start with PM2
|
||||
pm2 start src/main.js --name hestia
|
||||
# Save the process list
|
||||
pm2 save
|
||||
# Setup PM2 daemon
|
||||
pm2 startup
|
||||
```
|
||||
# Logic Flow
|
||||
|
||||
### Boot
|
||||
* Server starts
|
||||
* Call python to check pin states and reset them
|
||||
* Establish connection to MQTT
|
||||
* Listen for messages published to MQTT
|
||||
|
||||
### Auto-Start
|
||||
* Check pin states for safety
|
||||
* Power on igniter, 30 second pre-heat
|
||||
* Power on exhaust
|
||||
* Power on auger
|
||||
* Wait X minutes
|
||||
* Check Proof of Fire switch
|
||||
* No Fire:
|
||||
* Shutdown
|
||||
* Alert
|
||||
* Fire:
|
||||
*Power off igniter
|
||||
|
||||
### Shutdown
|
||||
* Check pin states
|
||||
* Power off auger
|
||||
* Wait X minutes
|
||||
* Check Proof of Fire switch
|
||||
* No Fire:
|
||||
* Wait X minutes
|
||||
* Power off exhaust
|
||||
* Fire:
|
||||
* Wait X minutes
|
||||
* Repeat PoF switch check
|
||||
|
||||
|
||||
# GPIO
|
||||
Three GPIO pins are used along with a common ground to control three relays, supplying 120VAC power to the igniter and combustion blower when appropriate, and supplying power to the auger motor in pulses. Two more GPIO pins are used to detect open/closed status of a temperature-controlled snap switch and a vacuum switch. Another temperature-controlled snap switch is used to supply power to the convection motor when the pellet stove has reached a suitable temperature. A final temperature-controlled snap switch us used to interrupt the circuit for the auger motor to shut the stove off when an over-temperature condition is met. I will be utilizing a OneWire DS18B20 temperature sensor to detect the temperature of air exiting the stove vents.
|
||||
|
||||
| Pi Pin | Function | Direction | Wire Color |
|
||||
| ------:| -------- | --------- | ---------- |
|
||||
7 | Auger Relay | Out | Blue
|
||||
13 | Igniter Relay | Out | Blue/White
|
||||
15 | Combustion Blower Relay | Out | Orange
|
||||
16 | Proof of Fire Switch | In | Orange/White
|
||||
18 | OneWire Temp Sensor | In | Brown
|
||||
22 | Vacuum Switch | In | Brown/White
|
||||
4 | +5VDC for Switches | N/A | Green
|
||||
6 | GND for Relays | N/A | Green/White
|
||||
| Board Pin | BCM Pin | Function | Direction | Wire Color |
|
||||
| ------:| -------- | -------- | --------- | ---------- |
|
||||
7 | 4 | Auger Relay | Out | Or/W
|
||||
13 | 27 | Igniter Relay | Out | Orange
|
||||
15 | 22 | Exhaust Relay | Out | Gr/W
|
||||
16 | 23 | Proof of Fire Switch | In | Blue
|
||||
~~18~~ | ~~24~~ | ~~OneWire Temp Sensor~~ | ~~In~~ | ~~Blue/W~~
|
||||
22 | 25 | Vacuum Switch | In | Green
|
||||
4 | N/A | +5VDC for Switches | N/A | Br/W
|
||||
6 | N/A | GND for Relays | N/A | Brown
|
||||
|
||||
|
||||
### Dev Board Guide
|
||||
8-Pin DIP:
|
||||
1: Exhaust
|
||||
2: Igniter
|
||||
3: Auger
|
||||
4: N/A
|
||||
5: Vacuum
|
||||
6: PoF
|
||||
7: Passthrough to 220 Ohm DIP; 7-seg common cathode
|
||||
8: Passthrough to 220 Ohm DIP; 7-seg common cathode
|
||||
# Schematics
|
||||
## The Current Setup
|
||||
![Current Schematic](/assets/currentschem.png)
|
||||
|
8
TODO
8
TODO
@ -1,8 +0,0 @@
|
||||
Logic Goal:
|
||||
|
||||
1. Start Script & Web Server
|
||||
2. Await a command from the user.
|
||||
3. Startup enables auger
|
||||
4. If auger is enabled, run the cycle command.
|
||||
5. Once per cycle, read then rewrite the config file for interoperability
|
||||
6. Set feed rate based on config.
|
15
TODO.md
Normal file
15
TODO.md
Normal file
@ -0,0 +1,15 @@
|
||||
# In Progress
|
||||
1. ~~Strip to bones~~
|
||||
2. Better commenting
|
||||
3. Add startup and shutdown logic (implemented, not tested)
|
||||
4. Connect to MQTT
|
||||
|
||||
# Done
|
||||
1. GPIO Interface
|
||||
1. Duplicate and adapt `HestiaClasses` from branch `v2-front`
|
||||
|
||||
# Immediate To-Do
|
||||
1. Connect to MQTT
|
||||
|
||||
# Roadmap
|
||||
1. Add wiring and sensors for safeties
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"database": {
|
||||
"createConfigTable": ""
|
||||
}
|
||||
}
|
93
hestia.sh
93
hestia.sh
@ -1,93 +0,0 @@
|
||||
#!/bin/bash
|
||||
#####################################################
|
||||
# Interactive script for managing Hestia Web Portal #
|
||||
#####################################################
|
||||
# Formatting Tips:
|
||||
# https://misc.flogisoft.com/bash/tip_colors_and_formatting
|
||||
#
|
||||
# Formatting:
|
||||
# \e[1m - Bold
|
||||
# \e[2m - Dim
|
||||
# \e[8m - Hidden (passwords)
|
||||
#
|
||||
# Reset:
|
||||
# \e[0m - Reset All Attributes
|
||||
# \e[21m - Reset Bold/Bright
|
||||
# \e[22m - Reset Dim
|
||||
# \e[28m - Reset Hidden
|
||||
#
|
||||
# Colors:
|
||||
# \e[39m - Default Foreground Color
|
||||
# \e[30m - Black
|
||||
# \e[31m - Red
|
||||
# \e[32m - Green
|
||||
# \e[34m - Blue
|
||||
#####################################################
|
||||
|
||||
# Some initial variables to work with
|
||||
timestamp=$(date "+%Y%m%d_%H%M")
|
||||
filename="backup_$timestamp.tar.gz"
|
||||
|
||||
# Initial Prompt
|
||||
# Bash allows for linebreaks in string literals and will
|
||||
# break lines accordingly in the shell
|
||||
echo -e "
|
||||
[ Hestia Control Panel ]
|
||||
|
||||
This script is being run from: '$(pwd)'
|
||||
Active Nodes: $(ps ax -o pid,user,command | grep 'node main.js' | grep -v grep)
|
||||
|
||||
Please enter an option from below:
|
||||
|
||||
[1] Launch Hestia Web Portal
|
||||
[2] Quit Hestia Web Portal
|
||||
[3] View the logs
|
||||
[4] Update Hestia
|
||||
[5] Set up database
|
||||
|
||||
[0] Quit Control Panel"
|
||||
|
||||
# Wait for input
|
||||
read -p "Option: " opt
|
||||
|
||||
# Execute the correct commands based on input.
|
||||
case "$opt" in
|
||||
1)
|
||||
# Launch Hestia Web Portal
|
||||
clear
|
||||
echo "Launching Hestia Web Portal"
|
||||
nohup node main.js > log.txt &
|
||||
;;
|
||||
2)
|
||||
# Quit Hestia Web Portal
|
||||
clear
|
||||
echo "Quitting Hestia Web Portal Gracefully"
|
||||
touch quit
|
||||
;;
|
||||
3)
|
||||
# View logs
|
||||
clear
|
||||
less +F log.txt
|
||||
;;
|
||||
4)
|
||||
# Update Hestia
|
||||
rm data/config.db
|
||||
git pull
|
||||
;;
|
||||
5)
|
||||
# Set up database
|
||||
node modules/_setupdb.js
|
||||
;;
|
||||
0)
|
||||
# Exit the script
|
||||
clear
|
||||
echo "Quitting..."
|
||||
exit
|
||||
;;
|
||||
*)
|
||||
clear
|
||||
echo "Invalid Option!"
|
||||
;;
|
||||
esac
|
||||
|
||||
exec ./hestia.sh
|
91
main.js
91
main.js
@ -1,91 +0,0 @@
|
||||
const fn = require('./modules/functions.js').functions;
|
||||
// Import the config file
|
||||
var config = require('./templates/config.json');
|
||||
// Database Functions
|
||||
const dbfn = require('./modules/database.js');
|
||||
// Web Portal
|
||||
const portal = require('./modules/_server.js');
|
||||
portal.start();
|
||||
|
||||
dbfn.run(`UPDATE timestamps SET value = ${Date.now()} WHERE key = 'process_start'`).catch(err => console.error(`Error setting process start time: ${err}`));
|
||||
|
||||
fn.commands.refreshConfig().then(res => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
|
||||
config = res.config;
|
||||
// Setup for use with the Pi's GPIO pins
|
||||
switch (process.env.ONPI) {
|
||||
case 'true':
|
||||
console.log(`== Running on a Raspberry Pi.`);
|
||||
var gpio = require('rpi-gpio');
|
||||
fn.init(gpio).then((res) => {
|
||||
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||
main(gpio);
|
||||
}).catch(rej => {
|
||||
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Error during initialization: ${rej}`);
|
||||
process.exit(1);
|
||||
});
|
||||
break;
|
||||
case 'false':
|
||||
console.log(`I: Not running on a Raspberry Pi.`);
|
||||
var gpio = 'gpio';
|
||||
fn.init(gpio).then(res => {
|
||||
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||
main(gpio);
|
||||
}).catch(rej => {
|
||||
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Error during initialization: ${rej}`);
|
||||
process.exit(1);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.log(`[${Date.now() - config.timestamps.procStart}] E: Problem with ENV file.`);
|
||||
process.exit(1);
|
||||
break;
|
||||
}
|
||||
}).catch(rej => {
|
||||
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
function main(gpio) {
|
||||
// If the auger is enabled
|
||||
if (config.status.auger == 1) {
|
||||
// Run a cycle of the auger
|
||||
fn.auger.cycle(gpio).then(res => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||
fn.checkForQuit().then(n => {
|
||||
fn.commands.refreshConfig().then(res => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
|
||||
config = res.config;
|
||||
// Recursion ecursion cursion ursion rsion sion ion on n
|
||||
main(gpio);
|
||||
}).catch(rej => {
|
||||
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`);
|
||||
// Recursion ecursion cursion ursion rsion sion ion on n
|
||||
main(gpio);
|
||||
});
|
||||
});
|
||||
}).catch(err => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`);
|
||||
});
|
||||
} else {
|
||||
// If the auger is disabled
|
||||
fn.commands.pause().then(res => {
|
||||
fn.checkForQuit().then(n => {
|
||||
fn.commands.refreshConfig().then(res => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
|
||||
config = res.config;
|
||||
// Recursion ecursion cursion ursion rsion sion ion on n
|
||||
main(gpio);
|
||||
}).catch(rej => {
|
||||
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`);
|
||||
// Recursion ecursion cursion ursion rsion sion ion on n
|
||||
main(gpio);
|
||||
});
|
||||
});
|
||||
}).catch(err => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`);
|
||||
// Recursion ecursion cursion ursion rsion sion ion on n
|
||||
main(gpio);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
/* Pellet Stove Control Panel
|
||||
* Web Configuration Server
|
||||
* v0.0.0 by Skylar Grant
|
||||
*
|
||||
* TODOs:
|
||||
* Implement Express to make it easier
|
||||
* Add actual data into the responses
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const fn = require('./functions.js').functions;
|
||||
var config;
|
||||
fn.commands.refreshConfig().then(newConfig => {
|
||||
config = newConfig.config;
|
||||
});
|
||||
const { dbfn } = require('./functions.js');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
app.use(express.urlencoded());
|
||||
// Our root directory for the public web files
|
||||
app.use(express.static(__dirname + '/../www/public'));
|
||||
// Our directory for views used to render the pages
|
||||
app.set('views', __dirname + '/../www/views');
|
||||
// Set .html as the file extension for views
|
||||
app.engine('html', require('ejs').renderFile);
|
||||
app.set('view engine', 'html');
|
||||
|
||||
// A normal load of the root page
|
||||
app.get('/', (req, res) => {
|
||||
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${JSON.stringify(config)}`);
|
||||
res.render('index', { config: JSON.stringify(config) });
|
||||
});
|
||||
|
||||
// A POST form submission to the root page
|
||||
app.post('/', (req, response) => {
|
||||
if (req.body.start != undefined) {
|
||||
fn.commands.startup();
|
||||
fn.commands.refreshConfig().then(res => {
|
||||
config = res.config;
|
||||
response.render('index', { config: JSON.stringify(config) });
|
||||
return;
|
||||
});
|
||||
}
|
||||
if (req.body.shutdown != undefined) {
|
||||
fn.commands.shutdown();
|
||||
fn.commands.refreshConfig().then(res => {
|
||||
config = res.config;
|
||||
response.render('index', { config: JSON.stringify(config) });
|
||||
return;
|
||||
});
|
||||
}
|
||||
if (req.body.reload != undefined) {
|
||||
const updateAugerOffIntervalQuery = `UPDATE intervals SET value = '${2000 - req.body.feedRate}' WHERE key = 'auger_off'`;
|
||||
const updateAugerOnIntervalQuery = `UPDATE intervals SET value = '${req.body.feedRate}' WHERE key = 'auger_on'`;
|
||||
dbfn.run(updateAugerOffIntervalQuery).then(res => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Auger off interval updated: ${res.data.changes}`);
|
||||
dbfn.run(updateAugerOnIntervalQuery).then(res => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Auger on interval updated: ${res.data.changes}`);
|
||||
fn.commands.refreshConfig().then(res => {
|
||||
config = res.config;
|
||||
response.render('index', { config: JSON.stringify(config) });
|
||||
return;
|
||||
});
|
||||
}).catch(err => console.log(`E: ${err}`));
|
||||
}).catch(err => console.log(`E: ${err}`));
|
||||
}
|
||||
if (req.body.quit != undefined) {
|
||||
fn.commands.quit();
|
||||
fn.commands.refreshConfig().then(res => {
|
||||
config = res.config;
|
||||
response.render('index', { config: JSON.stringify(config) });
|
||||
return;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
start: () => {
|
||||
server.listen(8080, "0.0.0.0");
|
||||
}
|
||||
};
|
@ -1,158 +0,0 @@
|
||||
const dbfn = require('../modules/database.js');
|
||||
|
||||
// Create `status` table
|
||||
/*
|
||||
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||
| Field | Type | Null | Key | Default | Extra |
|
||||
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||
| key | varchar(100) | No | | | |
|
||||
| value | varchar(1000) | No | | | |
|
||||
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||
|
||||
+ ------------------- +
|
||||
| igniter |
|
||||
| blower |
|
||||
| auger |
|
||||
| igniter_finished |
|
||||
| shutdown_initiated |
|
||||
| vacuum |
|
||||
| proof_of_fire |
|
||||
| shutdown_next_cycle |
|
||||
+ ------------------- +
|
||||
|
||||
CREATE TABLE IF NOT EXISTS status (
|
||||
key varchar(100) NOT NULL,
|
||||
value varchar(1000) NOT NULL
|
||||
);
|
||||
*/
|
||||
|
||||
const createStatusTableQuery = "CREATE TABLE IF NOT EXISTS status (key varchar(100) NOT NULL,value varchar(1000) NOT NULL);";
|
||||
dbfn.run(createStatusTableQuery).then(res => {
|
||||
console.log(res.status);
|
||||
const statusEntries = {
|
||||
igniter: 0,
|
||||
blower: 0,
|
||||
auger: 0,
|
||||
igniter_finished: false,
|
||||
shutdown_initiated: 0,
|
||||
vacuum: 0,
|
||||
proof_of_fire: 0,
|
||||
shutdown_next_cycle: 0
|
||||
};
|
||||
for ( key in statusEntries ){
|
||||
const insertStatusEntryQuery = `INSERT INTO status (key, value) VALUES ("${key}", "${statusEntries[key]}")`;
|
||||
dbfn.run(insertStatusEntryQuery).then(res => {
|
||||
console.log(`${res.status}: ${res.data.lastID}: ${res.data.changes} changes`);
|
||||
}).catch(err => console.error(err));
|
||||
}
|
||||
const selectAllStatusEntriesQuery = "SELECT * FROM status";
|
||||
dbfn.all(selectAllStatusEntriesQuery).then(res => {
|
||||
console.log(res.status);
|
||||
}).catch(err => console.error(err));
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
// Create `timestamps` table
|
||||
/*
|
||||
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||
| Field | Type | Null | Key | Default | Extra |
|
||||
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||
| key | varchar(100) | No | | | |
|
||||
| value | varchar(1000) | No | | | |
|
||||
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||
|
||||
+ ------------- +
|
||||
| process_start |
|
||||
| blower_on |
|
||||
| blower_off |
|
||||
| igniter_on |
|
||||
| igniter_off |
|
||||
+ ------------- +
|
||||
|
||||
CREATE TABLE IF NOT EXISTS timestamps (
|
||||
key varchar(100) NOT NULL,
|
||||
value varchar(1000) NOT NULL
|
||||
);
|
||||
*/
|
||||
|
||||
const createTimestampsTableQuery = "CREATE TABLE IF NOT EXISTS timestamps (key varchar(100) NOT NULL,value varchar(1000) NOT NULL);";
|
||||
dbfn.run(createTimestampsTableQuery).then(res => {
|
||||
console.log(res.status);
|
||||
const timestampsEntries = {
|
||||
process_start: 0,
|
||||
blower_on: 0,
|
||||
blower_off: 0,
|
||||
igniter_on: 0,
|
||||
igniter_off: 0
|
||||
};
|
||||
for ( key in timestampsEntries ){
|
||||
const insertTimestampsEntryQuery = `INSERT INTO timestamps (key, value) VALUES ("${key}", "${timestampsEntries[key]}")`;
|
||||
dbfn.run(insertTimestampsEntryQuery).then(res => {
|
||||
console.log(`${res.status}: ${res.data.lastID}: ${res.data.changes} changes`);
|
||||
}).catch(err => console.error(err));
|
||||
}
|
||||
const selectAllTimestampsEntriesQuery = "SELECT * FROM timestamps";
|
||||
dbfn.all(selectAllTimestampsEntriesQuery).then(res => {
|
||||
console.log(res.status);
|
||||
}).catch(err => console.error(err));
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
// Create `intervals` table
|
||||
/*
|
||||
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||
| Field | Type | Null | Key | Default | Extra |
|
||||
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||
| key | varchar(100) | No | | | |
|
||||
| value | varchar(1000) | No | | | |
|
||||
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||
|
||||
+ ------------- +
|
||||
| auger_on |
|
||||
| auger_off |
|
||||
| pause |
|
||||
| igniter_start |
|
||||
| blower_stop |
|
||||
+ ------------- +
|
||||
|
||||
CREATE TABLE IF NOT EXISTS intervals (
|
||||
key varchar(100) NOT NULL,
|
||||
value varchar(1000) NOT NULL
|
||||
);
|
||||
*/
|
||||
|
||||
const createIntervalsTableQuery = "CREATE TABLE IF NOT EXISTS intervals (key varchar(100) NOT NULL,value varchar(1000) NOT NULL);";
|
||||
dbfn.run(createIntervalsTableQuery).then(res => {
|
||||
console.log(res.status);
|
||||
const intervalsEntries = {
|
||||
auger_on: 600,
|
||||
auger_off: 1400,
|
||||
pause: 5000,
|
||||
igniter_start: 420000,
|
||||
blower_stop: 600000
|
||||
};
|
||||
for ( key in intervalsEntries ){
|
||||
const insertIntervalsEntryQuery = `INSERT INTO intervals (key, value) VALUES ("${key}", "${intervalsEntries[key]}")`;
|
||||
dbfn.run(insertIntervalsEntryQuery).then(res => {
|
||||
console.log(`${res.status}: ${res.data.lastID}: ${res.data.changes} changes`);
|
||||
}).catch(err => console.error(err));
|
||||
}
|
||||
const selectAllIntervalsEntriesQuery = "SELECT * FROM intervals";
|
||||
dbfn.all(selectAllIntervalsEntriesQuery).then(res => {
|
||||
console.log(res.status);
|
||||
}).catch(err => console.error(err));
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
// Show the tables to confirm they were created properly:
|
||||
|
||||
dbfn.showTables().then(res => {
|
||||
res.rows.forEach(row => {
|
||||
console.log("Table: " + JSON.stringify(row));
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
@ -1,53 +0,0 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
|
||||
const db = new sqlite3.Database('./data/config.db', (err) => {
|
||||
if (err) throw `E: DB Connection: ${err.message}`;
|
||||
console.log(`I: Connected to the database.`);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
run(query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
db.run(query, function(err) {
|
||||
if (err) {
|
||||
reject("Problem executing the query: " + err.message);
|
||||
return;
|
||||
}
|
||||
resolve( { "status": "Query executed successfully: " + query, "data": this });
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
all(query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
db.all(query, (err, rows) => {
|
||||
if (err) {
|
||||
reject("Problem executing the query: " + err.message);
|
||||
return;
|
||||
}
|
||||
// [ { key: 'key_name', value: '0' }, { key: 'key_name', value: '0' } ]
|
||||
let organizedRows = {};
|
||||
rows.forEach(row => {
|
||||
organizedRows[row.key] = row.value;
|
||||
});
|
||||
resolve({ "status": "Query executed successfully: " + query, "rows": organizedRows });
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
showTables() {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
db.all("SELECT name FROM sqlite_master WHERE type='table'", (err, rows) => {
|
||||
if (err) {
|
||||
reject("Problem executing the query: " + err.message);
|
||||
return;
|
||||
}
|
||||
resolve({ "status": "Tables retreived successfully", "rows": rows });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
@ -1,292 +0,0 @@
|
||||
// TODOs: Add tests for PoF and Vacuum switches, add delays for shutting down blower, test logic for igniter
|
||||
// TODO: Move these to config
|
||||
// Physical Pin numbers for GPIO
|
||||
const augerPin = 7; // Pin for controlling the relay for the pellet auger motor.
|
||||
|
||||
// Require the package for pulling version numbers
|
||||
const package = require('../package.json');
|
||||
// Database Functions
|
||||
const dbfn = require('./database.js');
|
||||
|
||||
|
||||
// Get environment variables
|
||||
const dotenv = require('dotenv').config();
|
||||
// Module for working with files
|
||||
const fs = require('fs');
|
||||
const { exec } = require('child_process');
|
||||
var config = require('../templates/config.json');
|
||||
|
||||
|
||||
// The functions we'll export to be used in other files
|
||||
const functions = {
|
||||
auger: {
|
||||
// Gets called once the Auger Pin has been setup by rpi-gpio
|
||||
ready(err) {
|
||||
if (err) throw err;
|
||||
console.log('Auger GPIO Ready');
|
||||
return;
|
||||
},
|
||||
// Turns the auger on (Pin 7 high)
|
||||
on(gpio) {
|
||||
return new Promise((resolve) => {
|
||||
if (process.env.ONPI == 'true') {
|
||||
gpio.write(augerPin, true, function (err) {
|
||||
if (err) throw err;
|
||||
resolve('Auger turned on.');
|
||||
});
|
||||
} else {
|
||||
resolve('Simulated auger turned on.');
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
// Turns the auger off (pin 7 low)
|
||||
off(gpio) {
|
||||
return new Promise((resolve) => {
|
||||
if (process.env.ONPI == 'true') {
|
||||
gpio.write(augerPin, false, function (err) {
|
||||
if (err) throw err;
|
||||
resolve('Auger turned off.');
|
||||
|
||||
});
|
||||
} else {
|
||||
resolve('Simulated auger turned off.');
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
// Cycles the auger using the two functions above this one (functions.auger.on() and functions.auger.off())
|
||||
// Sleeps in between cycles using functions.sleep()
|
||||
cycle(gpio) {
|
||||
return new Promise((resolve) => {
|
||||
// Turn the auger on
|
||||
this.on(gpio).then((res) => {
|
||||
// Log action if in debug mode
|
||||
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||
// Sleep for the time set in env variables
|
||||
functions.sleep(config.intervals.augerOn).then((res) => {
|
||||
// Log action if in debug mode
|
||||
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||
// Turn the auger off
|
||||
this.off(gpio).then((res) => {
|
||||
// Log action if in debug mode
|
||||
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||
// Sleep for the time set in env variables
|
||||
functions.sleep(config.intervals.augerOff).then((res) => {
|
||||
// Log action if in debug mode
|
||||
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||
// Resolve the promise, letting the main script know the cycle is complete
|
||||
resolve(`Auger cycled (${config.intervals.augerOn}/${config.intervals.augerOff})`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
// Prepare the stove for starting
|
||||
startup() {
|
||||
// Basic startup just enables the auger
|
||||
const enableAugerQuery = "UPDATE status SET value = 1 WHERE key = 'auger'";
|
||||
dbfn.run(enableAugerQuery).then(res => {
|
||||
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Auger enabled.`);
|
||||
return;
|
||||
}).catch(err => console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`));
|
||||
},
|
||||
shutdown() {
|
||||
// Basic shutdown only needs to disable the auger
|
||||
const disableAugerQuery = "UPDATE status SET value = 0 WHERE key = 'auger'";
|
||||
dbfn.run(disableAugerQuery).then(res => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
|
||||
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Auger disabled.`);
|
||||
return;
|
||||
}).catch(err => console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`));
|
||||
},
|
||||
// Pauses the script for the time defined in env variables
|
||||
pause() {
|
||||
return new Promise((resolve) => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Pausing for ${config.intervals.pause}ms`);
|
||||
|
||||
functions.sleep(config.intervals.pause).then((res) => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Pause finished.`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Reload the environment variables on the fly
|
||||
reload(envs) {
|
||||
return new Promise((resolve) => {
|
||||
// Re-require dotenv because inheritance in js sucks
|
||||
const dotenv = require('dotenv').config({ override: true });
|
||||
// Delete the reload file
|
||||
fs.unlink('./reload', (err) => {
|
||||
if (err) throw err;
|
||||
if (process.env.DEBUG) console.log('Deleted reload file.');
|
||||
});
|
||||
// Print out the new environment variables
|
||||
// This should be printed regardless of debug status, maybe prettied up TODO?
|
||||
console.log('Reloaded environment variables.');
|
||||
console.log(`ONTIME=${config.intervals.augerOn}\nOFFTIME=${config.intervals.augerOff}\nPAUSETIME=${config.intervals.pause}\nDEBUG=${process.env.DEBUG}\nONPI=${process.env.ONPI}`);
|
||||
// Resolve the promise, letting the main script know we're done reloading the variables and the cycle can continue
|
||||
resolve();
|
||||
});
|
||||
|
||||
},
|
||||
refreshConfig() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// When the reload button is pressed, the call to this function will contain new config values
|
||||
// {
|
||||
// augerOff: 500,
|
||||
// augerOn: 1500,
|
||||
// pause: 5000
|
||||
// }
|
||||
// if (newSettings != undefined) {
|
||||
// config.intervals.augerOff = newSettings.augerOff;
|
||||
// config.intervals.augerOn = newSettings.augerOn;
|
||||
// console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Intervals updated: (${newSettings.augerOn}/${newSettings.augerOff})`);
|
||||
|
||||
// }
|
||||
// fs.writeFile('./config.json', JSON.stringify(config), (err) => {
|
||||
// if (err) reject(err);
|
||||
// resolve();
|
||||
// });
|
||||
|
||||
// Get status
|
||||
const selectStatusQuery = "SELECT * FROM status";
|
||||
dbfn.all(selectStatusQuery).then(res => {
|
||||
let { status } = config;
|
||||
let { rows } = res;
|
||||
status.auger = rows.auger;
|
||||
status.blower = rows.blower;
|
||||
status.igniter = rows.igniter;
|
||||
status.igniterFinished = rows.igniter_finished;
|
||||
status.pof = rows.proof_of_fire;
|
||||
status.shutdownNextCycle = rows.shutdown_next_cycle;
|
||||
status.vacuum = rows.vacuum;
|
||||
|
||||
// Get timestamps
|
||||
const selectTimestampsQuery = "SELECT * FROM timestamps";
|
||||
dbfn.all(selectTimestampsQuery).then(res => {
|
||||
let { timestamps } = config;
|
||||
let { rows } = res;
|
||||
timestamps.blowerOff = rows.blower_off;
|
||||
timestamps.blowerOn = rows.blower_on;
|
||||
timestamps.igniterOff = rows.igniter_off;
|
||||
timestamps.igniterOn = rows.igniter_on;
|
||||
timestamps.procStart = rows.process_start;
|
||||
|
||||
// Get intervals
|
||||
const selectIntervalsQuery = "SELECT * FROM intervals";
|
||||
dbfn.all(selectIntervalsQuery).then(res => {
|
||||
let { intervals } = config;
|
||||
let { rows } = res;
|
||||
intervals.augerOff = rows.auger_off;
|
||||
intervals.augerOn = rows.auger_on;
|
||||
intervals.blowerStop = rows.blower_stop;
|
||||
intervals.igniterStart = rows.igniter_start;
|
||||
intervals.pause = rows.pause;
|
||||
resolve({ "status": "Refreshed the config", "config": config });
|
||||
}).catch(err => {
|
||||
reject(err);
|
||||
return;
|
||||
});
|
||||
}).catch(err => {
|
||||
reject(err);
|
||||
return;
|
||||
});
|
||||
}).catch(err => {
|
||||
reject(err);
|
||||
return;
|
||||
});
|
||||
});
|
||||
},
|
||||
quit() {
|
||||
functions.commands.shutdown();
|
||||
functions.auger.off(gpio).then(res => {
|
||||
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Exiting app...`);
|
||||
process.exit(0);
|
||||
}).catch(err => {
|
||||
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] E: Unable to shut off auger, rebooting Pi!`);
|
||||
exec('shutdown -r 0');
|
||||
});
|
||||
}
|
||||
},
|
||||
// Sleeps for any given milliseconds
|
||||
sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
// if (process.env.DEBUG) console.log(`Sleeping for ${ms}ms`);
|
||||
// Function to be called when setTimeout finishes
|
||||
const finish = () => {
|
||||
// Resolve the promise
|
||||
resolve(`Slept for ${ms}ms`);
|
||||
};
|
||||
// The actual sleep function, sleeps for ms then calls finish()
|
||||
setTimeout(finish, ms);
|
||||
});
|
||||
},
|
||||
// Initializes rpi-gpio, or resolves if not on a raspberry pi
|
||||
init(gpio) {
|
||||
fs.readFile('./templates/config.json', (err, data) => {
|
||||
fs.writeFile('./config.json', data, (err) => {
|
||||
if (err) throw err;
|
||||
config = require('../config.json');
|
||||
})
|
||||
})
|
||||
// TODO this boot splash needs updating
|
||||
return new Promise((resolve, reject) => {
|
||||
// Boot/About/Info
|
||||
console.log(`== Lennox Winslow PS40
|
||||
== Pellet Stove Control Panel
|
||||
== Author: Skylar Grant
|
||||
== Version: v${package.version}
|
||||
==
|
||||
== Startup Time: ${new Date().toISOString()}
|
||||
==
|
||||
== Environment variables:
|
||||
== == ONTIME=${config.intervals.augerOn}
|
||||
== == OFFTIME=${config.intervals.augerOff}
|
||||
== == PAUSETIME=${config.intervals.pause}
|
||||
== == DEBUG=${process.env.DEBUG}
|
||||
== == ONPI=${process.env.ONPI}`);
|
||||
// Set up GPIO 4 (pysical pin 7) as output, then call functions.auger.ready()
|
||||
if (process.env.ONPI == 'true') {
|
||||
// Init the Auger pin
|
||||
gpio.setup(augerPin, gpio.DIR_OUT, (err) => {
|
||||
if (err) reject(err);
|
||||
if (process.env.DEBUG) console.log('== Auger pin initialized.');
|
||||
// Resolve the promise now that all pins have been initialized
|
||||
resolve('== GPIO Initialized.');
|
||||
});
|
||||
} else {
|
||||
// Resolve the promise
|
||||
resolve('== GPIO Not Available');
|
||||
}
|
||||
});
|
||||
},
|
||||
checkForQuit() {
|
||||
if (config.status.shutdownNextCycle == 1) {
|
||||
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Exiting Process!`);
|
||||
process.exit();
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
if (fs.existsSync('./quit')) {
|
||||
fs.unlink('./quit', err => {
|
||||
if (err) console.log('Error removing the quit file: ' + err);
|
||||
config.status.shutdownNextCycle = 1;
|
||||
config.status.auger = 0;
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve('Not shutting down');
|
||||
}
|
||||
});
|
||||
},
|
||||
time(stamp) {
|
||||
const time = new Date(stamp);
|
||||
return `${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Export the above object, functions, as a module
|
||||
module.exports = { functions, dbfn };
|
3770
package-lock.json
generated
3770
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@ -1,15 +1,19 @@
|
||||
{
|
||||
"name": "pscontrolpanel",
|
||||
"version": "0.2.1",
|
||||
"requires": true,
|
||||
"packages": {},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"ejs": "^3.1.8",
|
||||
"express": "^4.18.2",
|
||||
"rpi-gpio": "^2.1.7",
|
||||
"sequelize": "^6.28.0",
|
||||
"sqlite3": "^5.1.4"
|
||||
}
|
||||
"name": "hestia",
|
||||
"version": "1.0.0",
|
||||
"description": "Hestia Pellet Stove Controller by Skylar Grant",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.vfsh.dev/voidf1sh/hestia"
|
||||
},
|
||||
"author": "Skylar Grant",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5",
|
||||
"mqtt": "^5.10.0"
|
||||
}
|
||||
}
|
||||
|
28
src/_EMR/auger_loop.js
Normal file
28
src/_EMR/auger_loop.js
Normal file
@ -0,0 +1,28 @@
|
||||
// Import modules
|
||||
const gpio = require('../custom_modules/VoidGPIO.js');
|
||||
const config = require('../custom_modules/config.json');
|
||||
const fn = require('../custom_modules/functions.js');
|
||||
|
||||
// Variables
|
||||
process.pinMap = new Map();
|
||||
|
||||
fn.log('Initializing pinMap', 'DEBUG');
|
||||
for (const pin of config.pins) {
|
||||
process.pinMap.set(pin.key, pin);
|
||||
}
|
||||
|
||||
// Auger Loop
|
||||
fn.log('Starting auger loop', 'DEBUG');
|
||||
setInterval(() => {
|
||||
fn.log('Running auger', 'DEBUG');
|
||||
gpio.setPin(process.pinMap.get('auger').board, 1);
|
||||
setTimeout(() => {
|
||||
gpio.setPin(process.pinMap.get('auger').board, 0);
|
||||
}, process.env.AUGER_CYCLE_TIME || 500);
|
||||
}, 2000);
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
fn.log(`Exiting gracefully...`, 'INFO');
|
||||
gpio.setPin(process.pinMap.get('auger').board, 0);
|
||||
process.exit();
|
||||
});
|
17
src/_EMR/exh_off.js
Normal file
17
src/_EMR/exh_off.js
Normal file
@ -0,0 +1,17 @@
|
||||
// Import modules
|
||||
const gpio = require('../custom_modules/VoidGPIO.js');
|
||||
const config = require('../custom_modules/config.json');
|
||||
const fn = require('../custom_modules/functions.js');
|
||||
|
||||
// Variables
|
||||
process.pinMap = new Map();
|
||||
|
||||
fn.log('Initializing pinMap', 'DEBUG');
|
||||
for (const pin of config.pins) {
|
||||
process.pinMap.set(pin.key, pin);
|
||||
}
|
||||
|
||||
fn.log('Turning off exhaust', 'INFO');
|
||||
gpio.setPin(process.pinMap.get('exhaust').board, 1);
|
||||
fn.log('Exhast off', 'INFO');
|
||||
process.exit();
|
17
src/_EMR/exh_on.js
Normal file
17
src/_EMR/exh_on.js
Normal file
@ -0,0 +1,17 @@
|
||||
// Import modules
|
||||
const gpio = require('../custom_modules/VoidGPIO.js');
|
||||
const config = require('../custom_modules/config.json');
|
||||
const fn = require('../custom_modules/functions.js');
|
||||
|
||||
// Variables
|
||||
process.pinMap = new Map();
|
||||
|
||||
fn.log('Initializing pinMap', 'DEBUG');
|
||||
for (const pin of config.pins) {
|
||||
process.pinMap.set(pin.key, pin);
|
||||
}
|
||||
|
||||
fn.log('Turning on exhaust', 'INFO');
|
||||
gpio.setPin(process.pinMap.get('exhaust').board, 0);
|
||||
fn.log('Exhast on', 'INFO');
|
||||
process.exit();
|
17
src/_EMR/ign_off.js
Normal file
17
src/_EMR/ign_off.js
Normal file
@ -0,0 +1,17 @@
|
||||
// Import modules
|
||||
const gpio = require('../custom_modules/VoidGPIO.js');
|
||||
const config = require('../custom_modules/config.json');
|
||||
const fn = require('../custom_modules/functions.js');
|
||||
|
||||
// Variables
|
||||
process.pinMap = new Map();
|
||||
|
||||
fn.log('Initializing pinMap', 'DEBUG');
|
||||
for (const pin of config.pins) {
|
||||
process.pinMap.set(pin.key, pin);
|
||||
}
|
||||
|
||||
fn.log('Turning off igniter', 'INFO');
|
||||
gpio.setPin(process.pinMap.get('igniter').board, 0);
|
||||
fn.log('Igniter off', 'INFO');
|
||||
process.exit();
|
17
src/_EMR/ign_on.js
Normal file
17
src/_EMR/ign_on.js
Normal file
@ -0,0 +1,17 @@
|
||||
// Import modules
|
||||
const gpio = require('../custom_modules/VoidGPIO.js');
|
||||
const config = require('../custom_modules/config.json');
|
||||
const fn = require('../custom_modules/functions.js');
|
||||
|
||||
// Variables
|
||||
process.pinMap = new Map();
|
||||
|
||||
fn.log('Initializing pinMap', 'DEBUG');
|
||||
for (const pin of config.pins) {
|
||||
process.pinMap.set(pin.key, pin);
|
||||
}
|
||||
|
||||
fn.log('Turning on igniter', 'INFO');
|
||||
gpio.setPin(process.pinMap.get('igniter').board, 1);
|
||||
fn.log('Igniter on', 'INFO');
|
||||
process.exit();
|
77
src/_EMR/panel.sh
Executable file
77
src/_EMR/panel.sh
Executable file
@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Path variables
|
||||
ROOT_PATH="/srv/hestia"
|
||||
EMR_FOLDER="src/_EMR"
|
||||
|
||||
|
||||
# Loop
|
||||
while true; do
|
||||
# Prompt for input
|
||||
echo "###################################"
|
||||
echo "# Hestia Emergency Control Panel #"
|
||||
echo "###################################"
|
||||
echo "# 1. Exhaust ON #"
|
||||
echo "# 2. Exhaust OFF #"
|
||||
echo "# #"
|
||||
echo "# 3. Igniter ON #"
|
||||
echo "# 4. Igniter OFF #"
|
||||
echo "# #"
|
||||
echo "# 5. Start Auger Loop #"
|
||||
echo "# 6. Stop Auger Loop #"
|
||||
echo "# #"
|
||||
echo "# 7. Restart Auger Loop #"
|
||||
echo "###################################"
|
||||
echo "# 0. Exit #"
|
||||
echo "###################################"
|
||||
|
||||
# Read user input
|
||||
read -p "Menu Option: " choice
|
||||
|
||||
# Switch case on input
|
||||
case $choice in
|
||||
1)
|
||||
echo "Turning Exhaust ON"
|
||||
cd $ROOT_PATH
|
||||
node $EMR_FOLDER/exh_on.js
|
||||
# Return to the prompt
|
||||
|
||||
;;
|
||||
2)
|
||||
echo "Turning Exhaust OFF"
|
||||
cd $ROOT_PATH
|
||||
node $EMR_FOLDER/exh_off.js
|
||||
;;
|
||||
3)
|
||||
echo "Turning Igniter ON"
|
||||
cd $ROOT_PATH
|
||||
node $EMR_FOLDER/ign_on.js
|
||||
;;
|
||||
4)
|
||||
echo "Turning Igniter OFF"
|
||||
cd $ROOT_PATH
|
||||
node $EMR_FOLDER/ign_off.js
|
||||
;;
|
||||
5)
|
||||
echo "Starting Auger Loop"
|
||||
cd $ROOT_PATH
|
||||
pm2 start $EMR_FOLDER/auger_loop.js --name hestia-emr
|
||||
;;
|
||||
6)
|
||||
echo "Stopping Auger Loop"
|
||||
pm2 stop hestia-emr
|
||||
;;
|
||||
7)
|
||||
echo "Restarting Auger Loop"
|
||||
pm2 restart hestia-emr
|
||||
;;
|
||||
0)
|
||||
echo "Exiting"
|
||||
exit
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Invalid input"
|
||||
;;
|
||||
esac
|
||||
done
|
160
src/custom_modules/HestiaClasses.js
Normal file
160
src/custom_modules/HestiaClasses.js
Normal file
@ -0,0 +1,160 @@
|
||||
const EventEmitter = require('events');
|
||||
const mqtt = require('mqtt');
|
||||
|
||||
module.exports = {
|
||||
// State class
|
||||
State: class State {
|
||||
constructor(config) {
|
||||
this.publisher = 'backend';
|
||||
|
||||
this.igniter = {
|
||||
on: false,
|
||||
name: "igniter",
|
||||
topic: config.mqtt.topics.igniter,
|
||||
publisher: this.publisher,
|
||||
power: (comlink, pinState) => {
|
||||
// Set the power based on the desired pinState
|
||||
this.igniter.on = pinState === 1 ? true : false;
|
||||
comlink.send(config.mqtt.topics.igniter, JSON.stringify(this.igniter));
|
||||
}
|
||||
};
|
||||
|
||||
this.exhaust = {
|
||||
on: false,
|
||||
name: "exhaust",
|
||||
topic: config.mqtt.topics.exhaust,
|
||||
publisher: this.publisher,
|
||||
power: (comlink, pinState) => {
|
||||
// Set the power based on the desired pinState
|
||||
this.exhaust.on = pinState === 1 ? true : false;
|
||||
comlink.send(config.mqtt.topics.exhaust, JSON.stringify(this.exhaust));
|
||||
}
|
||||
};
|
||||
|
||||
this.auger = {
|
||||
on: false,
|
||||
name: "auger",
|
||||
feedRate: 500,
|
||||
topic: config.mqtt.topics.auger,
|
||||
publisher: this.publisher,
|
||||
power: (comlink, pinState) => {
|
||||
// Set the power based on the desired pinState
|
||||
this.auger.on = pinState === 1 ? true : false;
|
||||
comlink.send(config.mqtt.topics.auger, JSON.stringify(this.auger));
|
||||
}
|
||||
};
|
||||
|
||||
this.pof = {
|
||||
on: false,
|
||||
name: "pof",
|
||||
topic: config.mqtt.topics.pof,
|
||||
publisher: this.publisher,
|
||||
power: (comlink, pinState) => {
|
||||
// Set the power based on the desired pinState
|
||||
this.pof.on = pinState === 1 ? true : false;
|
||||
comlink.send(config.mqtt.topics.pof, JSON.stringify(this.pof));
|
||||
}
|
||||
};
|
||||
|
||||
this.vacuum = {
|
||||
on: false,
|
||||
name: "vacuum",
|
||||
topic: config.mqtt.topics.vacuum,
|
||||
publisher: this.publisher,
|
||||
power: (comlink, pinState) => {
|
||||
// Set the power based on the desired pinState
|
||||
this.vacuum.on = pinState === 1 ? true : false;
|
||||
comlink.send(config.mqtt.topics.vacuum, JSON.stringify(this.vacuum));
|
||||
}
|
||||
};
|
||||
|
||||
this.startup = {
|
||||
topic: config.mqtt.topics.startup
|
||||
};
|
||||
|
||||
this.shutdown = {
|
||||
topic: config.mqtt.topics.shutdown
|
||||
};
|
||||
console.log(`State initialized.`)
|
||||
return this;
|
||||
};
|
||||
},
|
||||
// Communicator class
|
||||
Communicator: class Communicator extends EventEmitter {
|
||||
constructor(state) {
|
||||
super();
|
||||
this.publisher = state.publisher;
|
||||
}
|
||||
|
||||
init(state, config) {
|
||||
// Connect to the MQTT Broker
|
||||
console.log(`Attempting MQTT connection to broker: ${config.mqtt.address}, with username: ${config.mqtt.username}`);
|
||||
this.client = mqtt.connect(config.mqtt.address, {
|
||||
username: config.mqtt.username,
|
||||
password: config.mqtt.password
|
||||
});
|
||||
const { client } = this;
|
||||
|
||||
client.on('connect', () => {
|
||||
console.log('Connected to MQTT broker');
|
||||
// Subscribe to status topics
|
||||
config.states.elements.forEach(element => {
|
||||
client.subscribe(state[element].topic, (err) => {
|
||||
if (!err) {
|
||||
console.log(`Subscribed to ${state[element].topic}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
client.on('disconnect', () => {
|
||||
console.log('Disconnected from MQTT broker');
|
||||
});
|
||||
|
||||
// Handle when the Broker sends us a message
|
||||
client.on('message', (topic, message) => {
|
||||
if (topic.startsWith('hestia/status')) {
|
||||
// Save the existing state
|
||||
const oldState = JSON.parse(JSON.stringify(state));
|
||||
// The message is a buffer which will need to be converted to string
|
||||
const msgStr = message.toString();
|
||||
// Since the message is a JSON object, we can parse it
|
||||
const msgJson = JSON.parse(msgStr);
|
||||
// Log the message
|
||||
// console.log(`Message received on topic ${topic}:`);
|
||||
// console.log(msgJson);
|
||||
// Check if the message is from the backend
|
||||
if (msgJson.publisher === this.publisher) {
|
||||
// console.log('Message is from the backend, ignoring');
|
||||
return;
|
||||
}
|
||||
// console.log('Message is from the frontend, updating state');
|
||||
// Update the state
|
||||
state[msgJson.name].on = msgJson.on;
|
||||
// Emit the state change
|
||||
this.emit('stateChange', oldState, state);
|
||||
} else if (topic === 'hestia/command/startup') {
|
||||
// Empty block for 'hestia/command' topics
|
||||
this.emit('startup');
|
||||
} else if (topic === 'hestia/command/shutdown') {
|
||||
// Empty block for 'hestia/command' topics
|
||||
this.emit('shutdown');
|
||||
} else {
|
||||
console.log(`Unknown topic: ${topic}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Publish a message to the MQTT Broker
|
||||
send(topic, message) {
|
||||
// Publish with retain flag set to true
|
||||
this.client.publish(topic, message, { retain: true }, (err) => {
|
||||
if (err) {
|
||||
console.error('Failed to publish message:', err);
|
||||
} else {
|
||||
console.log('Message published and retained on topic:', topic);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
39
src/custom_modules/VoidGPIO.js
Normal file
39
src/custom_modules/VoidGPIO.js
Normal file
@ -0,0 +1,39 @@
|
||||
const { exec } = require('child_process');
|
||||
const config = require('./config.json');
|
||||
|
||||
module.exports = {
|
||||
// Calls the GPIO Interface script to toggle a pin's state opposite of its current state
|
||||
// togglePin(pin) {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// exec(`python3 ${config.gpioScript} toggle ${pin}`, (error, stdout, stderr) => {
|
||||
// if (error) reject(error);
|
||||
// if (stderr) reject(new Error(stderr));
|
||||
// resolve();
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
// Calls the GPIO Interface script to read a pin's state
|
||||
readPin(pin) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`python3 ${config.gpioScript} read ${pin}`, (error, stdout, stderr) => {
|
||||
if (error) reject(error);
|
||||
if (stderr) reject(new Error(stderr));
|
||||
resolve(stdout.trim());
|
||||
});
|
||||
});
|
||||
},
|
||||
// Calls the GPIO Interface script to set a pin's state regardless of its current state
|
||||
setPin(pin, state) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`python3 ${config.gpioScript} set ${pin} ${state}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
if (stderr) {
|
||||
reject(new Error(stderr));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
74
src/custom_modules/config.json
Normal file
74
src/custom_modules/config.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"mqtt": {
|
||||
"address": "wss://mqtt.asgrant.me",
|
||||
"username": "hestia",
|
||||
"password": "hestia",
|
||||
"topics": {
|
||||
"igniter": "hestia/status/igniter",
|
||||
"exhaust": "hestia/status/exhaust",
|
||||
"auger": "hestia/status/auger",
|
||||
"pof": "hestia/status/pof",
|
||||
"vacuum": "hestia/status/vacuum",
|
||||
"startup": "hestia/command/startup",
|
||||
"shutdown": "hestia/command/shutdown"
|
||||
}
|
||||
},
|
||||
"states": {
|
||||
"elements": [
|
||||
"igniter",
|
||||
"exhaust",
|
||||
"auger",
|
||||
"pof",
|
||||
"vacuum",
|
||||
"startup",
|
||||
"shutdown"
|
||||
]
|
||||
},
|
||||
"pins": [
|
||||
{
|
||||
"key": "igniter",
|
||||
"board": 13,
|
||||
"bcm": 27,
|
||||
"mode": "OUT",
|
||||
"defaultState": 0
|
||||
},
|
||||
{
|
||||
"key": "exhaust",
|
||||
"board": 15,
|
||||
"bcm": 22,
|
||||
"mode": "OUT",
|
||||
"defaultState": 0
|
||||
},
|
||||
{
|
||||
"key": "auger",
|
||||
"board": 7,
|
||||
"bcm": 4,
|
||||
"mode": "OUT",
|
||||
"defaultState": 0
|
||||
},
|
||||
{
|
||||
"key": "pof",
|
||||
"board": 16,
|
||||
"bcm": 23,
|
||||
"mode": "IN"
|
||||
},
|
||||
{
|
||||
"key": "vacuum",
|
||||
"board": 22,
|
||||
"bcm": 25,
|
||||
"mode": "IN"
|
||||
}
|
||||
],
|
||||
"power": {
|
||||
"start": {
|
||||
"vacuumCheckDelay": 5000,
|
||||
"igniterPreheat": 60000,
|
||||
"igniterDelay": 420000
|
||||
},
|
||||
"stop": {
|
||||
"exhaustDelay": 600000
|
||||
}
|
||||
},
|
||||
"gpioScript": "src/python/gpio_interface.py",
|
||||
"augerTotalCycleTime": 2000
|
||||
}
|
310
src/custom_modules/functions.js
Normal file
310
src/custom_modules/functions.js
Normal file
@ -0,0 +1,310 @@
|
||||
const dotenv = require('dotenv').config();
|
||||
const debug = process.env.DEBUG === "TRUE";
|
||||
const config = require('./config.json');
|
||||
const { pins } = config;
|
||||
const gpio = require('./VoidGPIO.js');
|
||||
process.pinMap = new Map();
|
||||
|
||||
for (const pin of pins) {
|
||||
process.pinMap.set(pin.key, pin);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
log(message, level) {
|
||||
if (level) {
|
||||
switch (level) {
|
||||
case 'INFO':
|
||||
console.log(`INFO: ${message}`);
|
||||
break;
|
||||
case 'DEBUG':
|
||||
if (debug) {
|
||||
console.log(`DEBUG: ${message}`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (debug) {
|
||||
console.log(`DEBUG: ${message}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
},
|
||||
gpio: {
|
||||
setDefaults(comlink, state) {
|
||||
// Set all output pins to their default states
|
||||
return new Promise((resolve, reject) => {
|
||||
// Create an array to store state changes
|
||||
let stateChanges = [];
|
||||
|
||||
// Create an array of promises for each pin
|
||||
const promises = pins.map(pin => {
|
||||
if (pin.mode === 'OUT') {
|
||||
return gpio.setPin(pin.board, pin.defaultState).then(() => {
|
||||
stateChanges.push(`Set ${pin.key} pin to ${pin.defaultState}.`);
|
||||
state[pin.key].power(comlink, pin.defaultState);
|
||||
setTimeout(() => {
|
||||
gpio.readPin(pin.board).then(pinState => {
|
||||
module.exports.log(`Read Pin: ${pin.key} is ${pinState}`);
|
||||
}).catch(e => console.error(e));
|
||||
}, 1000);
|
||||
}).catch(e => console.error(e));
|
||||
} else if (pin.mode === 'IN') {
|
||||
return gpio.readPin(pin.board).then(pinState => {
|
||||
const boolState = pinState === '1' ? true : false;
|
||||
state[pin.key].on = boolState;
|
||||
comlink.send(config.mqtt.topics[pin.key], JSON.stringify(state[pin.key]));
|
||||
stateChanges.push(`${pin.key}: ${state}`);
|
||||
}).catch(e => console.error(e));
|
||||
}
|
||||
});
|
||||
|
||||
Promise.all(promises).then(() => {
|
||||
const changes = stateChanges.join('\n');
|
||||
resolve(changes);
|
||||
}).catch(reject);
|
||||
});
|
||||
},
|
||||
async init(comlink, state) {
|
||||
module.exports.log('Resetting all output pins.');
|
||||
module.exports.gpio.setDefaults(comlink, state).then((changes) => {
|
||||
module.exports.log(changes);
|
||||
}).catch(e => console.error(e));
|
||||
}
|
||||
},
|
||||
routines: {
|
||||
startup(comlink, state) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const mod = module.exports;
|
||||
// Set pins to default states
|
||||
mod.gpio.setDefaults(comlink, state).then(changes => {
|
||||
mod.log(changes);
|
||||
}).catch(e => console.error(e));
|
||||
|
||||
// Turn on the exhaust
|
||||
mod.power.exhaust.on(comlink, state).then((res) => {
|
||||
// Wait for vacuum
|
||||
mod.sleep(config.power.start.vacuumCheckDelay).then(() => {
|
||||
// Vacuum switch is in series with auger so no need for logic check
|
||||
// Turn on the auger
|
||||
mod.power.auger.on(comlink, state).then((res) => {
|
||||
// Wait for auger to start
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
},
|
||||
shutdown(comlink, state) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
});
|
||||
},
|
||||
cycleAuger() {
|
||||
const mod = module.exports;
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// Check if the auger is enabled
|
||||
if (process.psState.auger.on) {
|
||||
mod.power.auger.on().then((res) => {
|
||||
// Sleep while auger is feeding
|
||||
mod.sleep(process.psState.auger.feedRate).then(() => {
|
||||
// Turn off auger
|
||||
mod.power.auger.off().then((res) => {
|
||||
resolve('Auger cycle complete.');
|
||||
}).catch(e => reject(e));
|
||||
});
|
||||
}).catch(e => reject(e));
|
||||
} else {
|
||||
resolve('Auger is disabled.');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
power: {
|
||||
auger: {
|
||||
on() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
gpio.setPin(process.pinMap.get('auger').board, 1).then(() => {
|
||||
resolve('Auger powered on.');
|
||||
}).catch(e => reject(e));
|
||||
});
|
||||
},
|
||||
off() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
gpio.setPin(process.pinMap.get('auger').board, 0).then(() => {
|
||||
resolve('Auger powered off.');
|
||||
}).catch(e => reject(e));
|
||||
});
|
||||
}
|
||||
},
|
||||
exhaust: {
|
||||
on() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
gpio.setPin(process.pinMap.get('exhaust').board, 1).then(() => {
|
||||
resolve('Exhaust powered on.');
|
||||
}).catch(e => reject(e));
|
||||
});
|
||||
},
|
||||
off() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
gpio.setPin(process.pinMap.get('exhaust').board, 0).then(() => {
|
||||
resolve('Exhaust powered off.');
|
||||
}).catch(e => reject(e));
|
||||
});
|
||||
}
|
||||
},
|
||||
igniter: {
|
||||
on() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
gpio.setPin(process.pinMap.get('igniter').board, 1).then(() => {
|
||||
resolve('Igniter powered on.');
|
||||
}).catch(e => reject(e));
|
||||
});
|
||||
},
|
||||
off() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
gpio.setPin(process.pinMap.get('igniter').board, 0).then(() => {
|
||||
resolve('Igniter powered off.');
|
||||
}).catch(e => reject(e));
|
||||
});
|
||||
}
|
||||
},
|
||||
start: {
|
||||
init(comlink, state) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// TODO: Check pin states?
|
||||
|
||||
|
||||
|
||||
// Start the exhaust
|
||||
this.exhaust().then((res) => {
|
||||
module.exports.log(res);
|
||||
// Check for vacuum
|
||||
this.vacuum().then((res) => {
|
||||
module.exports.log(res);
|
||||
// Start the auger
|
||||
this.auger().then((res) => {
|
||||
module.exports.log(res);
|
||||
resolve('Startup sequence complete.');
|
||||
}).catch(e => console.error(e));
|
||||
// Preheat the igniter
|
||||
this.igniter().then((res) => {
|
||||
module.exports.log(res);
|
||||
module.exports.sleep(config.power.start.igniterDelay).then((res) => {
|
||||
module.exports.log(res);
|
||||
// Check for fire
|
||||
this.fire().then((res) => {
|
||||
module.exports.log(res);
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
}).catch(e => console.error(e));
|
||||
}).catch(e => console.error(e));
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
},
|
||||
exhaust() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// Start exhaust
|
||||
gpio.setPin(process.pinMap.get('exhaust').board, 1).then(() => {
|
||||
// Wait to resolve
|
||||
module.exports.sleep(config.power.start.exhaustDelay).then(() => {
|
||||
resolve('Exhaust started.');
|
||||
});
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
},
|
||||
vacuum() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// Check for vacuum
|
||||
gpio.readPin(process.pinMap.get('vacuum').board).then(state => {
|
||||
if (state === '0') {
|
||||
reject(new Error('Vacuum failure.'));
|
||||
} else {
|
||||
resolve('Vacuum established.');
|
||||
}
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
},
|
||||
igniter() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// Start igniter
|
||||
gpio.setPin(process.pinMap.get('igniter').board, 1).then(() => {
|
||||
// Wait to resolve
|
||||
module.exports.sleep(config.power.start.igniterPreheat).then(() => {
|
||||
resolve('Igniter preheated.');
|
||||
});
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
},
|
||||
auger() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// Start auger
|
||||
gpio.setPin(process.pinMap.get('auger').board, 1).then(() => {
|
||||
resolve('Auger started.');
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
},
|
||||
fire() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// Check for fire
|
||||
gpio.readPin(process.pinMap.get('pof').board).then(state => {
|
||||
if (state === '0') {
|
||||
reject(new Error('Failed ignition.'));
|
||||
} else {
|
||||
resolve('Successful ignition.');
|
||||
}
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
}
|
||||
},
|
||||
stop: {
|
||||
init() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// Power off auger
|
||||
gpio.setPin(process.pinMap.auger.board, 0).then(async () => {
|
||||
// Wait for exhaust shutdown delay
|
||||
await module.exports.sleep(config.power.stop.exhaustDelay);
|
||||
// Power off exhaust
|
||||
gpio.setPin(process.pinMap.exhaust.board, 0).then(() => {
|
||||
// Report successful shutdown
|
||||
resolve('Successful shutdown.');
|
||||
}).catch(e => console.error(e));
|
||||
}).catch(e => console.error(e));
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
handlers: {
|
||||
stateChange(oldState, state) {
|
||||
// Create a promise for each state to check
|
||||
const promises = pins.map(pin => {
|
||||
// Check if the power state changed
|
||||
if (oldState[pin.key].on !== state[pin.key].on) {
|
||||
// Did it turn on or off?
|
||||
if (state[pin.key].on) {
|
||||
if (pin.key !== 'auger') {
|
||||
// Set the corresponding pin to the new state
|
||||
return gpio.setPin(pin.board, 1).then(() => {
|
||||
console.log(`State Change Handler: ${pin.key} powered on.`);
|
||||
}).catch(e => console.error(`GPIO Set Pin: ${e}`));
|
||||
}
|
||||
} else {
|
||||
if (pin.key !== 'auger') {
|
||||
// Set the corresponding pin to the new state
|
||||
return gpio.setPin(pin.board, 0).then(() => {
|
||||
console.log(`State Change Handler: ${pin.key} powered off.`);
|
||||
}).catch(e => console.error(`GPIO Set Pin: ${e}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all promises to resolve
|
||||
Promise.all(promises).then(() => {
|
||||
console.log('State change complete.');
|
||||
}).catch(e => console.error(e));
|
||||
}
|
||||
}
|
||||
}
|
66
src/main.js
Normal file
66
src/main.js
Normal file
@ -0,0 +1,66 @@
|
||||
// Variables
|
||||
process.debug = true;
|
||||
|
||||
// Import modules
|
||||
const gpio = require('./custom_modules/VoidGPIO.js');
|
||||
const config = require('./custom_modules/config.json');
|
||||
const fn = require('./custom_modules/functions.js');
|
||||
const { State, Communicator } = require('./custom_modules/HestiaClasses.js');
|
||||
|
||||
// Variables
|
||||
process.pinMap = new Map();
|
||||
|
||||
for (const pin of config.pins) {
|
||||
process.pinMap.set(pin.key, pin);
|
||||
}
|
||||
|
||||
// Initialize state and comlink
|
||||
process.psState = new State(config);
|
||||
const comms = new Communicator(process.psState);
|
||||
comms.init(process.psState, config);
|
||||
|
||||
// Initialize GPIO
|
||||
fn.gpio.init(comms, process.psState);
|
||||
|
||||
// Sensor detection loop
|
||||
setInterval(() => {
|
||||
// Iterate through pins
|
||||
for (const pin of config.pins) {
|
||||
// If pin is an input, read it
|
||||
if (pin.mode === 'IN') {
|
||||
fn.log(`I: Sensor Detection Loop: Reading pin ${pin.board}`);
|
||||
// Read pin
|
||||
gpio.readPin(pin.board).then(state => {
|
||||
fn.log(`I: Sensor Detection Loop: Pin ${pin.board} is ${state}`);
|
||||
// Convert the state from string to boolean
|
||||
const boolState = state === '1' ? true : false;
|
||||
// Compare the state to the last known state
|
||||
if (boolState !== process.psState[pin.key].on) {
|
||||
// Update the state
|
||||
process.psState[pin.key].on = boolState;
|
||||
// Send the state to the MQTT broker
|
||||
comms.send(config.mqtt.topics[pin.key], JSON.stringify(process.psState[pin.key]));
|
||||
fn.log(`I: Sensor Detection Loop: ${pin.key}: ${state}`);
|
||||
}
|
||||
}).catch(e => console.error(`E: Sensor Detection Loop: ${e}`));
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Auger feed loop
|
||||
setInterval(fn.routines.cycleAuger, config.augerTotalCycleTime);
|
||||
|
||||
comms.on('stateChange', (oldState, state) => {
|
||||
fn.log(`Event: State change detected.`);
|
||||
fn.handlers.stateChange(oldState, state);
|
||||
});
|
||||
|
||||
comms.on('startup', () => {
|
||||
fn.log(`I: Event: Startup detected.`);
|
||||
fn.power.start.init(comms, process.psState).catch(e => console.error(`E: Power Start Init: ${e}`));
|
||||
});
|
||||
|
||||
comms.on('shutdown', () => {
|
||||
fn.log(`I: Event: Shutdown detected.`);
|
||||
fn.power.stop.init(comms, process.psState).catch(e => console.error(`E: Power Stop Init: ${e}`));
|
||||
});
|
75
src/python/fake_gpio_interface.py
Normal file
75
src/python/fake_gpio_interface.py
Normal file
@ -0,0 +1,75 @@
|
||||
import sys
|
||||
|
||||
# Define the state of the pins
|
||||
PIN_STATES = {
|
||||
7: 0,
|
||||
13: 0,
|
||||
15: 0,
|
||||
16: 0,
|
||||
22: 0
|
||||
}
|
||||
|
||||
# Define a function to get the pin state from the array
|
||||
def get_pin_state(pin):
|
||||
if pin in PIN_STATES:
|
||||
return PIN_STATES[pin]
|
||||
else:
|
||||
print(f"Invalid pin: {pin}")
|
||||
return -1
|
||||
|
||||
# Modify the read_pin function to use the get_pin_state function
|
||||
def read_pin(pin):
|
||||
try:
|
||||
# Read pin state
|
||||
state = get_pin_state(pin)
|
||||
# Return 1 if pin is HIGH, 0 if LOW
|
||||
return 1 if state == 1 else 0
|
||||
except Exception as e:
|
||||
# Handle errors
|
||||
print(f"Error reading pin {pin}: {e}")
|
||||
# Return -1 on error
|
||||
return -1
|
||||
|
||||
# Modify the set_pin_state function to return success always
|
||||
def set_pin_state(pin, state):
|
||||
try:
|
||||
# Set the pin state to either HIGH (1) or LOW (0)
|
||||
# Exit with 0 for success
|
||||
return 0
|
||||
except Exception as e:
|
||||
# Handle errors
|
||||
print(f"Error setting pin {pin} to state {state}: {e}")
|
||||
# Exit with 1 for failure
|
||||
return 1
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python3 gpio_interface.py <command> <pin> [<state>]")
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1].lower()
|
||||
pin = int(sys.argv[2])
|
||||
|
||||
if command == "toggle":
|
||||
result = toggle_pin(pin)
|
||||
sys.exit(result)
|
||||
elif command == "read":
|
||||
result = read_pin(pin)
|
||||
print(result)
|
||||
sys.exit(0 if result >= 0 else 1)
|
||||
elif command == "set":
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: python3 gpio_interface.py set <pin> <state>")
|
||||
sys.exit(1)
|
||||
state = int(sys.argv[3])
|
||||
if state not in [0, 1]:
|
||||
print("Invalid state. Use 0 for LOW or 1 for HIGH.")
|
||||
sys.exit(1)
|
||||
result = set_pin_state(pin, state)
|
||||
sys.exit(result)
|
||||
else:
|
||||
print("Invalid command. Use 'toggle', 'read', or 'set'.")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
85
src/python/gpio_interface.py
Normal file
85
src/python/gpio_interface.py
Normal file
@ -0,0 +1,85 @@
|
||||
import RPi.GPIO as GPIO
|
||||
import sys
|
||||
|
||||
# Initialize GPIO using Board mode for pin numbering
|
||||
GPIO.setmode(GPIO.BOARD)
|
||||
GPIO.setwarnings(False)
|
||||
|
||||
# Define pin modes and states
|
||||
def setup_pin(pin, mode):
|
||||
if mode == 'OUT':
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
elif mode == 'IN':
|
||||
GPIO.setup(pin, GPIO.IN)
|
||||
|
||||
def toggle_pin(pin):
|
||||
setup_pin(pin, 'OUT')
|
||||
current_state = GPIO.input(pin)
|
||||
try:
|
||||
# Toggle pin state
|
||||
GPIO.output(pin, GPIO.LOW if current_state == GPIO.HIGH else GPIO.HIGH)
|
||||
# Exit with 0 for success
|
||||
return 0
|
||||
except Exception as e:
|
||||
# Handle errors
|
||||
print(f"Error toggling pin {pin}: {e}")
|
||||
# Exit with 1 for failure
|
||||
return 1
|
||||
|
||||
def read_pin(pin):
|
||||
setup_pin(pin, 'IN')
|
||||
try:
|
||||
# Read pin state
|
||||
state = GPIO.input(pin)
|
||||
# Return 1 if pin is HIGH, 0 if LOW
|
||||
return 1 if state == GPIO.HIGH else 0
|
||||
except Exception as e:
|
||||
# Handle errors
|
||||
print(f"Error reading pin {pin}: {e}")
|
||||
# Return -1 on error
|
||||
return -1
|
||||
|
||||
def set_pin_state(pin, state):
|
||||
setup_pin(pin, 'OUT')
|
||||
try:
|
||||
# Set the pin state to either HIGH (1) or LOW (0)
|
||||
GPIO.output(pin, GPIO.HIGH if state == 1 else GPIO.LOW)
|
||||
# Exit with 0 for success
|
||||
return 0
|
||||
except Exception as e:
|
||||
# Handle errors
|
||||
print(f"Error setting pin {pin} to state {state}: {e}")
|
||||
# Exit with 1 for failure
|
||||
return 1
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python3 gpio_interface.py <command> <pin> [<state>]")
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1].lower()
|
||||
pin = int(sys.argv[2])
|
||||
|
||||
if command == "toggle":
|
||||
result = toggle_pin(pin)
|
||||
sys.exit(result)
|
||||
elif command == "read":
|
||||
result = read_pin(pin)
|
||||
print(result)
|
||||
sys.exit(0 if result >= 0 else 1)
|
||||
elif command == "set":
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: python3 gpio_interface.py set <pin> <state>")
|
||||
sys.exit(1)
|
||||
state = int(sys.argv[3])
|
||||
if state not in [0, 1]:
|
||||
print("Invalid state. Use 0 for LOW or 1 for HIGH.")
|
||||
sys.exit(1)
|
||||
result = set_pin_state(pin, state)
|
||||
sys.exit(result)
|
||||
else:
|
||||
print("Invalid command. Use 'toggle', 'read', or 'set'.")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,30 +0,0 @@
|
||||
{
|
||||
"status": {
|
||||
"igniter": 0,
|
||||
"blower": 0,
|
||||
"auger": 0,
|
||||
"igniterFinished": false,
|
||||
"shutdown": 0,
|
||||
"vacuum": 0,
|
||||
"pof": 0,
|
||||
"shutdownNextCycle": 0
|
||||
},
|
||||
"timestamps": {
|
||||
"procStart": 0,
|
||||
"blowerOn": 0,
|
||||
"blowerOff": 0,
|
||||
"igniterOn": 0,
|
||||
"igniterOff": 0
|
||||
},
|
||||
"intervals": {
|
||||
"augerOn": "600",
|
||||
"augerOff": "1400",
|
||||
"pause": "3000",
|
||||
"igniterStart": "5000",
|
||||
"blowerStop": "5000"
|
||||
},
|
||||
"web": {
|
||||
"port": 8080,
|
||||
"ip": "0.0.0.0"
|
||||
}
|
||||
}
|
BIN
www/.DS_Store
vendored
BIN
www/.DS_Store
vendored
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 163 KiB |
@ -1,98 +0,0 @@
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #333;
|
||||
color: aqua;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
#title {
|
||||
text-align: center;
|
||||
font-size: 35px;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
#title a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#safeties {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.controls-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#buttons button {
|
||||
margin: 20px 5px;
|
||||
font-size: 20px;
|
||||
width: 150px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.controls-container input {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.subheading {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#log-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#log-area {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background-color: aqua;
|
||||
color: aqua;
|
||||
}
|
||||
|
||||
#trial {
|
||||
display:;
|
||||
color: yellow;
|
||||
font-size: 20px;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding: 0;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #333;
|
||||
color: aqua;
|
||||
}
|
||||
|
||||
.button-selected {
|
||||
margin: 5px;
|
||||
border-width: 0px;
|
||||
font-family: times;
|
||||
font-size: 20px;
|
||||
height: 35px;
|
||||
width: 100px;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 auto;
|
||||
color: aqua !important;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid;
|
||||
border-collapse: collapse;
|
||||
padding: 3px;
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hestia Web Portal</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- Bootstrap -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
</head>
|
||||
<body onload="refreshData()" class="container">
|
||||
<script>
|
||||
// Get the config file
|
||||
const config = <%- config %>;
|
||||
console.log(<%- config %>);
|
||||
</script>
|
||||
<%- include('trial.html') -%>
|
||||
<div id="title" class="text-center mb-4">
|
||||
<a href='./'>Hestia Web Portal</a>
|
||||
</div>
|
||||
<div id="status" class="row">
|
||||
<!--
|
||||
| Auger | rows[0].cells[1] | On Time | rows[0].cells[3] |
|
||||
| Feed Rate | rows[1].cells[1] | Off Time | rows[1].cells[3] |
|
||||
-->
|
||||
|
||||
<table id="status-table" class="table table-bordered col-sm-12 col-md-6">
|
||||
<tr>
|
||||
<td>Auger</td>
|
||||
<td></td>
|
||||
<td>On Time</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Feed Rate</td>
|
||||
<td></td>
|
||||
<td>Off Time</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="controls-container">
|
||||
<form action="/" method="post">
|
||||
<!-- Start | Shutdown | Reload Settings -->
|
||||
<div class="button-container d-flex justify-content-between">
|
||||
<input class="btn btn-outline-secondary" type="submit" id="ignite" value="Enable Auger" name="start">
|
||||
<input class="btn btn-outline-secondary" type="submit" id="shutdown" value="Disable Auger" name="shutdown">
|
||||
</div>
|
||||
<!-- Set feed rates -->
|
||||
<div class="form-group">
|
||||
<label for="feedRate">Feed Rate: </label>
|
||||
<select name="feedRate" class="form-control" id="feed-rate-select">
|
||||
<option value="600">Low</option>
|
||||
<option value="800">Medium</option>
|
||||
<option value="1000">High</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="button-container d-flex justify-content-end">
|
||||
<input class="btn btn-outline-secondary" type="submit" id="reload" value="Set Feed Rate" name="reload">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- <div class="text-center my-4">
|
||||
<img src="./dancing_jesus.gif" class="img-fluid">
|
||||
</div> -->
|
||||
<div class="controls-container">
|
||||
<form action="/" method="POST">
|
||||
<input class="btn btn-danger" type="submit" id="quit" value="Quit!!" name="quit" style="visibility: hidden;">
|
||||
</form>
|
||||
</div>
|
||||
<!-- <script src="./main.js"></script> -->
|
||||
<script>
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
function readJSON(path) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open("GET", path, false);
|
||||
request.send(null)
|
||||
var JSONObj = JSON.parse(request.responseText);
|
||||
return JSONObj;
|
||||
}
|
||||
|
||||
function parseStatus(data) {
|
||||
switch (data) {
|
||||
case "0":
|
||||
return "Off";
|
||||
break;
|
||||
case "1":
|
||||
return "On";
|
||||
break
|
||||
default:
|
||||
return "Error: " + data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
// const log = document.getElementById('log-area');
|
||||
// log.contentWindow.location.reload();
|
||||
// sleep(100).then(() => {
|
||||
// document.getElementById('log-area').contentWindow.scrollTo(0, 9999999999);
|
||||
// });
|
||||
|
||||
// Get the elements we need to update
|
||||
const statusTable = document.getElementById('status-table');
|
||||
const augerStatus = statusTable.rows[0].cells[1];
|
||||
const augerOn = statusTable.rows[0].cells[3];
|
||||
const augerOff = statusTable.rows[1].cells[3];
|
||||
const feedRate = statusTable.rows[1].cells[1];
|
||||
const feedRateSelect = document.getElementById('feed-rate-select');
|
||||
|
||||
// console.log(config);
|
||||
|
||||
augerStatus.innerHTML = parseStatus(config.status.auger);
|
||||
augerOn.innerHTML = config.intervals.augerOn;
|
||||
augerOff.innerHTML = config.intervals.augerOff;
|
||||
|
||||
switch (config.intervals.augerOn) {
|
||||
case '600':
|
||||
feedRate.innerHTML = 'Low';
|
||||
feedRateSelect.selectedIndex = 0;
|
||||
break;
|
||||
case '800':
|
||||
feedRate.innerHTML = 'Medium';
|
||||
feedRateSelect.selectedIndex = 1;
|
||||
break;
|
||||
case '1000':
|
||||
feedRate.innerHTML = 'High';
|
||||
feedRateSelect.selectedIndex = 2;
|
||||
break;
|
||||
default:
|
||||
feedRate.innerHTML = 'Unknown';
|
||||
break;
|
||||
}
|
||||
feedRate.value = config.intervals.augerOn;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,65 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hestia Web Portal</title>
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
</head>
|
||||
<body onload="refreshData()">
|
||||
<%- include('trial.html') -%>
|
||||
<div id="title"><a href='./'>Hestia Web Portal</a></div>
|
||||
<div id="status">
|
||||
<!--
|
||||
| Auger | rows[0].cells[1] | On Time | rows[0].cells[3] |
|
||||
| Feed Rate | rows[1].cells[1] | Off Time | rows[1].cells[3] |
|
||||
-->
|
||||
|
||||
<table id="status-table">
|
||||
<tr>
|
||||
<td>Auger</td>
|
||||
<td></td>
|
||||
<td>On Time</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Feed Rate</td>
|
||||
<td></td>
|
||||
<td>Off Time</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="controls-container">
|
||||
<form action="/" method="post">
|
||||
<!-- Start | Shutdown | Reload Settings -->
|
||||
<div class="button-container">
|
||||
<input class="button-unselected" type="submit" id="ignite" value="Enable Auger" name="start"><input class="button-unselected" type="submit" id="shutdown" value="Disable Auger" name="shutdown"><br>
|
||||
</div>
|
||||
<!-- Set feed rates -->
|
||||
<label for="feedRate">Feed Rate: </label>
|
||||
<select name="feedRate">
|
||||
<option value="600">Low</option>
|
||||
<option value="800">Medium</option>
|
||||
<option value="1000">High</option>
|
||||
</select>
|
||||
<div class="button-container">
|
||||
<input class="button-unselected" type="submit" id="reload" value="Reload" name="reload">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<img src="./dancing_jesus.gif">
|
||||
</div>
|
||||
<div id="log-container">
|
||||
<iframe id="log-area" src="log.txt"></iframe>
|
||||
</div>
|
||||
<div class="controls-container">
|
||||
<form action="/" method="POST">
|
||||
<input class="button-unselected" type="submit" id="quit" value="Quit!!" name="quit" style="visibility: hidden;">
|
||||
</form>
|
||||
</div>
|
||||
<script src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,3 +0,0 @@
|
||||
<body>
|
||||
<marquee id="trial">YOUR FREE TRIAL HAS ENDED, PLEASE PURCHASE A PELLET STOVE SUBSCRIPTION</marquee>
|
||||
</body>
|
Loading…
Reference in New Issue
Block a user