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 @@ +
+

Bioreactor

+ + +
+
+
+
+
+ + + +
+
+ + +
+
+
+
+ + + +
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+
+ +
+ +
+ +
+ +
+
+ +
+
+ + + +
+
+ + +
+
+
+
+
+ + + +
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + + +
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + +

Post Processing

+
+
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+
+ +
+ +
+ +
+ +
+
+ +
+
+ + + +
+
+
diff --git a/src/webpage/bioreactor.js b/src/webpage/bioreactor.js new file mode 100644 index 0000000..50bbd34 --- /dev/null +++ b/src/webpage/bioreactor.js @@ -0,0 +1,773 @@ + +//----------------Bioreactor thermocouple traces-----------------------// +var bio_thermo_traces = [ + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Thermocouple 1" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Thermocouple 2" + } +] + +//----------------Bioreactor pH traces-----------------------// +var bio_ph_traces = [ + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "pH Sensor 1" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "pH Sensor 2" + } +] + +//----------------Bioreactor dissolved oxygen traces-----------------------// +var bio_oxygen_traces = [ + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Dissolved O2 1" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Dissolved O2 2" + } +] + +//----------------Bioreactor turbidity traces-----------------------// +var bio_turbidity_traces = [ + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Turbidity 1" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Turbidity 2" + } +] + +//----------------Bioreactor Post Processing traces-----------------------// +var bio_pro_traces = [ + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Pasteurization" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Dryer" + } +] + +//----------------Thermocouple graph layout-----------------------// +var bio_thermo_layout = { + title: 'Temperature Data', + xaxis: { + title: 'Time', + autorange: true, + rangeselector: { + buttons: [ + { + count: 1, + label: '1h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 6, + label: '6h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 12, + label: '12h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 24, + label: '24h', + step: 'hour', + stepmode: 'backward' + }, + { step: 'all' } + ] + }, + type: 'date' + }, + yaxis: { + title: 'Degrees Celsius' + }, + showlegend: true +}; + + +//----------------pH graph layout-----------------------// +var pH_layout = { + title: 'pH Data', + xaxis: { + title: 'Time', + autorange: true, + rangeselector: { + buttons: [ + { + count: 1, + label: '1h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 6, + label: '6h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 12, + label: '12h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 24, + label: '24h', + step: 'hour', + stepmode: 'backward' + }, + { step: 'all' } + ] + }, + type: 'date' + }, + yaxis: { + title: 'pH' + }, + showlegend: true +}; + +//----------------Dissolved oxygen graph layout-----------------------// +var oxygen_layout = { + title: 'Dissolved Oxygen Data', + xaxis: { + title: 'Time', + autorange: true, + rangeselector: { + buttons: [ + { + count: 1, + label: '1h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 6, + label: '6h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 12, + label: '12h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 24, + label: '24h', + step: 'hour', + stepmode: 'backward' + }, + { step: 'all' } + ] + }, + type: 'date' + }, + yaxis: { + title: 'Dissolved Oxygen (mL)' + }, + showlegend: true +}; + +//----------------Turbidity graph layout-----------------------// +var turbidity_layout = { + title: 'Turbidity Data', + xaxis: { + title: 'Time', + autorange: true, + rangeselector: { + buttons: [ + { + count: 1, + label: '1h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 6, + label: '6h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 12, + label: '12h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 24, + label: '24h', + step: 'hour', + stepmode: 'backward' + }, + { step: 'all' } + ] + }, + type: 'date' + }, + yaxis: { + title: 'Turbidity (NTU)' + }, + showlegend: true +}; + + +//----------------Post Processing graph layout-----------------------// +var pro_layout = { + title: 'Post Processing Data', + xaxis: { + title: 'Time', + autorange: true, + rangeselector: { + buttons: [ + { + count: 1, + label: '1h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 6, + label: '6h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 12, + label: '12h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 24, + label: '24h', + step: 'hour', + stepmode: 'backward' + }, + { step: 'all' } + ] + }, + type: 'date' + }, + yaxis: { + title: 'Degrees Celsius' + }, + showlegend: true +}; + +//----------------Standard Gauge Layout-----------------------// +var gaugeLayout = { width: 270, height: 220, margin: { t: 0, b: 0, l: 40, r: 40 } }; + + +var thermo_bio_gauge = [{ + domain: { x: [0, 1], y: [0, 1] }, + value: 0, + title: { text: "" }, + type: "indicator", + mode: "gauge+number", + delta: { reference: 200 }, + number: { 'valueformat': '.1f' }, + gauge: { + axis: { range: [null, 400] }, + bar: { color: "blue" }, + steps: [ + { range: [0, 400], color: "#dddddd" }, + ], + } +}]; + + +//----------------pH Gauge-----------------------// +var pH_gauge = [{ + domain: { x: [0, 1], y: [0, 1] }, + value: 0, + title: { text: "" }, + type: "indicator", + mode: "gauge+number", + delta: { reference: 7 }, + number: { 'valueformat': '.2f' }, + gauge: { + axis: { range: [null, 14] }, + bar: { color: "green" }, + steps: [ + { range: [0, 14], color: "#dddddd" }, + ], + threshold: { + line: { color: "red", width: 4 }, + thickness: 0.75, + value: 7 + } + } +}]; + +//----------------Dissolved Oxygen Gauge-----------------------// +var oxygen_gauge = [{ + domain: { x: [0, 1], y: [0, 1] }, + value: 0, + title: { text: "" }, + type: "indicator", + mode: "gauge+number", + delta: { reference: 50 }, + number: { 'valueformat': '.2f' }, + gauge: { + axis: { range: [null, 100] }, + bar: { color: "green" }, + steps: [ + { range: [0, 100], color: "#dddddd" }, + ], + } +}]; + +//----------------Turbidity Gauge-----------------------// +var turbidity_gauge = [{ + domain: { x: [0, 1], y: [0, 1] }, + value: 0, + title: { text: "" }, + type: "indicator", + mode: "gauge+number", + delta: { reference: 1 }, + number: { 'valueformat': '.2f' }, + gauge: { + axis: { range: [null, 6] }, + bar: { color: "green" }, + steps: [ + { range: [0, 6], color: "#dddddd" }, + ], + } +}]; + + +//----------------Motor Gauge-----------------------// +var motor_gauge = [{ + domain: { x: [0, 1], y: [0, 1] }, + value: 0, + title: { text: "" }, + type: "indicator", + mode: "gauge+number", + delta: { reference: 50 }, + gauge: { + axis: { range: [null, 100] }, + bar: { color: "green" }, + steps: [ + { range: [0, 100], color: "#dddddd" }, + ], + } +}]; + + +//----------------Bioreactor Gauges-----------------------// +var bioGaugeInfo = { + "bio-thermo-gauge1": { + title: "Thermocouple 1", + value: 0, + }, + "bio-ph-gauge1": { + title: "pH Sensor 1", + value: 0, + setpoint: 0, + }, + "bio-oxygen-gauge1": { + title: "Dissolved Oxygen 1", + value: 0, + }, + "bio-turbidity-gauge1": { + title: "Turbidity 1", + value: 0, + }, + "bio-thermo-gauge2": { + title: "Thermocouple 2", + value: 0, + }, + "bio-ph-gauge2": { + title: "pH Sensor 2", + value: 0, + setpoint: 0, + }, + "bio-oxygen-gauge2": { + title: "Dissolved Oxygen 2", + value: 0, + }, + "bio-turbidity-gauge2": { + title: "Turbidity 2", + value: 0, + }, + "bio-pro-gauge1": { + title: "Pasteurization", + value: 0, + }, + "bio-pro-gauge2": { + title: "Dryer", + value: 0, + }, +} +var motorGaugeInfo = { + "bio-stirring-gauge1": { + title: "Stirring Motor 1", + }, + "bio-pump1-gauge1": { + title: "Base Pump 1", + }, + "bio-pump2-gauge1": { + title: "Acid Pump 1", + }, + "bio-pump3-gauge1": { + title: "Water Pump", + }, + "bio-pump4-gauge1": { + title: "Spent Media Pump", + }, + "bio-stirring-gauge2": { + title: "Stirring Motor 2", + }, + "bio-pump1-gauge2": { + title: "Base Pump 2", + }, + "bio-pump2-gauge2": { + title: "Acid Pump 2", + }, + "bio-pump3-gauge2": { + title: "Harvest Pump", + }, + "bio-pump4-gauge2": { + title: "TFF Pump", + }, +} + +function plotBioData() { + + //create new gauge plots + Plotly.newPlot('bio-thermo-gauge1', thermo_bio_gauge, gaugeLayout); + Plotly.newPlot('bio-thermo-gauge2', thermo_bio_gauge, gaugeLayout); + + Plotly.newPlot('bio-ph-gauge1', pH_gauge, gaugeLayout); + Plotly.newPlot('bio-ph-gauge2', pH_gauge, gaugeLayout); + + Plotly.newPlot('bio-oxygen-gauge1', oxygen_gauge, gaugeLayout); + Plotly.newPlot('bio-oxygen-gauge2', oxygen_gauge, gaugeLayout); + + Plotly.newPlot('bio-turbidity-gauge1', turbidity_gauge, gaugeLayout); + Plotly.newPlot('bio-turbidity-gauge2', turbidity_gauge, gaugeLayout); + + Plotly.newPlot("bio-stirring-gauge1", motor_gauge, gaugeLayout); + Plotly.newPlot("bio-stirring-gauge2", motor_gauge, gaugeLayout); + + Plotly.newPlot("bio-pump1-gauge1", motor_gauge, gaugeLayout); + Plotly.newPlot("bio-pump2-gauge1", motor_gauge, gaugeLayout); + Plotly.newPlot("bio-pump3-gauge1", motor_gauge, gaugeLayout); + Plotly.newPlot("bio-pump4-gauge1", motor_gauge, gaugeLayout); + Plotly.newPlot("bio-pump1-gauge2", motor_gauge, gaugeLayout); + Plotly.newPlot("bio-pump2-gauge2", motor_gauge, gaugeLayout); + Plotly.newPlot("bio-pump3-gauge2", motor_gauge, gaugeLayout); + Plotly.newPlot("bio-pump4-gauge2", motor_gauge, gaugeLayout); + + Plotly.newPlot('bio-pro-gauge1', thermo_gauge, gaugeLayout); + Plotly.newPlot('bio-pro-gauge2', thermo_gauge, gaugeLayout); + + //restyle + Plotly.restyle('bio-thermo-gauge1', { title: bioGaugeInfo["bio-thermo-gauge1"].title }) + Plotly.restyle('bio-thermo-gauge2', { title: bioGaugeInfo["bio-thermo-gauge2"].title }) + Plotly.restyle('bio-ph-gauge1', { title: bioGaugeInfo["bio-ph-gauge1"].title }) + Plotly.restyle('bio-ph-gauge2', { title: bioGaugeInfo["bio-ph-gauge2"].title }) + Plotly.restyle('bio-oxygen-gauge1', { title: bioGaugeInfo["bio-oxygen-gauge1"].title }) + Plotly.restyle('bio-oxygen-gauge2', { title: bioGaugeInfo["bio-oxygen-gauge2"].title }) + Plotly.restyle('bio-turbidity-gauge1', { title: bioGaugeInfo["bio-turbidity-gauge1"].title }) + Plotly.restyle('bio-turbidity-gauge2', { title: bioGaugeInfo["bio-turbidity-gauge2"].title }) + + Plotly.restyle("bio-stirring-gauge1", { title: motorGaugeInfo["bio-stirring-gauge1"].title }) + Plotly.restyle("bio-stirring-gauge2", { title: motorGaugeInfo["bio-stirring-gauge2"].title }) + + Plotly.restyle("bio-pump1-gauge1", { title: motorGaugeInfo["bio-pump1-gauge1"].title }) + Plotly.restyle("bio-pump2-gauge1", { title: motorGaugeInfo["bio-pump2-gauge1"].title }) + Plotly.restyle("bio-pump3-gauge1", { title: motorGaugeInfo["bio-pump3-gauge1"].title }) + Plotly.restyle("bio-pump4-gauge1", { title: motorGaugeInfo["bio-pump4-gauge1"].title }) + + Plotly.restyle("bio-pump1-gauge2", { title: motorGaugeInfo["bio-pump1-gauge2"].title }) + Plotly.restyle("bio-pump2-gauge2", { title: motorGaugeInfo["bio-pump2-gauge2"].title }) + Plotly.restyle("bio-pump3-gauge2", { title: motorGaugeInfo["bio-pump3-gauge2"].title }) + Plotly.restyle("bio-pump4-gauge2", { title: motorGaugeInfo["bio-pump4-gauge2"].title }) + + Plotly.restyle('bio-pro-gauge1', { title: bioGaugeInfo["bio-pro-gauge1"].title }) + Plotly.restyle('bio-pro-gauge2', { title: bioGaugeInfo["bio-pro-gauge2"].title }) +} + +function loadDataBioreactor() { + + fetch('bioreactor-data.csv') + .then(response => response.text()) + .then(text => { + let row = text.split("\r\n") //split entries by row + + for (let n = 1; n < row.length; n++) { + let data = row[n].split(',') + for (let x = 0; x < 2; x++) { + bio_thermo_traces[x].x.push(data[0]) //data[0] pushes time to the x axis + bio_ph_traces[x].x.push(data[0]) + bio_oxygen_traces[x].x.push(data[0]) + bio_turbidity_traces[x].x.push(data[0]) + bio_pro_traces[x].x.push(data[0]) + + bio_thermo_traces[x].y.push(data[x * 4 + 1]) + bio_ph_traces[x].y.push(data[x * 4 + 2]) + bio_oxygen_traces[x].y.push(data[x * 4 + 3]) + bio_turbidity_traces[x].y.push(data[x * 4 + 4]) + bio_pro_traces[x].y.push(data[x + 9]) + } + } + + //Plot data + Plotly.newPlot('bio-thermo-graph', bio_thermo_traces, bio_thermo_layout, { scrollZoom: true }); + Plotly.newPlot('bio-ph-graph', bio_ph_traces, pH_layout, { scrollZoom: true }); + Plotly.newPlot('bio-oxygen-graph', bio_oxygen_traces, oxygen_layout, { scrollZoom: true }); + Plotly.newPlot('bio-turbidity-graph', bio_turbidity_traces, turbidity_layout, { scrollZoom: true }); + Plotly.newPlot('bio-pro-graph', bio_pro_traces, pro_layout, { scrollZoom: true }); + }) +} + +function loadBioPhGauges(sec) { + let bioPh = sec.split(',') + for (let x = 0; x < bioPh.length; x++) { + let b = bioPh[x].split('|') + let id = "bio-ph-gauge" + (x + 1) + document.getElementById(id + "-amount").value = b[0] + bioGaugeInfo[id].setpoint = b[0] + document.getElementById(id + "-kp").value = b[1] + document.getElementById(id + "-ki").value = b[2] + document.getElementById(id + "-kd").value = b[3] + updateBioPhGauge(id) + } +} + +function loadTurbGauges(sec) { + let turb = sec.split(',') + document.getElementById("bio-turbidity-gauge1-amount").value = turb[0] + document.getElementById("bio-turbidity-gauge2-amount").value = turb[1] + Plotly.restyle('bio-turbidity-gauge1', { title: bioGaugeInfo["bio-turbidity-gauge1"].title, value: turb[0] }) + Plotly.restyle('bio-turbidity-gauge2', { title: bioGaugeInfo["bio-turbidity-gauge2"].title, value: turb[1] }) +} + +function loadBioProGauges(sec) { + let bioPro = sec.split(',') + for (let x = 0; x < bioPro.length; x++) { + let b = bioPro[x].split('|') + let id = "bio-pro-gauge" + (x + 1) + document.getElementById(id + "-amount").value = b[0] + bioGaugeInfo[id].setpoint = b[0] + document.getElementById(id + "-kp").value = b[1] + document.getElementById(id + "-ki").value = b[2] + document.getElementById(id + "-kd").value = b[3] + updateBioProGauge(id) + } +} + +function loadBioGauges(sec) { + + let bio = sec.split(',') + document.getElementById("bio-stirring-gauge1-amount").value = bio[0] + document.getElementById("bio-stirring-gauge2-amount").value = bio[6] + Plotly.restyle('bio-stirring-gauge1', { title: motorGaugeInfo["bio-stirring-gauge1"].title, value: bio[0] }) + Plotly.restyle('bio-stirring-gauge2', { title: motorGaugeInfo["bio-stirring-gauge2"].title, value: bio[6] }) + for (let x = 0; x < 4; x++) { + let id = "bio-pump" + (x + 1) + "-gauge1" + document.getElementById(id + "-amount").value = bio[x + 1] + Plotly.restyle(id, { title: motorGaugeInfo[id].title, value: bio[x + 1] }) + } + for (let x = 0; x < 4; x++) { + let id = "bio-pump" + (x + 1) + "-gauge2" + document.getElementById(id + "-amount").value = bio[x + 5] + Plotly.restyle(id, { title: motorGaugeInfo[id].title, value: bio[x + 7] }) + } +} + +function bioreactorEventListeners(source) { + + source.addEventListener('bioreactor-readings', function (e) { + var bioVals = e.data.split(',') + + bioGaugeInfo["bio-thermo-gauge1"].value = bioVals[1] + bioGaugeInfo["bio-ph-gauge1"].value = bioVals[2] + bioGaugeInfo["bio-oxygen-gauge1"].value = bioVals[3] + bioGaugeInfo["bio-turbidity-gauge1"].value = bioVals[4] + bioGaugeInfo["bio-thermo-gauge2"].value = bioVals[5] + bioGaugeInfo["bio-ph-gauge2"].value = bioVals[6] + bioGaugeInfo["bio-oxygen-gauge2"].value = bioVals[7] + bioGaugeInfo["bio-turbidity-gauge2"].value = bioVals[8] + bioGaugeInfo["bio-pro-gauge1"].value = bioVals[9] + bioGaugeInfo["bio-pro-gauge2"].value = bioVals[10] + + updateBioPhGauge("bio-ph-gauge1") + updateBioPhGauge("bio-ph-gauge2") + updateBioProGauge("bio-pro-gauge1") + updateBioProGauge("bio-pro-gauge2") + + let ids = Object.keys(bioGaugeInfo) + ids.forEach((id) => { + Plotly.update(id, { + value: bioVals[ids.indexOf(id) + 1], + title: bioGaugeInfo[id].title, + 'gauge.bar.color': "blue", + }) + }) + + //plot values + if (logging) { + bUpdateCounter += 1 + if (bUpdateCounter >= 2) { + bUpdateCounter = 0 + Plotly.extendTraces('bio-thermo-graph', { + x: [[bioVals[0]], [bioVals[0]]], + y: [[bioVals[1]], [bioVals[5]]] + }, [0, 1]) + Plotly.extendTraces('bio-ph-graph', { + x: [[bioVals[0]], [bioVals[0]]], + y: [[bioVals[2]], [bioVals[6]]] + }, [0, 1]) + Plotly.extendTraces('bio-oxygen-graph', { + x: [[bioVals[0]], [bioVals[0]]], + y: [[bioVals[3]], [bioVals[7]]] + }, [0, 1]) + Plotly.extendTraces('bio-turbidity-graph', { + x: [[bioVals[0]], [bioVals[0]]], + y: [[bioVals[4]], [bioVals[8]]] + }, [0, 1]) + Plotly.extendTraces('bio-pro-graph', { + x: [[bioVals[0]], [bioVals[0]]], + y: [[bioVals[9]], [bioVals[10]]] + }, [0, 1]) + } + } + }, false) +} + +function updateBioPhGauge(gaugeID) { + let setpoint = parseFloat(bioGaugeInfo[gaugeID].setpoint) + let value = parseFloat(bioGaugeInfo[gaugeID].value) + + let new_ph_gauge = { + domain: { x: [0, 1], y: [0, 1] }, + value: value, + title: { text: bioGaugeInfo[gaugeID].title }, + type: "indicator", + mode: "gauge+number", + gauge: { + axis: { range: [null, 14] }, + bar: { color: "blue" }, + steps: [ + { range: [0, 14], color: "#dddddd" }, + ], + threshold: { + line: { color: "red", width: 4 }, + thickness: 0.75, + value: setpoint, + } + } + } + Plotly.restyle(gaugeID, new_ph_gauge) +} + +function updateBioProGauge(gaugeID) { + let setpoint = parseFloat(bioGaugeInfo[gaugeID].setpoint) + let value = parseFloat(bioGaugeInfo[gaugeID].value) + + function pickColor() { + if (value < setpoint - 50) { return "yellow" } + else if (value > setpoint + 50) { return "red" } + else { return "green" } + } + + let new_thermo_gauge = { + domain: { x: [0, 1], y: [0, 1] }, + value: value, + title: { text: bioGaugeInfo[gaugeID].title }, + type: "indicator", + mode: "gauge+number", + gauge: { + axis: { range: [0, setpoint * 2] }, + bar: { color: pickColor() }, + steps: [ + { range: [0, setpoint * 2], color: "#dddddd" }, + { range: [setpoint * (1 - 0.275), setpoint * (1 - 0.225)], color: "yellow" }, + { range: [setpoint - (setpoint / 40), setpoint + (setpoint / 40)], color: "green" }, + { range: [setpoint * (1 + 0.275), setpoint * (1 + 0.225)], color: "red" } + ], + } + } + Plotly.restyle(gaugeID, new_thermo_gauge) +} + +function setGaugeValue(amount, gaugeID) { + Plotly.update(gaugeID, { value: amount > 100 ? 100 : amount, title: motorGaugeInfo[gaugeID].title }) +} + +function clearBioreactor() { + sendPOST('delete-bioreactor', 'delete-bioreactor') + + for (let x = 0; x < 2; x++) { + bio_thermo_traces[x].x = [] + bio_ph_traces[x].x = [] + bio_oxygen_traces[x].x = [] + bio_turbidity_traces[x].x = [] + bio_pro_traces[x].x = [] + + bio_thermo_traces[x].y = [] + bio_ph_traces[x].y = [] + bio_oxygen_traces[x].y = [] + bio_turbidity_traces[x].y = [] + bio_pro_traces[x].y = [] + } + Plotly.newPlot('bio-thermo-graph', bio_thermo_traces, bio_thermo_layout, { scrollZoom: true }); + Plotly.newPlot('bio-ph-graph', bio_ph_traces, pH_layout, { scrollZoom: true }); + Plotly.newPlot('bio-oxygen-graph', bio_oxygen_traces, oxygen_layout, { scrollZoom: true }); + Plotly.newPlot('bio-turbidity-graph', bio_turbidity_traces, turbidity_layout, { scrollZoom: true }); + Plotly.newPlot('bio-pro-graph', bio_pro_traces, pro_layout, { scrollZoom: true }); +} diff --git a/Website Code on SD Card/chemreactor-data.csv b/src/webpage/chemreactor-data.csv similarity index 100% rename from Website Code on SD Card/chemreactor-data.csv rename to src/webpage/chemreactor-data.csv diff --git a/src/webpage/chemreactor.html b/src/webpage/chemreactor.html new file mode 100644 index 0000000..9d6c1ac --- /dev/null +++ b/src/webpage/chemreactor.html @@ -0,0 +1,92 @@ +
+

