Functional.

This commit is contained in:
2024-02-24 22:44:48 +00:00
parent 03d58b549d
commit 6fffdc8743
7 changed files with 1520 additions and 82 deletions

13
data/css/materialize.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>eMAKER Printer Climate Control</title>
<title>eMAKER Reflow Controller</title>
<!--Import Google Icon Font-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<!--Let browser know website is optimized for mobile-->
@@ -9,27 +9,111 @@
<meta charset="utf-8" />
<link type="text/css" rel="stylesheet" href="css/materialize.min.css" media="screen,projection" />
<link rel="stylesheet" type="text/css" href="css/style.css" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="icon" type="image/x-icon" href="emaker.ico" />
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-dragdata@0.1.0/dist/chartjs-plugin-dragData.min.js"></script>
</head>
<body>
<div class="container">
<!-- Header row-->
<div id="header" class="row">
<div class="col 6 logo">
<img src="eMAKER.png" alt="eMAKER logo" />
</div>
<div class="col 6">
<h5>Printer Climate Control</h5>
<!-- Header row -->
<div class="row">
<div class="col 6 logo">
<img src="eMAKER.png" alt="eMAKER" />
</div>
<div class="col 6">
<h5>Oven control</h5>
</div>
</div>
<!-- Data tabs row -->
<div class="row">
<div class="card blue-grey darken-1 z-depth-3">
<div class="card-tabs">
<ul class="tabs tabs-transparent">
<li class="tab col s3"><a href="#tabTemp">Temperature control</a>
</li>
<li class="tab col s3"><a href="#tabPID">Edit PID</a>
</li>
</ul>
</div>
<div class="card-content blue-grey darken-1">
<div id="tabTemp" class="active">
<div class="row">
<div class="input-field col s3">
<input disabled id="tempinfo" type="number" value="-1" />
<label for="tempinfo">Temperature</label>
</div>
<div class="input-field col s3">
<input disabled id="targetinfo" type="number" value="-1" />
<label for="targetinfo">Target</label>
</div>
<div class="input-field col s3">
<input disabled id="powerinfo" type="number" value="-1" />
<label for="powerinfo">Power</label>
</div>
</div>
<div class="row">
<div class="input-field col s3">
<input type="number" name="Target" id="target" />
<label for="target">Target:</label>
</div>
<div class="row">
<div class="valign-wrapper">
<h5><a class="waves-effect waves-light btn" id="btnTarget" onclick="setTarget()">Set</a></h5>
</div>
</div>
</div>
</div>
<div id="tabPID">
<div class="row">
<div class="input-field col s3">
<input type="number" name="Kp" id="kp" />
<label for="kp">Kp:</label>
</div>
<div class="input-field col s3">
<input type="number" name="Ki" id="ki" />
<label for="ki">Ki:</label>
</div>
<div class="input-field col s3">
<label for="kd">Kd:</label>
<input type="number" name="Kd" id="kd" />
</div>
</div>
<div class="row">
<div class="valign-wrapper">
<h5><a class="waves-effect waves-light btn" id="btnPID" onclick="setPID()">Submit</a></h5>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Data row-->
<!-- Graph row -->
<div class="row">
<div class="card">
<div class="card-content">
<a href="/wifimanager.html">WiFi</a>
</div>
<div class="card blue-grey darken-1 z-depth-3">
<div class="card-content white-text chart-container">
<canvas id="myChart"></canvas>
</div>
<div class="card-action">
<div class="row">
<div class="input-field col s3">
<input type="number" name="Xmax" id="xmax" />
<label for="Xmax">Set overall time</label>
</div>
<div class="valign-wrapper col s3">
<h5><a class="waves-effect waves-light btn tooltipped" data-position="bottom" data-tooltip="Set time period." id="btnXmax" onclick="setXmax()">Set</a></h5>
</div>
<div class="col s6">
<div class="center-align">
<h5><a class="teal waves-effect waves-light btn tooltipped" data-position="bottom" data-tooltip="Run temperature profile." id="btnRunProfile" onclick="runProfile()">Run</a></h5>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer row -->
<div class="row">
ESP32
</div>
</div>
<script type="text/javascript" src="js/materialize.min.js"></script>

