123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579 |
- #!/usr/bin/env python3
- #
- # Moisture control - Graphical user interface
- #
- # Copyright (c) 2013 Michael Buesch <m@bues.ch>
- #
- # This program 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 2 of the License, or
- # (at your option) any later version.
- #
- # This program 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, write to the Free Software Foundation, Inc.,
- # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- #
- import sys
- if sys.version_info[0] < 3:
- print("The Python interpreter is too old.")
- print("PLEASE INSTALL Python 3.x")
- raw_input("Press enter to exit.")
- sys.exit(1)
- import math
- from pymoistcontrol import *
- # Serial communication parameters
- SERIAL_BAUDRATE = 19200
- SERIAL_PAYLOAD_LEN = 12
- class MainWidget(QWidget):
- """The central widget inside of the main window."""
- serialConnected = Signal()
- serialDisconnected = Signal()
- fetchCycle = [
- "global_state",
- "log",
- "rtc",
- "pot_state",
- "pot_rem_state",
- ]
- def __init__(self, parent):
- """Class constructor."""
- QWidget.__init__(self, parent)
- self.setLayout(QGridLayout(self))
- self.globConfWidget = GlobalConfigWidget(self)
- self.layout().addWidget(self.globConfWidget, 0, 0)
- self.tabWidget = QTabWidget(self)
- self.layout().addWidget(self.tabWidget, 0, 1)
- self.potWidgets = []
- for i in range(MAX_NR_FLOWERPOTS):
- potWidget = PotWidget(i, self)
- self.potWidgets.append(potWidget)
- self.tabWidget.addTab(potWidget,
- "Pot %d" % (i + 1))
- self.setUiEnabled(False)
- self.logWidget = LogWidget(self)
- self.layout().addWidget(self.logWidget, 1, 0, 1, 2)
- self.connected = False
- self.pollTimer = QTimer(self)
- self.pollTimer.setSingleShot(True)
- self.globConfWidget.configChanged.connect(self.__handleGlobConfigChange)
- self.globConfWidget.rtcEdited.connect(self.__handleRtcEdit)
- for pot in self.potWidgets:
- pot.configChanged.connect(self.__handlePotConfigChange)
- pot.manModeChanged.connect(self.__handleManModeChange)
- pot.watchdogRestartReq.connect(self.__handleWatchdogRestartReq)
- self.pollTimer.timeout.connect(self.__pollTimerEvent)
- def __handleCommError(self, exception):
- QMessageBox.critical(self,
- "Serial communication failed",
- "Serial communication failed:\n"
- "%s" % str(exception))
- self.disconnectDev()
- def __makeMsg_GlobalConfig(self):
- msg = MsgContrConf(flags = 0,
- sensor_lowest_value = self.globConfWidget.lowestRawSensorVal(),
- sensor_highest_value = self.globConfWidget.highestRawSensorVal())
- if self.globConfWidget.globalEnableActive():
- msg.flags |= msg.CONTR_FLG_ENABLE
- return msg
- def __makeMsg_RTC(self):
- dateTime = self.globConfWidget.getRtcDateTime()
- msg = MsgRtc(second = dateTime.time().second(),
- minute = dateTime.time().minute(),
- hour = dateTime.time().hour(),
- day = dateTime.date().day() - 1,
- month = dateTime.date().month() - 1,
- year = clamp(dateTime.date().year(), 2000, 2063) - 2000,
- day_of_week = dateTime.date().dayOfWeek() - 1)
- return msg
- def __makeMsg_PotConfig(self, potNumber):
- pot = self.potWidgets[potNumber]
- self.globConfWidget.handlePotEnableChange(potNumber,
- pot.isEnabled())
- msg = MsgContrPotConf(pot_number = potNumber,
- flags = 0,
- min_threshold = pot.getMinThreshold(),
- max_threshold = pot.getMaxThreshold(),
- start_time = pot.getStartTime(),
- end_time = pot.getEndTime(),
- dow_on_mask = pot.getDowEnableMask())
- if pot.isEnabled():
- msg.flags |= msg.POT_FLG_ENABLED
- if pot.loggingEnabled():
- msg.flags |= msg.POT_FLG_LOG
- if pot.verboseLoggingEnabled():
- msg.flags |= msg.POT_FLG_LOGVERBOSE
- return msg
- def __handleGlobConfigChange(self):
- try:
- self.serial.send(self.__makeMsg_GlobalConfig())
- except SerialError as e:
- self.__handleCommError(e)
- return
- def __handleRtcEdit(self):
- try:
- self.serial.send(self.__makeMsg_RTC())
- except SerialError as e:
- self.__handleCommError(e)
- return
- def __handlePotConfigChange(self, potNumber):
- try:
- self.serial.send(self.__makeMsg_PotConfig(potNumber))
- except SerialError as e:
- self.__handleCommError(e)
- return
- def __handleManModeChange(self):
- try:
- msg = MsgManMode()
- for i, pot in enumerate(self.potWidgets):
- if pot.forceStopWateringActive():
- msg.force_stop_watering_mask |= 1 << i
- if pot.forceOpenValveActive():
- msg.valve_manual_mask |= 1 << i
- msg.valve_manual_state |= 1 << i
- if pot.forceStartMeasActive():
- msg.force_start_measurement_mask |= 1 << i
- self.serial.send(msg)
- except SerialError as e:
- self.__handleCommError(e)
- return
- def __sendFreeze(self, freeze=True):
- msg = MsgManMode()
- msg.flags |= MsgManMode.MANFLG_FREEZE_CHANGE
- if freeze:
- msg.flags |= MsgManMode.MANFLG_FREEZE_ENABLE
- self.serial.send(msg)
- def __sendClearNotify(self):
- msg = MsgManMode()
- msg.flags |= MsgManMode.MANFLG_NOTIFY_CHANGE
- self.serial.send(msg)
- def __handleWatchdogRestartReq(self, potNumber):
- try:
- self.__stopPolling()
- self.__sendFreeze(True)
- # Remove WD-trigger flag from remanent state
- msg = self.__convertRxMsg(self.serial.sendSync(MsgContrPotRemStateFetch(potNumber)),
- fatalOnNoMsg = True)
- if not self.__checkRxMsg(msg, Message.MSG_CONTR_POT_REM_STATE):
- return
- msg.fc = 0
- msg.flags &= ~msg.POT_REMFLG_WDTRIGGER
- self.serial.send(msg)
- # Disable the notification LED
- self.__sendClearNotify()
- self.__sendFreeze(False)
- self.__startPolling()
- except SerialError as e:
- self.__handleCommError(e)
- return
- def setUiEnabled(self, enabled = True):
- self.globConfWidget.setEnabled(enabled)
- for i in range(self.tabWidget.count()):
- self.tabWidget.widget(i).setEnabled(enabled)
- def __fetchCycleNext(self):
- action = self.fetchCycle[self.fetchCycleNumber]
- try:
- if action == "global_state":
- msg = MsgContrStateFetch()
- elif action == "log":
- msg = MsgLogFetch()
- elif action == "rtc":
- msg = MsgRtcFetch()
- elif action == "pot_state":
- msg = MsgContrPotStateFetch(self.potCycleNumber)
- elif action == "pot_rem_state":
- msg = MsgContrPotRemStateFetch(self.potCycleNumber)
- else:
- assert(0)
- self.serial.send(msg)
- except SerialError as e:
- self.__handleCommError(e)
- return
- self.pollTimer.start(math.ceil(msg.calcFrameDuration() * 1000) + 10)
- def __startPolling(self):
- self.fetchCycleNumber = 0
- self.potCycleNumber = 0
- self.logCount = 0
- self.__pollRetries = 0
- self.__fetchCycleNext()
- def __stopPolling(self):
- self.pollTimer.stop()
- # Drain pending RX-messages
- time.sleep(0.1)
- while self.serial.poll():
- pass
- def __checkRxMsg(self, msg, expectedType, ignoreErrorCode=False):
- ok = True
- if not ignoreErrorCode:
- if msg.getErrorCode() != Message.COMM_ERR_OK:
- QMessageBox.critical(self,
- "Received message: Error",
- "Received an error: %d" % \
- msg.getErrorCode())
- ok = False
- if ok and\
- msg.getType() is not None and\
- msg.getType() != expectedType:
- QMessageBox.critical(self,
- "Received message: Unexpected type",
- "Received a message with an unexpected "
- "type. (got %d, expected %d)" % \
- (msg.getType(), expectedType))
- ok = False
- if not ok:
- self.disconnectDev()
- return ok
- def __convertRxMsg(self, msg, fatalOnNoMsg=False):
- msg = Message.fromRawMessage(msg)
- if not msg:
- if fatalOnNoMsg:
- QMessageBox.critical(self, "Communication failed",
- "Serial communication timeout")
- self.disconnectDev()
- return None
- error = msg.getErrorCode()
- if error not in (Message.COMM_ERR_OK, Message.COMM_ERR_FAIL):
- QMessageBox.critical(self, "Communication failed",
- "Serial communication error: %d" % error)
- self.disconnectDev()
- return None
- return msg
- def __pollTimerEvent(self):
- try:
- msg = self.__convertRxMsg(self.serial.poll())
- if not msg:
- self.__pollRetries += 1
- if self.__pollRetries >= 200:
- QMessageBox.critical(self,
- "Communication failed",
- "Communication failed. "
- "Retry timeout.")
- self.disconnectDev()
- return
- self.pollTimer.start(5) # Retry
- return
- self.__pollRetries = 0
- except SerialError as e:
- self.__handleCommError(e)
- return
- error = msg.getErrorCode()
- advanceFetchCycle = True
- action = self.fetchCycle[self.fetchCycleNumber]
- if action == "global_state":
- if self.__checkRxMsg(msg, Message.MSG_CONTR_STATE):
- self.globConfWidget.handleGlobalStateMessage(msg)
- elif action == "log":
- if self.__checkRxMsg(msg, Message.MSG_LOG,
- ignoreErrorCode = True):
- if error == Message.COMM_ERR_OK:
- self.logWidget.handleLogMessage(msg)
- self.logCount += 1
- if self.logCount < 8:
- advanceFetchCycle = False
- if advanceFetchCycle:
- self.logCount = 0
- elif action == "rtc":
- if self.__checkRxMsg(msg, Message.MSG_RTC):
- self.globConfWidget.handleRtcMessage(msg)
- elif action in ("pot_state", "pot_rem_state"):
- expected = { "pot_state" : Message.MSG_CONTR_POT_STATE,
- "pot_rem_state" : Message.MSG_CONTR_POT_REM_STATE,
- }[action]
- if self.__checkRxMsg(msg, expected):
- if msg.pot_number == self.potCycleNumber:
- if action == "pot_state":
- self.globConfWidget.handlePotStateMessage(msg)
- self.potWidgets[self.potCycleNumber].handlePotStateMessage(msg)
- elif action == "pot_rem_state":
- self.globConfWidget.handlePotRemStateMessage(msg)
- self.potWidgets[self.potCycleNumber].handlePotRemStateMessage(msg)
- else:
- assert(0)
- else:
- QMessageBox.critical(self,
- "%s message mismatch" % action,
- "Received pot state message for the"
- "wrong pot (was %d, expected %d)." % \
- (msg.pot_number, self.potCycleNumber))
- self.potCycleNumber += 1
- if self.potCycleNumber < MAX_NR_FLOWERPOTS:
- advanceFetchCycle = False
- else:
- self.potCycleNumber = 0
- else:
- assert(0)
- if advanceFetchCycle:
- self.fetchCycleNumber += 1
- if self.fetchCycleNumber >= len(self.fetchCycle):
- self.fetchCycleNumber = 0
- self.__fetchCycleNext()
- else:
- self.__fetchCycleNext()
- def __initializeDev(self):
- try:
- # Get the global configuration from the device
- msg = self.__convertRxMsg(self.serial.sendSync(MsgContrConfFetch()),
- fatalOnNoMsg = True)
- if not self.__checkRxMsg(msg, Message.MSG_CONTR_CONF):
- return
- self.globConfWidget.handleGlobalConfMessage(msg)
- # Get the pot configurations from the device
- for i in range(MAX_NR_FLOWERPOTS):
- msg = self.__convertRxMsg(self.serial.sendSync(MsgContrPotConfFetch(i)),
- fatalOnNoMsg = True)
- if not self.__checkRxMsg(msg, Message.MSG_CONTR_POT_CONF):
- return
- self.potWidgets[i].handlePotConfMessage(msg)
- self.globConfWidget.handlePotConfMessage(msg)
- # Reset manual mode
- msg = MsgManMode(force_stop_watering_mask = 0,
- valve_manual_mask = 0,
- valve_manual_state = 0)
- self.serial.send(msg)
- # Start cyclic data fetching
- self.__startPolling()
- except SerialError as e:
- self.__handleCommError(e)
- return False
- return True
- def isConnected(self):
- return self.connected
- def connectDev(self, port=None):
- if self.connected:
- return
- if not port:
- dlg = SerialOpenDialog(self)
- if dlg.exec_() != QDialog.Accepted:
- return
- port = dlg.getSelectedPort()
- try:
- self.serial = SerialComm(port, baudrate = SERIAL_BAUDRATE,
- payloadLen = SERIAL_PAYLOAD_LEN,
- debug = False)
- except SerialError as e:
- QMessageBox.critical(self, "Cannot connect serial port",
- "Cannot connect serial port:\n" + str(e))
- return
- self.logWidget.clear()
- self.setUiEnabled(True)
- if not self.__initializeDev():
- return
- self.connected = True
- self.serialConnected.emit()
- def disconnectDev(self):
- self.setUiEnabled(False)
- self.pollTimer.stop()
- if self.serial:
- self.serial.close()
- self.serial = None
- if self.connected:
- self.connected = False
- self.serialDisconnected.emit()
- def getSettingsText(self):
- settings = [
- "[MOISTCONTROL_SETTINGS]\n" \
- "file_version=0\n" \
- "date=%s\n" % \
- (QDateTime.currentDateTime().toUTC().toString("yyyy.MM.dd_hh:mm:ss.zzz_UTC"))
- ]
- # Write global config
- msg = self.__makeMsg_GlobalConfig()
- settings.append(msg.toText())
- # Write pot configs
- for i in range(MAX_NR_FLOWERPOTS):
- msg = self.__makeMsg_PotConfig(i)
- settings.append(msg.toText())
- return "\n".join(settings)
- def setSettingsText(self, settings):
- self.__stopPolling()
- # Parse and upload the new config
- try:
- p = configparser.ConfigParser()
- p.read_string(settings)
- ver = p.getint("MOISTCONTROL_SETTINGS", "file_version")
- if ver != 0:
- raise Error("Unsupported file version. "
- "Expected v0, but got v%d." % ver)
- # Read global config
- msg = MsgContrConf()
- msg.fromText(settings)
- self.serial.send(msg) # send to device
- # Read pot configs
- for i in range(MAX_NR_FLOWERPOTS):
- msg = MsgContrPotConf(i)
- msg.fromText(settings)
- self.serial.send(msg) # send to device
- except configparser.Error as e:
- raise Error(str(e))
- except SerialError as e:
- raise Error("Failed to send config to device:\n" % str(e))
- finally:
- # Restart the communication
- self.__initializeDev()
- def doLoadSettings(self, filename):
- try:
- fd = open(filename, "rb")
- settings = fd.read().decode("UTF-8")
- fd.close()
- self.setSettingsText(settings)
- except (IOError, UnicodeError, Error) as e:
- QMessageBox.critical(self,
- "Failed to read file",
- "Failed to read the settings file:\n"
- "%s" % str(e))
- def loadSettings(self):
- fn, filt = QFileDialog.getOpenFileName(self,
- "Load settings from file",
- "",
- "Settings file (*.moi);;"
- "All files (*)")
- if not fn:
- return
- self.doLoadSettings(fn)
- def doSaveSettingsAs(self, filename):
- try:
- fd = open(filename, "wb")
- fd.write(self.getSettingsText().encode("UTF-8"))
- fd.close()
- except (IOError, UnicodeError, Error) as e:
- QMessageBox.critical(self,
- "Failed to write file",
- "Failed to write the settings file:\n"
- "%s" % str(e))
- def saveSettingsAs(self):
- fn, filt = QFileDialog.getSaveFileName(self,
- "Save settings to file",
- "",
- "Settings file (*.moi);;"
- "All files (*)")
- if not fn:
- return
- if "(*.moi)" in filt:
- if not fn.endswith(".moi"):
- fn += ".moi"
- self.doSaveSettingsAs(fn)
- class MainWindow(QMainWindow):
- """The main program window."""
- def __init__(self):
- """Class constructor."""
- QMainWindow.__init__(self)
- self.setWindowTitle("Flowerpot moisture control")
- mainWidget = MainWidget(self)
- self.setCentralWidget(mainWidget)
- self.setMenuBar(QMenuBar(self))
- menu = QMenu("&File", self)
- self.loadButton = menu.addAction("&Load settings...", self.loadSettings)
- self.saveButton = menu.addAction("&Save settings as...", self.saveSettingsAs)
- menu.addSeparator()
- menu.addAction("&Exit", self.close)
- self.menuBar().addMenu(menu)
- menu = QMenu("&Device", self)
- self.connMenuButton = menu.addAction("&Connect", self.connectDev)
- self.disconnMenuButton = menu.addAction("&Disconnect", self.disconnectDev)
- self.menuBar().addMenu(menu)
- toolBar = QToolBar(self)
- self.connToolButton = toolBar.addAction("Connect", self.connectDev)
- self.disconnToolButton = toolBar.addAction("Disconnect", self.disconnectDev)
- self.addToolBar(toolBar)
- self.updateConnButtons()
- mainWidget.serialConnected.connect(self.updateConnButtons)
- mainWidget.serialDisconnected.connect(self.updateConnButtons)
- def updateConnButtons(self):
- connected = self.centralWidget().isConnected()
- self.connMenuButton.setEnabled(not connected)
- self.connToolButton.setEnabled(not connected)
- self.disconnMenuButton.setEnabled(connected)
- self.disconnToolButton.setEnabled(connected)
- self.loadButton.setEnabled(connected)
- self.saveButton.setEnabled(connected)
- def loadSettings(self):
- self.centralWidget().loadSettings()
- def saveSettingsAs(self):
- self.centralWidget().saveSettingsAs()
- def connectDev(self, port=None):
- self.centralWidget().connectDev(port)
- def disconnectDev(self):
- self.centralWidget().disconnectDev()
- # Program entry point
- def main():
- # Create the main QT application object
- qApp = QApplication(sys.argv)
- # Create and show the main window
- mainWnd = MainWindow()
- mainWnd.show()
- if len(sys.argv) >= 2:
- mainWnd.connectDev(sys.argv[1])
- # Enter the QT event loop
- return qApp.exec_()
- if __name__ == "__main__":
- sys.exit(main())
|