Chemical Deconstruction Reactor

+ + +
+
+
+
+ + + +
+
+ +
+ +
+ + + +
+
+ +
+
+
+ + + +
+
+ +
+ +
+ + + +
+
+ +
+
+
+ + + +
+
+ +
+
+
+ + + +
+
+ +
+
+
+ + + +
+
+ +
+
+
+ + + +
+
+
+ + +
+
+ + + +
+
diff --git a/src/webpage/chemreactor.js b/src/webpage/chemreactor.js new file mode 100644 index 0000000..571d41b --- /dev/null +++ b/src/webpage/chemreactor.js @@ -0,0 +1,246 @@ +//----------------Chemreactor thermocouple traces-----------------------// +var chem_thermo_traces = [ + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Reactor 1" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Reactor 2" + } +] + +//----------------Thermocouple graph layout-----------------------// +var chem_thermo_layout = { + title: 'Temperature Data', + xaxis: { + title: 'Time', + autorange: true, + rangeselector: { + buttons: [ + { + count: 1, + label: '1h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 6, + label: '6h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 12, + label: '12h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 24, + label: '24h', + step: 'hour', + stepmode: 'backward' + }, + { step: 'all' } + ] + }, + type: 'date' + }, + yaxis: { + title: 'Degrees Celsius' + }, + showlegend: true +}; + +//----------------Standard Gauge Layout-----------------------// +var gaugeLayout = { width: 270, height: 220, margin: { t: 0, b: 0, l: 40, r: 40 } }; + +var motorGaugeInfo = { + "chem-pump1-gauge1": { + title: "Ammon. Hydrox. Loading" + }, + "chem-pump2-gauge1": { + title: "Liquid Transfer" + }, + "chem-pump3-gauge1": { + title: "Water Dilute" + }, + "chem-pump4-gauge1": { + title: "Reactor Wash" + } +} + +//----------------Thermocouple Gauge-----------------------// +var thermo_gauge = [{ + domain: { x: [0, 1], y: [0, 1] }, + value: 0, + title: { text: "" }, + type: "indicator", + mode: "gauge+number", + delta: { reference: 200 }, + number: { 'valueformat': '.1f' }, + gauge: { + axis: { range: [null, 400] }, + bar: { color: "blue" }, + steps: [ + { range: [0, 400], color: "#dddddd" }, + { range: [145, 155], color: "yellow" }, + { range: [195, 205], color: "green" }, + { range: [245, 255], color: "red" } + ], + } +}]; + + +//----------------Chemreactor Gauges-----------------------// +var chemGaugeInfo = { + "chem-thermo-gauge1": { + title: "Reactor 1", + value: 0, + setpoint: 0 + }, + "chem-thermo-gauge2": { + title: "Reactor 2", + value: 0, + setpoint: 0 + } +} + +function loadDataChemreactor() { + fetch('chemreactor-data.csv') + .then(response => response.text()) + .then(text => { + let row = text.split("\r\n") //split entries by row + for (let n = 1; n < row.length; n++) { + let data = row[n].split(',') + for (let x = 0; x < 2; x++) { + chem_thermo_traces[x].x.push(data[0]) + chem_thermo_traces[x].y.push(data[x + 1]) + } + } + Plotly.newPlot('chem-thermo-graph', chem_thermo_traces, chem_thermo_layout, { scrollZoom: true }); + }) +} + +function loadChemgauges(sec) { + let chem = sec.split(',') + document.getElementById("chem-pump1-gauge1-amount").value = parseInt(chem[0]) + document.getElementById("chem-pump2-gauge1-amount").value = parseInt(chem[1]) + document.getElementById("chem-pump3-gauge1-amount").value = parseInt(chem[2]) + document.getElementById("chem-pump4-gauge1-amount").value = parseInt(chem[3]) + + Plotly.restyle('chem-pump1-gauge1', { title: motorGaugeInfo["chem-pump1-gauge1"].title, value: parseInt(chem[0]) }) + Plotly.restyle('chem-pump2-gauge1', { title: motorGaugeInfo["chem-pump2-gauge1"].title, value: parseInt(chem[1]) }) + Plotly.restyle('chem-pump3-gauge1', { title: motorGaugeInfo["chem-pump3-gauge1"].title, value: parseInt(chem[2]) }) + Plotly.restyle('chem-pump4-gauge1', { title: motorGaugeInfo["chem-pump4-gauge1"].title, value: parseInt(chem[3]) }) +} + +function loadChemThermoGauges(sec) { + let chemThermo = sections[7].split(',') + for (let x = 0; x < chemThermo.length; x++) { + let c = chemThermo[x].split('|') + let id = "chem-thermo-gauge" + (x + 1) + document.getElementById(id + "-amount").value = c[0] + chemGaugeInfo[id].setpoint = c[0] + document.getElementById(id + "-kp").value = c[1] + document.getElementById(id + "-ki").value = c[2] + document.getElementById(id + "-kd").value = c[3] + updateChemGauge(id) + } +} + +function chemreactorEventListeners(source) { + + source.addEventListener('chemreactor-readings', function (e) { + var chemVals = e.data.split(',') + + chemGaugeInfo["chem-thermo-gauge1"].value = chemVals[1] + chemGaugeInfo["chem-thermo-gauge2"].value = chemVals[2] + + let ids = Object.keys(chemGaugeInfo) + ids.forEach((id) => { + chemGaugeInfo[id].value = chemVals[ids.indexOf(id) + 1] + updateChemGauge(id) + }) + //Plotly.restyle('chem-thermo-gauge1', {value: chemVals[1], title: 'Thermocouple 1', 'gauge.bar.color': "blue"}) + + if (logging) { + cUpdateCounter += 1 + if (cUpdateCounter >= 2) { + cUpdateCounter = 0 + Plotly.extendTraces('chem-thermo-graph', { + x: [[chemVals[0]], [chemVals[0]]], + y: [[chemVals[1]], [chemVals[2]]] + }, [0, 1]) + } + } + }, false) +} + +function plotChemData() { + Plotly.newPlot("chem-thermo-gauge1", thermo_gauge, gaugeLayout); + Plotly.newPlot("chem-thermo-gauge2", thermo_gauge, gaugeLayout); + + Plotly.newPlot("chem-pump1-gauge1", motor_gauge, gaugeLayout); + Plotly.newPlot("chem-pump2-gauge1", motor_gauge, gaugeLayout); + Plotly.newPlot("chem-pump3-gauge1", motor_gauge, gaugeLayout); + Plotly.newPlot("chem-pump4-gauge1", motor_gauge, gaugeLayout); + + Plotly.restyle('chem-thermo-gauge1', { title: chemGaugeInfo["chem-thermo-gauge1"].title }) + Plotly.restyle('chem-thermo-gauge2', { title: chemGaugeInfo["chem-thermo-gauge2"].title }) + + Plotly.restyle('chem-pump1-gauge1', { title: motorGaugeInfo["chem-pump1-gauge1"].title }) + Plotly.restyle('chem-pump2-gauge1', { title: motorGaugeInfo["chem-pump2-gauge1"].title }) + Plotly.restyle('chem-pump3-gauge1', { title: motorGaugeInfo["chem-pump3-gauge1"].title }) + Plotly.restyle('chem-pump4-gauge1', { title: motorGaugeInfo["chem-pump4-gauge1"].title }) +} + +function updateChemGauge(gaugeID) { + let setpoint = parseFloat(chemGaugeInfo[gaugeID].setpoint) + let value = parseFloat(chemGaugeInfo[gaugeID].value) + function pickColor() { + if (value < setpoint - 50) { return "yellow" } + else if (value > setpoint + 50) { return "red" } + else { return "green" } + } + let new_thermo_gauge = { + domain: { x: [0, 1], y: [0, 1] }, + value: value, + title: { text: chemGaugeInfo[gaugeID].title }, + type: "indicator", + mode: "gauge+number", + gauge: { + axis: { range: [0, setpoint * 2] }, + bar: { color: pickColor() }, + steps: [ + { range: [0, setpoint * 2], color: "#dddddd" }, + { range: [setpoint * (1 - 0.275), setpoint * (1 - 0.225)], color: "yellow" }, + { range: [setpoint - (setpoint / 40), setpoint + (setpoint / 40)], color: "green" }, + { range: [setpoint * (1 + 0.275), setpoint * (1 + 0.225)], color: "red" } + ], + } + }; + Plotly.restyle(gaugeID, new_thermo_gauge) +} + +function setGaugeValue(amount, gaugeID) { + Plotly.update(gaugeID, { value: amount > 100 ? 100 : amount, title: motorGaugeInfo[gaugeID].title }) +} + +function clearChemreactor() { + sendPOST('delete-chemreactor', 'delete-chemreactor') + for (let x = 0; x < 2; x++) { + chem_thermo_traces[x].x = [] + chem_thermo_traces[x].y = [] + } + Plotly.newPlot('chem-thermo-graph', chem_thermo_traces, chem_thermo_layout, { scrollZoom: true }); +} + +//--------------------------------------------------------------------------// \ No newline at end of file diff --git a/src/webpage/favicon.ico b/src/webpage/favicon.ico new file mode 100644 index 0000000..27ef384 Binary files /dev/null and b/src/webpage/favicon.ico differ diff --git a/src/webpage/index.html b/src/webpage/index.html new file mode 100644 index 0000000..557c0ff --- /dev/null +++ b/src/webpage/index.html @@ -0,0 +1,240 @@ + + + + + Dashboard + + + + + +