985
data/js/dragData.min.js vendored Normal file

File diff suppressed because one or more lines are too long

6
data/js/materialize.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,37 +1,106 @@
var oven_status = "";
var _run = 0;
var _elapsedTime = 0;
function getParams() {
const url = '/paraams';
fetch(url)
.then((respone)=>{
return respone.json();
})
.then((data)=>{
document.getElementById('Iim').value = data.Iim || -1;
document.getElementById('Imp').value = data.Imp || -1;
document.getElementById('Tu').value = data.Tu || -1;
document.getElementById('Tl').value = data.Tl || -1;
M.updateTextFields();
})
}
function setParams() {
const data = new URLSearchParams();
data.append('Iim',document.getElementById('Iim').value);
data.append('Imp',document.getElementById('Imp').value);
data.append('Tu',document.getElementById('Tu').value);
data.append('Tl',document.getElementById('Tl').value);
fetch('/params', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data
setInterval(function () {
const url = '/status';
fetch(url)
.then((respone)=>{
return respone.json();
})
.then((data)=>{
document.getElementById("tempinfo").value = data.temp || -1;
document.getElementById("targetinfo").value = data.target || -1;
document.getElementById("powerinfo").value = data.power || -1;
_run = data.runProfile || 0;
_elapsedTime = data.elapsedTime || 0;
M.updateTextFields();
})
if(_run == 1){
myChart.data.datasets[1].data.push({
x:_elapsedTime / 1000,
y:document.getElementById("tempinfo").value
});
myChart.update();
}
}, 4000);
function setXmax() {
myChart.data.datasets[0].data[9].x = document.getElementById('xmax').value;
myChart.update();
}
function setTarget() {
const data = new URLSearchParams();
data.append('value',document.getElementById('target').value);
fetch('/target', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data
})
}
function getPID() {
const url = '/pid';
fetch(url)
.then((respone)=>{
return respone.json();
})
.then((data)=>{
document.getElementById('kp').value = data.kp || -1;
document.getElementById('ki').value = data.ki || -1;
document.getElementById('kd').value = data.kd || -1;
M.updateTextFields();
})
}
function setPID() {
const data = new URLSearchParams();
data.append('kp', document.getElementById('kp').value);
data.append('ki', document.getElementById('ki').value);
data.append('kd', document.getElementById('kd').value);
fetch('/pid', {
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data
})
}
function runProfile() {
var btn = document.getElementById("btnRunProfile");
const data = new URLSearchParams();
if(_run == 1){
//stop profile
_run = 0;
btn.classList.remove("red");
btn.classList.add("teal");
btn.innerHTML = "Run";
data.append('run', _run);
}else{
//run profile
_run = 1;
data.append('run', _run);
data.append('profile', JSON.stringify(myChart.data.datasets[0].data));
btn.classList.remove("teal");
btn.classList.add("red");
btn.innerHTML = "Stop";
_elapsedTime = 0;
myChart.data.datasets[1].data.length = 0;
myChart.update();
}
fetch('/profile', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data
})
}
function joinNetwork() {
@@ -61,24 +130,80 @@ async function scanNetworks() {
document.getElementById("networks").innerHTML = listHTML;
}
setInterval(function () {
aja()
.url('/sensors')
.on('success', function (data) {
document.getElementById("tempinfo").value = data.temperature || -1;
document.getElementById("humidityinfo").value = data.humidity || -1;
document.getElementById("pressureinfo").value = data.pressure || -1;
//document.getElementById("targetinfo").value = data.target.toFixed(2) || -1;
_run = data.runProfile || 0;
_elapsedTime = data.elapsedTime || 0;
M.updateTextFields();
})
.go();
}, 4000);
var ctx = document.getElementById("myChart");
Chart.defaults.global.defaultFontColor = 'red';
var myChart = new Chart(ctx, {
type: 'scatter',
data: {
datasets: [{
label: 'Temperature profile',
data: [{
x: 0, y: 25
}, {
x: 90, y: 150
}, {
x: 270, y: 200
}, {
x: 300, y: 235
}, {
x: 330, y: 250
}, {
x: 360, y: 217
}, {
x: 420, y: 25
}, {
x: 480, y: 25
}, {
x: 540, y: 25
}, {
x: 600, y: 25
}],
lineTension: 0,
showLine: true
},{
label: 'History',
data: [{
x:0, y:0
}],
lineTension: 0,
showLine: true,
pointBackgroundColor: 'red'
}]
},
options: {
layout: {
padding: {
bottom: 20
}
},
responsive: true,
scales: {
yAxes: [{
ticks: {
beginAtZero:true,
min: 0,
max: 280
}
}]
},
dragData: true,
dragX: true,
onDragStart: function (event, element) {},
onDrag: function (event, datasetIndex, index, value) {},
onDragEnd: function (event, datasetIndex, index, value) {}
}
});
var ttOptions = {
enterDelay: 500,
inDuration: 300
};
document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.tooltipped');
var instances = M.Tooltip.init(elems, ttOptions);
getPID();
M.AutoInit();
getParams();
// var elems = document.querySelectorAll('.tooltipped');
// var instances = M.Tooltip.init(elems, ttOptions);
});

