Fri, 13 Jan 2023 12:00:58 +0100
Added a button to automatic recreate the yeast starter steps. Some code cleanup
/** * DetailFermenter.cpp is part of bmsapp. * * bmsapp is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * bmsapp is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ #include "DetailFermenter.h" #include "ChartFermenter.h" #include "Webcam.h" #include "../ui/ui_DetailFermenter.h" #include "global.h" #include "MainWindow.h" /* * Results are available via MySQL and websockets. Because we initialize using * MySQL we only use that for the results and up to date status. * Commands are send via websockets only. */ DetailFermenter::DetailFermenter(int id, QWidget *parent) : QDialog(parent), ui(new Ui::DetailFermenter) { QSqlQuery query; qDebug() << "DetailFermenter record:" << id; ui->setupUi(this); this->recno = id; setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint); setWindowTitle(tr("BMSapp - Details Fermenter")); ui->airThermo->setMaximum(40.0); ui->airThermo->setNominal(15.0); ui->airThermo->setCritical(20.0); ui->airThermo->setSuffix(QString("°C")); ui->beerThermo->setMaximum(40.0); ui->beerThermo->setNominal(15.0); ui->beerThermo->setCritical(20.0); ui->beerThermo->setSuffix(QString("°C")); ui->chillerThermo->setMinimum(-15.0); ui->chillerThermo->setMaximum(25.0); ui->chillerThermo->setNominal(0.0); ui->chillerThermo->setCritical(15.0); ui->chillerThermo->setSuffix(QString("°C")); ui->modeEdit->addItem("OFF"); ui->modeEdit->addItem("NONE"); ui->modeEdit->addItem("FRIDGE"); ui->modeEdit->addItem("BEER"); ui->modeEdit->addItem("PROFILE"); ui->stageEdit->addItem("PRIMARY"); ui->stageEdit->addItem("SECONDARY"); ui->stageEdit->addItem("TERTIARY"); ui->stageEdit->addItem("CARBONATION"); ui->codePick->addItem("Erase beer"); query.exec("SELECT code,name FROM products WHERE stage='1' OR stage='2' OR stage='3' OR stage='4' OR stage='5' OR stage='6' OR stage='7' ORDER BY code"); while (query.next()) { ui->codePick->addItem(query.value("code").toString()+" - "+query.value("name").toString()); } ui->profilePick->addItem(tr("Erase profile")); query.exec("SELECT name FROM profile_fermentation ORDER BY name"); while (query.next()) { ui->profilePick->addItem(query.value("name").toString()); } connect(ui->loEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &DetailFermenter::lo_changed); connect(ui->hiEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &DetailFermenter::hi_changed); connect(ui->heatSwitch, SIGNAL(clicked()), this, SLOT(heat_switched())); connect(ui->coolSwitch, SIGNAL(clicked()), this, SLOT(cool_switched())); connect(ui->fanSwitch, SIGNAL(clicked()), this, SLOT(fan_switched())); connect(ui->modeButton1, SIGNAL(clicked()), this, SLOT(button1_pressed())); connect(ui->modeButton2, SIGNAL(clicked()), this, SLOT(button2_pressed())); connect(ui->modeEdit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DetailFermenter::mode_changed); connect(ui->stageEdit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DetailFermenter::stage_changed); connect(ui->codePick, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DetailFermenter::code_changed); connect(ui->profilePick, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DetailFermenter::profile_changed); connect(ui->logButton, SIGNAL(clicked()), this, SLOT(on_ChartButton_clicked())); connect(ui->camButton, SIGNAL(clicked()), this, SLOT(on_WebcamButton_clicked())); connect(parent, SIGNAL(updateFermenter(QString)), this, SLOT(refreshFermenter(QString))); emit refreshTable(); } void DetailFermenter::refreshTable() { QSqlQuery query; qDebug() << "refreshTable fermenter rec:" << this->recno; QIcon icon_done, icon_start, icon_abort, icon_pause, icon_cont; icon_done.addFile(QString::fromUtf8(":icons/silk/accept.png"), QSize(), QIcon::Normal, QIcon::Off); icon_start.addFile(QString::fromUtf8(":icons/silk/resultset_next.png"), QSize(), QIcon::Normal, QIcon::Off); icon_abort.addFile(QString::fromUtf8(":icons/silk/bomb.png"), QSize(), QIcon::Normal, QIcon::Off); icon_pause.addFile(QString::fromUtf8(":icons/silk/cup.png"), QSize(), QIcon::Normal, QIcon::Off); icon_cont.addFile(QString::fromUtf8(":icons/silk/cup_go.png"), QSize(), QIcon::Normal, QIcon::Off); query.prepare("SELECT * FROM mon_fermenters WHERE record = :recno"); query.bindValue(":recno", this->recno); query.exec(); if (query.next()) { const QSignalBlocker blocker1(ui->codePick); const QSignalBlocker blocker2(ui->modeEdit); const QSignalBlocker blocker3(ui->loEdit); const QSignalBlocker blocker4(ui->hiEdit); const QSignalBlocker blocker5(ui->stageEdit); _node = query.value("node").toString(); _alias = query.value("alias").toString(); _uuid = query.value("uuid").toString(); _beercode = query.value("beercode").toString(); _beername = query.value("beername").toString(); _webcam_url = query.value("webcam_url").toString(); _webcam_light = query.value("webcam_light").toInt(); ui->uuidEdit->setText(_uuid); ui->systemEdit->setText(_node+"/"+_alias); ui->codePick->setItemText(0, _alias.toUpper()+" - "+_alias); if (query.value("online").toInt()) { ui->statusEdit->setText(tr("Online")); ui->codeEdit->show(); ui->codeEdit->setText(_beercode+" - "+_beername); ui->airThermo->setNominal(query.value("yeast_lo").toDouble()); ui->airThermo->setCritical(query.value("yeast_hi").toDouble()); ui->beerThermo->setNominal(query.value("yeast_lo").toDouble()); ui->beerThermo->setCritical(query.value("yeast_hi").toDouble()); ui->modeEdit->show(); if (query.value("mode").toString() == "OFF") ui->modeEdit->setCurrentIndex(0); else if (query.value("mode").toString() == "NONE") ui->modeEdit->setCurrentIndex(1); else if (query.value("mode").toString() == "FRIDGE") ui->modeEdit->setCurrentIndex(2); else if (query.value("mode").toString() == "BEER") ui->modeEdit->setCurrentIndex(3); else if (query.value("mode").toString() == "PROFILE") ui->modeEdit->setCurrentIndex(4); ui->stageEdit->show(); if (query.value("stage").toString() == "PRIMARY") ui->stageEdit->setCurrentIndex(0); else if (query.value("stage").toString() == "SECONDARY") ui->stageEdit->setCurrentIndex(1); else if (query.value("stage").toString() == "TERTIARY") ui->stageEdit->setCurrentIndex(2); else if (query.value("stage").toString() == "CARBONATION") ui->stageEdit->setCurrentIndex(3); if (query.value("door_address").toString() != "") { ui->doorLED->show(); ui->doorLabel->show(); ui->doorLED->setChecked((query.value("door_state").toInt() != 0) ? true:false); } else { ui->doorLED->hide(); ui->doorLabel->hide(); } if (query.value("light_address").toString() != "") { ui->lightLED->show(); ui->lightLabel->show(); ui->lightLED->setChecked((query.value("light_state").toInt() != 0) ? true:false); } else { ui->lightLED->hide(); ui->lightLabel->hide(); } if (query.value("mode").toString() == "OFF") { ui->powerLED->setChecked(false); ui->codePick->show(); } else { ui->powerLED->setChecked(true); ui->codePick->hide(); } ui->alarmLED->setChecked((query.value("alarm").toInt() != 0) ? true:false); ui->tempsetBox->show(); if ((query.value("mode").toString() == "FRIDGE") || (query.value("mode").toString() == "BEER")) { ui->loEdit->setReadOnly(false); ui->loEdit->setButtonSymbols(QAbstractSpinBox::UpDownArrows); ui->hiEdit->setReadOnly(false); ui->hiEdit->setButtonSymbols(QAbstractSpinBox::UpDownArrows); } else { ui->loEdit->setReadOnly(true); ui->loEdit->setButtonSymbols(QAbstractSpinBox::NoButtons); ui->hiEdit->setReadOnly(true); ui->hiEdit->setButtonSymbols(QAbstractSpinBox::NoButtons); } lo_set = query.value("setpoint_low").toDouble(); hi_set = query.value("setpoint_high").toDouble(); ui->loEdit->setValue(lo_set); ui->hiEdit->setValue(hi_set); ui->switchBox->show(); ui->heatLED->setChecked((query.value("heater_state").toInt() != 0) ? true:false); ui->coolLED->setChecked((query.value("cooler_state").toInt() != 0) ? true:false); ui->fanLED->setChecked((query.value("fan_state").toInt() != 0) ? true:false); if (query.value("mode").toString() == "NONE") { ui->heatSwitch->show(); ui->coolSwitch->show(); ui->fanSwitch->show(); heat_state = (query.value("heater_state").toInt()) ? true:false; cool_state = (query.value("cooler_state").toInt()) ? true:false; fan_state = (query.value("fan_state").toInt()) ? true:false; ui->heatSwitch->setChecked(heat_state); ui->coolSwitch->setChecked(cool_state); ui->fanSwitch->setChecked(fan_state); // Copy state values to variables and set the switches. } else { ui->heatSwitch->hide(); ui->coolSwitch->hide(); ui->fanSwitch->hide(); heat_state = cool_state = fan_state = false; } if (query.value("profile_name").toString() == "") { qobject_cast<QStandardItemModel*>(ui->modeEdit->model())->item(4)->setEnabled(false); } else { qobject_cast<QStandardItemModel*>(ui->modeEdit->model())->item(4)->setEnabled(true); } ui->profileEdit->show(); ui->profileEdit->setText(query.value("profile_name").toString()); if (query.value("mode").toString() == "PROFILE") { _profile = query.value("profile_state").toString(); // So we know the profile state anywhere. qDebug() << "profile state" << query.value("profile_state").toString(); if (query.value("profile_state").toString() == "OFF") { ui->profilePick->show(); ui->profileShow->hide(); // Both on the same location. ui->modeButton1->show(); ui->modeButton1->setText(tr("Start")); ui->modeButton1->setIcon(icon_start); ui->modeButton2->hide(); } else if (query.value("profile_state").toString() == "RUN") { ui->profilePick->hide(); ui->profileShow->show(); ui->profileShow->setText(QString(tr("Profile active %1% done")).arg(query.value("profile_percent").toDouble())); ui->modeButton1->show(); ui->modeButton1->setText(tr("Abort")); ui->modeButton1->setIcon(icon_abort); ui->modeButton2->show(); ui->modeButton2->setText(tr("Pause")); ui->modeButton2->setIcon(icon_pause); } else if (query.value("profile_state").toString() == "PAUSE") { ui->profilePick->hide(); ui->profileShow->show(); ui->profileShow->setText(QString(tr("Profile paused %1% done")).arg(query.value("profile_percent").toDouble())); ui->modeButton1->show(); ui->modeButton1->setText(tr("Abort")); ui->modeButton1->setIcon(icon_abort); ui->modeButton2->show(); ui->modeButton2->setText(tr("Continue")); ui->modeButton2->setIcon(icon_cont); } else if (query.value("profile_state").toString() == "DONE") { ui->profilePick->hide(); ui->profileShow->show(); ui->profileShow->setText(QString(tr("Profile ready"))); ui->modeButton1->show(); ui->modeButton1->setText(tr("Profile Ok")); ui->modeButton1->setIcon(icon_done); ui->modeButton2->hide(); } } else { ui->profilePick->show(); ui->profileShow->hide(); // Both on the same location. ui->modeButton1->hide(); ui->modeButton2->hide(); _profile = QString(""); } if (_webcam_url == "") { ui->camButton->hide(); } else { ui->camButton->show(); } ui->thermoBox->show(); if (query.value("air_state").toString() == "OK") { ui->airThermo->setValue(query.value("air_temperature").toDouble()); } if (query.value("beer_state").toString() == "OK") { ui->beerThermo->setValue(query.value("beer_temperature").toDouble()); } if (query.value("chiller_state").toString() == "OK") { ui->chillerThermo->setValue(query.value("chiller_temperature").toDouble()); } } else { /* Offline */ ui->statusEdit->setText(tr("Offline")); ui->powerLED->setChecked(false); ui->alarmLED->setChecked(true); ui->codeEdit->hide(); ui->codePick->hide(); ui->modeEdit->hide(); ui->profileEdit->hide(); ui->profileShow->hide(); ui->profilePick->hide(); ui->modeButton1->hide(); ui->modeButton2->hide(); ui->stageEdit->hide(); ui->thermoBox->hide(); ui->tempsetBox->hide(); ui->switchBox->hide(); ui->camButton->hide(); ui->logButton->hide(); } } } DetailFermenter::~DetailFermenter() { qDebug() << "DetailFermenter done"; delete ui; emit entry_changed(); } /* * Receive signals destined for all fermenters. * Check if the signal is for us. */ void DetailFermenter::refreshFermenter(QString data) { if (_node+"/"+_alias == data) { emit refreshTable(); } } void DetailFermenter::on_ChartButton_clicked() { ChartFermenter dialog(_beercode, _beername, this); } void DetailFermenter::on_WebcamButton_clicked() { QString msg = QString("{\"device\":\"fermenters\",\"node\":\""+_node+"\",\"unit\":\""+_alias+"\",\"light_state\":100}"); webSocket->sendTextMessage(msg); Webcam dialog(_webcam_url, this); } void DetailFermenter::on_quitButton_clicked() { this->close(); this->setResult(1); } void DetailFermenter::lo_changed(double val) { double hi = ui->hiEdit->value(); if (val >= hi) hi = val + 0.1; QString msg = QString("{\"device\":\"fermenters\",\"node\":\""+_node+"\",\"unit\":\""+_alias+"\",\"setpoint_low\":%1,\"setpoint_high\":%2}") .arg(val, 2, 'f', 1, '0').arg(hi, 2, 'f', 1, '0'); qDebug() << "lo_changed" << val << msg; webSocket->sendTextMessage(msg); } void DetailFermenter::hi_changed(double val) { double lo = ui->loEdit->value(); if (val <= lo) lo = val - 0.1; QString msg = QString("{\"device\":\"fermenters\",\"node\":\""+_node+"\",\"unit\":\""+_alias+"\",\"setpoint_low\":%1,\"setpoint_high\":%2}") .arg(lo, 2, 'f', 1, '0').arg(val, 2, 'f', 1, '0'); qDebug() << "hi_changed" << val << msg; webSocket->sendTextMessage(msg); } void DetailFermenter::send_switches() { QString msg=QString("{\"device\":\"fermenters\",\"node\":\""+_node+"\",\"unit\":\""+_alias+"\",\"heater_state\":%1,\"cooler_state\":%2,\"fan_state\":%3}") .arg((heat_state)?100:0).arg((cool_state)?100:0).arg((fan_state)?100:0); //qDebug() << msg; webSocket->sendTextMessage(msg); } void DetailFermenter::heat_switched() { heat_state = !heat_state; cool_state = false; send_switches(); } void DetailFermenter::cool_switched() { cool_state = !cool_state; heat_state = false; send_switches(); } void DetailFermenter::fan_switched() { fan_state = !fan_state; send_switches(); } void DetailFermenter::button1_pressed() { qDebug() << "button1" << _profile; if (_profile == "OFF") { QString msg = QString("{\"device\":\"fermenters\",\"node\":\"" + _node + "\",\"unit\":\"" + _alias + "\",\"profile\":{\"command\":\"start\"}}"); qDebug() << msg; webSocket->sendTextMessage(msg); return; } if ((_profile == "RUN") || (_profile == "PAUSE")) { int rc = QMessageBox::warning(this, tr("Profile running"), tr("Profile is active, really abort?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (rc == QMessageBox::Yes) { QString msg = QString("{\"device\":\"fermenters\",\"node\":\"" + _node + "\",\"unit\":\"" + _alias + "\",\"profile\":{\"command\":\"abort\"}}"); qDebug() << msg; webSocket->sendTextMessage(msg); } return; } if (_profile == "DONE") { QString msg = QString("{\"device\":\"fermenters\",\"node\":\"" + _node + "\",\"unit\":\"" + _alias + "\",\"profile\":{\"command\":\"done\"}}"); qDebug() << msg; webSocket->sendTextMessage(msg); } } void DetailFermenter::button2_pressed() { qDebug() << "button2" << _profile; if ((_profile == "RUN") || (_profile == "PAUSE")) { QString msg = QString("{\"device\":\"fermenters\",\"node\":\"" + _node + "\",\"unit\":\"" + _alias + "\",\"profile\":{\"command\":\"pause\"}}"); qDebug() << msg; webSocket->sendTextMessage(msg); } } void DetailFermenter::mode_changed(int val) { QStringList mode ({ "OFF", "NONE", "FRIDGE", "BEER", "PROFILE" }); QString msg = QString("{\"device\":\"fermenters\",\"node\":\"" + _node + "\",\"unit\":\"" + _alias + "\",\"mode\":\"" + mode[val] + "\"}"); //qDebug() << "mode_changed" << val << msg; webSocket->sendTextMessage(msg); } void DetailFermenter::stage_changed(int val) { QStringList stage ({ "PRIMARY", "SECONDARY", "TERTIARY", "CARBONATION" }); QString msg = QString("{\"device\":\"fermenters\",\"node\":\"" + _node + "\",\"unit\":\"" + _alias + "\",\"stage\":\"" + stage[val] + "\"}"); //qDebug() << "stage_changed" << val << msg; webSocket->sendTextMessage(msg); } void DetailFermenter::code_changed(int val) { QJsonParseError parseError; QSqlQuery query; QString msg = QString("{\"device\":\"fermenters\",\"node\":\"" + _node + "\",\"unit\":\"" + _alias + "\","); if (val == 0) { msg.append(QString("\"beeruuid\":\"") + _uuid + "\","); msg.append(QString("\"beercode\":\"") + _alias.toUpper() + "\","); msg.append(QString("\"beername\":\"") + _alias + "\","); msg.append(QString("\"yeast_lo\":12.0,")); msg.append(QString("\"yeast_hi\":24.0}")); } else { query.exec("SELECT code,name,uuid,stage,json_yeasts FROM products WHERE stage='1' OR stage='2' OR stage='3' OR stage='4' OR stage='5' OR stage='6' OR stage='7' ORDER BY code"); for (int i = 0; i < val; i++) { query.next(); } double yl = 0; double yh = 40; const auto& y_json = query.value("json_yeasts").toString(); if (! y_json.trimmed().isEmpty()) { const auto& formattedJson = QString("%1").arg(y_json); QJsonDocument yeasts = QJsonDocument::fromJson(formattedJson.toUtf8(), &parseError); if (parseError.error != QJsonParseError::NoError) { qWarning() << "Parse error: " << parseError.errorString() << "at" << parseError.offset ; } else if (yeasts.isArray()) { for (int i = 0; i < yeasts.array().size(); i++) { QJsonObject obj = yeasts.array().at(i).toObject(); if (obj["y_use"].toInt() == 0) { // Primary yeast if (obj["y_min_temperature"].toDouble() > yl) yl = obj["y_min_temperature"].toDouble(); if (obj["y_max_temperature"].toDouble() < yh) yh = obj["y_max_temperature"].toDouble(); } } } } msg.append(QString("\"beeruuid\":\"") + query.value("uuid").toString() + "\","); msg.append(QString("\"beercode\":\"") + query.value("code").toString() + "\","); msg.append(QString("\"beername\":\"") + query.value("name").toString() + "\","); msg.append(QString("\"yeast_lo\":%1,").arg(yl)); msg.append(QString("\"yeast_hi\":%1}").arg(yh)); } qDebug() << "code_changed" << val << msg; webSocket->sendTextMessage(msg); } void DetailFermenter::profile_changed(int val) { QString payload; QSqlQuery query; QJsonParseError parseError; if (val == 0) { payload = QString("\"profile\":null"); } else { query.exec("SELECT * FROM profile_fermentation ORDER BY name"); for (int i = 0; i < val; i++) { query.next(); } payload = QString("\"profile\":{\"uuid\":\""+query.value("uuid").toString()+"\",\"name\":\""+query.value("name").toString()); payload.append(QString("\",\"inittemp\":{\"low\":") + QString("%1").arg(query.value("inittemp_lo").toDouble(), 2, 'f', 1, '0' )); payload.append(QString(",\"high\":") + QString("%1").arg(query.value("inittemp_hi").toDouble(), 2, 'f', 1, '0' ) + QString("},")); payload.append(QString("\"fridgemode\":") + QString("%1").arg(query.value("fridgemode").toInt()) ); payload.append(QString(",\"steps\":[")); const auto& s_json = query.value("steps").toString(); if (! s_json.trimmed().isEmpty()) { const auto& formattedJson = QString("%1").arg(s_json); QJsonDocument steps = QJsonDocument::fromJson(formattedJson.toUtf8(), &parseError); if (parseError.error != QJsonParseError::NoError) { qWarning() << "Parse error: " << parseError.errorString() << "at" << parseError.offset ; } else if (steps.isArray()) { for (int i = 0; i < steps.array().size(); i++) { QJsonObject obj = steps.array().at(i).toObject(); if (i > 0) payload.append(QString(",")); if (obj["steptime"].isString()) payload.append(QString("{\"steptime\":%1").arg(obj["steptime"].toString().toDouble())); else payload.append(QString("{\"steptime\":%1").arg(obj["steptime"].toDouble())); if (obj["resttime"].isString()) payload.append(QString(",\"resttime\":%1").arg(obj["resttime"].toString().toDouble())); else payload.append(QString(",\"resttime\":%1").arg(obj["resttime"].toDouble())); if (obj["target_lo"].isString()) payload.append(QString(",\"target_lo\":%1").arg(obj["target_lo"].toString().toDouble())); else payload.append(QString(",\"target_lo\":%1").arg(obj["target_lo"].toDouble())); if (obj["target_hi"].isString()) payload.append(QString(",\"target_hi\":%1").arg(obj["target_hi"].toString().toDouble())); else payload.append(QString(",\"target_hi\":%1").arg(obj["target_hi"].toDouble())); if (obj["fridgemode"].isString()) payload.append(QString(",\"fridgemode\":%1").arg(obj["fridgemode"].toString().toInt())); else payload.append(QString(",\"fridgemode\":%1").arg(obj["fridgemode"].toInt())); payload.append(QString(",\"name\":\"") + obj["name"].toString() + QString("\"}")); } } } payload.append(QString("]}")); // qDebug() << query.value("steps").toString(); } QString msg = QString("{\"device\":\"fermenters\",\"node\":\"" + _node + "\",\"unit\":\"" + _alias + "\",%1}").arg(payload); qDebug() << "profile_changed" << val << msg; webSocket->sendTextMessage(msg); }