BREAD Configuration and Controls

+ + +
+ +
+ + +
+ + +
+ + + + + + +
+ + +
+ +
+ + +
+

Connection to ESP32:

+
+
+
+ +
+ + +
+ + +
+ + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/Website Code on SD Card/plotly.js b/src/webpage/plotly.js similarity index 100% rename from Website Code on SD Card/plotly.js rename to src/webpage/plotly.js diff --git a/Website Code on SD Card/pyrolysis-data.csv b/src/webpage/pyrolysis-data.csv similarity index 100% rename from Website Code on SD Card/pyrolysis-data.csv rename to src/webpage/pyrolysis-data.csv diff --git a/src/webpage/pyrolysis.html b/src/webpage/pyrolysis.html new file mode 100644 index 0000000..033cbb1 --- /dev/null +++ b/src/webpage/pyrolysis.html @@ -0,0 +1,214 @@ +
+

Pyrolysis Reactor

+ + +
+
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+

Thermocouple #:

+

Heater #:

+
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+

Thermocouple #:

+

Heater #:

+
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+

Thermocouple #:

+

Heater #:

+
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+

Thermocouple #:

+

Heater #:

+
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+

Thermocouple #:

+

Heater #:

+
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+

Thermocouple #:

+

Heater #:

+
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+