View File

@@ -26,6 +26,9 @@ lib_deps =
adafruit/Adafruit GFX Library@^1.11.9
adafruit/Adafruit SSD1306@^2.5.9
adafruit/MAX6675 library@^1.1.2
br3ttb/PID@^1.2.1
bblanchon/ArduinoJson@^7.0.3
dlloydev/QuickPID@^3.1.9
[env:lolin32]
board = lolin32
@@ -33,10 +36,13 @@ lib_deps =
adafruit/Adafruit GFX Library@^1.11.9
adafruit/Adafruit SSD1306@^2.5.9
https://github.com/me-no-dev/ESPAsyncWebServer.git
me-no-dev/AsyncTCP@^1.1.1
me-no-dev/AsyncTCP
ayushsharma82/AsyncElegantOTA@^2.2.7
arduino-libraries/Arduino_JSON@^0.1.0
arduino-libraries/Arduino_JSON
adafruit/MAX6675 library@^1.1.2
bblanchon/ArduinoJson@^7.0.3
; dlloydev/QuickPID@^3.1.9
br3ttb/PID@^1.2.1
build_flags =
'-D ELEGANTOTA_USE_ASYNC_WEBSERVER=1'
monitor_speed = 115200
@@ -51,3 +57,6 @@ lib_deps =
adafruit/Adafruit GFX Library@^1.11.9
adafruit/Adafruit SSD1306@^2.5.9
adafruit/MAX6675 library@^1.1.2
br3ttb/PID@^1.2.1
bblanchon/ArduinoJson@^7.0.3
dlloydev/QuickPID@^3.1.9

View File

