diff --git a/.github/workflows/draft-pdf.yml b/.github/workflows/draft-pdf.yml
index d483eaf..afb98bb 100644
--- a/.github/workflows/draft-pdf.yml
+++ b/.github/workflows/draft-pdf.yml
@@ -13,7 +13,7 @@ jobs:
with:
journal: joss
# This should be the path to the paper within your repo.
- paper-path: joss_paper/paper.md
+ paper-path: docs/joss_paper/paper.md
- name: Upload
uses: actions/upload-artifact@v3
with:
@@ -21,4 +21,4 @@ jobs:
# This is the output path where Pandoc will write the compiled
# PDF. Note, this should be the same directory as the input
# paper.md
- path: joss_paper/paper.pdf
\ No newline at end of file
+ path: docs/joss_paper/paper.pdf
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..89cc49c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.pio
+.vscode/.browse.c_cpp.db*
+.vscode/c_cpp_properties.json
+.vscode/launch.json
+.vscode/ipch
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..080e70d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+ // See http://go.microsoft.com/fwlink/?LinkId=827846
+ // for the documentation about the extensions.json format
+ "recommendations": [
+ "platformio.platformio-ide"
+ ],
+ "unwantedRecommendations": [
+ "ms-vscode.cpptools-extension-pack"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 6f3a291..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "liveServer.settings.port": 5501
-}
\ No newline at end of file
diff --git a/Butter-Image.png b/Butter-Image.png
index 6b4bbba..d16bb2b 100644
Binary files a/Butter-Image.png and b/Butter-Image.png differ
diff --git a/README.md b/README.md
index 8004d80..82b3ae3 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,18 @@
# BREAD-Local-Software (Butter)
+## v1 (WIP)
## Installation And Use:
-1. Load files from the "Website Code on SD Card" folder onto the SD card and mount the card on the ESP32
-2. Upload the code in the .ino file in the "Arduino Code" folder to the ESP32
+1. Load files from the [`src/webpage`](src\webpage) folder onto the SD card and mount the card on the ESP32
+2. Upload the code in the .ino file under [`src/loafControl`](src\loafControl) folder to the ESP32
4. Once uploaded, connect to the newly created Wi-Fi access point "BREAD-DARPA" with the password "12345678"
5. Open the Arduino IDE serial monitor and copy the IP address
6. Paste the IP address into a web browser like Google Chrome
-## Things To Change/Add In The Future:
-1. Use JSON objects to send and receive data to and from the ESP32
-2. Better CSS styling (To make it look pretty)
+# In-Progress
+1. Modularization of code and separation of code across multiple files
+2. CSS styling
+
+## TODO
+1. Use JSON objects to send and receive data to and from the ESP32
\ No newline at end of file
diff --git a/RLHT_R2/RLHT_R2.ino b/RLHT_R2/RLHT_R2.ino
index c45c821..e34529a 100644
--- a/RLHT_R2/RLHT_R2.ino
+++ b/RLHT_R2/RLHT_R2.ino
@@ -20,6 +20,10 @@
long lastThermoRead = 0;
long lastSerialPrint = 0;
+//Edit these offsets to calibrate thermocouples as needed
+double thermo1_offset = 4.0;
+double thermo2_offset = 4.0;
+
bool E_STOP = false;
CRGB led;
@@ -224,8 +228,8 @@ void measureThermocouples()
{
if(millis() - lastThermoRead >= THERMO_UPDATE_TIME_MS)
{
- RLHT.thermo1 = CH1.readCelsius();
- RLHT.thermo2 = CH2.readCelsius();
+ RLHT.thermo1 = CH1.readCelsius() + thermo1_offset;
+ RLHT.thermo2 = CH2.readCelsius() + thermo2_offset;
lastThermoRead = millis();
}
}
@@ -357,4 +361,4 @@ void setParametersRLHT(char *in_data)
}
break;
}
-}
\ No newline at end of file
+}
diff --git a/Website Code on SD Card/index.html b/Website Code on SD Card/index.html
deleted file mode 100644
index 7bef873..0000000
--- a/Website Code on SD Card/index.html
+++ /dev/null
@@ -1,2019 +0,0 @@
-
-
-
-
-
-
-
-
-
BREAD Configuration and Controls
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Connection to ESP32:
-
-
-
-
-
-
-
-
-
Pyrolysis Reactor
-
-
-
-
-
-
-
-
-
Thermocouple #:
-
Heater #:
-
-
-
-
-
-
-
-
Thermocouple #:
-
Heater #:
-
-
-
-
-
-
-
-
Thermocouple #:
-
Heater #:
-
-
-
-
-
-
-
-
Thermocouple #:
-
Heater #:
-
-
-
-
-
-
-
-
Thermocouple #:
-
Heater #:
-
-
-
-
-
-
-
-
Thermocouple #:
-
Heater #:
-
-
-
-
-
-
-
-
Thermocouple #:
-
Heater #:
-
-
-
-
-
-
-
-
Thermocouple #:
-
Heater #:
-
-
-
-
-
-
-
-
Thermocouple #:
-
Heater #:
-
-
-
-
-
-
-
Thermocouple #:
-
Heater #:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Bioreactor
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Post Processing
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Chemical Deconstruction Reactor
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/README b/docs/README
new file mode 100644
index 0000000..1dc6372
--- /dev/null
+++ b/docs/README
@@ -0,0 +1 @@
+# docs
\ No newline at end of file
diff --git a/docs/favicon_io/android-chrome-192x192.png b/docs/favicon_io/android-chrome-192x192.png
new file mode 100644
index 0000000..4d37979
Binary files /dev/null and b/docs/favicon_io/android-chrome-192x192.png differ
diff --git a/docs/favicon_io/android-chrome-512x512.png b/docs/favicon_io/android-chrome-512x512.png
new file mode 100644
index 0000000..4824df2
Binary files /dev/null and b/docs/favicon_io/android-chrome-512x512.png differ
diff --git a/docs/favicon_io/apple-touch-icon.png b/docs/favicon_io/apple-touch-icon.png
new file mode 100644
index 0000000..ccbbcef
Binary files /dev/null and b/docs/favicon_io/apple-touch-icon.png differ
diff --git a/docs/favicon_io/favicon-16x16.png b/docs/favicon_io/favicon-16x16.png
new file mode 100644
index 0000000..ced4226
Binary files /dev/null and b/docs/favicon_io/favicon-16x16.png differ
diff --git a/docs/favicon_io/favicon-32x32.png b/docs/favicon_io/favicon-32x32.png
new file mode 100644
index 0000000..15050e1
Binary files /dev/null and b/docs/favicon_io/favicon-32x32.png differ
diff --git a/docs/favicon_io/favicon.ico b/docs/favicon_io/favicon.ico
new file mode 100644
index 0000000..27ef384
Binary files /dev/null and b/docs/favicon_io/favicon.ico differ
diff --git a/docs/favicon_io/site.webmanifest b/docs/favicon_io/site.webmanifest
new file mode 100644
index 0000000..45dc8a2
--- /dev/null
+++ b/docs/favicon_io/site.webmanifest
@@ -0,0 +1 @@
+{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
\ No newline at end of file
diff --git a/joss_paper/bioreactor-webpage.png b/docs/joss_paper/bioreactor-webpage.png
similarity index 100%
rename from joss_paper/bioreactor-webpage.png
rename to docs/joss_paper/bioreactor-webpage.png
diff --git a/joss_paper/flowchart.png b/docs/joss_paper/flowchart.png
similarity index 100%
rename from joss_paper/flowchart.png
rename to docs/joss_paper/flowchart.png
diff --git a/joss_paper/paper.bib b/docs/joss_paper/paper.bib
similarity index 100%
rename from joss_paper/paper.bib
rename to docs/joss_paper/paper.bib
diff --git a/joss_paper/paper.md b/docs/joss_paper/paper.md
similarity index 100%
rename from joss_paper/paper.md
rename to docs/joss_paper/paper.md
diff --git a/joss_paper/paper.pdf b/docs/joss_paper/paper.pdf
similarity index 100%
rename from joss_paper/paper.pdf
rename to docs/joss_paper/paper.pdf
diff --git a/include/README b/include/README
new file mode 100644
index 0000000..194dcd4
--- /dev/null
+++ b/include/README
@@ -0,0 +1,39 @@
+
+This directory is intended for project header files.
+
+A header file is a file containing C declarations and macro definitions
+to be shared between several project source files. You request the use of a
+header file in your project source file (C, C++, etc) located in `src` folder
+by including it, with the C preprocessing directive `#include'.
+
+```src/main.c
+
+#include "header.h"
+
+int main (void)
+{
+ ...
+}
+```
+
+Including a header file produces the same results as copying the header file
+into each source file that needs it. Such copying would be time-consuming
+and error-prone. With a header file, the related declarations appear
+in only one place. If they need to be changed, they can be changed in one
+place, and programs that include the header file will automatically use the
+new version when next recompiled. The header file eliminates the labor of
+finding and changing all the copies as well as the risk that a failure to
+find one copy will result in inconsistencies within a program.
+
+In C, the usual convention is to give header files names that end with `.h'.
+It is most portable to use only letters, digits, dashes, and underscores in
+header file names, and at most one dot.
+
+Read more about using header files in official GCC documentation:
+
+* Include Syntax
+* Include Operation
+* Once-Only Headers
+* Computed Includes
+
+https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
diff --git a/lib/README b/lib/README
new file mode 100644
index 0000000..2593a33
--- /dev/null
+++ b/lib/README
@@ -0,0 +1,46 @@
+
+This directory is intended for project specific (private) libraries.
+PlatformIO will compile them to static libraries and link into executable file.
+
+The source code of each library should be placed in an own separate directory
+("lib/your_library_name/[here are source files]").
+
+For example, see a structure of the following two libraries `Foo` and `Bar`:
+
+|--lib
+| |
+| |--Bar
+| | |--docs
+| | |--examples
+| | |--src
+| | |- Bar.c
+| | |- Bar.h
+| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
+| |
+| |--Foo
+| | |- Foo.c
+| | |- Foo.h
+| |
+| |- README --> THIS FILE
+|
+|- platformio.ini
+|--src
+ |- main.c
+
+and a contents of `src/main.c`:
+```
+#include
+#include
+
+int main (void)
+{
+ ...
+}
+
+```
+
+PlatformIO Library Dependency Finder will find automatically dependent
+libraries scanning project source files.
+
+More information about PlatformIO Library Dependency Finder
+- https://docs.platformio.org/page/librarymanager/ldf.html
diff --git a/platformio.ini b/platformio.ini
new file mode 100644
index 0000000..6d24252
--- /dev/null
+++ b/platformio.ini
@@ -0,0 +1,23 @@
+; PlatformIO Project Configuration File
+;
+; Build options: build flags, source filter
+; Upload options: custom upload port, speed and extra flags
+; Library options: dependencies, extra library storages
+; Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[platformio]
+src_dir = src/spread-beta/main
+
+[env:sparkfun_esp32s2_thing_plus_c]
+platform = espressif32
+board = sparkfun_esp32s2_thing_plus_c
+monitor_speed = 115200
+framework = arduino
+lib_deps =
+ esphome/AsyncTCP-esphome@^2.1.4
+ bblanchon/ArduinoJson@^7.2.0
+ fbiego/ESP32Time@^2.0.6
+ esphome/ESPAsyncWebServer-esphome@^3.2.2
diff --git a/Arduino Code/BREAD-DARPA-Controls/BREAD-DARPA-Controls.ino b/src/loafControl/loafControl.ino
similarity index 62%
rename from Arduino Code/BREAD-DARPA-Controls/BREAD-DARPA-Controls.ino
rename to src/loafControl/loafControl.ino
index 5f94204..64e7ec2 100644
--- a/Arduino Code/BREAD-DARPA-Controls/BREAD-DARPA-Controls.ino
+++ b/src/loafControl/loafControl.ino
@@ -25,38 +25,39 @@ ESP32Time rtc;
#define SLICE_DATA_INTERVAL_MS 10000
-#define NUM_DCMT_SLICES 8
-#define NUM_RLHT_SLICES 8
-#define NUM_PHDO_SLICES 2
+#define NUM_DCMT_SLICES 8
+#define NUM_RLHT_SLICES 8
+#define NUM_PHDO_SLICES 2
// microSD Card Reader connections
-#define SD_CS 5
-#define SPI_MOSI 23
-#define SPI_MISO 19
-#define SPI_SCK 18
+#define SD_CS 5
+#define SPI_MOSI 23
+#define SPI_MISO 19
+#define SPI_SCK 18
-#define ESTOP 32
+#define ESTOP 32
-const char* ssid = "BREAD-DARPA";
-const char* password = "12345678";
+const char *ssid = "BREAD-DARPA";
+const char *password = "12345678";
// Useful unions for breaking data into bytes
-union FLOATUNION_t //Define a float that can be broken up and sent via I2C
+union FLOATUNION_t // Define a float that can be broken up and sent via I2C
{
float number;
uint8_t bytes[4];
};
-union INTUNION_t //Define an int that can be broken up and sent via I2C
+union INTUNION_t // Define an int that can be broken up and sent via I2C
{
int number;
uint8_t bytes[4];
};
// Slice structures for managing data
-struct DCMT_t {
- int address; // I2C address of Slice
- int m1Speed; // motor 1 speed (-100-100)
- int m2Speed; // motor 2 speed (-100-100)
+struct DCMT_t
+{
+ int address; // I2C address of Slice
+ int m1Speed; // motor 1 speed (-100-100)
+ int m2Speed; // motor 2 speed (-100-100)
bool m1En; // motor 1 enable (0:off/apply brake, 1:on/coast)
bool m2En; // motor 2 enable (0:off/apply brake, 1:on/coast)
float pHSetpoint;
@@ -66,25 +67,27 @@ struct DCMT_t {
float Kd_pH;
};
-struct RLHT_t {
- int address; // I2C address of Slice
- float heatSetpoint_1; // set desired temperature for heater 1 (if using relay as heater actuator)
- float Kp_1; // PID proportional gain for heater 1
- float Ki_1; // PID integral gain for heater 1
- float Kd_1; // PID derivative gain for heater 1
+struct RLHT_t
+{
+ int address; // I2C address of Slice
+ float heatSetpoint_1; // set desired temperature for heater 1 (if using relay as heater actuator)
+ float Kp_1; // PID proportional gain for heater 1
+ float Ki_1; // PID integral gain for heater 1
+ float Kd_1; // PID derivative gain for heater 1
float heatSetpoint_2;
float Kp_2;
float Ki_2;
float Kd_2;
- float thermo1; // thermocouple 1 measurement
- float thermo2; // thermocouple 2 measurement
+ float thermo1; // thermocouple 1 measurement
+ float thermo2; // thermocouple 2 measurement
};
-struct PHDO_t {
+struct PHDO_t
+{
int addressPH; // I2C address of Atlas Scientific pH EZO board (default = 99)
int addressDO; // I2C address of Atlas Scientific dissolved oxygen EZO board (default = 97)
- float pH; // current pH reading
- float DO; // current dissolved oxygen reading
+ float pH; // current pH reading
+ float DO; // current dissolved oxygen reading
bool calPH = false;
bool calDO = false;
};
@@ -93,100 +96,109 @@ DCMT_t DCMT[NUM_DCMT_SLICES];
RLHT_t RLHT[NUM_RLHT_SLICES];
PHDO_t PHDO[NUM_PHDO_SLICES];
-int pyro_thermo[9][2] = { // pyrolyis thermocouple layout {address, thermocouple}
- {16, 2}, // DT
- {17, 1}, // DHT
- {16, 1}, // V
- {15, 2}, // CC
- {15, 1}, // SR
- {14, 1}, // KD
- {10, 1}, // C0
- {11, 1}, // C1
- {12, 1} // C2
+int pyro_thermo[9][2] = {
+ // pyrolyis thermocouple layout {address, thermocouple}
+ {16, 2}, // DT
+ {17, 1}, // DHT
+ {16, 1}, // V
+ {15, 2}, // CC
+ {15, 1}, // SR
+ {14, 1}, // KD
+ {10, 1}, // C0
+ {11, 1}, // C1
+ {12, 1} // C2
};
-float pyro_thermo_val[9] = {0,0,0,0,0,0,0,0,0}; // stores thermocouple values
-
-int pyro_heater[10][4] = { // pyrolysis heater/fan layout {address, heater, thermocouple, reverseEnable}
- {16, 2, 2, 0}, // DT
- {16, 1, 1, 0}, // V
- {15, 2, 2, 0}, // CC
- {15, 1, 1, 0}, // SR
- {14, 1, 1, 0}, // KD
- {10, 1, 1, 0}, // C0H
- {10, 2, 1, 1}, // C0F
- {11, 1, 1, 0}, // C1H
- {11, 2, 1, 1}, // C1F
- {12, 1, 1, 1} // C2F
+float pyro_thermo_val[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; // stores thermocouple values
+
+int pyro_heater[10][4] = {
+ // pyrolysis heater/fan layout {address, heater, thermocouple, reverseEnable}
+ {16, 2, 2, 0}, // DT
+ {16, 1, 1, 0}, // V
+ {15, 2, 2, 0}, // CC
+ {15, 1, 1, 0}, // SR
+ {14, 1, 1, 0}, // KD
+ {10, 1, 1, 0}, // C0H
+ {10, 2, 1, 1}, // C0F
+ {11, 1, 1, 0}, // C1H
+ {11, 2, 1, 1}, // C1F
+ {12, 1, 1, 1} // C2F
};
-float pyro_heater_pid[10][4]; // pyrolysis heater/fan PID {setpoint, Kp, Ki, Kd}
+float pyro_heater_pid[10][4]; // pyrolysis heater/fan PID {setpoint, Kp, Ki, Kd}
-int bio_thermo[2][2] = { // bioreator thermocouples {address, thermocouple}
- {10, 2},
- {11, 2}
-};
-int bio_post_heaters[2][4] = { // bioreactor post processing heaters {address, heater, thermocouple, reverseEnable}
- {12, 2, 2, 0}, // pasteurization
- {14, 2, 2, 0} // dryer
-};
-float bio_post_heater_pid[2][4]; // {setpoint, Kp, Ki, Kd}
-float bio_thermo_val[4] = {0,0,0,0};
+int bio_thermo[2][2] = { // bioreator thermocouples {address, thermocouple}
+ {10, 2},
+ {11, 2}};
-int bio_ph[2][2] = { // bioreactor pH layout {PHDO address, DCMT address}
- {98, 20},
- {100, 21}
+int bio_post_heaters[2][4] = {
+ // bioreactor post processing heaters {address, heater, thermocouple, reverseEnable}
+ {12, 2, 2, 0}, // pasteurization
+ {14, 2, 2, 0} // dryer
};
-float bio_ph_val[2][5]; // bioreactor pH settings (pH, Setpoint, Kp, Ki, Kd)
-int bio_do[2] = {97, 99}; // bioreactor DO addresses
-float bio_do_val[2] = {0,0}; // bioreactor DO values
+float bio_post_heater_pid[2][4]; // {setpoint, Kp, Ki, Kd}
+float bio_thermo_val[4] = {0, 0, 0, 0};
-int bio_turbidity[2][4] = { // bioreactor turbidity pump {address, motor, direction, sampleTime}
- {22, 1, 1, 0},
- {22, 2, 1, 0}
-};
-float bio_turbidity_val[2] = {0,0}; // bioreactor turbidity value
-
-int bio_motors[10][3] = { // bioreactor motor config {address, motor, speed}
- {23, 1, 0}, // stirring 1
- {20, 1, 0}, // base 1
- {20, 2, 0}, // acid 1
- {24, 1, 0}, // water
- {24, 2, 0}, // spent media
- {23, 2, 0}, // stirring 2
- {21, 1, 0}, // base 2
- {21, 2, 0}, // acid 2
- {25, 1, 0}, // harvest
- {25, 2, 0} // TFF
+int bio_ph[2][2] = { // bioreactor pH layout {PHDO address, DCMT address}
+ {98, 20},
+ {100, 21}};
+
+float bio_ph_val[2][5]; // bioreactor pH settings (pH, Setpoint, Kp, Ki, Kd)
+
+int bio_do[2] = {97, 99}; // bioreactor DO addresses
+float bio_do_val[2] = {0, 0}; // bioreactor DO values
+
+int bio_turbidity[2][4] = { // bioreactor turbidity pump {address, motor, direction, sampleTime}
+ {22, 1, 1, 0},
+ {22, 2, 1, 0}};
+
+float bio_turbidity_val[2] = {0, 0}; // bioreactor turbidity value
+
+int bio_motors[10][3] = {
+ // bioreactor motor config {address, motor, speed}
+ {23, 1, 0}, // stirring 1
+ {20, 1, 0}, // base 1
+ {20, 2, 0}, // acid 1
+ {24, 1, 0}, // water
+ {24, 2, 0}, // spent media
+ {23, 2, 0}, // stirring 2
+ {21, 1, 0}, // base 2
+ {21, 2, 0}, // acid 2
+ {25, 1, 0}, // harvest
+ {25, 2, 0} // TFF
};
-int chem_thermo[2][4] = { // {address, heater, thermocouple, reverseEnable}
- {13, 1, 1, 0}, // reactor 1
- {13, 2, 2, 0} // reactor 2
+int chem_thermo[2][4] = {
+ // {address, heater, thermocouple, reverseEnable}
+ {13, 1, 1, 0}, // reactor 1
+ {13, 2, 2, 0} // reactor 2
};
-float chem_thermo_val[2] = {0,0};
+float chem_thermo_val[2] = {0, 0};
float chem_heater_pid[2][4];
int chem_motors[4][3] = {
- {26, 1, 0}, // ammonium hydroxide
- {26, 2, 0}, // liquid transfer
- {27, 1, 0}, // water dilute
- {27, 2, 0} // reactor wash
+ {26, 1, 0}, // ammonium hydroxide
+ {26, 2, 0}, // liquid transfer
+ {27, 1, 0}, // water dilute
+ {27, 2, 0} // reactor wash
};
AsyncWebServer server(80);
AsyncEventSource events("/events");
-bool logging = false; //logging data to SD card and graphs
+bool logging = false; // logging data to SD card and graphs
-void initSlices(){
+void initSlices()
+{
// DCMT Slice address assignment
- for(int i=0;isend(SD, "/index.html", "text/html");
- });
-
- //load data from slice form
-
-/*
-Pyrolysis Commands:
- ps1: pyrolysis setpoint 1
- pp1: pyrolysis Kp 1
- pi1: pyrolysis Ki 1
- pd1: pyrolysis Kd 1
-
-Bioreactor Commands:
- bs1: bioreactor setpoint 1
- bp1: bioreactor Kp 1
- bi1: bioreactor Ki 1
- bd1: bioreactor Kd 1
- bm1: bioreactor motor 1
- bt1: bioreactor turbidity sample time 1
-
-Chem Decon Commands:
- cm1: chemreactor motor 1 (pump)
- cs1: chemreactor setpoint 1
- cp1: chemreactor Kp 1
- ci1: chemreactor Ki 1
- cd1: chemreactor Kd 1
-*/
-
- server.on("/form-submit", HTTP_POST, [](AsyncWebServerRequest * request) {
+ // send html data to server
+ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ { request->send(SD, "/index.html", "text/html"); });
+
+ // load data from slice form
+
+ /*
+ Pyrolysis Commands:
+ ps1: pyrolysis setpoint 1
+ pp1: pyrolysis Kp 1
+ pi1: pyrolysis Ki 1
+ pd1: pyrolysis Kd 1
+
+ Bioreactor Commands:
+ bs1: bioreactor setpoint 1
+ bp1: bioreactor Kp 1
+ bi1: bioreactor Ki 1
+ bd1: bioreactor Kd 1
+ bm1: bioreactor motor 1
+ bt1: bioreactor turbidity sample time 1
+
+ Chem Decon Commands:
+ cm1: chemreactor motor 1 (pump)
+ cs1: chemreactor setpoint 1
+ cp1: chemreactor Kp 1
+ ci1: chemreactor Ki 1
+ cd1: chemreactor Kd 1
+ */
+
+ server.on("/form-submit", HTTP_POST, [](AsyncWebServerRequest *request)
+ {
int params = request->params();
for (int i = 0; i < params; i++) {
AsyncWebParameter* p = request->getParam(i);
@@ -423,11 +460,11 @@ Chem Decon Commands:
break;
}
}
- }
- });
+ } });
- //load any non-form POST request body and url
- server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){
+ // load any non-form POST request body and url
+ server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
+ {
Serial.println(request->url());
if (request->url() == "/all-data") {
String s = "";
@@ -535,18 +572,17 @@ Chem Decon Commands:
writeFile(SD, "/bioreactor-data.csv", "Date and Time,Thermocouple 1,pH Sensor 1,Dissolved Oxygen 1,Turbidity 1,Thermocouple 2, pH Sensor 2, Dissolved Oxygen 2,Turbidity 2,Pasteurization,Dryer");
} else if(request->url() == "/delete-chemreactor") {
writeFile(SD, "/chemreactor-data.csv", "Date and Time,Reactor 1,Reactor 2");
- }
- });
+ } });
// Handle Web Server Events
- events.onConnect([](AsyncEventSourceClient *client){
+ events.onConnect([](AsyncEventSourceClient *client)
+ {
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
}
// send event with message "hello!", id current millis
// and set reconnect delay to 1 second
- client->send("hello!", NULL, millis(), 10000);
- });
+ client->send("hello!", NULL, millis(), 10000); });
server.addHandler(&events);
server.serveStatic("/", SD, "/");
@@ -560,106 +596,121 @@ int calDelay = 900;
bool readRequestedPHDO = false;
uint8_t loggingCounter = 6;
-void loop() {
- //get slice data from slices
- if(millis() - lastPOST > SLICE_DATA_INTERVAL_MS) {
+void loop()
+{
+ // get slice data from slices
+ if (millis() - lastPOST > SLICE_DATA_INTERVAL_MS)
+ {
Serial.println("getting data");
- //get time
+ // get time
String timeToServer = rtc.getTime("%F %T");
String pyrolysisToServer = timeToServer;
String bioToServer = timeToServer;
String chemToServer = timeToServer;
-
- //Heating data
- for(int i=0;i<9;i++){
+
+ // Heating data
+ for (int i = 0; i < 9; i++)
+ {
float temp;
- if(pyro_thermo[i][1] == 1)
+ if (pyro_thermo[i][1] == 1)
RLHTRequestThermo(pyro_thermo[i][0], &(pyro_thermo_val[i]), &(temp));
- if(pyro_thermo[i][1] == 2)
+ if (pyro_thermo[i][1] == 2)
RLHTRequestThermo(pyro_thermo[i][0], &(temp), &(pyro_thermo_val[i]));
}
- //Bioreactor data
+ // Bioreactor data
- for(uint8_t n = 0; n < 2; n++) {
- if(readRequestedPHDO) bio_ph_val[n][0] = PHDORequest(bio_ph[n][0]);
- if(readRequestedPHDO) bio_do_val[n] = PHDORequest(bio_do[n]);
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ if (readRequestedPHDO)
+ bio_ph_val[n][0] = PHDORequest(bio_ph[n][0]);
+ if (readRequestedPHDO)
+ bio_do_val[n] = PHDORequest(bio_do[n]);
DCMTRequestTurbidity(bio_turbidity[n][0], &(bio_turbidity_val[0]), &(bio_turbidity_val[1]));
// update pH pump controller with new pH values if setpoint is nonzero
- if(bio_ph_val[n][1] != 0)
+ if (bio_ph_val[n][1] != 0)
DCMTCommandPH(10 + n, bio_ph_val[n][1], bio_ph_val[n][0]);
float temp;
- if(bio_thermo[n][1] == 1)
+ if (bio_thermo[n][1] == 1)
RLHTRequestThermo(bio_thermo[n][0], &(bio_thermo_val[n]), &(temp));
- if(bio_thermo[n][1] == 2)
+ if (bio_thermo[n][1] == 2)
RLHTRequestThermo(bio_thermo[n][0], &(temp), &(bio_thermo_val[n]));
}
- for(int i=0;i<2;i++){
+ for (int i = 0; i < 2; i++)
+ {
float temp;
- if(bio_post_heaters[i][2] == 1)
- RLHTRequestThermo(bio_post_heaters[i][0], &(bio_thermo_val[i+2]), &(temp));
- if(bio_post_heaters[i][2] == 2)
- RLHTRequestThermo(bio_post_heaters[i][0], &(temp), &(bio_thermo_val[i+2]));
+ if (bio_post_heaters[i][2] == 1)
+ RLHTRequestThermo(bio_post_heaters[i][0], &(bio_thermo_val[i + 2]), &(temp));
+ if (bio_post_heaters[i][2] == 2)
+ RLHTRequestThermo(bio_post_heaters[i][0], &(temp), &(bio_thermo_val[i + 2]));
}
- readRequestedPHDO = false; // reload new read request
-
- //Chemreactor data
- for(uint8_t n = 0; n < 2; n++) {
+ readRequestedPHDO = false; // reload new read request
+
+ // Chemreactor data
+ for (uint8_t n = 0; n < 2; n++)
+ {
float temp;
- if(chem_thermo[n][2] == 1)
+ if (chem_thermo[n][2] == 1)
RLHTRequestThermo(chem_thermo[n][0], &(chem_thermo_val[n]), &(temp));
- if(chem_thermo[n][2] == 2)
+ if (chem_thermo[n][2] == 2)
RLHTRequestThermo(chem_thermo[n][0], &(temp), &(chem_thermo_val[n]));
}
- //convert to data to string to save to SD card and send to server
- for(uint8_t n = 0; n < 9; n++) {
+ // convert to data to string to save to SD card and send to server
+ for (uint8_t n = 0; n < 9; n++)
+ {
pyrolysisToServer += "," + String(pyro_thermo_val[n]);
}
- for(uint8_t n = 0; n < 2; n++) {
+ for (uint8_t n = 0; n < 2; n++)
+ {
bioToServer += "," + String(bio_thermo_val[n]) + "," + String(bio_ph_val[n][0]) + "," + String(bio_do_val[n]) + "," + String(bio_turbidity_val[n]);
}
bioToServer += "," + String(bio_thermo_val[2]) + "," + String(bio_thermo_val[3]);
- for(uint8_t n = 0; n < 2; n++) {
+ for (uint8_t n = 0; n < 2; n++)
+ {
chemToServer += "," + String(chem_thermo_val[n]);
}
- //send data to website
-/*
-Readings to Webpage Format
-Pyrolysis Reactor:
- Format: Date and Time,Dissolution Tank,Dissolution Heating Tape,Valve,Char Chamber,Secondary Reactor,Knockout Drum,Condenser 0,Condenser 1,Condenser 2
- Index: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9
-Bioreactor:
- Format: Date and Time,Thermo1,pH1,DO1,Turb1,Thermo2,pH2,DO2,Turb2,Past,Dry
- Index: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10
-Chemreactor:
- Format: Date and Time,Reactor 1,Reactor 2
- Index: 0 , 1 , 2
-*/
+ // send data to website
+ /*
+ Readings to Webpage Format
+ Pyrolysis Reactor:
+ Format: Date and Time,Dissolution Tank,Dissolution Heating Tape,Valve,Char Chamber,Secondary Reactor,Knockout Drum,Condenser 0,Condenser 1,Condenser 2
+ Index: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9
+ Bioreactor:
+ Format: Date and Time,Thermo1,pH1,DO1,Turb1,Thermo2,pH2,DO2,Turb2,Past,Dry
+ Index: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10
+ Chemreactor:
+ Format: Date and Time,Reactor 1,Reactor 2
+ Index: 0 , 1 , 2
+ */
events.send(pyrolysisToServer.c_str(), "pyrolysis-readings", millis());
events.send(bioToServer.c_str(), "bioreactor-readings", millis());
events.send(chemToServer.c_str(), "chemreactor-readings", millis());
-
- //log onto the SD card
- if(logging){
+
+ // log onto the SD card
+ if (logging)
+ {
appendFile(SD, "/pyrolysis-data.csv", "\r\n" + pyrolysisToServer);
appendFile(SD, "/bioreactor-data.csv", "\r\n" + bioToServer);
appendFile(SD, "/chemreactor-data.csv", "\r\n" + chemToServer);
}
lastPOST = millis();
- }else if(!readRequestedPHDO){
- for(uint8_t n = 0; n < 2; n++){
- PHDOCommand(bio_do[n], "r"); // request reading from DO sensors
- PHDOCommand(bio_ph[n][0], "r"); // request reading from pH sensors
+ }
+ else if (!readRequestedPHDO)
+ {
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ PHDOCommand(bio_do[n], "r"); // request reading from DO sensors
+ PHDOCommand(bio_ph[n][0], "r"); // request reading from pH sensors
}
readRequestedPHDO = true;
}
}
-void RLHTRequestThermo(int address, float* t1, float* t2)
+void RLHTRequestThermo(int address, float *t1, float *t2)
{
bool data_received = false;
byte in_char;
@@ -669,21 +720,23 @@ void RLHTRequestThermo(int address, float* t1, float* t2)
thermo1.number = 0;
thermo2.number = 0;
-
+
Wire.requestFrom(address, 8, 1);
- int i=0;
- while (Wire.available()) { //are there bytes to receive.
+ int i = 0;
+ while (Wire.available())
+ { // are there bytes to receive.
data_received = true;
- in_char = Wire.read(); //receive a byte.
- in_data[i] = in_char; //load this byte into our array.
- i++; //incur the counter for the array element.
+ in_char = Wire.read(); // receive a byte.
+ in_data[i] = in_char; // load this byte into our array.
+ i++; // incur the counter for the array element.
}
- if(data_received){
- for(int x=0;x<4;x++)
+ if (data_received)
+ {
+ for (int x = 0; x < 4; x++)
{
thermo1.bytes[x] = in_data[x];
- thermo2.bytes[x] = in_data[x+4];
+ thermo2.bytes[x] = in_data[x + 4];
}
}
@@ -699,12 +752,13 @@ void RLHTCommandSetpoint(int address, byte heater, float heatSetpoint, byte ther
Wire.beginTransmission(address);
Wire.write('H');
Wire.write(heater);
- for(int i=0; i<4; i++){
- Wire.write(setpoint.bytes[i]); // sends one byte
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(setpoint.bytes[i]); // sends one byte
}
Wire.write(thermocouple);
Wire.write(enableReverse);
- Wire.endTransmission(); // stop transmitting
+ Wire.endTransmission(); // stop transmitting
Serial.print(address);
Serial.print('H');
@@ -727,16 +781,19 @@ void RLHTCommandPID(int address, byte heater, float Kp_set, float Ki_set, float
Wire.beginTransmission(address);
Wire.write('P');
Wire.write(heater);
- for(int i=0; i<4; i++){
- Wire.write(Kp.bytes[i]); // sends one byte
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kp.bytes[i]); // sends one byte
}
- for(int i=0; i<4; i++){
- Wire.write(Ki.bytes[i]); // sends one byte
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Ki.bytes[i]); // sends one byte
}
- for(int i=0; i<4; i++){
- Wire.write(Kd.bytes[i]); // sends one byte
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kd.bytes[i]); // sends one byte
}
- Wire.endTransmission(); // stop transmitting
+ Wire.endTransmission(); // stop transmitting
Serial.print(address);
Serial.print('P');
@@ -746,7 +803,7 @@ void RLHTCommandPID(int address, byte heater, float Kp_set, float Ki_set, float
Serial.println(Kd.number);
}
-void DCMTRequestTurbidity(int address, float* turbidity1, float* turbidity2)
+void DCMTRequestTurbidity(int address, float *turbidity1, float *turbidity2)
{
bool data_received = false;
byte in_char;
@@ -755,20 +812,23 @@ void DCMTRequestTurbidity(int address, float* turbidity1, float* turbidity2)
turb1.number = 0;
turb2.number = 0;
-
+
Wire.requestFrom(address, 10, 1);
- int i=0;
- while (Wire.available()) { //are there bytes to receive.
+ int i = 0;
+ while (Wire.available())
+ { // are there bytes to receive.
data_received = true;
- in_char = Wire.read(); //receive a byte.
- in_data[i] = in_char; //load this byte into our array.
- i++; //incur the counter for the array element.
+ in_char = Wire.read(); // receive a byte.
+ in_data[i] = in_char; // load this byte into our array.
+ i++; // incur the counter for the array element.
}
- if(data_received){
- for(int x=0;x<4;x++){
+ if (data_received)
+ {
+ for (int x = 0; x < 4; x++)
+ {
turb1.bytes[x] = in_data[x];
- turb2.bytes[x] = in_data[x+4];
+ turb2.bytes[x] = in_data[x + 4];
}
}
@@ -786,7 +846,8 @@ void DCMTCommandTurbidity(int address, byte motor, byte direction, bool enable,
Wire.write(motor);
Wire.write(direction);
Wire.write(enable);
- for(int i=0;i<4;i++) Wire.write(sample_p.bytes[i]);
+ for (int i = 0; i < 4; i++)
+ Wire.write(sample_p.bytes[i]);
Wire.endTransmission();
}
@@ -798,11 +859,12 @@ void DCMTCommandSpeed(int address, byte motor, int speed_set, bool enable)
Wire.beginTransmission(address);
Wire.write('M');
Wire.write(motor);
- for(int i=0; i<4; i++){
- Wire.write(speed.bytes[i]); // sends one byte
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(speed.bytes[i]); // sends one byte
}
Wire.write(enable);
- Wire.endTransmission(); // stop transmitting
+ Wire.endTransmission(); // stop transmitting
}
void DCMTCommandPH(int address, float pHSetpoint_set, float currentPH_set)
@@ -813,9 +875,11 @@ void DCMTCommandPH(int address, float pHSetpoint_set, float currentPH_set)
Wire.beginTransmission(address);
Wire.write('p');
- for(int i=0; i<4; i++) Wire.write(currentPH.bytes[i]);
- for(int i=0; i<4; i++) Wire.write(pHSetpoint.bytes[i]);
- Wire.endTransmission(); // stop transmitting
+ for (int i = 0; i < 4; i++)
+ Wire.write(currentPH.bytes[i]);
+ for (int i = 0; i < 4; i++)
+ Wire.write(pHSetpoint.bytes[i]);
+ Wire.endTransmission(); // stop transmitting
}
void DCMTCommandPHPID(int address, float Kp_set, float Ki_set, float Kd_set)
@@ -827,19 +891,21 @@ void DCMTCommandPHPID(int address, float Kp_set, float Ki_set, float Kd_set)
Wire.beginTransmission(address);
Wire.write('P');
- for(int i=0; i<4; i++) Wire.write(Kp.bytes[i]);
- for(int i=0; i<4; i++) Wire.write(Ki.bytes[i]);
- for(int i=0; i<4; i++) Wire.write(Kd.bytes[i]);
- Wire.endTransmission(); // stop transmitting
+ for (int i = 0; i < 4; i++)
+ Wire.write(Kp.bytes[i]);
+ for (int i = 0; i < 4; i++)
+ Wire.write(Ki.bytes[i]);
+ for (int i = 0; i < 4; i++)
+ Wire.write(Kd.bytes[i]);
+ Wire.endTransmission(); // stop transmitting
}
-
-void PHDOCommand(int address, char *commandData)
+void PHDOCommand(int address, const char *commandData)
{
Wire.beginTransmission(address);
Wire.write(commandData);
Serial.println(commandData);
- Wire.endTransmission(); // stop transmitting
+ Wire.endTransmission(); // stop transmitting
}
float PHDORequest(int address)
@@ -851,41 +917,46 @@ float PHDORequest(int address)
Wire.requestFrom(address, 20, 1);
byte code = 0;
- code = Wire.read(); //the first byte is the response code, we read this separately.
+ code = Wire.read(); // the first byte is the response code, we read this separately.
- switch (code) { //switch case based on what the response code is.
- case 1: //decimal 1.
- Serial.println("Success"); //means the command was successful.
- break; //exits the switch case.
+ switch (code)
+ { // switch case based on what the response code is.
+ case 1: // decimal 1.
+ Serial.println("Success"); // means the command was successful.
+ break; // exits the switch case.
- case 2: //decimal 2.
- Serial.println("Failed"); //means the command has failed.
- break; //exits the switch case.
+ case 2: // decimal 2.
+ Serial.println("Failed"); // means the command has failed.
+ break; // exits the switch case.
- case 254: //decimal 254.
- Serial.println("Pending"); //means the command has not yet been finished calculating.
- break; //exits the switch case.
+ case 254: // decimal 254.
+ Serial.println("Pending"); // means the command has not yet been finished calculating.
+ break; // exits the switch case.
- case 255: //decimal 255.
- Serial.println("No Data"); //means there is no further data to send.
- break; //exits the switch case.
+ case 255: // decimal 255.
+ Serial.println("No Data"); // means there is no further data to send.
+ break; // exits the switch case.
}
- int i=0;
- if(code == 1){
- while (Wire.available()) { //are there bytes to receive.
- in_char = Wire.read(); //receive a byte.
- if(code != 1) // check if incoming data is valid
- in_data[i] = 0; // set values to zero if not valid data
+ int i = 0;
+ if (code == 1)
+ {
+ while (Wire.available())
+ { // are there bytes to receive.
+ in_char = Wire.read(); // receive a byte.
+ if (code != 1) // check if incoming data is valid
+ in_data[i] = 0; // set values to zero if not valid data
else
- in_data[i] = in_char; //load this byte into our array.
- i++; //incur the counter for the array element.
- if (in_char == 0) { //if we see that we have been sent a null command.
- i = 0; //reset the counter i to 0.
- break; //exit the while loop.
+ in_data[i] = in_char; // load this byte into our array.
+ i++; // incur the counter for the array element.
+ if (in_char == 0)
+ { // if we see that we have been sent a null command.
+ i = 0; // reset the counter i to 0.
+ break; // exit the while loop.
}
}
return atof(in_data);
- }else
+ }
+ else
return 0;
}
\ No newline at end of file
diff --git a/DCMT_R1/DCMT_R1.ino b/src/slice-code/DCMT_R1/DCMT_R1.ino
similarity index 99%
rename from DCMT_R1/DCMT_R1.ino
rename to src/slice-code/DCMT_R1/DCMT_R1.ino
index 519f8d9..b34661c 100644
--- a/DCMT_R1/DCMT_R1.ino
+++ b/src/slice-code/DCMT_R1/DCMT_R1.ino
@@ -2,7 +2,7 @@
#include
#include
-#define I2C_ADR 27
+#define I2C_ADR 21
#define ESTOP 2
#define LED_PIN 5
diff --git a/src/slice-code/RLHT_R2/RLHT_R2.ino b/src/slice-code/RLHT_R2/RLHT_R2.ino
new file mode 100644
index 0000000..ae739e1
--- /dev/null
+++ b/src/slice-code/RLHT_R2/RLHT_R2.ino
@@ -0,0 +1,360 @@
+#include
+#include "max6675.h"
+#include
+#include
+
+#define I2C_ADR 10
+
+#define ESTOP 2
+#define LED_PIN 5
+#define CS1 10
+#define CS2 11
+#define DATA 12
+#define CLK 13
+#define RELAY1 6
+#define RELAY2 7
+
+#define THERMO_UPDATE_TIME_MS 300
+#define SERIAL_UPDATE_TIME_MS 1000
+
+long lastThermoRead = 0;
+long lastSerialPrint = 0;
+
+bool E_STOP = false;
+
+CRGB led;
+int hue = 0;
+
+struct RLHT_t
+{
+ double heatSetpoint_1 = 0; // set desired temperature for heater 1 (if using relay as heater actuator)
+ double relay1Input; // input to relay 1 taken from selected thermocouple
+ double rOnTime_1; // time relay is on in ms (0-period)
+ int rPeriod_1 = 2000; // duration of relay 1 cycle in ms (1000ms = 1s)
+ double Kp_1 = 0; // PID proportional gain for heater 1
+ double Ki_1 = 0; // PID integral gain for heater 1
+ double Kd_1 = 0; // PID derivative gain for heater 1
+ double heatSetpoint_2 = 0;
+ double relay2Input;
+ double rOnTime_2;
+ int rPeriod_2 = 2000;
+ double Kp_2 = 0;
+ double Ki_2 = 0;
+ double Kd_2 = 0;
+ double thermo1; // thermocouple 1 measurement
+ double thermo2;
+ char thermoSelect[2]; // select which thermocouples pair with relays {relay1, relay2}
+} RLHT, RLHT_old;
+
+union FLOATUNION_t //Define a float that can be broken up and sent via I2C
+{
+ float number;
+ uint8_t bytes[4];
+};
+
+// initialize the Thermocouples
+MAX6675 CH1(CLK, CS1, DATA);
+MAX6675 CH2(CLK, CS2, DATA);
+
+//Specify the links and initial tuning parameters
+PID relay1PID(&(RLHT.relay1Input), &(RLHT.rOnTime_1), &(RLHT.heatSetpoint_1), RLHT.Kp_1, RLHT.Ki_1, RLHT.Kd_1, DIRECT);
+PID relay2PID(&(RLHT.relay2Input), &(RLHT.rOnTime_2), &(RLHT.heatSetpoint_2), RLHT.Kp_2, RLHT.Ki_2, RLHT.Kd_2, DIRECT);
+
+unsigned long relay1StartTime;
+unsigned long relay2StartTime;
+
+void estop()
+{
+ if(digitalRead(ESTOP) == HIGH) // when set to HIGH state
+ {
+ led = CRGB::Red;
+ FastLED.show();
+
+ // save states
+ RLHT_old.heatSetpoint_1 = RLHT.heatSetpoint_1;
+ RLHT_old.heatSetpoint_2 = RLHT.heatSetpoint_2;
+ RLHT_old.rOnTime_1 = RLHT.rOnTime_1;
+ RLHT_old.rOnTime_2 = RLHT.rOnTime_2;
+
+ // set critical states to zero and turn off relays
+ RLHT.heatSetpoint_1 = 0;
+ RLHT.heatSetpoint_2 = 0;
+ RLHT.rOnTime_1 = 0;
+ RLHT.rOnTime_2 = 0;
+
+ digitalWrite(RELAY1, LOW);
+ digitalWrite(RELAY2, LOW);
+
+ E_STOP = true;
+ Serial.println("ESTOP PRESSED!");
+ }
+ else // when ESTOP state is LOW
+ {
+ led = CRGB::Black;
+ FastLED.show();
+
+ // reassign old states
+ RLHT_old.heatSetpoint_1 = RLHT_old.heatSetpoint_1;
+ RLHT_old.heatSetpoint_2 = RLHT_old.heatSetpoint_2;
+ RLHT_old.rOnTime_1 = RLHT_old.rOnTime_1;
+ RLHT_old.rOnTime_2 = RLHT_old.rOnTime_2;
+
+ E_STOP = false;
+
+ Serial.println("ESTOP RELEASED!");
+ }
+}
+
+void setup() {
+ // put your setup code here, to run once:
+ Serial.begin(9600);
+ FastLED.addLeds(&led, 1);
+
+ pinMode(ESTOP, INPUT);
+ attachInterrupt(digitalPinToInterrupt(ESTOP), estop, CHANGE);
+
+ pinMode(RELAY1, OUTPUT);
+ pinMode(RELAY2, OUTPUT);
+
+ Wire.begin(I2C_ADR);
+ Wire.onRequest(requestEvent);
+ Wire.onReceive(receiveEvent);
+
+ //tell the PID to range between 0 and the full window size
+ relay1PID.SetOutputLimits(0, RLHT.rPeriod_1);
+ relay2PID.SetOutputLimits(0, RLHT.rPeriod_2);
+
+ //turn the PID on
+ relay1PID.SetMode(AUTOMATIC);
+ relay2PID.SetMode(AUTOMATIC);
+
+ relay1StartTime = millis();
+ relay2StartTime = millis();
+
+ delay(2000);
+}
+
+void loop() {
+ // put your main code here, to run repeatedly:
+
+ measureThermocouples();
+ if(!E_STOP)
+ {
+ setPIDTunings();
+ // assign thermocouple readings to relay inputs
+ switch(RLHT.thermoSelect[0]){
+ case 1: // thermocouple 1 to relay 1
+ RLHT.relay1Input = RLHT.thermo1;
+ break;
+ case 2: // thermocouple 2 to relay 1
+ RLHT.relay1Input = RLHT.thermo2;
+ break;
+ }
+ if(isnan(RLHT.relay1Input))
+ RLHT.rOnTime_1 = 0;
+ else
+ relay1PID.Compute();
+
+ switch(RLHT.thermoSelect[1]){
+ case 1: // thermocouple 1 to relay 2
+ RLHT.relay2Input = RLHT.thermo1;
+ break;
+ case 2: // thermocouple 2 to relay 2
+ RLHT.relay2Input = RLHT.thermo2;
+ break;
+ }
+ if(isnan(RLHT.relay2Input))
+ RLHT.rOnTime_2 = 0;
+ else
+ relay2PID.Compute();
+
+ actuateRelays();
+ printOutput();
+ }
+}
+
+void setPIDTunings()
+{
+ relay1PID.SetTunings(RLHT.Kp_1, RLHT.Ki_1, RLHT.Kd_1);
+ relay2PID.SetTunings(RLHT.Kp_2, RLHT.Ki_2, RLHT.Kd_2);
+}
+
+void printOutput()
+{
+ if(millis() - lastSerialPrint >= SERIAL_UPDATE_TIME_MS)
+ {
+ Serial.print("T1: ");
+ Serial.print(RLHT.thermo1);
+ Serial.print("\tT2: ");
+ Serial.print(RLHT.thermo2);
+
+ Serial.print("\tPID1: ");
+ Serial.print(RLHT.Kp_1);
+ Serial.print(",");
+ Serial.print(RLHT.Ki_1);
+ Serial.print(",");
+ Serial.print(RLHT.Kd_1);
+
+ Serial.print("\tRelay1Input: ");
+ Serial.print(RLHT.relay1Input);
+ Serial.print("\tSetpoint: ");
+ Serial.print(RLHT.heatSetpoint_1);
+ Serial.print("\tonTime2: ");
+ Serial.print((int)(RLHT.rOnTime_1));
+
+ Serial.print("\tPID2: ");
+ Serial.print(RLHT.Kp_2);
+ Serial.print(",");
+ Serial.print(RLHT.Ki_2);
+ Serial.print(",");
+ Serial.print(RLHT.Kd_2);
+
+ Serial.print("\tRelay2Input: ");
+ Serial.print(RLHT.relay2Input);
+ Serial.print("\tSetpoint: ");
+ Serial.print(RLHT.heatSetpoint_2);
+ Serial.print("\tonTime2: ");
+ Serial.println((int)(RLHT.rOnTime_2));
+
+ lastSerialPrint = millis();
+ }
+}
+
+void measureThermocouples()
+{
+ if(millis() - lastThermoRead >= THERMO_UPDATE_TIME_MS)
+ {
+ RLHT.thermo1 = CH1.readCelsius();
+ RLHT.thermo2 = CH2.readCelsius();
+ lastThermoRead = millis();
+ }
+}
+
+void actuateRelays()
+{
+ if(RLHT.heatSetpoint_1 == 0)
+ RLHT.rOnTime_1 = 0;
+ if(RLHT.heatSetpoint_2 == 0)
+ RLHT.rOnTime_2 = 0;
+ // Relay 1
+ if (millis() - relay1StartTime > RLHT.rPeriod_1)
+ { //time to shift the Relay Window
+ relay1StartTime += RLHT.rPeriod_1;
+ }
+ if ((int)(RLHT.rOnTime_1) > millis() - relay1StartTime) digitalWrite(RELAY1, HIGH);
+ else digitalWrite(RELAY1, LOW);
+
+ // Relay 2
+ if (millis() - relay2StartTime > RLHT.rPeriod_2)
+ { //time to shift the Relay Window
+ relay2StartTime += RLHT.rPeriod_2;
+ }
+ if ((int)(RLHT.rOnTime_2) > millis() - relay2StartTime) digitalWrite(RELAY2, HIGH);
+ else digitalWrite(RELAY2, LOW);
+}
+
+void requestEvent()
+{
+ FLOATUNION_t t1;
+ FLOATUNION_t t2;
+
+ if(isnan(RLHT.thermo1))
+ t1.number = 0;
+ else
+ t1.number = RLHT.thermo1;
+
+ if(isnan(RLHT.thermo2))
+ t2.number = 0;
+ else
+ t2.number = RLHT.thermo2;
+
+ for (int i = 0; i < 4; i++) Wire.write(t1.bytes[i]);
+ for (int i = 0; i < 4; i++) Wire.write(t2.bytes[i]);
+}
+
+/* Command layout
+ Byte 1: T (want temperature reading)
+ H (change heater setpoint)
+ P (change PID tuning)
+ Byte 2: 1 (first heater/thermocouple)
+ 2 (second heater/thermocouple)
+ Byte 3-6: Kp or heatSetpoint
+ Byte 7-10: Ki
+ Byte 11-14: Kd
+*/
+
+void receiveEvent(int howMany)
+{
+ byte in_char;
+ char in_data[20];
+
+ led = CRGB::Green;
+ FastLED.show();
+
+ //Serial.println("Receiving: ");
+ int i=0;
+ while(Wire.available())
+ {
+ in_char = Wire.read();
+ in_data[i] = in_char;
+ Serial.print((byte)in_data[i]);
+ Serial.print(",");
+ i++;
+ }
+ Serial.println();
+
+ setParametersRLHT(in_data);
+
+ led = CRGB::Black;
+ FastLED.show();
+}
+
+/*
+RLHT Command Format:
+ H,1,setpoint,2,0: temperature setpoint between relay 1 and thermo 2. Enable reverse response = 0 (false)
+ P,Kp,Ki,Kd: PID tuning
+*/
+
+void setParametersRLHT(char *in_data)
+{
+ FLOATUNION_t float1;
+ FLOATUNION_t float2;
+ FLOATUNION_t float3;
+
+ switch(in_data[0])
+ {
+ case 'H':
+ for(int i=0; i<4; i++) float1.bytes[i] = in_data[i+2];
+ if(in_data[1] == 1){
+ RLHT.heatSetpoint_1 = float1.number;
+ RLHT.thermoSelect[0] = in_data[6];
+ relay1PID.SetControllerDirection(in_data[7]); // Direct = 0, Reverse = 1
+ }
+ if(in_data[1] == 2){
+ RLHT.heatSetpoint_2 = float1.number;
+ RLHT.thermoSelect[1] = in_data[6];
+ relay2PID.SetControllerDirection(in_data[7]); // Direct = 0, Reverse = 1
+ }
+ break;
+ case 'P':
+ for(int i=0; i<4; i++) // populate variables for PID tuning
+ {
+ float1.bytes[i] = in_data[i+2];
+ float2.bytes[i] = in_data[i+6];
+ float3.bytes[i] = in_data[i+10];
+ }
+ if(in_data[1] == 1)
+ {
+ RLHT.Kp_1 = (double)float1.number;
+ RLHT.Ki_1 = (double)float2.number;
+ RLHT.Kd_1 = (double)float3.number;
+ }
+ if(in_data[1] == 2)
+ {
+ RLHT.Kp_2 = (double)float1.number;
+ RLHT.Ki_2 = (double)float2.number;
+ RLHT.Kd_2 = (double)float3.number;
+ }
+ break;
+ }
+}
\ No newline at end of file
diff --git a/src/spread-alpha/README.md b/src/spread-alpha/README.md
new file mode 100644
index 0000000..2029b09
--- /dev/null
+++ b/src/spread-alpha/README.md
@@ -0,0 +1,2 @@
+# spread
+
diff --git a/src/spread-alpha/spread-alpha.ino b/src/spread-alpha/spread-alpha.ino
new file mode 100644
index 0000000..64e7ec2
--- /dev/null
+++ b/src/spread-alpha/spread-alpha.ino
@@ -0,0 +1,962 @@
+/* Function layouts
+Get thermocouple data: void RLHTRequestThermo(int address, float* t1, float* t2)
+Set heating setpoint: void RLHTCommandSetpoint(int address, byte heater, float heatSetpoint, byte thermocouple, bool enableReverse)
+Set heating PID tunings: void RLHTCommandPID(int address, byte heater, float Kp_set, float Ki_set, float Kd_set)
+Set motor speeds: void DCMTCommandSpeed(int address, byte motor, int speed_set, bool enable)
+Set as pump with pH setpoint: void DCMTCommandPH(int address, float pHSetpoint_set, float currentPH_set)
+Set as pH PID tunings: void DCMTCommandPHPID(int address, float Kp_set, float Ki_set, float Kd_set)
+Set as turbidity pump: void DCMTCommandTurbidity(int address, byte motor, byte direction, bool enable, int sample_period)
+Send commands to pH & DO: void PHDOCommand(int address, char *commandData)
+Get pH or DO readings: float PHDORequest(int address)
+*/
+
+#include
+#include
+#include
+#include
+
+#include "SD.h"
+#include "FS.h"
+#include "SPI.h"
+#include
+
+#include
+ESP32Time rtc;
+
+#define SLICE_DATA_INTERVAL_MS 10000
+
+#define NUM_DCMT_SLICES 8
+#define NUM_RLHT_SLICES 8
+#define NUM_PHDO_SLICES 2
+
+// microSD Card Reader connections
+#define SD_CS 5
+#define SPI_MOSI 23
+#define SPI_MISO 19
+#define SPI_SCK 18
+
+#define ESTOP 32
+
+const char *ssid = "BREAD-DARPA";
+const char *password = "12345678";
+
+// Useful unions for breaking data into bytes
+union FLOATUNION_t // Define a float that can be broken up and sent via I2C
+{
+ float number;
+ uint8_t bytes[4];
+};
+union INTUNION_t // Define an int that can be broken up and sent via I2C
+{
+ int number;
+ uint8_t bytes[4];
+};
+
+// Slice structures for managing data
+struct DCMT_t
+{
+ int address; // I2C address of Slice
+ int m1Speed; // motor 1 speed (-100-100)
+ int m2Speed; // motor 2 speed (-100-100)
+ bool m1En; // motor 1 enable (0:off/apply brake, 1:on/coast)
+ bool m2En; // motor 2 enable (0:off/apply brake, 1:on/coast)
+ float pHSetpoint;
+ float currentPH;
+ float Kp_pH;
+ float Ki_pH;
+ float Kd_pH;
+};
+
+struct RLHT_t
+{
+ int address; // I2C address of Slice
+ float heatSetpoint_1; // set desired temperature for heater 1 (if using relay as heater actuator)
+ float Kp_1; // PID proportional gain for heater 1
+ float Ki_1; // PID integral gain for heater 1
+ float Kd_1; // PID derivative gain for heater 1
+ float heatSetpoint_2;
+ float Kp_2;
+ float Ki_2;
+ float Kd_2;
+ float thermo1; // thermocouple 1 measurement
+ float thermo2; // thermocouple 2 measurement
+};
+
+struct PHDO_t
+{
+ int addressPH; // I2C address of Atlas Scientific pH EZO board (default = 99)
+ int addressDO; // I2C address of Atlas Scientific dissolved oxygen EZO board (default = 97)
+ float pH; // current pH reading
+ float DO; // current dissolved oxygen reading
+ bool calPH = false;
+ bool calDO = false;
+};
+
+DCMT_t DCMT[NUM_DCMT_SLICES];
+RLHT_t RLHT[NUM_RLHT_SLICES];
+PHDO_t PHDO[NUM_PHDO_SLICES];
+
+int pyro_thermo[9][2] = {
+ // pyrolyis thermocouple layout {address, thermocouple}
+ {16, 2}, // DT
+ {17, 1}, // DHT
+ {16, 1}, // V
+ {15, 2}, // CC
+ {15, 1}, // SR
+ {14, 1}, // KD
+ {10, 1}, // C0
+ {11, 1}, // C1
+ {12, 1} // C2
+};
+float pyro_thermo_val[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; // stores thermocouple values
+
+int pyro_heater[10][4] = {
+ // pyrolysis heater/fan layout {address, heater, thermocouple, reverseEnable}
+ {16, 2, 2, 0}, // DT
+ {16, 1, 1, 0}, // V
+ {15, 2, 2, 0}, // CC
+ {15, 1, 1, 0}, // SR
+ {14, 1, 1, 0}, // KD
+ {10, 1, 1, 0}, // C0H
+ {10, 2, 1, 1}, // C0F
+ {11, 1, 1, 0}, // C1H
+ {11, 2, 1, 1}, // C1F
+ {12, 1, 1, 1} // C2F
+};
+float pyro_heater_pid[10][4]; // pyrolysis heater/fan PID {setpoint, Kp, Ki, Kd}
+
+int bio_thermo[2][2] = { // bioreator thermocouples {address, thermocouple}
+ {10, 2},
+ {11, 2}};
+
+int bio_post_heaters[2][4] = {
+ // bioreactor post processing heaters {address, heater, thermocouple, reverseEnable}
+ {12, 2, 2, 0}, // pasteurization
+ {14, 2, 2, 0} // dryer
+};
+
+float bio_post_heater_pid[2][4]; // {setpoint, Kp, Ki, Kd}
+float bio_thermo_val[4] = {0, 0, 0, 0};
+
+int bio_ph[2][2] = { // bioreactor pH layout {PHDO address, DCMT address}
+ {98, 20},
+ {100, 21}};
+
+float bio_ph_val[2][5]; // bioreactor pH settings (pH, Setpoint, Kp, Ki, Kd)
+
+int bio_do[2] = {97, 99}; // bioreactor DO addresses
+float bio_do_val[2] = {0, 0}; // bioreactor DO values
+
+int bio_turbidity[2][4] = { // bioreactor turbidity pump {address, motor, direction, sampleTime}
+ {22, 1, 1, 0},
+ {22, 2, 1, 0}};
+
+float bio_turbidity_val[2] = {0, 0}; // bioreactor turbidity value
+
+int bio_motors[10][3] = {
+ // bioreactor motor config {address, motor, speed}
+ {23, 1, 0}, // stirring 1
+ {20, 1, 0}, // base 1
+ {20, 2, 0}, // acid 1
+ {24, 1, 0}, // water
+ {24, 2, 0}, // spent media
+ {23, 2, 0}, // stirring 2
+ {21, 1, 0}, // base 2
+ {21, 2, 0}, // acid 2
+ {25, 1, 0}, // harvest
+ {25, 2, 0} // TFF
+};
+
+int chem_thermo[2][4] = {
+ // {address, heater, thermocouple, reverseEnable}
+ {13, 1, 1, 0}, // reactor 1
+ {13, 2, 2, 0} // reactor 2
+};
+float chem_thermo_val[2] = {0, 0};
+float chem_heater_pid[2][4];
+
+int chem_motors[4][3] = {
+ {26, 1, 0}, // ammonium hydroxide
+ {26, 2, 0}, // liquid transfer
+ {27, 1, 0}, // water dilute
+ {27, 2, 0} // reactor wash
+};
+
+AsyncWebServer server(80);
+
+AsyncEventSource events("/events");
+
+bool logging = false; // logging data to SD card and graphs
+
+void initSlices()
+{
+ // DCMT Slice address assignment
+ for (int i = 0; i < NUM_DCMT_SLICES; i++)
+ {
+ DCMT[i].address = 20 + i; // addresses 20-26
+ }
+ // RLHT Slice address assignment
+ for (int i = 0; i < NUM_RLHT_SLICES; i++)
+ {
+ RLHT[i].address = 10 + i; // addresses 10-15
+ }
+ // addresses of Atlas Scientific Sensors
+ PHDO[0].addressDO = 97;
+ PHDO[0].addressPH = 98;
+ PHDO[1].addressDO = 99;
+ PHDO[1].addressPH = 100;
+}
+
+void initSDCard()
+{
+ pinMode(SD_CS, OUTPUT);
+ digitalWrite(SD_CS, HIGH);
+
+ // Initialize SPI bus for microSD Card
+ SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
+
+ if (!SD.begin(SD_CS))
+ {
+ Serial.println("Card Mount Failed");
+ return;
+ }
+ uint8_t cardType = SD.cardType();
+
+ if (cardType == CARD_NONE)
+ {
+ Serial.println("No SD card attached");
+ return;
+ }
+
+ Serial.print("SD Card Type: ");
+ if (cardType == CARD_MMC)
+ {
+ Serial.println("MMC");
+ }
+ else if (cardType == CARD_SD)
+ {
+ Serial.println("SDSC");
+ }
+ else if (cardType == CARD_SDHC)
+ {
+ Serial.println("SDHC");
+ }
+ else
+ {
+ Serial.println("UNKNOWN");
+ }
+ uint64_t cardSize = SD.cardSize() / (1024 * 1024);
+ Serial.printf("SD Card Size: %lluMB\n", cardSize);
+}
+
+void removeFile(fs::FS &fs, const char *path)
+{
+ Serial.printf("Removing file: %s\n", path);
+
+ bool file = fs.remove(path);
+ if (!file)
+ {
+ Serial.println("Failed to remove file");
+ return;
+ }
+ else
+ {
+ Serial.println("Removed file");
+ }
+}
+
+void appendFile(fs::FS &fs, const char *path, String message)
+{
+ Serial.printf("Appending to file: %s\n", path);
+
+ File file = fs.open(path, FILE_APPEND);
+ if (!file)
+ {
+ Serial.println("Failed to open file for appending");
+ return;
+ }
+ if (file.print(message))
+ {
+ Serial.println("Message appended");
+ }
+ else
+ {
+ Serial.println("Append failed");
+ }
+ file.close();
+}
+
+void writeFile(fs::FS &fs, const char *path, const char *message)
+{
+ Serial.printf("Writing file: %s\n", path);
+
+ File file = fs.open(path, FILE_WRITE);
+ if (!file)
+ {
+ Serial.println("Failed to open file for writing");
+ return;
+ }
+ if (file.print(message))
+ {
+ Serial.println("File written");
+ }
+ else
+ {
+ Serial.println("Write failed");
+ }
+ file.close();
+}
+
+uint32_t lastPOST;
+
+void setup()
+{
+ Serial.begin(115200);
+
+ Wire.begin();
+
+ initSDCard();
+
+ initSlices();
+
+ pinMode(ESTOP, OUTPUT);
+
+ // start access point wifi
+ WiFi.softAP(ssid, password);
+ IPAddress myIP = WiFi.softAPIP();
+ Serial.print("AP IP address: ");
+ Serial.println(myIP);
+ Serial.println("Server started");
+
+ // send html data to server
+ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ { request->send(SD, "/index.html", "text/html"); });
+
+ // load data from slice form
+
+ /*
+ Pyrolysis Commands:
+ ps1: pyrolysis setpoint 1
+ pp1: pyrolysis Kp 1
+ pi1: pyrolysis Ki 1
+ pd1: pyrolysis Kd 1
+
+ Bioreactor Commands:
+ bs1: bioreactor setpoint 1
+ bp1: bioreactor Kp 1
+ bi1: bioreactor Ki 1
+ bd1: bioreactor Kd 1
+ bm1: bioreactor motor 1
+ bt1: bioreactor turbidity sample time 1
+
+ Chem Decon Commands:
+ cm1: chemreactor motor 1 (pump)
+ cs1: chemreactor setpoint 1
+ cp1: chemreactor Kp 1
+ ci1: chemreactor Ki 1
+ cd1: chemreactor Kd 1
+ */
+
+ server.on("/form-submit", HTTP_POST, [](AsyncWebServerRequest *request)
+ {
+ int params = request->params();
+ for (int i = 0; i < params; i++) {
+ AsyncWebParameter* p = request->getParam(i);
+ Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
+ String postName = p->name().c_str();
+ String postValue = p->value().c_str();
+ uint8_t index = postName.substring(2).toInt() - 1;
+
+ if(postName.charAt(0) == 'p') { // pyrolysis reactor
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ pyro_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(pyro_heater[index][0], pyro_heater[index][1], pyro_heater_pid[index][0], pyro_heater[index][2], pyro_heater[index][3]);
+ break;
+ case 'p': //Kp
+ pyro_heater_pid[index][1] = postValue.toFloat();
+ break;
+ case 'i': //Ki
+ pyro_heater_pid[index][2] = postValue.toFloat();
+ break;
+ case 'd': //Kd
+ pyro_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(pyro_heater[index][0], pyro_heater[index][1], pyro_heater_pid[index][1], pyro_heater_pid[index][2], pyro_heater_pid[index][3]);
+ break;
+ }
+ //float converted into 4 bytes
+ }else if(postName.charAt(0) == 'b') {
+ uint8_t index = postName.substring(2).toInt() - 1;
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ bio_ph_val[index][1] = postValue.toFloat();
+ DCMTCommandPH(bio_ph[index][1], bio_ph_val[index][1], bio_ph_val[index][0]);
+ break;
+ case 'p': //Kp
+ bio_ph_val[index][2] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'i': //Ki
+ bio_ph_val[index][3] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'd': //Kd
+ bio_ph_val[index][2] = postValue.toFloat();
+ Serial.println("Sending pH PID");
+ DCMTCommandPHPID(bio_ph[index][1], bio_ph_val[index][2], bio_ph_val[index][3], bio_ph_val[index][4]);
+ break;
+ case 'm':
+ bio_motors[index][2] = postValue.toInt();
+ DCMTCommandSpeed(bio_motors[index][0], bio_motors[index][1], bio_motors[index][2], 1);
+ break;
+ case 't':
+ bio_turbidity[index][3] = postValue.toInt(); // sample time
+ DCMTCommandTurbidity(bio_turbidity[index][0], bio_turbidity[index][1], bio_turbidity[index][2], 1, bio_turbidity[index][3]);
+ break;
+ }
+ } else if(postName.charAt(0) == 'c') {
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ chem_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(chem_thermo[index][0], chem_thermo[index][1], chem_heater_pid[index][0], chem_thermo[index][2], chem_thermo[index][3]);
+ //send command to BREAD here:
+ break;
+ case 'p': //Kp
+ chem_heater_pid[index][1] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'i': //Ki
+ chem_heater_pid[index][2] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'd': //Kd
+ chem_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(chem_thermo[index][0], chem_thermo[index][1], chem_heater_pid[index][1], chem_heater_pid[index][2], chem_heater_pid[index][3]);
+ break;
+ case 'm':
+ chem_motors[index][2] = postValue.toInt();
+ DCMTCommandSpeed(chem_motors[index][0], chem_motors[index][1], chem_motors[index][2], 1);
+ break;
+ }
+ }else if(postName.charAt(0) == 'P') { // bioreactor post processing
+ uint8_t index = postName.substring(2).toInt() - 1;
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ bio_post_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(bio_post_heaters[index][0], bio_post_heaters[index][1], bio_post_heater_pid[index][0], bio_post_heaters[index][2], bio_post_heaters[index][3]);
+ break;
+ case 'p': //Kp
+ bio_post_heater_pid[index][1] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'i': //Ki
+ bio_post_heater_pid[index][2] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'd': //Kd
+ bio_post_heater_pid[index][3] = postValue.toFloat();
+ Serial.println("Sending pH PID");
+ RLHTCommandPID(bio_post_heaters[index][0], bio_post_heaters[index][1], bio_post_heater_pid[index][1], bio_post_heater_pid[index][2], bio_post_heater_pid[index][3]);
+ break;
+ }
+ }
+ } });
+
+ // load any non-form POST request body and url
+ server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
+ {
+ Serial.println(request->url());
+ if (request->url() == "/all-data") {
+ String s = "";
+ for(size_t i=0; iurl() == "/get-variables") {
+ //set time on esp32 rtc
+ String s = "";
+ for(size_t i=0; isend(200, "text/plain", toServer);
+ } if(request->url() == "/logging") {
+ Serial.println("log to SD card");
+ logging = true;
+ } if(request->url() == "/not-logging") {
+ Serial.println("stop log to SD card");
+ logging = false;
+ } if(request->url() == "/estop-on") {
+ Serial.println("estop on");
+ digitalWrite(ESTOP, HIGH);
+ } if(request->url() == "/estop-off") {
+ Serial.println("estop off");
+ digitalWrite(ESTOP, LOW);
+ } else if(request->url() == "/delete-pyrolysis") {
+ writeFile(SD, "/pyrolysis-data.csv", "Date and Time,Dissolution Tank,Dissolution Heating Tape,Valve,Char Chamber,Secondary Reactor,Knockout Drum,Condenser 0,Condenser 1,Condenser 2");
+ } else if(request->url() == "/delete-bioreactor") {
+ writeFile(SD, "/bioreactor-data.csv", "Date and Time,Thermocouple 1,pH Sensor 1,Dissolved Oxygen 1,Turbidity 1,Thermocouple 2, pH Sensor 2, Dissolved Oxygen 2,Turbidity 2,Pasteurization,Dryer");
+ } else if(request->url() == "/delete-chemreactor") {
+ writeFile(SD, "/chemreactor-data.csv", "Date and Time,Reactor 1,Reactor 2");
+ } });
+
+ // Handle Web Server Events
+ events.onConnect([](AsyncEventSourceClient *client)
+ {
+ if(client->lastId()){
+ Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
+ }
+ // send event with message "hello!", id current millis
+ // and set reconnect delay to 1 second
+ client->send("hello!", NULL, millis(), 10000); });
+ server.addHandler(&events);
+
+ server.serveStatic("/", SD, "/");
+ server.begin();
+
+ lastPOST = millis();
+}
+
+uint32_t calTime = 0;
+int calDelay = 900;
+bool readRequestedPHDO = false;
+
+uint8_t loggingCounter = 6;
+void loop()
+{
+ // get slice data from slices
+ if (millis() - lastPOST > SLICE_DATA_INTERVAL_MS)
+ {
+ Serial.println("getting data");
+ // get time
+ String timeToServer = rtc.getTime("%F %T");
+ String pyrolysisToServer = timeToServer;
+ String bioToServer = timeToServer;
+ String chemToServer = timeToServer;
+
+ // Heating data
+ for (int i = 0; i < 9; i++)
+ {
+ float temp;
+ if (pyro_thermo[i][1] == 1)
+ RLHTRequestThermo(pyro_thermo[i][0], &(pyro_thermo_val[i]), &(temp));
+ if (pyro_thermo[i][1] == 2)
+ RLHTRequestThermo(pyro_thermo[i][0], &(temp), &(pyro_thermo_val[i]));
+ }
+
+ // Bioreactor data
+
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ if (readRequestedPHDO)
+ bio_ph_val[n][0] = PHDORequest(bio_ph[n][0]);
+ if (readRequestedPHDO)
+ bio_do_val[n] = PHDORequest(bio_do[n]);
+ DCMTRequestTurbidity(bio_turbidity[n][0], &(bio_turbidity_val[0]), &(bio_turbidity_val[1]));
+
+ // update pH pump controller with new pH values if setpoint is nonzero
+ if (bio_ph_val[n][1] != 0)
+ DCMTCommandPH(10 + n, bio_ph_val[n][1], bio_ph_val[n][0]);
+
+ float temp;
+ if (bio_thermo[n][1] == 1)
+ RLHTRequestThermo(bio_thermo[n][0], &(bio_thermo_val[n]), &(temp));
+ if (bio_thermo[n][1] == 2)
+ RLHTRequestThermo(bio_thermo[n][0], &(temp), &(bio_thermo_val[n]));
+ }
+ for (int i = 0; i < 2; i++)
+ {
+ float temp;
+ if (bio_post_heaters[i][2] == 1)
+ RLHTRequestThermo(bio_post_heaters[i][0], &(bio_thermo_val[i + 2]), &(temp));
+ if (bio_post_heaters[i][2] == 2)
+ RLHTRequestThermo(bio_post_heaters[i][0], &(temp), &(bio_thermo_val[i + 2]));
+ }
+ readRequestedPHDO = false; // reload new read request
+
+ // Chemreactor data
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ float temp;
+ if (chem_thermo[n][2] == 1)
+ RLHTRequestThermo(chem_thermo[n][0], &(chem_thermo_val[n]), &(temp));
+ if (chem_thermo[n][2] == 2)
+ RLHTRequestThermo(chem_thermo[n][0], &(temp), &(chem_thermo_val[n]));
+ }
+
+ // convert to data to string to save to SD card and send to server
+ for (uint8_t n = 0; n < 9; n++)
+ {
+ pyrolysisToServer += "," + String(pyro_thermo_val[n]);
+ }
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ bioToServer += "," + String(bio_thermo_val[n]) + "," + String(bio_ph_val[n][0]) + "," + String(bio_do_val[n]) + "," + String(bio_turbidity_val[n]);
+ }
+ bioToServer += "," + String(bio_thermo_val[2]) + "," + String(bio_thermo_val[3]);
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ chemToServer += "," + String(chem_thermo_val[n]);
+ }
+
+ // send data to website
+ /*
+ Readings to Webpage Format
+ Pyrolysis Reactor:
+ Format: Date and Time,Dissolution Tank,Dissolution Heating Tape,Valve,Char Chamber,Secondary Reactor,Knockout Drum,Condenser 0,Condenser 1,Condenser 2
+ Index: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9
+ Bioreactor:
+ Format: Date and Time,Thermo1,pH1,DO1,Turb1,Thermo2,pH2,DO2,Turb2,Past,Dry
+ Index: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10
+ Chemreactor:
+ Format: Date and Time,Reactor 1,Reactor 2
+ Index: 0 , 1 , 2
+ */
+ events.send(pyrolysisToServer.c_str(), "pyrolysis-readings", millis());
+ events.send(bioToServer.c_str(), "bioreactor-readings", millis());
+ events.send(chemToServer.c_str(), "chemreactor-readings", millis());
+
+ // log onto the SD card
+ if (logging)
+ {
+ appendFile(SD, "/pyrolysis-data.csv", "\r\n" + pyrolysisToServer);
+ appendFile(SD, "/bioreactor-data.csv", "\r\n" + bioToServer);
+ appendFile(SD, "/chemreactor-data.csv", "\r\n" + chemToServer);
+ }
+ lastPOST = millis();
+ }
+ else if (!readRequestedPHDO)
+ {
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ PHDOCommand(bio_do[n], "r"); // request reading from DO sensors
+ PHDOCommand(bio_ph[n][0], "r"); // request reading from pH sensors
+ }
+ readRequestedPHDO = true;
+ }
+}
+
+void RLHTRequestThermo(int address, float *t1, float *t2)
+{
+ bool data_received = false;
+ byte in_char;
+ char in_data[20];
+ FLOATUNION_t thermo1;
+ FLOATUNION_t thermo2;
+
+ thermo1.number = 0;
+ thermo2.number = 0;
+
+ Wire.requestFrom(address, 8, 1);
+ int i = 0;
+ while (Wire.available())
+ { // are there bytes to receive.
+ data_received = true;
+ in_char = Wire.read(); // receive a byte.
+ in_data[i] = in_char; // load this byte into our array.
+ i++; // incur the counter for the array element.
+ }
+
+ if (data_received)
+ {
+ for (int x = 0; x < 4; x++)
+ {
+ thermo1.bytes[x] = in_data[x];
+ thermo2.bytes[x] = in_data[x + 4];
+ }
+ }
+
+ *t1 = thermo1.number;
+ *t2 = thermo2.number;
+}
+
+void RLHTCommandSetpoint(int address, byte heater, float heatSetpoint, byte thermocouple, bool enableReverse)
+{
+ FLOATUNION_t setpoint;
+ setpoint.number = heatSetpoint;
+
+ Wire.beginTransmission(address);
+ Wire.write('H');
+ Wire.write(heater);
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(setpoint.bytes[i]); // sends one byte
+ }
+ Wire.write(thermocouple);
+ Wire.write(enableReverse);
+ Wire.endTransmission(); // stop transmitting
+
+ Serial.print(address);
+ Serial.print('H');
+ Serial.print(heater);
+ Serial.print(setpoint.number);
+ Serial.print(thermocouple);
+ Serial.println(enableReverse);
+}
+
+void RLHTCommandPID(int address, byte heater, float Kp_set, float Ki_set, float Kd_set)
+{
+ FLOATUNION_t Kp;
+ FLOATUNION_t Ki;
+ FLOATUNION_t Kd;
+
+ Kp.number = Kp_set;
+ Ki.number = Ki_set;
+ Kd.number = Kd_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('P');
+ Wire.write(heater);
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kp.bytes[i]); // sends one byte
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Ki.bytes[i]); // sends one byte
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kd.bytes[i]); // sends one byte
+ }
+ Wire.endTransmission(); // stop transmitting
+
+ Serial.print(address);
+ Serial.print('P');
+ Serial.print(heater);
+ Serial.print(Kp.number);
+ Serial.print(Ki.number);
+ Serial.println(Kd.number);
+}
+
+void DCMTRequestTurbidity(int address, float *turbidity1, float *turbidity2)
+{
+ bool data_received = false;
+ byte in_char;
+ char in_data[20];
+ FLOATUNION_t turb1, turb2;
+
+ turb1.number = 0;
+ turb2.number = 0;
+
+ Wire.requestFrom(address, 10, 1);
+ int i = 0;
+ while (Wire.available())
+ { // are there bytes to receive.
+ data_received = true;
+ in_char = Wire.read(); // receive a byte.
+ in_data[i] = in_char; // load this byte into our array.
+ i++; // incur the counter for the array element.
+ }
+
+ if (data_received)
+ {
+ for (int x = 0; x < 4; x++)
+ {
+ turb1.bytes[x] = in_data[x];
+ turb2.bytes[x] = in_data[x + 4];
+ }
+ }
+
+ *turbidity1 = turb1.number;
+ *turbidity2 = turb2.number;
+}
+
+void DCMTCommandTurbidity(int address, byte motor, byte direction, bool enable, int sample_period)
+{
+ INTUNION_t sample_p;
+ sample_p.number = sample_period;
+
+ Wire.beginTransmission(address);
+ Wire.write('T');
+ Wire.write(motor);
+ Wire.write(direction);
+ Wire.write(enable);
+ for (int i = 0; i < 4; i++)
+ Wire.write(sample_p.bytes[i]);
+ Wire.endTransmission();
+}
+
+void DCMTCommandSpeed(int address, byte motor, int speed_set, bool enable)
+{
+ INTUNION_t speed;
+ speed.number = speed_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('M');
+ Wire.write(motor);
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(speed.bytes[i]); // sends one byte
+ }
+ Wire.write(enable);
+ Wire.endTransmission(); // stop transmitting
+}
+
+void DCMTCommandPH(int address, float pHSetpoint_set, float currentPH_set)
+{
+ FLOATUNION_t pHSetpoint, currentPH;
+ pHSetpoint.number = pHSetpoint_set;
+ currentPH.number = currentPH_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('p');
+ for (int i = 0; i < 4; i++)
+ Wire.write(currentPH.bytes[i]);
+ for (int i = 0; i < 4; i++)
+ Wire.write(pHSetpoint.bytes[i]);
+ Wire.endTransmission(); // stop transmitting
+}
+
+void DCMTCommandPHPID(int address, float Kp_set, float Ki_set, float Kd_set)
+{
+ FLOATUNION_t Kp, Ki, Kd;
+ Kp.number = Kp_set;
+ Ki.number = Ki_set;
+ Kd.number = Kd_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('P');
+ for (int i = 0; i < 4; i++)
+ Wire.write(Kp.bytes[i]);
+ for (int i = 0; i < 4; i++)
+ Wire.write(Ki.bytes[i]);
+ for (int i = 0; i < 4; i++)
+ Wire.write(Kd.bytes[i]);
+ Wire.endTransmission(); // stop transmitting
+}
+
+void PHDOCommand(int address, const char *commandData)
+{
+ Wire.beginTransmission(address);
+ Wire.write(commandData);
+ Serial.println(commandData);
+ Wire.endTransmission(); // stop transmitting
+}
+
+float PHDORequest(int address)
+{
+ byte in_char;
+ char in_data[20];
+ FLOATUNION_t data;
+
+ Wire.requestFrom(address, 20, 1);
+
+ byte code = 0;
+ code = Wire.read(); // the first byte is the response code, we read this separately.
+
+ switch (code)
+ { // switch case based on what the response code is.
+ case 1: // decimal 1.
+ Serial.println("Success"); // means the command was successful.
+ break; // exits the switch case.
+
+ case 2: // decimal 2.
+ Serial.println("Failed"); // means the command has failed.
+ break; // exits the switch case.
+
+ case 254: // decimal 254.
+ Serial.println("Pending"); // means the command has not yet been finished calculating.
+ break; // exits the switch case.
+
+ case 255: // decimal 255.
+ Serial.println("No Data"); // means there is no further data to send.
+ break; // exits the switch case.
+ }
+
+ int i = 0;
+ if (code == 1)
+ {
+ while (Wire.available())
+ { // are there bytes to receive.
+ in_char = Wire.read(); // receive a byte.
+ if (code != 1) // check if incoming data is valid
+ in_data[i] = 0; // set values to zero if not valid data
+ else
+ in_data[i] = in_char; // load this byte into our array.
+ i++; // incur the counter for the array element.
+ if (in_char == 0)
+ { // if we see that we have been sent a null command.
+ i = 0; // reset the counter i to 0.
+ break; // exit the while loop.
+ }
+ }
+ return atof(in_data);
+ }
+ else
+ return 0;
+}
\ No newline at end of file
diff --git a/src/spread-beta/main/Config.h b/src/spread-beta/main/Config.h
new file mode 100644
index 0000000..7a47af7
--- /dev/null
+++ b/src/spread-beta/main/Config.h
@@ -0,0 +1,21 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#define SLICE_DATA_INTERVAL_MS 10000
+
+#define NUM_DCMT_SLICES 8
+#define NUM_RLHT_SLICES 8
+#define NUM_PHDO_SLICES 2
+
+// microSD Card Reader connections
+#define SD_CS 5
+#define SPI_MOSI 23
+#define SPI_MISO 19
+#define SPI_SCK 18
+
+#define ESTOP 32
+
+const char *ssid = "BREAD-DARPA";
+const char *password = "12345678";
+
+#endif
diff --git a/src/spread-beta/main/DataStructures.h b/src/spread-beta/main/DataStructures.h
new file mode 100644
index 0000000..a06b683
--- /dev/null
+++ b/src/spread-beta/main/DataStructures.h
@@ -0,0 +1,145 @@
+#ifndef DATA_STRUCTURES_H
+#define DATA_STRUCTURES_H
+
+#include
+#include
+
+// Useful unions for breaking data into bytes
+union FLOATUNION_t
+{
+ float number;
+ uint8_t bytes[4];
+};
+union INTUNION_t
+{
+ int number;
+ uint8_t bytes[4];
+};
+
+// Slice structures for managing data
+struct DCMT_t
+{
+ int address; // I2C address of Slice
+ int m1Speed; // motor 1 speed (-100-100)
+ int m2Speed; // motor 2 speed (-100-100)
+ bool m1En; // motor 1 enable (0:off/apply brake, 1:on/coast)
+ bool m2En; // motor 2 enable (0:off/apply brake, 1:on/coast)
+ float pHSetpoint;
+ float currentPH;
+ float Kp_pH;
+ float Ki_pH;
+ float Kd_pH;
+};
+
+struct RLHT_t
+{
+ int address; // I2C address of Slice
+ float heatSetpoint_1; // set desired temperature for heater 1 (if using relay as heater actuator)
+ float Kp_1; // PID proportional gain for heater 1
+ float Ki_1; // PID integral gain for heater 1
+ float Kd_1; // PID derivative gain for heater 1
+ float heatSetpoint_2;
+ float Kp_2;
+ float Ki_2;
+ float Kd_2;
+ float thermo1; // thermocouple 1 measurement
+ float thermo2; // thermocouple 2 measurement
+};
+
+struct PHDO_t
+{
+ int addressPH; // I2C address of Atlas Scientific pH EZO board (default = 99)
+ int addressDO; // I2C address of Atlas Scientific dissolved oxygen EZO board (default = 97)
+ float pH; // current pH reading
+ float DO; // current dissolved oxygen reading
+ bool calPH = false;
+ bool calDO = false;
+};
+
+int pyro_thermo[9][2] = {
+ // pyrolyis thermocouple layout {address, thermocouple}
+ {16, 2}, // DT
+ {17, 1}, // DHT
+ {16, 1}, // V
+ {15, 2}, // CC
+ {15, 1}, // SR
+ {14, 1}, // KD
+ {10, 1}, // C0
+ {11, 1}, // C1
+ {12, 1} // C2
+};
+float pyro_thermo_val[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; // stores thermocouple values
+
+int pyro_heater[10][4] = {
+ // pyrolysis heater/fan layout {address, heater, thermocouple, reverseEnable}
+ {16, 2, 2, 0}, // DT
+ {16, 1, 1, 0}, // V
+ {15, 2, 2, 0}, // CC
+ {15, 1, 1, 0}, // SR
+ {14, 1, 1, 0}, // KD
+ {10, 1, 1, 0}, // C0H
+ {10, 2, 1, 1}, // C0F
+ {11, 1, 1, 0}, // C1H
+ {11, 2, 1, 1}, // C1F
+ {12, 1, 1, 1} // C2F
+};
+float pyro_heater_pid[10][4]; // pyrolysis heater/fan PID {setpoint, Kp, Ki, Kd}
+
+int bio_thermo[2][2] = { // bioreator thermocouples {address, thermocouple}
+ {10, 2},
+ {11, 2}};
+
+int bio_post_heaters[2][4] = {
+ // bioreactor post processing heaters {address, heater, thermocouple, reverseEnable}
+ {12, 2, 2, 0}, // pasteurization
+ {14, 2, 2, 0} // dryer
+};
+
+float bio_post_heater_pid[2][4]; // {setpoint, Kp, Ki, Kd}
+float bio_thermo_val[4] = {0, 0, 0, 0};
+
+int bio_ph[2][2] = { // bioreactor pH layout {PHDO address, DCMT address}
+ {98, 20},
+ {100, 21}};
+
+float bio_ph_val[2][5]; // bioreactor pH settings (pH, Setpoint, Kp, Ki, Kd)
+
+int bio_do[2] = {97, 99}; // bioreactor DO addresses
+float bio_do_val[2] = {0, 0}; // bioreactor DO values
+
+int bio_turbidity[2][4] = { // bioreactor turbidity pump {address, motor, direction, sampleTime}
+ {22, 1, 1, 0},
+ {22, 2, 1, 0}};
+
+float bio_turbidity_val[2] = {0, 0}; // bioreactor turbidity value
+
+int bio_motors[10][3] = {
+ // bioreactor motor config {address, motor, speed}
+ {23, 1, 0}, // stirring 1
+ {20, 1, 0}, // base 1
+ {20, 2, 0}, // acid 1
+ {24, 1, 0}, // water
+ {24, 2, 0}, // spent media
+ {23, 2, 0}, // stirring 2
+ {21, 1, 0}, // base 2
+ {21, 2, 0}, // acid 2
+ {25, 1, 0}, // harvest
+ {25, 2, 0} // TFF
+};
+
+int chem_thermo[2][4] = {
+ // {address, heater, thermocouple, reverseEnable}
+ {13, 1, 1, 0}, // reactor 1
+ {13, 2, 2, 0} // reactor 2
+};
+float chem_thermo_val[2] = {0, 0};
+float chem_heater_pid[2][4];
+
+int chem_motors[4][3] = {
+ {26, 1, 0}, // ammonium hydroxide
+ {26, 2, 0}, // liquid transfer
+ {27, 1, 0}, // water dilute
+ {27, 2, 0} // reactor wash
+};
+
+#endif
diff --git a/src/spread-beta/main/README.md b/src/spread-beta/main/README.md
new file mode 100644
index 0000000..2029b09
--- /dev/null
+++ b/src/spread-beta/main/README.md
@@ -0,0 +1,2 @@
+# spread
+
diff --git a/src/spread-beta/main/SliceControl.cpp b/src/spread-beta/main/SliceControl.cpp
new file mode 100644
index 0000000..8365f30
--- /dev/null
+++ b/src/spread-beta/main/SliceControl.cpp
@@ -0,0 +1,259 @@
+#include "SliceControl.h"
+#include
+#include
+#include "Config.h"
+
+extern DCMT_t DCMT[NUM_DCMT_SLICES];
+extern RLHT_t RLHT[NUM_RLHT_SLICES];
+extern PHDO_t PHDO[NUM_PHDO_SLICES];
+
+void initSlices()
+{
+ for (int i = 0; i < NUM_DCMT_SLICES; i++)
+ {
+ DCMT[i].address = 20 + i; // Assign addresses 20-26
+ }
+ for (int i = 0; i < NUM_RLHT_SLICES; i++)
+ {
+ RLHT[i].address = 10 + i; // Assign addresses 10-15
+ }
+ PHDO[0].addressDO = 97;
+ PHDO[0].addressPH = 98;
+ PHDO[1].addressDO = 99;
+ PHDO[1].addressPH = 100;
+}
+
+void RLHTRequestThermo(int address, float *t1, float *t2)
+{
+ bool data_received = false;
+ byte in_char;
+ char in_data[20];
+ FLOATUNION_t thermo1, thermo2;
+
+ thermo1.number = 0;
+ thermo2.number = 0;
+
+ Wire.requestFrom(address, 8, 1);
+ int i = 0;
+ while (Wire.available())
+ {
+ data_received = true;
+ in_char = Wire.read();
+ in_data[i] = in_char;
+ i++;
+ }
+
+ if (data_received)
+ {
+ for (int x = 0; x < 4; x++)
+ {
+ thermo1.bytes[x] = in_data[x];
+ thermo2.bytes[x] = in_data[x + 4];
+ }
+ }
+
+ *t1 = thermo1.number;
+ *t2 = thermo2.number;
+}
+
+void RLHTCommandSetpoint(int address, byte heater, float heatSetpoint, byte thermocouple, bool enableReverse)
+{
+ FLOATUNION_t setpoint;
+ setpoint.number = heatSetpoint;
+
+ Wire.beginTransmission(address);
+ Wire.write('H');
+ Wire.write(heater);
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(setpoint.bytes[i]);
+ }
+ Wire.write(thermocouple);
+ Wire.write(enableReverse);
+ Wire.endTransmission();
+}
+
+void RLHTCommandPID(int address, byte heater, float Kp_set, float Ki_set, float Kd_set)
+{
+ FLOATUNION_t Kp, Ki, Kd;
+ Kp.number = Kp_set;
+ Ki.number = Ki_set;
+ Kd.number = Kd_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('P');
+ Wire.write(heater);
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kp.bytes[i]);
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Ki.bytes[i]);
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kd.bytes[i]);
+ }
+ Wire.endTransmission();
+}
+
+void DCMTCommandSpeed(int address, byte motor, int speed_set, bool enable)
+{
+ INTUNION_t speed;
+ speed.number = speed_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('M');
+ Wire.write(motor);
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(speed.bytes[i]);
+ }
+ Wire.write(enable);
+ Wire.endTransmission();
+}
+
+void DCMTCommandPH(int address, float pHSetpoint_set, float currentPH_set)
+{
+ FLOATUNION_t pHSetpoint, currentPH;
+ pHSetpoint.number = pHSetpoint_set;
+ currentPH.number = currentPH_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('p');
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(currentPH.bytes[i]);
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(pHSetpoint.bytes[i]);
+ }
+ Wire.endTransmission();
+}
+
+void DCMTCommandPHPID(int address, float Kp_set, float Ki_set, float Kd_set)
+{
+ FLOATUNION_t Kp, Ki, Kd;
+ Kp.number = Kp_set;
+ Ki.number = Ki_set;
+ Kd.number = Kd_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('P');
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kp.bytes[i]);
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Ki.bytes[i]);
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kd.bytes[i]);
+ }
+ Wire.endTransmission();
+}
+
+void DCMTCommandTurbidity(int address, byte motor, byte direction, bool enable, int sample_period)
+{
+ INTUNION_t sample_p;
+ sample_p.number = sample_period;
+
+ Wire.beginTransmission(address);
+ Wire.write('T');
+ Wire.write(motor);
+ Wire.write(direction);
+ Wire.write(enable);
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(sample_p.bytes[i]);
+ }
+ Wire.endTransmission();
+}
+
+void DCMTRequestTurbidity(int address, float *turbidity1, float *turbidity2)
+{
+ bool data_received = false;
+ byte in_char;
+ char in_data[20];
+ FLOATUNION_t turb1, turb2;
+
+ turb1.number = 0;
+ turb2.number = 0;
+
+ Wire.requestFrom(address, 10, 1);
+ int i = 0;
+ while (Wire.available())
+ {
+ data_received = true;
+ in_char = Wire.read();
+ in_data[i] = in_char;
+ i++;
+ }
+
+ if (data_received)
+ {
+ for (int x = 0; x < 4; x++)
+ {
+ turb1.bytes[x] = in_data[x];
+ turb2.bytes[x] = in_data[x + 4];
+ }
+ }
+
+ *turbidity1 = turb1.number;
+ *turbidity2 = turb2.number;
+}
+
+void PHDOCommand(int address, const char *commandData)
+{
+ Wire.beginTransmission(address);
+ Wire.write(commandData);
+ Wire.endTransmission();
+}
+
+float PHDORequest(int address)
+{
+ byte in_char;
+ char in_data[20];
+ FLOATUNION_t data;
+
+ Wire.requestFrom(address, 20, 1);
+
+ byte code = Wire.read();
+
+ switch (code)
+ {
+ case 1:
+ Serial.println("Success");
+ break;
+ case 2:
+ Serial.println("Failed");
+ break;
+ case 254:
+ Serial.println("Pending");
+ break;
+ case 255:
+ Serial.println("No Data");
+ return 0;
+ default:
+ Serial.println("Unknown code");
+ return 0;
+ }
+
+ int i = 0;
+ while (Wire.available() && code == 1)
+ {
+ in_char = Wire.read();
+ in_data[i] = in_char;
+ i++;
+ if (in_char == 0)
+ {
+ break;
+ }
+ }
+
+ return atof(in_data);
+}
diff --git a/src/spread-beta/main/SliceControl.h b/src/spread-beta/main/SliceControl.h
new file mode 100644
index 0000000..efa890d
--- /dev/null
+++ b/src/spread-beta/main/SliceControl.h
@@ -0,0 +1,19 @@
+#ifndef SLICE_CONTROL_H
+#define SLICE_CONTROL_H
+
+#include "DataStructures.h"
+
+// Function declarations for initializing slices and I2C control
+void initSlices();
+void RLHTRequestThermo(int address, float *t1, float *t2);
+void RLHTCommandSetpoint(int address, byte heater, float heatSetpoint, byte thermocouple, bool enableReverse);
+void RLHTCommandPID(int address, byte heater, float Kp_set, float Ki_set, float Kd_set);
+void DCMTCommandSpeed(int address, byte motor, int speed_set, bool enable);
+void DCMTCommandPH(int address, float pHSetpoint_set, float currentPH_set);
+void DCMTCommandPHPID(int address, float Kp_set, float Ki_set, float Kd_set);
+void DCMTCommandTurbidity(int address, byte motor, byte direction, bool enable, int sample_period);
+void DCMTRequestTurbidity(int address, float *turbidity1, float *turbidity2);
+void PHDOCommand(int address, const char *commandData);
+float PHDORequest(int address);
+
+#endif
diff --git a/src/spread-beta/main/Storage.cpp b/src/spread-beta/main/Storage.cpp
new file mode 100644
index 0000000..1be8c67
--- /dev/null
+++ b/src/spread-beta/main/Storage.cpp
@@ -0,0 +1,103 @@
+#include "Storage.h"
+#include "DataStructures.h"
+#include "Config.h"
+
+void initSDCard()
+{
+ pinMode(SD_CS, OUTPUT);
+ digitalWrite(SD_CS, HIGH);
+
+ // Initialize SPI bus for microSD Card
+ SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
+
+ if (!SD.begin(SD_CS))
+ {
+ Serial.println("Card Mount Failed");
+ return;
+ }
+ uint8_t cardType = SD.cardType();
+
+ if (cardType == CARD_NONE)
+ {
+ Serial.println("No SD card attached");
+ return;
+ }
+
+ Serial.print("SD Card Type: ");
+ if (cardType == CARD_MMC)
+ {
+ Serial.println("MMC");
+ }
+ else if (cardType == CARD_SD)
+ {
+ Serial.println("SDSC");
+ }
+ else if (cardType == CARD_SDHC)
+ {
+ Serial.println("SDHC");
+ }
+ else
+ {
+ Serial.println("UNKNOWN");
+ }
+ uint64_t cardSize = SD.cardSize() / (1024 * 1024);
+ Serial.printf("SD Card Size: %lluMB\n", cardSize);
+}
+
+void removeFile(fs::FS &fs, const char *path)
+{
+ Serial.printf("Removing file: %s\n", path);
+
+ bool file = fs.remove(path);
+ if (!file)
+ {
+ Serial.println("Failed to remove file");
+ return;
+ }
+ else
+ {
+ Serial.println("Removed file");
+ }
+}
+
+void appendFile(fs::FS &fs, const char *path, String message)
+{
+ Serial.printf("Appending to file: %s\n", path);
+
+ File file = fs.open(path, FILE_APPEND);
+ if (!file)
+ {
+ Serial.println("Failed to open file for appending");
+ return;
+ }
+ if (file.print(message))
+ {
+ Serial.println("Message appended");
+ }
+ else
+ {
+ Serial.println("Append failed");
+ }
+ file.close();
+}
+
+void writeFile(fs::FS &fs, const char *path, const char *message)
+{
+ Serial.printf("Writing file: %s\n", path);
+
+ File file = fs.open(path, FILE_WRITE);
+ if (!file)
+ {
+ Serial.println("Failed to open file for writing");
+ return;
+ }
+ if (file.print(message))
+ {
+ Serial.println("File written");
+ }
+ else
+ {
+ Serial.println("Write failed");
+ }
+ file.close();
+}
\ No newline at end of file
diff --git a/src/spread-beta/main/Storage.h b/src/spread-beta/main/Storage.h
new file mode 100644
index 0000000..e6f58f6
--- /dev/null
+++ b/src/spread-beta/main/Storage.h
@@ -0,0 +1,14 @@
+#ifndef STORAGE_H
+#define STORAGE_H
+
+#include
+#include
+
+bool logging = false;
+
+void initSDCard();
+void writeFile(fs::FS &fs, const char *path, const char *message);
+void appendFile(fs::FS &fs, const char *path, String message);
+void removeFile(fs::FS &fs, const char *path);
+
+#endif
diff --git a/src/spread-beta/main/WebServer.cpp b/src/spread-beta/main/WebServer.cpp
new file mode 100644
index 0000000..464b83c
--- /dev/null
+++ b/src/spread-beta/main/WebServer.cpp
@@ -0,0 +1,433 @@
+#include "WebServer.h"
+#include "Storage.h"
+#include "SliceControl.h"
+#include "DataStructures.h"
+
+#include "FS.h"
+#include "SPI.h"
+#include
+#include
+
+ESP32Time rtc;
+
+
+AsyncWebServer server(80);
+AsyncEventSource events("/events");
+
+// WiFi credentials
+const char *ssid = "BREAD-DARPA";
+const char *password = "12345678";
+
+void startWiFi() {
+ WiFi.softAP(ssid, password);
+ IPAddress myIP = WiFi.softAPIP();
+ Serial.print("AP IP address: ");
+ Serial.println(myIP);
+ Serial.println("Server started");
+}
+
+void setupWebServer() {
+
+
+ // send html data to server
+ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ { request->send(SD, "/index.html", "text/html"); });
+
+ // load data from slice form
+
+ /*
+ Pyrolysis Commands:
+ ps1: pyrolysis setpoint 1
+ pp1: pyrolysis Kp 1
+ pi1: pyrolysis Ki 1
+ pd1: pyrolysis Kd 1
+
+ Bioreactor Commands:
+ bs1: bioreactor setpoint 1
+ bp1: bioreactor Kp 1
+ bi1: bioreactor Ki 1
+ bd1: bioreactor Kd 1
+ bm1: bioreactor motor 1
+ bt1: bioreactor turbidity sample time 1
+
+ Chem Decon Commands:
+ cm1: chemreactor motor 1 (pump)
+ cs1: chemreactor setpoint 1
+ cp1: chemreactor Kp 1
+ ci1: chemreactor Ki 1
+ cd1: chemreactor Kd 1
+ */
+
+ server.on("/form-submit", HTTP_POST, [](AsyncWebServerRequest *request)
+ {
+ int params = request->params();
+ for (int i = 0; i < params; i++) {
+ AsyncWebParameter* p = request->getParam(i);
+ Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
+ String postName = p->name().c_str();
+ String postValue = p->value().c_str();
+ uint8_t index = postName.substring(2).toInt() - 1;
+
+ if(postName.charAt(0) == 'p') { // pyrolysis reactor
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ pyro_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(pyro_heater[index][0], pyro_heater[index][1], pyro_heater_pid[index][0], pyro_heater[index][2], pyro_heater[index][3]);
+ break;
+ case 'p': //Kp
+ pyro_heater_pid[index][1] = postValue.toFloat();
+ break;
+ case 'i': //Ki
+ pyro_heater_pid[index][2] = postValue.toFloat();
+ break;
+ case 'd': //Kd
+ pyro_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(pyro_heater[index][0], pyro_heater[index][1], pyro_heater_pid[index][1], pyro_heater_pid[index][2], pyro_heater_pid[index][3]);
+ break;
+ }
+ //float converted into 4 bytes
+ }else if(postName.charAt(0) == 'b') {
+ uint8_t index = postName.substring(2).toInt() - 1;
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ bio_ph_val[index][1] = postValue.toFloat();
+ DCMTCommandPH(bio_ph[index][1], bio_ph_val[index][1], bio_ph_val[index][0]);
+ break;
+ case 'p': //Kp
+ bio_ph_val[index][2] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'i': //Ki
+ bio_ph_val[index][3] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'd': //Kd
+ bio_ph_val[index][2] = postValue.toFloat();
+ Serial.println("Sending pH PID");
+ DCMTCommandPHPID(bio_ph[index][1], bio_ph_val[index][2], bio_ph_val[index][3], bio_ph_val[index][4]);
+ break;
+ case 'm':
+ bio_motors[index][2] = postValue.toInt();
+ DCMTCommandSpeed(bio_motors[index][0], bio_motors[index][1], bio_motors[index][2], 1);
+ break;
+ case 't':
+ bio_turbidity[index][3] = postValue.toInt(); // sample time
+ DCMTCommandTurbidity(bio_turbidity[index][0], bio_turbidity[index][1], bio_turbidity[index][2], 1, bio_turbidity[index][3]);
+ break;
+ }
+ } else if(postName.charAt(0) == 'c') {
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ chem_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(chem_thermo[index][0], chem_thermo[index][1], chem_heater_pid[index][0], chem_thermo[index][2], chem_thermo[index][3]);
+ //send command to BREAD here:
+ break;
+ case 'p': //Kp
+ chem_heater_pid[index][1] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'i': //Ki
+ chem_heater_pid[index][2] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'd': //Kd
+ chem_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(chem_thermo[index][0], chem_thermo[index][1], chem_heater_pid[index][1], chem_heater_pid[index][2], chem_heater_pid[index][3]);
+ break;
+ case 'm':
+ chem_motors[index][2] = postValue.toInt();
+ DCMTCommandSpeed(chem_motors[index][0], chem_motors[index][1], chem_motors[index][2], 1);
+ break;
+ }
+ }else if(postName.charAt(0) == 'P') { // bioreactor post processing
+ uint8_t index = postName.substring(2).toInt() - 1;
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ bio_post_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(bio_post_heaters[index][0], bio_post_heaters[index][1], bio_post_heater_pid[index][0], bio_post_heaters[index][2], bio_post_heaters[index][3]);
+ break;
+ case 'p': //Kp
+ bio_post_heater_pid[index][1] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'i': //Ki
+ bio_post_heater_pid[index][2] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'd': //Kd
+ bio_post_heater_pid[index][3] = postValue.toFloat();
+ Serial.println("Sending pH PID");
+ RLHTCommandPID(bio_post_heaters[index][0], bio_post_heaters[index][1], bio_post_heater_pid[index][1], bio_post_heater_pid[index][2], bio_post_heater_pid[index][3]);
+ break;
+ }
+ }
+ } });
+
+ // load any non-form POST request body and url
+ server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
+ {
+ Serial.println(request->url());
+ if (request->url() == "/all-data") {
+ String s = "";
+ for(size_t i=0; iurl() == "/get-variables") {
+ //set time on esp32 rtc
+ String s = "";
+ for(size_t i=0; isend(200, "text/plain", toServer);
+ } if(request->url() == "/logging") {
+ Serial.println("log to SD card");
+ logging = true;
+ } if(request->url() == "/not-logging") {
+ Serial.println("stop log to SD card");
+ logging = false;
+ } if(request->url() == "/estop-on") {
+ Serial.println("estop on");
+ digitalWrite(ESTOP, HIGH);
+ } if(request->url() == "/estop-off") {
+ Serial.println("estop off");
+ digitalWrite(ESTOP, LOW);
+ } else if(request->url() == "/delete-pyrolysis") {
+ writeFile(SD, "/pyrolysis-data.csv", "Date and Time,Dissolution Tank,Dissolution Heating Tape,Valve,Char Chamber,Secondary Reactor,Knockout Drum,Condenser 0,Condenser 1,Condenser 2");
+ } else if(request->url() == "/delete-bioreactor") {
+ writeFile(SD, "/bioreactor-data.csv", "Date and Time,Thermocouple 1,pH Sensor 1,Dissolved Oxygen 1,Turbidity 1,Thermocouple 2, pH Sensor 2, Dissolved Oxygen 2,Turbidity 2,Pasteurization,Dryer");
+ } else if(request->url() == "/delete-chemreactor") {
+ writeFile(SD, "/chemreactor-data.csv", "Date and Time,Reactor 1,Reactor 2");
+ } });
+
+ // Handle Web Server Events
+ events.onConnect([](AsyncEventSourceClient *client)
+ {
+ if(client->lastId()){
+ Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
+ }
+ // send event with message "hello!", id current millis
+ // and set reconnect delay to 1 second
+ client->send("hello!", NULL, millis(), 10000); });
+ server.addHandler(&events);
+
+ server.serveStatic("/", SD, "/");
+ server.begin();
+
+}
+
+void handleRequests() {
+ server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
+ if (request->url() == "/all-data") {
+ handleAllDataRequest(data, len);
+ } else if (request->url() == "/get-variables") {
+ handleGetVariablesRequest(data, len);
+ } else if (request->url() == "/logging") {
+ Serial.println("log to SD card");
+ logging = true;
+ } else if (request->url() == "/not-logging") {
+ Serial.println("stop log to SD card");
+ logging = false;
+ } else if (request->url() == "/estop-on") {
+ Serial.println("estop on");
+ digitalWrite(ESTOP, HIGH);
+ } else if (request->url() == "/estop-off") {
+ Serial.println("estop off");
+ digitalWrite(ESTOP, LOW);
+ } else if (request->url() == "/delete-pyrolysis") {
+ writeFile(SD, "/pyrolysis-data.csv", "Date and Time,Dissolution Tank,Dissolution Heating Tape,Valve,Char Chamber,Secondary Reactor,Knockout Drum,Condenser 0,Condenser 1,Condenser 2");
+ } else if (request->url() == "/delete-bioreactor") {
+ writeFile(SD, "/bioreactor-data.csv", "Date and Time,Thermocouple 1,pH Sensor 1,Dissolved Oxygen 1,Turbidity 1,Thermocouple 2, pH Sensor 2, Dissolved Oxygen 2,Turbidity 2,Pasteurization,Dryer");
+ } else if (request->url() == "/delete-chemreactor") {
+ writeFile(SD, "/chemreactor-data.csv", "Date and Time,Reactor 1,Reactor 2");
+ }
+ });
+}
+
+// Helper functions for handling specific types of requests
+void handlePyrolysisCommands(String postName, String postValue, uint8_t index) {
+ switch (postName.charAt(1)) {
+ case 's': // Setpoint
+ pyro_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(pyro_heater[index][0], pyro_heater[index][1], pyro_heater_pid[index][0], pyro_heater[index][2], pyro_heater[index][3]);
+ break;
+ case 'p': // Kp
+ pyro_heater_pid[index][1] = postValue.toFloat();
+ break;
+ case 'i': // Ki
+ pyro_heater_pid[index][2] = postValue.toFloat();
+ break;
+ case 'd': // Kd
+ pyro_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(pyro_heater[index][0], pyro_heater[index][1], pyro_heater_pid[index][1], pyro_heater_pid[index][2], pyro_heater_pid[index][3]);
+ break;
+ }
+}
+
+void handleBioreactorCommands(String postName, String postValue, uint8_t index) {
+ switch (postName.charAt(1)) {
+ case 's': // Setpoint
+ bio_ph_val[index][1] = postValue.toFloat();
+ DCMTCommandPH(bio_ph[index][1], bio_ph_val[index][1], bio_ph_val[index][0]);
+ break;
+ case 'p': // Kp
+ bio_ph_val[index][2] = postValue.toFloat();
+ break;
+ case 'i': // Ki
+ bio_ph_val[index][3] = postValue.toFloat();
+ break;
+ case 'd': // Kd
+ bio_ph_val[index][4] = postValue.toFloat();
+ DCMTCommandPHPID(bio_ph[index][1], bio_ph_val[index][2], bio_ph_val[index][3], bio_ph_val[index][4]);
+ break;
+ case 'm': // Motor speed
+ bio_motors[index][2] = postValue.toInt();
+ DCMTCommandSpeed(bio_motors[index][0], bio_motors[index][1], bio_motors[index][2], 1);
+ break;
+ case 't': // Turbidity sample time
+ bio_turbidity[index][3] = postValue.toInt();
+ DCMTCommandTurbidity(bio_turbidity[index][0], bio_turbidity[index][1], bio_turbidity[index][2], 1, bio_turbidity[index][3]);
+ break;
+ }
+}
+
+void handleChemReactorCommands(String postName, String postValue, uint8_t index) {
+ switch (postName.charAt(1)) {
+ case 's': // Setpoint
+ chem_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(chem_thermo[index][0], chem_thermo[index][1], chem_heater_pid[index][0], chem_thermo[index][2], chem_thermo[index][3]);
+ break;
+ case 'p': // Kp
+ chem_heater_pid[index][1] = postValue.toFloat();
+ break;
+ case 'i': // Ki
+ chem_heater_pid[index][2] = postValue.toFloat();
+ break;
+ case 'd': // Kd
+ chem_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(chem_thermo[index][0], chem_thermo[index][1], chem_heater_pid[index][1], chem_heater_pid[index][2], chem_heater_pid[index][3]);
+ break;
+ case 'm': // Motor speed
+ chem_motors[index][2] = postValue.toInt();
+ DCMTCommandSpeed(chem_motors[index][0], chem_motors[index][1], chem_motors[index][2], 1);
+ break;
+ }
+}
+
+void handleBioreactorPostProcessingCommands(String postName, String postValue, uint8_t index) {
+ switch (postName.charAt(1)) {
+ case 's': // Setpoint
+ bio_post_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(bio_post_heaters[index][0], bio_post_heaters[index][1], bio_post_heater_pid[index][0], bio_post_heaters[index][2], bio_post_heaters[index][3]);
+ break;
+ case 'p': // Kp
+ bio_post_heater_pid[index][1] = postValue.toFloat();
+ break;
+ case 'i': // Ki
+ bio_post_heater_pid[index][2] = postValue.toFloat();
+ break;
+ case 'd': // Kd
+ bio_post_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(bio_post_heaters[index][0], bio_post_heaters[index][1], bio_post_heater_pid[index][1], bio_post_heater_pid[index][2], bio_post_heater_pid[index][3]);
+ break;
+ }
+}
+
+void handleAllDataRequest(uint8_t *data, size_t len) {
+ String s = "";
+ for (size_t i = 0; i < len; i++) {
+ s += char(data[i]);
+ }
+ Serial.println(s);
+}
+
+void handleGetVariablesRequest(uint8_t *data, size_t len) {
+ String s = "";
+ for (size_t i = 0; i < len; i++) {
+ s += char(data[i]);
+ }
+ uint16_t timeDate[6];
+ for (uint8_t x = 0; x < 5; x++) {
+ uint8_t nextComma = s.indexOf(',');
+ timeDate[x] = s.substring(0, nextComma).toInt();
+ s = s.substring(nextComma + 1);
+ }
+ timeDate[5] = s.toInt();
+ rtc.setTime(timeDate[0], timeDate[1], timeDate[2], timeDate[3], timeDate[4], timeDate[5]);
+}
+
diff --git a/src/spread-beta/main/WebServer.h b/src/spread-beta/main/WebServer.h
new file mode 100644
index 0000000..0561a5c
--- /dev/null
+++ b/src/spread-beta/main/WebServer.h
@@ -0,0 +1,17 @@
+#ifndef WEBSERVER_H
+#define WEBSERVER_H
+
+#include
+#include
+#include
+#include
+
+extern AsyncWebServer server;
+extern AsyncEventSource events;
+
+// Function declarations for web server setup
+void setupWebServer();
+void startWiFi();
+void handleRequests();
+
+#endif
diff --git a/src/spread-beta/main/main.ino b/src/spread-beta/main/main.ino
new file mode 100644
index 0000000..cfee098
--- /dev/null
+++ b/src/spread-beta/main/main.ino
@@ -0,0 +1,170 @@
+#include "FS.h"
+#include "SPI.h"
+#include
+
+#include "Config.h"
+#include "DataStructures.h"
+#include "SliceControl.h"
+#include "Storage.h"
+#include "WebServer.h"
+
+#include
+
+AsyncWebServer server(80);
+AsyncEventSource events("/events");
+
+void setup()
+{
+ Serial.begin(115200);
+
+ Wire.begin();
+
+ initSDCard();
+
+ initSlices();
+
+ pinMode(ESTOP, OUTPUT);
+
+ // Start Wi-Fi Access Point
+ startWiFi();
+
+ // Set up the web server
+ setupWebServer();
+
+ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ { request->send(SD, "/index.html", "text/html"); });
+
+ // Load any non-form POST request body and URL
+ handleRequests();
+
+ // Handle Web Server Events
+ events.onConnect([](AsyncEventSourceClient *client)
+ {
+ if (client->lastId())
+ {
+ Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
+ }
+ client->send("hello!", NULL, millis(), 10000); });
+ server.addHandler(&events);
+
+ server.serveStatic("/", SD, "/");
+ server.begin();
+}
+
+uint32_t lastPOST;
+bool readRequestedPHDO = false;
+uint8_t loggingCounter = 6;
+
+void loop()
+{
+ // get slice data from slices
+ if (millis() - lastPOST > SLICE_DATA_INTERVAL_MS)
+ {
+ Serial.println("getting data");
+ // get time
+ String timeToServer = rtc.getTime("%F %T");
+ String pyrolysisToServer = timeToServer;
+ String bioToServer = timeToServer;
+ String chemToServer = timeToServer;
+
+ // Heating data
+ for (int i = 0; i < 9; i++)
+ {
+ float temp;
+ if (pyro_thermo[i][1] == 1)
+ RLHTRequestThermo(pyro_thermo[i][0], &(pyro_thermo_val[i]), &(temp));
+ if (pyro_thermo[i][1] == 2)
+ RLHTRequestThermo(pyro_thermo[i][0], &(temp), &(pyro_thermo_val[i]));
+ }
+
+ // Bioreactor data
+
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ if (readRequestedPHDO)
+ bio_ph_val[n][0] = PHDORequest(bio_ph[n][0]);
+ if (readRequestedPHDO)
+ bio_do_val[n] = PHDORequest(bio_do[n]);
+ DCMTRequestTurbidity(bio_turbidity[n][0], &(bio_turbidity_val[0]), &(bio_turbidity_val[1]));
+
+ // update pH pump controller with new pH values if setpoint is nonzero
+ if (bio_ph_val[n][1] != 0)
+ DCMTCommandPH(10 + n, bio_ph_val[n][1], bio_ph_val[n][0]);
+
+ float temp;
+ if (bio_thermo[n][1] == 1)
+ RLHTRequestThermo(bio_thermo[n][0], &(bio_thermo_val[n]), &(temp));
+ if (bio_thermo[n][1] == 2)
+ RLHTRequestThermo(bio_thermo[n][0], &(temp), &(bio_thermo_val[n]));
+ }
+ for (int i = 0; i < 2; i++)
+ {
+ float temp;
+ if (bio_post_heaters[i][2] == 1)
+ RLHTRequestThermo(bio_post_heaters[i][0], &(bio_thermo_val[i + 2]), &(temp));
+ if (bio_post_heaters[i][2] == 2)
+ RLHTRequestThermo(bio_post_heaters[i][0], &(temp), &(bio_thermo_val[i + 2]));
+ }
+ readRequestedPHDO = false; // reload new read request
+
+ // Chemreactor data
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ float temp;
+ if (chem_thermo[n][2] == 1)
+ RLHTRequestThermo(chem_thermo[n][0], &(chem_thermo_val[n]), &(temp));
+ if (chem_thermo[n][2] == 2)
+ RLHTRequestThermo(chem_thermo[n][0], &(temp), &(chem_thermo_val[n]));
+ }
+
+ // convert to data to string to save to SD card and send to server
+ for (uint8_t n = 0; n < 9; n++)
+ {
+ pyrolysisToServer += "," + String(pyro_thermo_val[n]);
+ }
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ bioToServer += "," + String(bio_thermo_val[n]) + "," + String(bio_ph_val[n][0]) + "," + String(bio_do_val[n]) + "," + String(bio_turbidity_val[n]);
+ }
+ bioToServer += "," + String(bio_thermo_val[2]) + "," + String(bio_thermo_val[3]);
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ chemToServer += "," + String(chem_thermo_val[n]);
+ }
+
+ // send data to website
+ /*
+ Readings to Webpage Format
+ Pyrolysis Reactor:
+ Format: Date and Time,Dissolution Tank,Dissolution Heating Tape,Valve,Char Chamber,Secondary Reactor,Knockout Drum,Condenser 0,Condenser 1,Condenser 2
+ Index: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9
+ Bioreactor:
+ Format: Date and Time,Thermo1,pH1,DO1,Turb1,Thermo2,pH2,DO2,Turb2,Past,Dry
+ Index: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10
+ Chemreactor:
+ Format: Date and Time,Reactor 1,Reactor 2
+ Index: 0 , 1 , 2
+ */
+ events.send(pyrolysisToServer.c_str(), "pyrolysis-readings", millis());
+ events.send(bioToServer.c_str(), "bioreactor-readings", millis());
+ events.send(chemToServer.c_str(), "chemreactor-readings", millis());
+
+ // log onto the SD card
+ if (logging)
+ {
+ appendFile(SD, "/pyrolysis-data.csv", "\r\n" + pyrolysisToServer);
+ appendFile(SD, "/bioreactor-data.csv", "\r\n" + bioToServer);
+ appendFile(SD, "/chemreactor-data.csv", "\r\n" + chemToServer);
+ }
+ lastPOST = millis();
+ }
+ else if (!readRequestedPHDO)
+ {
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ PHDOCommand(bio_do[n], "r"); // request reading from DO sensors
+ PHDOCommand(bio_ph[n][0], "r"); // request reading from pH sensors
+ }
+ readRequestedPHDO = true;
+ }
+}
\ No newline at end of file
diff --git a/src/spread-beta/main/output.txt b/src/spread-beta/main/output.txt
new file mode 100644
index 0000000..071a721
--- /dev/null
+++ b/src/spread-beta/main/output.txt
@@ -0,0 +1,1222 @@
+Config.h:
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#define SLICE_DATA_INTERVAL_MS 10000
+
+#define NUM_DCMT_SLICES 8
+#define NUM_RLHT_SLICES 8
+#define NUM_PHDO_SLICES 2
+
+// microSD Card Reader connections
+#define SD_CS 5
+#define SPI_MOSI 23
+#define SPI_MISO 19
+#define SPI_SCK 18
+
+#define ESTOP 32
+
+const char *ssid = "BREAD-DARPA";
+const char *password = "12345678";
+
+#endif
+
+
+
+DataStructures.h:
+#ifndef DATA_STRUCTURES_H
+#define DATA_STRUCTURES_H
+
+#include
+#include
+
+// Useful unions for breaking data into bytes
+union FLOATUNION_t
+{
+ float number;
+ uint8_t bytes[4];
+};
+union INTUNION_t
+{
+ int number;
+ uint8_t bytes[4];
+};
+
+// Slice structures for managing data
+struct DCMT_t
+{
+ int address; // I2C address of Slice
+ int m1Speed; // motor 1 speed (-100-100)
+ int m2Speed; // motor 2 speed (-100-100)
+ bool m1En; // motor 1 enable (0:off/apply brake, 1:on/coast)
+ bool m2En; // motor 2 enable (0:off/apply brake, 1:on/coast)
+ float pHSetpoint;
+ float currentPH;
+ float Kp_pH;
+ float Ki_pH;
+ float Kd_pH;
+};
+
+struct RLHT_t
+{
+ int address; // I2C address of Slice
+ float heatSetpoint_1; // set desired temperature for heater 1 (if using relay as heater actuator)
+ float Kp_1; // PID proportional gain for heater 1
+ float Ki_1; // PID integral gain for heater 1
+ float Kd_1; // PID derivative gain for heater 1
+ float heatSetpoint_2;
+ float Kp_2;
+ float Ki_2;
+ float Kd_2;
+ float thermo1; // thermocouple 1 measurement
+ float thermo2; // thermocouple 2 measurement
+};
+
+struct PHDO_t
+{
+ int addressPH; // I2C address of Atlas Scientific pH EZO board (default = 99)
+ int addressDO; // I2C address of Atlas Scientific dissolved oxygen EZO board (default = 97)
+ float pH; // current pH reading
+ float DO; // current dissolved oxygen reading
+ bool calPH = false;
+ bool calDO = false;
+};
+
+int pyro_thermo[9][2] = {
+ // pyrolyis thermocouple layout {address, thermocouple}
+ {16, 2}, // DT
+ {17, 1}, // DHT
+ {16, 1}, // V
+ {15, 2}, // CC
+ {15, 1}, // SR
+ {14, 1}, // KD
+ {10, 1}, // C0
+ {11, 1}, // C1
+ {12, 1} // C2
+};
+float pyro_thermo_val[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; // stores thermocouple values
+
+int pyro_heater[10][4] = {
+ // pyrolysis heater/fan layout {address, heater, thermocouple, reverseEnable}
+ {16, 2, 2, 0}, // DT
+ {16, 1, 1, 0}, // V
+ {15, 2, 2, 0}, // CC
+ {15, 1, 1, 0}, // SR
+ {14, 1, 1, 0}, // KD
+ {10, 1, 1, 0}, // C0H
+ {10, 2, 1, 1}, // C0F
+ {11, 1, 1, 0}, // C1H
+ {11, 2, 1, 1}, // C1F
+ {12, 1, 1, 1} // C2F
+};
+float pyro_heater_pid[10][4]; // pyrolysis heater/fan PID {setpoint, Kp, Ki, Kd}
+
+int bio_thermo[2][2] = { // bioreator thermocouples {address, thermocouple}
+ {10, 2},
+ {11, 2}};
+
+int bio_post_heaters[2][4] = {
+ // bioreactor post processing heaters {address, heater, thermocouple, reverseEnable}
+ {12, 2, 2, 0}, // pasteurization
+ {14, 2, 2, 0} // dryer
+};
+
+float bio_post_heater_pid[2][4]; // {setpoint, Kp, Ki, Kd}
+float bio_thermo_val[4] = {0, 0, 0, 0};
+
+int bio_ph[2][2] = { // bioreactor pH layout {PHDO address, DCMT address}
+ {98, 20},
+ {100, 21}};
+
+float bio_ph_val[2][5]; // bioreactor pH settings (pH, Setpoint, Kp, Ki, Kd)
+
+int bio_do[2] = {97, 99}; // bioreactor DO addresses
+float bio_do_val[2] = {0, 0}; // bioreactor DO values
+
+int bio_turbidity[2][4] = { // bioreactor turbidity pump {address, motor, direction, sampleTime}
+ {22, 1, 1, 0},
+ {22, 2, 1, 0}};
+
+float bio_turbidity_val[2] = {0, 0}; // bioreactor turbidity value
+
+int bio_motors[10][3] = {
+ // bioreactor motor config {address, motor, speed}
+ {23, 1, 0}, // stirring 1
+ {20, 1, 0}, // base 1
+ {20, 2, 0}, // acid 1
+ {24, 1, 0}, // water
+ {24, 2, 0}, // spent media
+ {23, 2, 0}, // stirring 2
+ {21, 1, 0}, // base 2
+ {21, 2, 0}, // acid 2
+ {25, 1, 0}, // harvest
+ {25, 2, 0} // TFF
+};
+
+int chem_thermo[2][4] = {
+ // {address, heater, thermocouple, reverseEnable}
+ {13, 1, 1, 0}, // reactor 1
+ {13, 2, 2, 0} // reactor 2
+};
+float chem_thermo_val[2] = {0, 0};
+float chem_heater_pid[2][4];
+
+int chem_motors[4][3] = {
+ {26, 1, 0}, // ammonium hydroxide
+ {26, 2, 0}, // liquid transfer
+ {27, 1, 0}, // water dilute
+ {27, 2, 0} // reactor wash
+};
+
+#endif
+
+
+
+main.ino:
+#include
+#include
+#include
+#include
+#include "FS.h"
+#include "SPI.h"
+#include
+
+#include "Config.h"
+#include "DataStructures.h"
+#include "SliceControl.h"
+#include "Storage.h"
+#include "WebServer.h"
+
+#include
+ESP32Time rtc;
+
+AsyncWebServer server(80);
+AsyncEventSource events("/events");
+
+bool logging = false;
+
+void setup()
+{
+ Serial.begin(115200);
+
+ Wire.begin();
+
+ initSDCard();
+
+ initSlices();
+
+ pinMode(ESTOP, OUTPUT);
+
+ // Start Wi-Fi Access Point
+ startWiFi();
+
+ // Set up the web server
+ setupWebServer();
+
+ // Load any non-form POST request body and URL
+ handleRequests();
+
+ // Handle Web Server Events
+ events.onConnect([](AsyncEventSourceClient *client)
+ {
+ if (client->lastId())
+ {
+ Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
+ }
+ client->send("hello!", NULL, millis(), 10000); });
+ server.addHandler(&events);
+
+ server.serveStatic("/", SD, "/");
+ server.begin();
+}
+
+uint32_t lastPOST;
+bool readRequestedPHDO = false;
+uint8_t loggingCounter = 6;
+
+void loop()
+{
+ // get slice data from slices
+ if (millis() - lastPOST > SLICE_DATA_INTERVAL_MS)
+ {
+ Serial.println("getting data");
+ // get time
+ String timeToServer = rtc.getTime("%F %T");
+ String pyrolysisToServer = timeToServer;
+ String bioToServer = timeToServer;
+ String chemToServer = timeToServer;
+
+ // Heating data
+ for (int i = 0; i < 9; i++)
+ {
+ float temp;
+ if (pyro_thermo[i][1] == 1)
+ RLHTRequestThermo(pyro_thermo[i][0], &(pyro_thermo_val[i]), &(temp));
+ if (pyro_thermo[i][1] == 2)
+ RLHTRequestThermo(pyro_thermo[i][0], &(temp), &(pyro_thermo_val[i]));
+ }
+
+ // Bioreactor data
+
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ if (readRequestedPHDO)
+ bio_ph_val[n][0] = PHDORequest(bio_ph[n][0]);
+ if (readRequestedPHDO)
+ bio_do_val[n] = PHDORequest(bio_do[n]);
+ DCMTRequestTurbidity(bio_turbidity[n][0], &(bio_turbidity_val[0]), &(bio_turbidity_val[1]));
+
+ // update pH pump controller with new pH values if setpoint is nonzero
+ if (bio_ph_val[n][1] != 0)
+ DCMTCommandPH(10 + n, bio_ph_val[n][1], bio_ph_val[n][0]);
+
+ float temp;
+ if (bio_thermo[n][1] == 1)
+ RLHTRequestThermo(bio_thermo[n][0], &(bio_thermo_val[n]), &(temp));
+ if (bio_thermo[n][1] == 2)
+ RLHTRequestThermo(bio_thermo[n][0], &(temp), &(bio_thermo_val[n]));
+ }
+ for (int i = 0; i < 2; i++)
+ {
+ float temp;
+ if (bio_post_heaters[i][2] == 1)
+ RLHTRequestThermo(bio_post_heaters[i][0], &(bio_thermo_val[i + 2]), &(temp));
+ if (bio_post_heaters[i][2] == 2)
+ RLHTRequestThermo(bio_post_heaters[i][0], &(temp), &(bio_thermo_val[i + 2]));
+ }
+ readRequestedPHDO = false; // reload new read request
+
+ // Chemreactor data
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ float temp;
+ if (chem_thermo[n][2] == 1)
+ RLHTRequestThermo(chem_thermo[n][0], &(chem_thermo_val[n]), &(temp));
+ if (chem_thermo[n][2] == 2)
+ RLHTRequestThermo(chem_thermo[n][0], &(temp), &(chem_thermo_val[n]));
+ }
+
+ // convert to data to string to save to SD card and send to server
+ for (uint8_t n = 0; n < 9; n++)
+ {
+ pyrolysisToServer += "," + String(pyro_thermo_val[n]);
+ }
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ bioToServer += "," + String(bio_thermo_val[n]) + "," + String(bio_ph_val[n][0]) + "," + String(bio_do_val[n]) + "," + String(bio_turbidity_val[n]);
+ }
+ bioToServer += "," + String(bio_thermo_val[2]) + "," + String(bio_thermo_val[3]);
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ chemToServer += "," + String(chem_thermo_val[n]);
+ }
+
+ // send data to website
+ /*
+ Readings to Webpage Format
+ Pyrolysis Reactor:
+ Format: Date and Time,Dissolution Tank,Dissolution Heating Tape,Valve,Char Chamber,Secondary Reactor,Knockout Drum,Condenser 0,Condenser 1,Condenser 2
+ Index: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9
+ Bioreactor:
+ Format: Date and Time,Thermo1,pH1,DO1,Turb1,Thermo2,pH2,DO2,Turb2,Past,Dry
+ Index: 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10
+ Chemreactor:
+ Format: Date and Time,Reactor 1,Reactor 2
+ Index: 0 , 1 , 2
+ */
+ events.send(pyrolysisToServer.c_str(), "pyrolysis-readings", millis());
+ events.send(bioToServer.c_str(), "bioreactor-readings", millis());
+ events.send(chemToServer.c_str(), "chemreactor-readings", millis());
+
+ // log onto the SD card
+ if (logging)
+ {
+ appendFile(SD, "/pyrolysis-data.csv", "\r\n" + pyrolysisToServer);
+ appendFile(SD, "/bioreactor-data.csv", "\r\n" + bioToServer);
+ appendFile(SD, "/chemreactor-data.csv", "\r\n" + chemToServer);
+ }
+ lastPOST = millis();
+ }
+ else if (!readRequestedPHDO)
+ {
+ for (uint8_t n = 0; n < 2; n++)
+ {
+ PHDOCommand(bio_do[n], "r"); // request reading from DO sensors
+ PHDOCommand(bio_ph[n][0], "r"); // request reading from pH sensors
+ }
+ readRequestedPHDO = true;
+ }
+}
+
+
+output.txt:
+
+
+
+README.md:
+# spread
+
+
+
+
+SliceControl.cpp:
+#include "SliceControl.h"
+#include
+#include
+#include "Config.h"
+
+extern DCMT_t DCMT[NUM_DCMT_SLICES];
+extern RLHT_t RLHT[NUM_RLHT_SLICES];
+extern PHDO_t PHDO[NUM_PHDO_SLICES];
+
+void initSlices()
+{
+ for (int i = 0; i < NUM_DCMT_SLICES; i++)
+ {
+ DCMT[i].address = 20 + i; // Assign addresses 20-26
+ }
+ for (int i = 0; i < NUM_RLHT_SLICES; i++)
+ {
+ RLHT[i].address = 10 + i; // Assign addresses 10-15
+ }
+ PHDO[0].addressDO = 97;
+ PHDO[0].addressPH = 98;
+ PHDO[1].addressDO = 99;
+ PHDO[1].addressPH = 100;
+}
+
+void RLHTRequestThermo(int address, float *t1, float *t2)
+{
+ bool data_received = false;
+ byte in_char;
+ char in_data[20];
+ FLOATUNION_t thermo1, thermo2;
+
+ thermo1.number = 0;
+ thermo2.number = 0;
+
+ Wire.requestFrom(address, 8, 1);
+ int i = 0;
+ while (Wire.available())
+ {
+ data_received = true;
+ in_char = Wire.read();
+ in_data[i] = in_char;
+ i++;
+ }
+
+ if (data_received)
+ {
+ for (int x = 0; x < 4; x++)
+ {
+ thermo1.bytes[x] = in_data[x];
+ thermo2.bytes[x] = in_data[x + 4];
+ }
+ }
+
+ *t1 = thermo1.number;
+ *t2 = thermo2.number;
+}
+
+void RLHTCommandSetpoint(int address, byte heater, float heatSetpoint, byte thermocouple, bool enableReverse)
+{
+ FLOATUNION_t setpoint;
+ setpoint.number = heatSetpoint;
+
+ Wire.beginTransmission(address);
+ Wire.write('H');
+ Wire.write(heater);
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(setpoint.bytes[i]);
+ }
+ Wire.write(thermocouple);
+ Wire.write(enableReverse);
+ Wire.endTransmission();
+}
+
+void RLHTCommandPID(int address, byte heater, float Kp_set, float Ki_set, float Kd_set)
+{
+ FLOATUNION_t Kp, Ki, Kd;
+ Kp.number = Kp_set;
+ Ki.number = Ki_set;
+ Kd.number = Kd_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('P');
+ Wire.write(heater);
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kp.bytes[i]);
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Ki.bytes[i]);
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kd.bytes[i]);
+ }
+ Wire.endTransmission();
+}
+
+void DCMTCommandSpeed(int address, byte motor, int speed_set, bool enable)
+{
+ INTUNION_t speed;
+ speed.number = speed_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('M');
+ Wire.write(motor);
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(speed.bytes[i]);
+ }
+ Wire.write(enable);
+ Wire.endTransmission();
+}
+
+void DCMTCommandPH(int address, float pHSetpoint_set, float currentPH_set)
+{
+ FLOATUNION_t pHSetpoint, currentPH;
+ pHSetpoint.number = pHSetpoint_set;
+ currentPH.number = currentPH_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('p');
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(currentPH.bytes[i]);
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(pHSetpoint.bytes[i]);
+ }
+ Wire.endTransmission();
+}
+
+void DCMTCommandPHPID(int address, float Kp_set, float Ki_set, float Kd_set)
+{
+ FLOATUNION_t Kp, Ki, Kd;
+ Kp.number = Kp_set;
+ Ki.number = Ki_set;
+ Kd.number = Kd_set;
+
+ Wire.beginTransmission(address);
+ Wire.write('P');
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kp.bytes[i]);
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Ki.bytes[i]);
+ }
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(Kd.bytes[i]);
+ }
+ Wire.endTransmission();
+}
+
+void DCMTCommandTurbidity(int address, byte motor, byte direction, bool enable, int sample_period)
+{
+ INTUNION_t sample_p;
+ sample_p.number = sample_period;
+
+ Wire.beginTransmission(address);
+ Wire.write('T');
+ Wire.write(motor);
+ Wire.write(direction);
+ Wire.write(enable);
+ for (int i = 0; i < 4; i++)
+ {
+ Wire.write(sample_p.bytes[i]);
+ }
+ Wire.endTransmission();
+}
+
+void DCMTRequestTurbidity(int address, float *turbidity1, float *turbidity2)
+{
+ bool data_received = false;
+ byte in_char;
+ char in_data[20];
+ FLOATUNION_t turb1, turb2;
+
+ turb1.number = 0;
+ turb2.number = 0;
+
+ Wire.requestFrom(address, 10, 1);
+ int i = 0;
+ while (Wire.available())
+ {
+ data_received = true;
+ in_char = Wire.read();
+ in_data[i] = in_char;
+ i++;
+ }
+
+ if (data_received)
+ {
+ for (int x = 0; x < 4; x++)
+ {
+ turb1.bytes[x] = in_data[x];
+ turb2.bytes[x] = in_data[x + 4];
+ }
+ }
+
+ *turbidity1 = turb1.number;
+ *turbidity2 = turb2.number;
+}
+
+void PHDOCommand(int address, const char *commandData)
+{
+ Wire.beginTransmission(address);
+ Wire.write(commandData);
+ Wire.endTransmission();
+}
+
+float PHDORequest(int address)
+{
+ byte in_char;
+ char in_data[20];
+ FLOATUNION_t data;
+
+ Wire.requestFrom(address, 20, 1);
+
+ byte code = Wire.read();
+
+ switch (code)
+ {
+ case 1:
+ Serial.println("Success");
+ break;
+ case 2:
+ Serial.println("Failed");
+ break;
+ case 254:
+ Serial.println("Pending");
+ break;
+ case 255:
+ Serial.println("No Data");
+ return 0;
+ default:
+ Serial.println("Unknown code");
+ return 0;
+ }
+
+ int i = 0;
+ while (Wire.available() && code == 1)
+ {
+ in_char = Wire.read();
+ in_data[i] = in_char;
+ i++;
+ if (in_char == 0)
+ {
+ break;
+ }
+ }
+
+ return atof(in_data);
+}
+
+
+
+SliceControl.h:
+#ifndef SLICE_CONTROL_H
+#define SLICE_CONTROL_H
+
+#include "DataStructures.h"
+
+// Function declarations for initializing slices and I2C control
+void initSlices();
+void RLHTRequestThermo(int address, float *t1, float *t2);
+void RLHTCommandSetpoint(int address, byte heater, float heatSetpoint, byte thermocouple, bool enableReverse);
+void RLHTCommandPID(int address, byte heater, float Kp_set, float Ki_set, float Kd_set);
+void DCMTCommandSpeed(int address, byte motor, int speed_set, bool enable);
+void DCMTCommandPH(int address, float pHSetpoint_set, float currentPH_set);
+void DCMTCommandPHPID(int address, float Kp_set, float Ki_set, float Kd_set);
+void DCMTCommandTurbidity(int address, byte motor, byte direction, bool enable, int sample_period);
+void DCMTRequestTurbidity(int address, float *turbidity1, float *turbidity2);
+void PHDOCommand(int address, const char *commandData);
+float PHDORequest(int address);
+
+#endif
+
+
+
+Storage.cpp:
+#include "Storage.h"
+#include
+#include "Config.h"
+
+void initSDCard()
+{
+ pinMode(SD_CS, OUTPUT);
+ digitalWrite(SD_CS, HIGH);
+
+ // Initialize SPI bus for microSD Card
+ SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
+
+ if (!SD.begin(SD_CS))
+ {
+ Serial.println("Card Mount Failed");
+ return;
+ }
+ uint8_t cardType = SD.cardType();
+
+ if (cardType == CARD_NONE)
+ {
+ Serial.println("No SD card attached");
+ return;
+ }
+
+ Serial.print("SD Card Type: ");
+ if (cardType == CARD_MMC)
+ {
+ Serial.println("MMC");
+ }
+ else if (cardType == CARD_SD)
+ {
+ Serial.println("SDSC");
+ }
+ else if (cardType == CARD_SDHC)
+ {
+ Serial.println("SDHC");
+ }
+ else
+ {
+ Serial.println("UNKNOWN");
+ }
+ uint64_t cardSize = SD.cardSize() / (1024 * 1024);
+ Serial.printf("SD Card Size: %lluMB\n", cardSize);
+}
+
+void removeFile(fs::FS &fs, const char *path)
+{
+ Serial.printf("Removing file: %s\n", path);
+
+ bool file = fs.remove(path);
+ if (!file)
+ {
+ Serial.println("Failed to remove file");
+ return;
+ }
+ else
+ {
+ Serial.println("Removed file");
+ }
+}
+
+void appendFile(fs::FS &fs, const char *path, String message)
+{
+ Serial.printf("Appending to file: %s\n", path);
+
+ File file = fs.open(path, FILE_APPEND);
+ if (!file)
+ {
+ Serial.println("Failed to open file for appending");
+ return;
+ }
+ if (file.print(message))
+ {
+ Serial.println("Message appended");
+ }
+ else
+ {
+ Serial.println("Append failed");
+ }
+ file.close();
+}
+
+void writeFile(fs::FS &fs, const char *path, const char *message)
+{
+ Serial.printf("Writing file: %s\n", path);
+
+ File file = fs.open(path, FILE_WRITE);
+ if (!file)
+ {
+ Serial.println("Failed to open file for writing");
+ return;
+ }
+ if (file.print(message))
+ {
+ Serial.println("File written");
+ }
+ else
+ {
+ Serial.println("Write failed");
+ }
+ file.close();
+}
+
+
+Storage.h:
+#ifndef STORAGE_H
+#define STORAGE_H
+
+#include
+#include "SD.h"
+
+void initSDCard();
+void writeFile(fs::FS &fs, const char *path, const char *message);
+void appendFile(fs::FS &fs, const char *path, String message);
+void removeFile(fs::FS &fs, const char *path);
+
+#endif
+
+
+
+WebServer.cpp:
+#include "WebServer.h"
+#include "Storage.h"
+#include "SliceControl.h"
+#include
+#include
+#include
+#include
+#include "FS.h"
+#include "SD.h"
+#include
+
+AsyncWebServer server(80);
+AsyncEventSource events("/events");
+
+// WiFi credentials
+const char *ssid = "BREAD-DARPA";
+const char *password = "12345678";
+
+void startWiFi() {
+ WiFi.softAP(ssid, password);
+ IPAddress myIP = WiFi.softAPIP();
+ Serial.print("AP IP address: ");
+ Serial.println(myIP);
+ Serial.println("Server started");
+}
+
+void setupWebServer() {
+
+
+ // send html data to server
+ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ { request->send(SD, "/index.html", "text/html"); });
+
+ // load data from slice form
+
+ /*
+ Pyrolysis Commands:
+ ps1: pyrolysis setpoint 1
+ pp1: pyrolysis Kp 1
+ pi1: pyrolysis Ki 1
+ pd1: pyrolysis Kd 1
+
+ Bioreactor Commands:
+ bs1: bioreactor setpoint 1
+ bp1: bioreactor Kp 1
+ bi1: bioreactor Ki 1
+ bd1: bioreactor Kd 1
+ bm1: bioreactor motor 1
+ bt1: bioreactor turbidity sample time 1
+
+ Chem Decon Commands:
+ cm1: chemreactor motor 1 (pump)
+ cs1: chemreactor setpoint 1
+ cp1: chemreactor Kp 1
+ ci1: chemreactor Ki 1
+ cd1: chemreactor Kd 1
+ */
+
+ server.on("/form-submit", HTTP_POST, [](AsyncWebServerRequest *request)
+ {
+ int params = request->params();
+ for (int i = 0; i < params; i++) {
+ AsyncWebParameter* p = request->getParam(i);
+ Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
+ String postName = p->name().c_str();
+ String postValue = p->value().c_str();
+ uint8_t index = postName.substring(2).toInt() - 1;
+
+ if(postName.charAt(0) == 'p') { // pyrolysis reactor
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ pyro_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(pyro_heater[index][0], pyro_heater[index][1], pyro_heater_pid[index][0], pyro_heater[index][2], pyro_heater[index][3]);
+ break;
+ case 'p': //Kp
+ pyro_heater_pid[index][1] = postValue.toFloat();
+ break;
+ case 'i': //Ki
+ pyro_heater_pid[index][2] = postValue.toFloat();
+ break;
+ case 'd': //Kd
+ pyro_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(pyro_heater[index][0], pyro_heater[index][1], pyro_heater_pid[index][1], pyro_heater_pid[index][2], pyro_heater_pid[index][3]);
+ break;
+ }
+ //float converted into 4 bytes
+ }else if(postName.charAt(0) == 'b') {
+ uint8_t index = postName.substring(2).toInt() - 1;
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ bio_ph_val[index][1] = postValue.toFloat();
+ DCMTCommandPH(bio_ph[index][1], bio_ph_val[index][1], bio_ph_val[index][0]);
+ break;
+ case 'p': //Kp
+ bio_ph_val[index][2] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'i': //Ki
+ bio_ph_val[index][3] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'd': //Kd
+ bio_ph_val[index][2] = postValue.toFloat();
+ Serial.println("Sending pH PID");
+ DCMTCommandPHPID(bio_ph[index][1], bio_ph_val[index][2], bio_ph_val[index][3], bio_ph_val[index][4]);
+ break;
+ case 'm':
+ bio_motors[index][2] = postValue.toInt();
+ DCMTCommandSpeed(bio_motors[index][0], bio_motors[index][1], bio_motors[index][2], 1);
+ break;
+ case 't':
+ bio_turbidity[index][3] = postValue.toInt(); // sample time
+ DCMTCommandTurbidity(bio_turbidity[index][0], bio_turbidity[index][1], bio_turbidity[index][2], 1, bio_turbidity[index][3]);
+ break;
+ }
+ } else if(postName.charAt(0) == 'c') {
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ chem_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(chem_thermo[index][0], chem_thermo[index][1], chem_heater_pid[index][0], chem_thermo[index][2], chem_thermo[index][3]);
+ //send command to BREAD here:
+ break;
+ case 'p': //Kp
+ chem_heater_pid[index][1] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'i': //Ki
+ chem_heater_pid[index][2] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'd': //Kd
+ chem_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(chem_thermo[index][0], chem_thermo[index][1], chem_heater_pid[index][1], chem_heater_pid[index][2], chem_heater_pid[index][3]);
+ break;
+ case 'm':
+ chem_motors[index][2] = postValue.toInt();
+ DCMTCommandSpeed(chem_motors[index][0], chem_motors[index][1], chem_motors[index][2], 1);
+ break;
+ }
+ }else if(postName.charAt(0) == 'P') { // bioreactor post processing
+ uint8_t index = postName.substring(2).toInt() - 1;
+ switch(postName.charAt(1)) {
+ case 's': //setpoint
+ bio_post_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(bio_post_heaters[index][0], bio_post_heaters[index][1], bio_post_heater_pid[index][0], bio_post_heaters[index][2], bio_post_heaters[index][3]);
+ break;
+ case 'p': //Kp
+ bio_post_heater_pid[index][1] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'i': //Ki
+ bio_post_heater_pid[index][2] = postValue.toFloat();
+ //send command to BREAD here:
+ break;
+ case 'd': //Kd
+ bio_post_heater_pid[index][3] = postValue.toFloat();
+ Serial.println("Sending pH PID");
+ RLHTCommandPID(bio_post_heaters[index][0], bio_post_heaters[index][1], bio_post_heater_pid[index][1], bio_post_heater_pid[index][2], bio_post_heater_pid[index][3]);
+ break;
+ }
+ }
+ } });
+
+ // load any non-form POST request body and url
+ server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
+ {
+ Serial.println(request->url());
+ if (request->url() == "/all-data") {
+ String s = "";
+ for(size_t i=0; iurl() == "/get-variables") {
+ //set time on esp32 rtc
+ String s = "";
+ for(size_t i=0; isend(200, "text/plain", toServer);
+ } if(request->url() == "/logging") {
+ Serial.println("log to SD card");
+ logging = true;
+ } if(request->url() == "/not-logging") {
+ Serial.println("stop log to SD card");
+ logging = false;
+ } if(request->url() == "/estop-on") {
+ Serial.println("estop on");
+ digitalWrite(ESTOP, HIGH);
+ } if(request->url() == "/estop-off") {
+ Serial.println("estop off");
+ digitalWrite(ESTOP, LOW);
+ } else if(request->url() == "/delete-pyrolysis") {
+ writeFile(SD, "/pyrolysis-data.csv", "Date and Time,Dissolution Tank,Dissolution Heating Tape,Valve,Char Chamber,Secondary Reactor,Knockout Drum,Condenser 0,Condenser 1,Condenser 2");
+ } else if(request->url() == "/delete-bioreactor") {
+ writeFile(SD, "/bioreactor-data.csv", "Date and Time,Thermocouple 1,pH Sensor 1,Dissolved Oxygen 1,Turbidity 1,Thermocouple 2, pH Sensor 2, Dissolved Oxygen 2,Turbidity 2,Pasteurization,Dryer");
+ } else if(request->url() == "/delete-chemreactor") {
+ writeFile(SD, "/chemreactor-data.csv", "Date and Time,Reactor 1,Reactor 2");
+ } });
+
+ // Handle Web Server Events
+ events.onConnect([](AsyncEventSourceClient *client)
+ {
+ if(client->lastId()){
+ Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
+ }
+ // send event with message "hello!", id current millis
+ // and set reconnect delay to 1 second
+ client->send("hello!", NULL, millis(), 10000); });
+ server.addHandler(&events);
+
+ server.serveStatic("/", SD, "/");
+ server.begin();
+
+}
+
+void handleRequests() {
+ server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
+ if (request->url() == "/all-data") {
+ handleAllDataRequest(data, len);
+ } else if (request->url() == "/get-variables") {
+ handleGetVariablesRequest(data, len);
+ } else if (request->url() == "/logging") {
+ Serial.println("log to SD card");
+ logging = true;
+ } else if (request->url() == "/not-logging") {
+ Serial.println("stop log to SD card");
+ logging = false;
+ } else if (request->url() == "/estop-on") {
+ Serial.println("estop on");
+ digitalWrite(ESTOP, HIGH);
+ } else if (request->url() == "/estop-off") {
+ Serial.println("estop off");
+ digitalWrite(ESTOP, LOW);
+ } else if (request->url() == "/delete-pyrolysis") {
+ writeFile(SD, "/pyrolysis-data.csv", "Date and Time,Dissolution Tank,Dissolution Heating Tape,Valve,Char Chamber,Secondary Reactor,Knockout Drum,Condenser 0,Condenser 1,Condenser 2");
+ } else if (request->url() == "/delete-bioreactor") {
+ writeFile(SD, "/bioreactor-data.csv", "Date and Time,Thermocouple 1,pH Sensor 1,Dissolved Oxygen 1,Turbidity 1,Thermocouple 2, pH Sensor 2, Dissolved Oxygen 2,Turbidity 2,Pasteurization,Dryer");
+ } else if (request->url() == "/delete-chemreactor") {
+ writeFile(SD, "/chemreactor-data.csv", "Date and Time,Reactor 1,Reactor 2");
+ }
+ });
+}
+
+// Helper functions for handling specific types of requests
+void handlePyrolysisCommands(String postName, String postValue, uint8_t index) {
+ switch (postName.charAt(1)) {
+ case 's': // Setpoint
+ pyro_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(pyro_heater[index][0], pyro_heater[index][1], pyro_heater_pid[index][0], pyro_heater[index][2], pyro_heater[index][3]);
+ break;
+ case 'p': // Kp
+ pyro_heater_pid[index][1] = postValue.toFloat();
+ break;
+ case 'i': // Ki
+ pyro_heater_pid[index][2] = postValue.toFloat();
+ break;
+ case 'd': // Kd
+ pyro_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(pyro_heater[index][0], pyro_heater[index][1], pyro_heater_pid[index][1], pyro_heater_pid[index][2], pyro_heater_pid[index][3]);
+ break;
+ }
+}
+
+void handleBioreactorCommands(String postName, String postValue, uint8_t index) {
+ switch (postName.charAt(1)) {
+ case 's': // Setpoint
+ bio_ph_val[index][1] = postValue.toFloat();
+ DCMTCommandPH(bio_ph[index][1], bio_ph_val[index][1], bio_ph_val[index][0]);
+ break;
+ case 'p': // Kp
+ bio_ph_val[index][2] = postValue.toFloat();
+ break;
+ case 'i': // Ki
+ bio_ph_val[index][3] = postValue.toFloat();
+ break;
+ case 'd': // Kd
+ bio_ph_val[index][4] = postValue.toFloat();
+ DCMTCommandPHPID(bio_ph[index][1], bio_ph_val[index][2], bio_ph_val[index][3], bio_ph_val[index][4]);
+ break;
+ case 'm': // Motor speed
+ bio_motors[index][2] = postValue.toInt();
+ DCMTCommandSpeed(bio_motors[index][0], bio_motors[index][1], bio_motors[index][2], 1);
+ break;
+ case 't': // Turbidity sample time
+ bio_turbidity[index][3] = postValue.toInt();
+ DCMTCommandTurbidity(bio_turbidity[index][0], bio_turbidity[index][1], bio_turbidity[index][2], 1, bio_turbidity[index][3]);
+ break;
+ }
+}
+
+void handleChemReactorCommands(String postName, String postValue, uint8_t index) {
+ switch (postName.charAt(1)) {
+ case 's': // Setpoint
+ chem_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(chem_thermo[index][0], chem_thermo[index][1], chem_heater_pid[index][0], chem_thermo[index][2], chem_thermo[index][3]);
+ break;
+ case 'p': // Kp
+ chem_heater_pid[index][1] = postValue.toFloat();
+ break;
+ case 'i': // Ki
+ chem_heater_pid[index][2] = postValue.toFloat();
+ break;
+ case 'd': // Kd
+ chem_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(chem_thermo[index][0], chem_thermo[index][1], chem_heater_pid[index][1], chem_heater_pid[index][2], chem_heater_pid[index][3]);
+ break;
+ case 'm': // Motor speed
+ chem_motors[index][2] = postValue.toInt();
+ DCMTCommandSpeed(chem_motors[index][0], chem_motors[index][1], chem_motors[index][2], 1);
+ break;
+ }
+}
+
+void handleBioreactorPostProcessingCommands(String postName, String postValue, uint8_t index) {
+ switch (postName.charAt(1)) {
+ case 's': // Setpoint
+ bio_post_heater_pid[index][0] = postValue.toFloat();
+ RLHTCommandSetpoint(bio_post_heaters[index][0], bio_post_heaters[index][1], bio_post_heater_pid[index][0], bio_post_heaters[index][2], bio_post_heaters[index][3]);
+ break;
+ case 'p': // Kp
+ bio_post_heater_pid[index][1] = postValue.toFloat();
+ break;
+ case 'i': // Ki
+ bio_post_heater_pid[index][2] = postValue.toFloat();
+ break;
+ case 'd': // Kd
+ bio_post_heater_pid[index][3] = postValue.toFloat();
+ RLHTCommandPID(bio_post_heaters[index][0], bio_post_heaters[index][1], bio_post_heater_pid[index][1], bio_post_heater_pid[index][2], bio_post_heater_pid[index][3]);
+ break;
+ }
+}
+
+void handleAllDataRequest(uint8_t *data, size_t len) {
+ String s = "";
+ for (size_t i = 0; i < len; i++) {
+ s += char(data[i]);
+ }
+ Serial.println(s);
+}
+
+void handleGetVariablesRequest(uint8_t *data, size_t len) {
+ String s = "";
+ for (size_t i = 0; i < len; i++) {
+ s += char(data[i]);
+ }
+ uint16_t timeDate[6];
+ for (uint8_t x = 0; x < 5; x++) {
+ uint8_t nextComma = s.indexOf(',');
+ timeDate[x] = s.substring(0, nextComma).toInt();
+ s = s.substring(nextComma + 1);
+ }
+ timeDate[5] = s.toInt();
+ rtc.setTime(timeDate[0], timeDate[1], timeDate[2], timeDate[3], timeDate[4], timeDate[5]);
+}
+
+
+
+
+WebServer.h:
+#ifndef WEBSERVER_H
+#define WEBSERVER_H
+
+#include
+
+extern AsyncWebServer server;
+extern AsyncEventSource events;
+
+// Function declarations for web server setup
+void setupWebServer();
+void startWiFi();
+void handleRequests();
+
+#endif
+
+
+
diff --git a/Website Code on SD Card/bioreactor-data.csv b/src/webpage/bioreactor-data.csv
similarity index 100%
rename from Website Code on SD Card/bioreactor-data.csv
rename to src/webpage/bioreactor-data.csv
diff --git a/src/webpage/bioreactor.html b/src/webpage/bioreactor.html
new file mode 100644
index 0000000..2049e77
--- /dev/null
+++ b/src/webpage/bioreactor.html
@@ -0,0 +1,210 @@
+