Thermocouple #:

+

Heater #:

+
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+

Thermocouple #:

+

Heater #:

+
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+

Thermocouple #:

+

Heater #:

+
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+

Thermocouple #:

+

Heater #:

+
+
+ + +
+
+ + + +
+
diff --git a/src/webpage/pyrolysis.js b/src/webpage/pyrolysis.js new file mode 100644 index 0000000..d7ac657 --- /dev/null +++ b/src/webpage/pyrolysis.js @@ -0,0 +1,343 @@ +//----------------Pyrolysis thermocouple traces-----------------------// +var pyrolysis_traces = [ + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Dissolution Tank" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Dissolution Heating Tape" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Valve" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Char Chamber" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Secondary Reactor" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Knockout Drum" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Condenser 0" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Condenser 1" + }, + { + y: [], + x: [], + type: 'scatter', + mode: "lines", + name: "Condenser 2" + }, +] + +//----------------Thermocouple graph layout-----------------------// +var thermocouple_layout = { + title: 'Temperature Data', + xaxis: { + title: 'Time', + autorange: true, + rangeselector: { + buttons: [ + { + count: 1, + label: '1h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 6, + label: '6h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 12, + label: '12h', + step: 'hour', + stepmode: 'backward' + }, + { + count: 24, + label: '24h', + step: 'hour', + stepmode: 'backward' + }, + { step: 'all' } + ] + }, + type: 'date' + }, + yaxis: { + title: 'Degrees Celsius' + }, + showlegend: true +}; + +//----------------Standard Gauge Layout-----------------------// +var gaugeLayout = { width: 270, height: 220, margin: { t: 0, b: 0, l: 40, r: 40 } }; + + +//----------------Thermocouple Gauge-----------------------// +var thermo_gauge = [{ + domain: { x: [0, 1], y: [0, 1] }, + value: 0, + title: { text: "" }, + type: "indicator", + mode: "gauge+number", + delta: { reference: 200 }, + number: { 'valueformat': '.1f' }, + gauge: { + axis: { range: [null, 400] }, + bar: { color: "blue" }, + steps: [ + { range: [0, 400], color: "#dddddd" }, + { range: [145, 155], color: "yellow" }, + { range: [195, 205], color: "green" }, + { range: [245, 255], color: "red" } + ], + } +}]; + + +//----------------Pyrolysis Gauges-----------------------// +var pyrolysisGaugeInfo = { + "pyrolysis-thermo-gauge1": { + title: "Dissolution Tank", + setpoint: 200, + value: 0, + }, + "pyrolysis-thermo-gauge2": { + title: "Valve", + setpoint: 200, + value: 0, + }, + "pyrolysis-thermo-gauge3": { + title: "Char Chamber", + setpoint: 200, + value: 0, + }, + "pyrolysis-thermo-gauge4": { + title: "Secondary Reactor", + setpoint: 200, + value: 0, + }, + "pyrolysis-thermo-gauge5": { + title: "Knockout Drum", + setpoint: 200, + value: 0, + }, + "pyrolysis-thermo-gauge6": { + title: "Condenser 0 Heater", + setpoint: 200, + value: 0, + }, + "pyrolysis-thermo-gauge7": { + title: "Condenser 0 Fan", + setpoint: 200, + value: 0, + }, + "pyrolysis-thermo-gauge8": { + title: "Condenser 1 Heater", + setpoint: 200, + value: 0, + }, + "pyrolysis-thermo-gauge9": { + title: "Condenser 1 Fan", + setpoint: 200, + value: 0, + }, + "pyrolysis-thermo-gauge10": { + title: "Condenser 2 Fan", + setpoint: 200, + value: 0, + }, +} + +function loadDataPyrolysis() { + fetch('pyrolysis-data.csv') + .then(response => response.text()) + .then(text => { + let row = text.split("\r\n") //split entries by row + + for (let n = 1; n < row.length; n++) { + let data = row[n].split(',') + for (let x = 0; x < pyrolysis_traces.length; x++) { + pyrolysis_traces[x].x.push(data[0]) //data[0] pushes time to the x axis + pyrolysis_traces[x].y.push(data[x + 1]) //data[x+1] to avoid pushing the date and time + } + } + + //Plot data + Plotly.newPlot('pyrolysis-thermo-graph', pyrolysis_traces, thermocouple_layout, { scrollZoom: true }); + }) +} + +function loadPyrolysisGauges(sec) { + let pyrolysis = sec.split(',') + for (let x = 0; x < pyrolysis.length; x++) { + //p[0] setpoint, p[1] Kp, p[2] Ki, p[3] Kd + let p = pyrolysis[x].split('|') + let id = "pyrolysis-thermo-gauge" + (x + 1) + document.getElementById(id + "-amount").value = p[0] + pyrolysisGaugeInfo[id].setpoint = p[0] + document.getElementById(id + "-kp").value = p[1] + document.getElementById(id + "-ki").value = p[2] + document.getElementById(id + "-kd").value = p[3] + updatePyrolysisGauge(id) + } +} + +function pyrolysisEventListeners(source) { + + source.addEventListener('pyrolysis-readings', function (e) { + var pyrolysisVals = e.data.split(',') + + pyrolysisGaugeInfo["pyrolysis-thermo-gauge1"].value = pyrolysisVals[1]; //DT + pyrolysisGaugeInfo["pyrolysis-thermo-gauge2"].value = pyrolysisVals[3]; // V + pyrolysisGaugeInfo["pyrolysis-thermo-gauge3"].value = pyrolysisVals[4]; // CC + pyrolysisGaugeInfo["pyrolysis-thermo-gauge4"].value = pyrolysisVals[5]; // SR + pyrolysisGaugeInfo["pyrolysis-thermo-gauge5"].value = pyrolysisVals[6]; // KD + pyrolysisGaugeInfo["pyrolysis-thermo-gauge6"].value = pyrolysisVals[7]; // C0H + pyrolysisGaugeInfo["pyrolysis-thermo-gauge7"].value = pyrolysisVals[7]; // C0F + pyrolysisGaugeInfo["pyrolysis-thermo-gauge8"].value = pyrolysisVals[8]; // C1H + pyrolysisGaugeInfo["pyrolysis-thermo-gauge9"].value = pyrolysisVals[8]; // C1F + pyrolysisGaugeInfo["pyrolysis-thermo-gauge10"].value = pyrolysisVals[9]; // C2F + + let ids = Object.keys(pyrolysisGaugeInfo) + ids.forEach((id) => { + updatePyrolysisGauge(id) + }) + + //plot values + if (logging) { + pUpdateCounter += 1 + //could use a for loop but I only want to call extendTraces once + if (pUpdateCounter >= 2) { + pUpdateCounter = 0 + Plotly.extendTraces('pyrolysis-thermo-graph', { + x: [[pyrolysisVals[0]], [pyrolysisVals[0]], [pyrolysisVals[0]], [pyrolysisVals[0]], [pyrolysisVals[0]], [pyrolysisVals[0]], [pyrolysisVals[0]], [pyrolysisVals[0]], [pyrolysisVals[0]]], + y: [[pyrolysisVals[1]], [pyrolysisVals[2]], [pyrolysisVals[3]], [pyrolysisVals[4]], [pyrolysisVals[5]], [pyrolysisVals[6]], [pyrolysisVals[7]], [pyrolysisVals[8]], [pyrolysisVals[9]]] + }, [0, 1, 2, 3, 4, 5, 6, 7, 8]) + } + + if (document.getElementById('pyrolysis-tracking').textContent == 'Tracking Data...') { + trackData(document.getElementById('pyrolysis-tracking-amount').value, 'pyrolysis-thermo-graph') + } + if (document.getElementById('bio-thermo-tracking').textContent == 'Tracking Data...') { + trackData(document.getElementById('bio-thermo-tracking-amount').value, 'bio-thermo-graph') + } + if (document.getElementById('bio-ph-tracking').textContent == 'Tracking Data...') { + trackData(document.getElementById('bio-ph-tracking-amount').value, 'bio-ph-graph') + } + if (document.getElementById('bio-oxygen-tracking').textContent == 'Tracking Data...') { + trackData(document.getElementById('bio-oxygen-tracking-amount').value, 'bio-oxygen-graph') + } + if (document.getElementById('bio-turbidity-tracking').textContent == 'Tracking Data...') { + trackData(document.getElementById('bio-turbidity-tracking-amount').value, 'bio-turbidity-graph') + } + if (document.getElementById('chem-thermo-tracking').textContent == 'Tracking Data...') { + trackData(document.getElementById('chem-thermo-tracking-amount').value, 'chem-thermo-graph') + } + } + }, false); +} + +function plotPyrolysisData() { + //create new gauge plots + Plotly.newPlot('pyrolysis-thermo-gauge1', thermo_gauge, gaugeLayout); + Plotly.newPlot('pyrolysis-thermo-gauge2', thermo_gauge, gaugeLayout); + Plotly.newPlot('pyrolysis-thermo-gauge3', thermo_gauge, gaugeLayout); + Plotly.newPlot('pyrolysis-thermo-gauge4', thermo_gauge, gaugeLayout); + Plotly.newPlot('pyrolysis-thermo-gauge5', thermo_gauge, gaugeLayout); + Plotly.newPlot('pyrolysis-thermo-gauge6', thermo_gauge, gaugeLayout); + Plotly.newPlot('pyrolysis-thermo-gauge7', thermo_gauge, gaugeLayout); + Plotly.newPlot('pyrolysis-thermo-gauge8', thermo_gauge, gaugeLayout); + Plotly.newPlot('pyrolysis-thermo-gauge9', thermo_gauge, gaugeLayout); + Plotly.newPlot('pyrolysis-thermo-gauge10', thermo_gauge, gaugeLayout); + + //adjust titles + Plotly.restyle('pyrolysis-thermo-gauge1', { title: pyrolysisGaugeInfo['pyrolysis-thermo-gauge1'].title }) + Plotly.restyle('pyrolysis-thermo-gauge2', { title: pyrolysisGaugeInfo['pyrolysis-thermo-gauge2'].title }) + Plotly.restyle('pyrolysis-thermo-gauge3', { title: pyrolysisGaugeInfo['pyrolysis-thermo-gauge3'].title }) + Plotly.restyle('pyrolysis-thermo-gauge4', { title: pyrolysisGaugeInfo['pyrolysis-thermo-gauge4'].title }) + Plotly.restyle('pyrolysis-thermo-gauge5', { title: pyrolysisGaugeInfo['pyrolysis-thermo-gauge5'].title }) + Plotly.restyle('pyrolysis-thermo-gauge6', { title: pyrolysisGaugeInfo['pyrolysis-thermo-gauge6'].title }) + Plotly.restyle('pyrolysis-thermo-gauge7', { title: pyrolysisGaugeInfo['pyrolysis-thermo-gauge7'].title }) + Plotly.restyle('pyrolysis-thermo-gauge8', { title: pyrolysisGaugeInfo['pyrolysis-thermo-gauge8'].title }) + Plotly.restyle('pyrolysis-thermo-gauge9', { title: pyrolysisGaugeInfo['pyrolysis-thermo-gauge9'].title }) + Plotly.restyle('pyrolysis-thermo-gauge10', { title: pyrolysisGaugeInfo['pyrolysis-thermo-gauge10'].title }) +} + + +function updatePyrolysisGauge(gaugeID) { + let setpoint = parseFloat(pyrolysisGaugeInfo[gaugeID].setpoint) + let value = parseFloat(pyrolysisGaugeInfo[gaugeID].value) + function pickColor() { + if (value < setpoint - 50) { return "yellow" } + else if (value > setpoint + 50) { return "red" } + else { return "green" } + } + let new_thermo_gauge = { + domain: { x: [0, 1], y: [0, 1] }, + value: value, + title: { text: pyrolysisGaugeInfo[gaugeID].title }, + type: "indicator", + mode: "gauge+number", + gauge: { + axis: { range: [0, setpoint * 2] }, + bar: { color: pickColor() }, + steps: [ + { range: [0, setpoint * 2], color: "#dddddd" }, + { range: [setpoint * (1 - 0.275), setpoint * (1 - 0.225)], color: "yellow" }, + { range: [setpoint - (setpoint / 40), setpoint + (setpoint / 40)], color: "green" }, + { range: [setpoint * (1 + 0.275), setpoint * (1 + 0.225)], color: "red" } + ], + } + }; + Plotly.restyle(gaugeID, new_thermo_gauge) +} + +function clearPyrolysis() { + sendPOST('delete-pyrolysis', 'delete-pyrolysis') + + for (let x = 0; x < pyrolysis_traces.length; x++) { + pyrolysis_traces[x].x = [] //data[0] pushes time to the x axis + pyrolysis_traces[x].y = [] //data[x+1] to avoid pushing the date and time + } + Plotly.newPlot('pyrolysis-thermo-graph', pyrolysis_traces, thermocouple_layout, { scrollZoom: true }); +} \ No newline at end of file diff --git a/src/webpage/styles.css b/src/webpage/styles.css new file mode 100644 index 0000000..ebb1b72 --- /dev/null +++ b/src/webpage/styles.css @@ -0,0 +1,150 @@ +/* Global Styles */ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f9; + margin: 2; + padding: 2; + text-align: center; +} + +h1 { + color: #333; + margin-top: 20px; +} + +form { + margin: 20px auto; + padding: 10px; + max-width: 300px; +} + +input, select, button { + width: 100%; + margin: 10px auto; + padding: 10px; + display: block; +} + +button { + background-color: #007bff; + color: white; + border: none; + padding: 10px 20px; + cursor: pointer; + border-radius: 5px; +} + +button:hover { + background-color: #0056b3; +} + +/* Container Color Themes */ +#pyrolysis { + background-color: #ffe5e5; /* Light Red Theme */ + padding: 20px; + border-radius: 10px; +} + +#bioreactor { + background-color: #e5ffe5; /* Light Green Theme */ + padding: 20px; + border-radius: 10px; +} + +#chemreactor { + background-color: #e5f0ff; /* Light Blue Theme */ + padding: 20px; + border-radius: 10px; +} + +/* Grouping Gauges and Forms */ +.gauge-container { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 20px; + margin: 20px 0; +} + +.gauge-container > div { + text-align: center; +} + +/* Graph Styling */ +.graph-container { + margin: 20px auto; + width: 80%; +} + +#pyrolysis-thermo-graph, +#bio-thermo-graph, +#bio-ph-graph, +#bio-oxygen-graph, +#bio-turbidity-graph, +#bio-pro-graph, +#chem-thermo-graph { + height: 350px; + width: 1000px; + margin: 0 auto; +} + +/* First row: Start Logging and E-STOP buttons */ +.start-stop { + display: flex; + justify-content: space-around; + margin: 20px auto; + width: 80%; /* Take 80% of the screen width */ +} + +.estop-button { + background-color: red; +} + +.estop-button:hover { + background-color: darkred; +} + +/* Second row: Download/Clear buttons in a single line */ +.buttons-row { + display: flex; + justify-content: space-around; + margin: 20px auto; + width: 80%; /* Take 80% of the screen width */ + gap: 10px; + flex-wrap: nowrap; /* Ensure that buttons do not wrap to new lines */ +} + +/* Third row: Clear All Data button */ +.clear-all-row { + margin: 20px auto; + width: 80%; /* Take 80% of the screen width */ +} + +.clear-all-button { + background-color: orange; + color: white; +} + +.clear-all-button:hover { + background-color: darkorange; +} + +/* Connection Status */ +.connection-status { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + margin-top: 20px; +} + +/* Media Query for Small Screens */ +@media (max-width: 768px) { + .gauge-container { + grid-template-columns: 1fr; + } + + .buttons-row, .clear-all-row { + flex-direction: column; + width: 100%; + } +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/utils/write_files.py b/utils/write_files.py new file mode 100644 index 0000000..81ab83d --- /dev/null +++ b/utils/write_files.py @@ -0,0 +1,22 @@ +import os + +def read_and_write_all_files(output_filename="output.txt"): + # Open the output file in write mode + with open(output_filename, "w") as output_file: + # List all files in the current directory + for filename in os.listdir(): + # Make sure we're dealing with a file + if os.path.isfile(filename): + # Write the file name and extension to the output file + output_file.write(f"{filename}:\n") + + # Open and read the contents of the file + with open(filename, "r", encoding="utf-8", errors="ignore") as input_file: + contents = input_file.read() + output_file.write(contents + "\n") + + # Write a separator between files + output_file.write("\n\n") + +if __name__ == "__main__": + read_and_write_all_files()