@@ -1,4 +1,5 @@
#include <Arduino.h>
#define DEBUG
//Peripherals includes
#include <Wire.h>
@@ -6,6 +7,8 @@
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <max6675.h>
// #include <QuickPID.h>
#include <PID_v1.h>
//Web server includes
#include <AsyncElegantOTA.h>
@@ -16,14 +19,49 @@
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFSEditor.h>
#include <Arduino_JSON.h>
// #include <Arduino_JSON.h>
#include "AsyncJson.h"
#include <ArduinoJson.h>
#include <Preferences.h>
Preferences settings;
#define MAX6675_SCLK 12
#define MAX6675_CS 13
#define MAX6675_MISO 15
#define I2C_SDA 5//13
#define I2C_SCL 4//15
#define LED_PIN 5
#define LED_PIN 16
#define HEATER_PIN 14
//PID
#define T_SAMPLE_PERIOD 1000 // in milliseconds.
uint32_t temperature_sample_timer = 0;
double target = 0;
double temperature = 0;
double power = 0;
int runProfile = 0;
long int profileStartTime = 0;
long int elapsedTime = 0;
struct dataPoint{
double time;
double temp;
};
struct dataPoint sProfile[10];
double kp = 950;
double ki = 0.05;
double kd = 100;
PID outputPID(&temperature, &power, &target, kp, ki, kd, DIRECT);
int cycle = 10000; //WindowSize
unsigned long duty = 0; //WindowStartTime
// boolean on_off = false;
//DATA
// StaticJsonBuffer<JSON_OBJECT_SIZE(5)> jbStatus;
// JsonObject& objStatus = jbStatus.createObject();
//*****
// Display defines
@@ -39,7 +77,6 @@ unsigned long lastScreenRefresh = 0;
//MAX6675(int8_t SCLK, int8_t CS, int8_t MISO);
MAX6675 tc(12, 13, 15);
unsigned long lastTempTime = 0;
float temperature;
//*********************************************************
// Web server variable declarations
@@ -51,7 +88,7 @@ AsyncWebServer server(80);
// AsyncEventSource events("/events");
//Variables to save values from HTML form
JSONVar scannedSSIDs;
// JSONVar scannedSSIDs;
String ssid;
String pass;
String ip;
@@ -86,14 +123,43 @@ bool initWiFi();
String getLEDState();
// ----------------------------------------------
String getStatus(void)
{
// String json;
JsonDocument jsonStatus;
jsonStatus["temp"] = String(temperature);
jsonStatus["target"] = String(target);
jsonStatus["power"] = String(power);
jsonStatus["runProfile"] = String(runProfile);
jsonStatus["elapsedTime"] = String(elapsedTime);
String out;
serializeJson(jsonStatus,out);
return out;
// return JSON.stringify(jsonStatus);
}
String getPID(AsyncWebServerRequest * request)
{
JsonDocument jsonPID;
jsonPID["kp"] = String(kp);
jsonPID["ki"] = String(ki);
jsonPID["kd"] = String(kd);
String out;
serializeJson(jsonPID,out);
return out;
}
String getJSONTemp(){
JSONVar jsonTemp;
JsonDocument jsonTemp;
jsonTemp["t"]=String(temperature);
return JSON.stringify(jsonTemp);
String out;
serializeJson(jsonTemp,out);
return out;
}
void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(HEATER_PIN, OUTPUT);
Serial.begin(115200);
@@ -146,7 +212,7 @@ server.addHandler(new SPIFFSEditor(SPIFFS,http_username,http_password));
server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
String json = getJSONTemp();
request->send(200, "application/json", json);
json = String();
// json = String();
});
server.on("/scanNetworks", HTTP_GET, [](AsyncWebServerRequest *request){
@@ -288,32 +354,182 @@ server.addHandler(new SPIFFSEditor(SPIFFS,http_username,http_password));
ESP.restart();
}
});
server.on("/heap", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/plain", String(ESP.getFreeHeap()));
});
server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/plain", getStatus());
});
server.on("/pid", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/plain", getPID(request));
});
server.on("/pid",HTTP_POST, [](AsyncWebServerRequest *request){
if (request->hasParam("kp",true))
{
kp = request->getParam("kp",true)->value().toFloat();
#ifdef DEBUG
Serial.print("kp=");
Serial.print(kp,2);
Serial.print("; ");
#endif
}
if (request->hasParam("ki",true))
{
ki = request->getParam("ki",true)->value().toFloat();
#ifdef DEBUG
Serial.print("ki=");
Serial.print(ki,2);
Serial.print("; ");
#endif
}
if (request->hasParam("kd",true))
{
kd = request->getParam("kd",true)->value().toFloat();
#ifdef DEBUG
Serial.print("kd=");
Serial.print(kd,2);
Serial.print("; ");
#endif
}
#ifdef DEBUG
Serial.print("\n");
#endif
outputPID.SetTunings(kp, ki, kd);
server.begin();
request->send(200, "text/plain", getPID(request));
});
server.on("/target", HTTP_POST, [](AsyncWebServerRequest *request){
int params = request->params();
if(params > 0){
// AsyncWebParameter* p = request->getParam("value",true)->value().toFloat();
target = request->getParam("value",true)->value().toDouble();
Serial.printf("Target: %3.2f\n",target);
}else{
target = -273;
}
// if(request->hasParam("value")){
// target = request->getParam("value")->value().toFloat();
// Serial.printf("Target: %d",target);
// }
request->send(200, "text/plain", (String)target);
});
server.on("/profile", HTTP_POST,[](AsyncWebServerRequest *request){
// AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler("/profile", [](AsyncWebServerRequest *request, JsonVariant &json) {
int params = request->params();
if(request->hasParam("profile",true)){
JsonDocument jsonObj;
Serial.println(request->getParam("profile",true)->value());
DeserializationError error = deserializeJson(jsonObj, request->getParam("profile",true)->value());
if (error) {
// Handle the error
Serial.println(error.c_str()); request->send(400, "text/plain", "Invalid JSON payload"); return;
}else{
for(int i = 0;i<10;i++) {
//double point[2];
//int pointCount = jsonObj["profile"][i].as<JsonArray>().copyTo(point);
sProfile[i].time = jsonObj[i]["x"];
sProfile[i].temp = jsonObj[i]["y"];
#ifdef DEBUG
Serial.print("Point:");
Serial.print(i);
Serial.print(", Time:");
Serial.print(sProfile[i].time);
Serial.print(", Temp:");
Serial.println(sProfile[i].temp);
#endif
}
}
}
if(request->hasParam("run",true)){
runProfile = request->getParam("run",true)->value().toInt();
Serial.println(runProfile);
if(runProfile == 1){
profileStartTime = millis();
elapsedTime = 0;
}else{
target = 0;
}
}
request->send(200, "text/plain", "{test: \"ok\"}");
});
server.begin();
duty = millis();
outputPID.SetOutputLimits(0, cycle);
outputPID.SetSampleTime(T_SAMPLE_PERIOD);
outputPID.SetTunings(kp, ki, kd);
outputPID.SetMode(AUTOMATIC);
}
void loop() {
digitalWrite(LED_PIN,HIGH);
temperature = tc.readCelsius();
delay(500);
digitalWrite(LED_PIN,LOW);
delay(500);
if ((millis() - lastScreenRefresh) > SCREEN_REFRESH) {
long now = millis();
if(now - temperature_sample_timer >= T_SAMPLE_PERIOD)
{
temperature = tc.readCelsius();
temperature_sample_timer = now;
//deal with profile
if(runProfile == 1){
elapsedTime = millis() - profileStartTime;
if(sProfile[9].time * 1000 > elapsedTime){
//find segment
int segment = 0;
for(segment;segment<10;segment++){
if(sProfile[segment].time * 1000 < elapsedTime && sProfile[segment+1].time * 1000 >= elapsedTime){
break;
};
}
Serial.print("Segment:");
Serial.print(segment);
double t_t1 = elapsedTime - (sProfile[segment].time * 1000);
double t2_t1 = (sProfile[segment+1].time - sProfile[segment].time) * 1000;
double T2_T1 = sProfile[segment+1].temp - sProfile[segment].temp;
target = sProfile[segment].temp + (t_t1 / t2_t1) * T2_T1;
Serial.print(", Target:");
Serial.println(target);
}else{
runProfile = 0;
target = 0;
}
}
}
//set heater output
outputPID.Compute();
unsigned long rightNow = millis();
if (rightNow - duty > cycle)
{ //time to shift the Relay Window
duty += cycle;
}
if (power > rightNow - duty){
digitalWrite(HEATER_PIN, HIGH);
}else{
digitalWrite(HEATER_PIN, LOW);
}
if ((now - lastScreenRefresh) > SCREEN_REFRESH) {
Serial.printf("T:%.2f\n", temperature);
display.clearDisplay();
display.setTextSize(3);
display.setCursor(0, 5);
display.printf("T:%.1f\n", temperature);
display.setTextSize(1);
display.setCursor(0, 3);
display.printf("T:%.1f/%.1f\n", temperature, target);
display.setCursor(0,24);
display.printf("P:%.1f\n", power);
display.setCursor(0,48);
display.printf("%s\n",ssid);
display.printf("%s",ip);
display.display();
lastScreenRefresh = millis();
lastScreenRefresh = now;
}
}