pctl-remote 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. #!/usr/bin/env python3
  2. """
  3. # Copyright (C) 2008-2019 Michael Buesch <m@bues.ch>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License version 3
  7. # as published by the Free Software Foundation.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. """
  17. import getopt
  18. import sys
  19. try:
  20. from serial import *
  21. except ImportError:
  22. print("ERROR: pyserial module not available.")
  23. print("On Debian Linux please do: apt install python3-serial")
  24. sys.exit(1)
  25. from PyQt5.QtCore import *
  26. from PyQt5.QtGui import *
  27. from PyQt5.QtWidgets import *
  28. # Serial communication port configuration
  29. CONFIG_BAUDRATE = 115200
  30. CONFIG_BYTESIZE = 8
  31. CONFIG_PARITY = PARITY_NONE
  32. CONFIG_STOPBITS = 2
  33. # The size of one message
  34. MSG_SIZE = 6
  35. MSG_PAYLOAD_SIZE = 4
  36. # Message IDs
  37. MSG_ID_MASK = 0x3F
  38. MSG_INVALID = 0
  39. MSG_ERROR = 1
  40. MSG_LOGMESSAGE = 2
  41. MSG_PING = 3
  42. MSG_PONG = 4
  43. MSG_GET_CURRENT_PRESSURE = 5
  44. MSG_CURRENT_PRESSURE = 6
  45. MSG_GET_DESIRED_PRESSURE = 7
  46. MSG_DESIRED_PRESSURE = 8
  47. MSG_SET_DESIRED_PRESSURE = 9
  48. MSG_GET_HYSTERESIS = 10
  49. MSG_HYSTERESIS = 11
  50. MSG_SET_HYSTERESIS = 12
  51. MSG_GET_CONFIG_FLAGS = 13
  52. MSG_CONFIG_FLAGS = 14
  53. MSG_SET_CONFIG_FLAGS = 15
  54. MSG_SET_VALVE = 16
  55. MSG_RESTARTED = 17
  56. MSG_SHUTDOWN = 18
  57. MSG_TURNON = 19
  58. MSG_GET_MAXIMA = 20
  59. MSG_MAXIMA = 21
  60. # Message flags
  61. MSG_FLAG_QOVERFLOW = 0x40
  62. MSG_FLAG_REQ_ERRCODE = 0x80
  63. # Message error codes
  64. MSG_ERR_NONE = 0 # No error
  65. MSG_ERR_CHKSUM = 1 # Checksum error
  66. MSG_ERR_NOCMD = 2 # Unknown command
  67. MSG_ERR_BUSY = 3 # Busy
  68. MSG_ERR_INVAL = 4 # Invalid argument
  69. MSG_ERR_NOREPLY = -1 # internal. Not sent over wire.
  70. # Config flags
  71. CFG_FLAG_AUTOADJUST_ENABLE = 0
  72. def usage():
  73. print("Pressure control - remote configuration")
  74. print("")
  75. print("Usage: pctl-remote [OPTIONS] /dev/ttyS0")
  76. print("")
  77. print("-h|--help Print this help text")
  78. print("-p|--noping Don't initially ping the device")
  79. print("-f|--nofetch Don't initially fetch the device state")
  80. print("-k|--noka Don't send keep-alive pings")
  81. print("-l|--log FILE Log status information to FILE")
  82. def parseArgs():
  83. global opt_ttyfile
  84. global opt_noping
  85. global opt_nofetch
  86. global opt_noka
  87. global opt_logfile
  88. if len(sys.argv) < 2:
  89. usage()
  90. sys.exit(1)
  91. opt_ttyfile = sys.argv[-1]
  92. opt_noping = 0
  93. opt_nofetch = 0
  94. opt_noka = 0
  95. opt_logfile = None
  96. try:
  97. (opts, args) = getopt.getopt(sys.argv[1:-1],
  98. "hpfkl:",
  99. [ "help", "noping", "nofetch", "noka", "log=", ])
  100. except getopt.GetoptError:
  101. usage()
  102. sys.exit(1)
  103. for (o, v) in opts:
  104. if o in ("-h", "--help"):
  105. usage()
  106. sys.exit(0)
  107. if o in ("-p", "--noping"):
  108. opt_noping = 1
  109. if o in ("-f", "--nofetch"):
  110. opt_nofetch = 1
  111. if o in ("-k", "--noka"):
  112. opt_noka = 1
  113. if o in ("-l", "--log"):
  114. opt_logfile = v
  115. class LogFile(QObject):
  116. def __init__(self, logfileName):
  117. QObject.__init__(self)
  118. self.fd = None
  119. if not logfileName:
  120. return
  121. try:
  122. self.fd = open(logfileName, "w+b")
  123. except IOError as e:
  124. print("Failed to open logfile %s: %s" % (logfileName, e.strerror))
  125. sys.exit(1)
  126. self.write("X/Y,X/Y lower threshold,X/Y upper threshold,"+\
  127. "Z,Z lower threshold,Z upper threshold,\n")
  128. print("Logging to: %s" % logfileName)
  129. def write(self, message):
  130. if not self.fd:
  131. return
  132. if isinstance(message, str):
  133. message = message.encode("UTF-8", "ignore")
  134. self.fd.write(message)
  135. self.fd.flush()
  136. def logPressure(self, xy, xy_desired, xy_hyst, z, z_desired, z_hyst):
  137. xy_lower = xy_desired - xy_hyst
  138. xy_upper = xy_desired + xy_hyst
  139. z_lower = z_desired - z_hyst
  140. z_upper = z_desired + z_hyst
  141. self.write("%d,%d,%d,%d,%d,%d,\n" %\
  142. (xy, xy_lower, xy_upper, z, z_lower, z_upper))
  143. class RemoteProtocol(QObject):
  144. def __init__(self, ttyfile):
  145. QObject.__init__(self)
  146. global remote
  147. try:
  148. remote = self
  149. self.serial = Serial(ttyfile, CONFIG_BAUDRATE,
  150. CONFIG_BYTESIZE, CONFIG_PARITY,
  151. CONFIG_STOPBITS)
  152. self.__setResetLine(False)
  153. QThread.msleep(100)
  154. self.serial.flushInput()
  155. self.devRestarted = False
  156. self.pollTimer = QTimer(self)
  157. self.pollTimer.timeout.connect(self.__poll)
  158. self.pollTimer.start(50)
  159. self.keepAliveTimer = QTimer(self)
  160. self.keepAliveTimer.timeout.connect(self.__keepAlive)
  161. if not opt_noka:
  162. self.keepAliveTimer.start(1000)
  163. if not opt_noping:
  164. reply = self.sendMessageSyncReply(MSG_PING, 0, b"", MSG_PONG)
  165. if reply:
  166. mainwnd.centralWidget().log.hostLog(
  167. "PING->PONG success. Device is alife.\n")
  168. else:
  169. mainwnd.centralWidget().log.hostLog(
  170. "Communication with device failed. "+\
  171. "No reply to PING request.\n")
  172. except (SerialException, OSError, IOError) as e:
  173. print(e)
  174. sys.exit(1)
  175. def __setResetLine(self, resetOn):
  176. # RTS is connected to the microcontroller reset line.
  177. # If RTS is logic high, the reset line is pulled low
  178. # and the microcontroller is put into reset.
  179. # If RTS is logic low, the microcontroller is in
  180. # normal operation.
  181. try:
  182. self.serial.setRTS(bool(resetOn))
  183. except (SerialException, OSError, IOError) as e:
  184. mainwnd.statusBar().showMessage("Failed to toggle reset. %s" % e)
  185. def rebootDevice(self):
  186. # Reboot the microcontroller
  187. self.__setResetLine(True)
  188. QThread.msleep(50)
  189. self.__setResetLine(False)
  190. QThread.msleep(50)
  191. def stopKeepAliveTimer(self):
  192. self.keepAliveTimer.stop()
  193. def __keepAlive(self):
  194. reply = self.sendMessageSyncReply(MSG_PING, 0, b"", MSG_PONG)
  195. if not reply:
  196. mainwnd.centralWidget().log.hostLog(self.tr(
  197. "Keep-alife: Device did not reply to ping "+\
  198. "request. Rebooting device...\n"))
  199. self.rebootDevice()
  200. if not mainwnd.centralWidget().fetchState():
  201. mainwnd.centralWidget().log.hostLog(self.tr(
  202. "Keep-alife: Failed to fetch configuration\n"))
  203. def __poll(self):
  204. try:
  205. if self.serial.inWaiting() >= MSG_SIZE:
  206. self.parseMessage(self.__readMessage())
  207. except (SerialException, OSError, IOError) as e:
  208. mainwnd.statusBar().showMessage("Failed to poll message. %s" % e)
  209. return
  210. if self.devRestarted:
  211. if mainwnd.centralWidget().fetchState():
  212. mainwnd.centralWidget().log.hostLog(self.tr("Device rebooted\n"))
  213. mainwnd.centralWidget().turnOnDevice()
  214. self.devRestarted = False
  215. def checksumMessage(self, msg):
  216. calc_crc = self.__crc8_update_buffer(0xFF, msg[0:-1])
  217. calc_crc ^= 0xFF
  218. want_crc = msg[-1]
  219. if calc_crc != want_crc:
  220. text = self.tr("ERROR: message CRC mismatch\n")
  221. mainwnd.centralWidget().log.hostLog(text)
  222. try:
  223. self.serial.flushInput()
  224. except (SerialException, OSError, IOError) as e:
  225. pass
  226. return False
  227. return True
  228. def __readMessage(self):
  229. msg = self.serial.read(MSG_SIZE)
  230. flags = msg[0] & ~MSG_ID_MASK
  231. if flags & MSG_FLAG_QOVERFLOW:
  232. mainwnd.centralWidget().log.hostLog(
  233. "Warning: TX queue overflow on the device")
  234. return msg
  235. def parseMessage(self, msg):
  236. if not self.checksumMessage(msg):
  237. return
  238. id = msg[0] & MSG_ID_MASK
  239. if id == MSG_LOGMESSAGE:
  240. str = self.getPayload(msg).rstrip(b'\0').decode("UTF-8", "ignore")
  241. mainwnd.centralWidget().log.devLog(str)
  242. if id == MSG_CURRENT_PRESSURE:
  243. mainwnd.centralWidget().parseCurrentPressureMsg(msg)
  244. if id == MSG_RESTARTED:
  245. self.devRestarted = True
  246. def getPayload(self, msg):
  247. return msg[1:-1]
  248. def sendMessage(self, id, flags, payload):
  249. """Send a message"""
  250. assert(len(payload) <= MSG_PAYLOAD_SIZE)
  251. # Create the header
  252. msg = b"%c" % (id | flags)
  253. # Add the payload
  254. msg += payload
  255. # Pad the payload up to the constant size
  256. msg += b'\0' * (MSG_PAYLOAD_SIZE - len(payload))
  257. # Calculate the CRC
  258. crc = self.__crc8_update_buffer(0xFF, msg)
  259. crc ^= 0xFF
  260. # Add the CRC to the message
  261. msg += b"%c" % crc
  262. # Send the message
  263. assert(len(msg) == MSG_SIZE)
  264. try:
  265. self.serial.write(msg)
  266. except (SerialException, OSError, IOError) as e:
  267. mainwnd.statusBar().showMessage("Failed to send message. %s" % e)
  268. def sendMessageSyncReply(self, id, flags, payload, replyId):
  269. """Send a message and synchronously wait for the reply."""
  270. self.pollTimer.stop()
  271. self.sendMessage(id, flags, payload)
  272. timeout = QDateTime.currentDateTime().addMSecs(500)
  273. try:
  274. while True:
  275. if QDateTime.currentDateTime() >= timeout:
  276. msg = None
  277. break
  278. if self.serial.inWaiting() < MSG_SIZE:
  279. QThread.msleep(1)
  280. continue
  281. msg = self.__readMessage()
  282. if not self.checksumMessage(msg):
  283. continue
  284. msgid = msg[0] & MSG_ID_MASK
  285. if msgid == replyId:
  286. break
  287. # This is not a reply to our message.
  288. self.parseMessage(msg)
  289. except (SerialException, OSError, IOError) as e:
  290. mainwnd.statusBar().showMessage("Failed to fetch reply. %s" % e)
  291. msg = b"\0" * MSG_SIZE
  292. self.pollTimer.start()
  293. return msg
  294. def sendMessageSyncError(self, id, flags, payload):
  295. """Sends a message and synchronously waits for the MSG_ERROR reply."""
  296. flags |= MSG_FLAG_REQ_ERRCODE
  297. reply = self.sendMessageSyncReply(id, flags, payload, MSG_ERROR)
  298. if not reply:
  299. return MSG_ERR_NOREPLY
  300. return self.getPayload(reply)[0]
  301. def configFlagsFetch(self):
  302. reply = self.sendMessageSyncReply(MSG_GET_CONFIG_FLAGS, 0, b"",
  303. MSG_CONFIG_FLAGS)
  304. if not reply:
  305. return None
  306. reply = remote.getPayload(reply)
  307. xy = reply[0]
  308. z = reply[1]
  309. return (xy, z)
  310. def configFlagsSet(self, island, flags):
  311. data = b"%c%c" % (island, flags)
  312. err = self.sendMessageSyncError(MSG_SET_CONFIG_FLAGS, 0, data)
  313. return err
  314. def setValve(self, islandId, valveNr, state):
  315. data = b"%c%c%c" % (islandId, valveNr, (state != 0))
  316. i = 5 # Retry a few times
  317. while i != 0:
  318. err = self.sendMessageSyncError(MSG_SET_VALVE, 0, data)
  319. if err == MSG_ERR_NONE:
  320. break
  321. i -= 1
  322. return err
  323. def __crc8_update_buffer(self, crc, buf):
  324. for c in buf:
  325. crc ^= c
  326. for i in range(8):
  327. if crc & 1:
  328. crc = (crc >> 1) ^ 0x8C
  329. else:
  330. crc >>= 1
  331. return crc & 0xFF
  332. class StatusBar(QStatusBar):
  333. def showMessage(self, msg):
  334. print(msg)
  335. QStatusBar.showMessage(self, msg, 3000)
  336. class LogBrowser(QTextEdit):
  337. def __init__(self, parent=None):
  338. QTextEdit.__init__(self, parent)
  339. self.msgs = []
  340. self.curDevMsg = ""
  341. self.setReadOnly(1)
  342. self.hostLog(self.tr("Pressure Control logging started\n"));
  343. def __commit(self):
  344. MSG_LIMIT = 100
  345. if len(self.msgs) > MSG_LIMIT:
  346. self.msgs = self.msgs[1:]
  347. assert(len(self.msgs) == MSG_LIMIT)
  348. self.setPlainText("".join(self.msgs))
  349. # Scroll to the end of the log
  350. vScroll = self.verticalScrollBar()
  351. vScroll.setValue(vScroll.maximum())
  352. def __dateString(self):
  353. date = QDateTime.currentDateTime()
  354. return str(date.toString("[hh:mm:ss] "))
  355. def devLog(self, text):
  356. text = str(text) # to python string
  357. if not text:
  358. return
  359. if not self.curDevMsg:
  360. text = self.__dateString() + "Dev: " + text
  361. self.curDevMsg += text
  362. if text[-1] in "\r\n":
  363. self.msgs.append(self.curDevMsg)
  364. self.curDevMsg = ""
  365. self.__commit()
  366. def hostLog(self, text):
  367. text = str(text) # to python string
  368. text = self.__dateString() + "Host: " + text
  369. self.msgs.append(text)
  370. self.__commit()
  371. class PressureGauge(QWidget):
  372. def __init__(self, name, min, max, units, parent):
  373. QWidget.__init__(self, parent)
  374. self.min = min
  375. self.max = max
  376. self.units = units
  377. self.setLayout(QVBoxLayout())
  378. self.title = QLabel(name, self)
  379. self.title.setAlignment(Qt.AlignHCenter)
  380. self.layout().addWidget(self.title)
  381. self.dial = QDial(self)
  382. self.dial.setNotchesVisible(1)
  383. self.dial.setEnabled(0)
  384. self.dial.setSingleStep(100)
  385. self.dial.setPageStep(1000)
  386. self.dial.setNotchTarget(2)
  387. self.dial.setMinimum(int(min * 1000))
  388. self.dial.setMaximum(int(max * 1000))
  389. self.dial.setFixedSize(100, 100)
  390. self.layout().addWidget(self.dial)
  391. self.num = QLabel(self)
  392. self.num.setAlignment(Qt.AlignHCenter)
  393. self.layout().addWidget(self.num)
  394. self.layout().addStretch()
  395. self.setValue(0)
  396. def setValue(self, value):
  397. if (value < self.min):
  398. value = self.min
  399. if (value > self.max):
  400. value = self.max
  401. self.num.setText("%.2f %s" % (float(value), self.units))
  402. self.dial.setValue(int(value * 1000))
  403. class ValveIslandWidget(QGroupBox):
  404. def __init__(self, name, islandId, parent):
  405. QGroupBox.__init__(self, name, parent)
  406. self.islandId = islandId
  407. self.setLayout(QGridLayout())
  408. self.gauge = PressureGauge("Current pressure", 0, 10, "Bar", self)
  409. self.layout().addWidget(self.gauge, 0, 0, 3, 1)
  410. h = QHBoxLayout()
  411. h.addStretch()
  412. self.autoCheckbox = QCheckBox(self.tr("Automatically adjust pressure"), self)
  413. self.autoCheckbox.stateChanged.connect(self.autoadjustChanged)
  414. h.addWidget(self.autoCheckbox)
  415. self.layout().addLayout(h, 0, 1)
  416. h = QHBoxLayout()
  417. h.addStretch()
  418. label = QLabel(self.tr("Desired pressure:"), self)
  419. h.addWidget(label)
  420. self.pressureSpin = QDoubleSpinBox(self)
  421. self.pressureSpin.setMinimum(0.5)
  422. self.pressureSpin.setMaximum(8)
  423. self.pressureSpin.setSingleStep(0.1)
  424. self.pressureSpin.setSuffix(self.tr(" Bar"))
  425. self.pressureSpin.valueChanged.connect(self.desiredPressureChanged)
  426. h.addWidget(self.pressureSpin)
  427. self.layout().addLayout(h, 1, 1)
  428. h = QHBoxLayout()
  429. h.addStretch()
  430. label = QLabel(self.tr("Hysteresis:"), self)
  431. h.addWidget(label)
  432. self.hystSpin = QDoubleSpinBox(self)
  433. self.hystSpin.setMinimum(0.05)
  434. self.hystSpin.setMaximum(8)
  435. self.hystSpin.setSingleStep(0.05)
  436. self.hystSpin.setSuffix(self.tr(" Bar"))
  437. self.hystSpin.valueChanged.connect(self.desiredHysteresisChanged)
  438. h.addWidget(self.hystSpin)
  439. self.layout().addLayout(h, 2, 1)
  440. h = QHBoxLayout()
  441. self.inButton = QPushButton(self.tr("IN-Valve"), self)
  442. self.inButton.pressed.connect(self.inValvePressed)
  443. self.inButton.released.connect(self.inValveReleased)
  444. h.addWidget(self.inButton)
  445. self.outButton = QPushButton(self.tr("OUT-Valve"), self)
  446. h.addWidget(self.outButton)
  447. self.outButton.pressed.connect(self.outValvePressed)
  448. self.outButton.released.connect(self.outValveReleased)
  449. self.layout().addLayout(h, 3, 0, 1, 2)
  450. self.autoadjustChanged(Qt.Unchecked)
  451. def desiredPressureChanged(self, value):
  452. if not self.parent().initialized:
  453. return
  454. mainwnd.centralWidget().pokeUiTimer()
  455. mbar = int(value * 1000)
  456. data = b"%c%c%c" % (self.islandId, (mbar & 0xFF), ((mbar >> 8) & 0xFF))
  457. err = remote.sendMessageSyncError(MSG_SET_DESIRED_PRESSURE, 0, data)
  458. if err != MSG_ERR_NONE:
  459. self.parent().log.hostLog(self.tr("Failed to change pressure. Error=%u\n" % err))
  460. def getDesiredPressure(self):
  461. return int(self.pressureSpin.value() * 1000)
  462. def desiredHysteresisChanged(self, value):
  463. if not self.parent().initialized:
  464. return
  465. mainwnd.centralWidget().pokeUiTimer()
  466. mbar = int(value * 1000)
  467. data = b"%c%c%c" % (self.islandId, (mbar & 0xFF), ((mbar >> 8) & 0xFF))
  468. err = remote.sendMessageSyncError(MSG_SET_HYSTERESIS, 0, data)
  469. if err != MSG_ERR_NONE:
  470. self.parent().log.hostLog(self.tr("Failed to change hysteresis. Error=%u\n" % err))
  471. def getHysteresis(self):
  472. return int(self.hystSpin.value() * 1000)
  473. def autoadjustChanged(self, state):
  474. self.inButton.setEnabled(state == Qt.Unchecked)
  475. self.outButton.setEnabled(state == Qt.Unchecked)
  476. if not self.parent().initialized:
  477. return
  478. mainwnd.centralWidget().pokeUiTimer()
  479. flags = remote.configFlagsFetch()
  480. if flags is None:
  481. self.parent().log.hostLog(self.tr("Failed to fetch config flags\n"))
  482. return
  483. flags = flags[self.islandId]
  484. if state == Qt.Checked:
  485. flags |= (1 << CFG_FLAG_AUTOADJUST_ENABLE)
  486. else:
  487. flags &= ~(1 << CFG_FLAG_AUTOADJUST_ENABLE)
  488. err = remote.configFlagsSet(self.islandId, flags)
  489. if err != MSG_ERR_NONE:
  490. self.parent().log.hostLog(self.tr("Failed to set config flags\n"))
  491. def inValvePressed(self):
  492. mainwnd.centralWidget().stopUiTimer()
  493. err = remote.setValve(self.islandId, 0, 1)
  494. if err != MSG_ERR_NONE:
  495. self.parent().log.hostLog(self.tr("Failed to switch valve 0 ON\n"))
  496. def inValveReleased(self):
  497. mainwnd.centralWidget().startUiTimer()
  498. err = remote.setValve(self.islandId, 0, 0)
  499. if err != MSG_ERR_NONE:
  500. self.parent().log.hostLog(self.tr("Failed to switch valve 0 OFF\n"))
  501. def outValvePressed(self):
  502. mainwnd.centralWidget().stopUiTimer()
  503. err = remote.setValve(self.islandId, 1, 1)
  504. if err != MSG_ERR_NONE:
  505. self.parent().log.hostLog(self.tr("Failed to switch valve 1 ON\n"))
  506. def outValveReleased(self):
  507. mainwnd.centralWidget().startUiTimer()
  508. err = remote.setValve(self.islandId, 1, 0)
  509. if err != MSG_ERR_NONE:
  510. self.parent().log.hostLog(self.tr("Failed to switch valve 1 OFF\n"))
  511. class MainWidget(QWidget):
  512. def __init__(self, parent=None):
  513. QWidget.__init__(self, parent)
  514. self.initialized = False
  515. self.setLayout(QVBoxLayout())
  516. self.uiLock = QCheckBox(self.tr("User interface enabled"), self)
  517. self.uiLock.setCheckState(Qt.Unchecked)
  518. self.uiLock.stateChanged.connect(self.__uiLockChanged)
  519. self.layout().addWidget(self.uiLock)
  520. self.uiLockTimer = QTimer(self)
  521. self.uiLockTimer.timeout.connect(self.__uiLockTimerExpired)
  522. self.xy = ValveIslandWidget("X/Y joints", 0, self)
  523. self.layout().addWidget(self.xy)
  524. self.z = ValveIslandWidget("Z joint", 1, self)
  525. self.layout().addWidget(self.z)
  526. self.log = LogBrowser(self)
  527. self.layout().addWidget(self.log)
  528. self.__uiEnable(False)
  529. def __uiEnable(self, enabled):
  530. self.xy.setEnabled(enabled)
  531. self.z.setEnabled(enabled)
  532. def stopUiTimer(self):
  533. self.uiLockTimer.stop()
  534. def startUiTimer(self):
  535. self.uiLockTimer.start(10000)
  536. def pokeUiTimer(self):
  537. self.stopUiTimer()
  538. self.startUiTimer()
  539. def __uiLockChanged(self, state):
  540. enabled = (state != Qt.Unchecked)
  541. self.__uiEnable(enabled)
  542. if enabled:
  543. self.pokeUiTimer()
  544. else:
  545. self.stopUiTimer()
  546. def __uiLockTimerExpired(self):
  547. self.uiLock.setCheckState(Qt.Unchecked)
  548. def initializeState(self):
  549. if not opt_nofetch:
  550. if not self.fetchState():
  551. self.log.hostLog(
  552. "Failed to fetch active configuration "+\
  553. "from device.\n")
  554. self.turnOnDevice()
  555. self.initialized = True
  556. def turnOnDevice(self):
  557. error = remote.sendMessageSyncError(MSG_TURNON, 0, b"")
  558. if error:
  559. self.log.hostLog("Failed to turn on device\n")
  560. else:
  561. self.log.hostLog("Device turned on\n")
  562. def shutdown(self):
  563. remote.stopKeepAliveTimer()
  564. if self.initialized:
  565. error = remote.sendMessageSyncError(MSG_SHUTDOWN, 0, b"")
  566. if error != MSG_ERR_NONE:
  567. QMessageBox.critical(self,
  568. "Pressure Control",
  569. "Failed to shutdown device")
  570. def fetchState(self):
  571. # Get the current pressure
  572. reply = remote.sendMessageSyncReply(MSG_GET_CURRENT_PRESSURE, 0, b"",
  573. MSG_CURRENT_PRESSURE)
  574. if not reply:
  575. print("Failed to fetch current pressure. No reply.")
  576. return False
  577. self.parseCurrentPressureMsg(reply)
  578. # Get the X/Y maxima
  579. reply = remote.sendMessageSyncReply(MSG_GET_MAXIMA, 0, b"%c" % 0,
  580. MSG_MAXIMA)
  581. if not reply:
  582. print("Failed to fetch X/Y maxima. No reply.")
  583. return False
  584. reply = remote.getPayload(reply)
  585. pressureMbar = reply[0] | (reply[1] << 8)
  586. hysteresisMbar = reply[2] | (reply[3] << 8)
  587. self.xy.pressureSpin.setMaximum(float(pressureMbar) / 1000)
  588. self.xy.hystSpin.setMaximum(float(pressureMbar) / 1000)
  589. # Get the Z maxima
  590. reply = remote.sendMessageSyncReply(MSG_GET_MAXIMA, 0, b"%c" % 1,
  591. MSG_MAXIMA)
  592. if not reply:
  593. print("Failed to fetch Z maxima. No reply.")
  594. return False
  595. reply = remote.getPayload(reply)
  596. pressureMbar = reply[0] | (reply[1] << 8)
  597. hysteresisMbar = reply[2] | (reply[3] << 8)
  598. self.z.pressureSpin.setMaximum(float(pressureMbar) / 1000)
  599. self.z.hystSpin.setMaximum(float(pressureMbar) / 1000)
  600. # Get the desired pressure
  601. reply = remote.sendMessageSyncReply(MSG_GET_DESIRED_PRESSURE, 0, b"",
  602. MSG_DESIRED_PRESSURE)
  603. if not reply:
  604. print("Failed to fetch desired pressure. No reply.")
  605. return False
  606. reply = remote.getPayload(reply)
  607. xy_mbar = reply[0] | (reply[1] << 8)
  608. z_mbar = reply[2] | (reply[3] << 8)
  609. self.xy.pressureSpin.setValue(float(xy_mbar) / 1000)
  610. self.z.pressureSpin.setValue(float(z_mbar) / 1000)
  611. # Get the hysteresis
  612. reply = remote.sendMessageSyncReply(MSG_GET_HYSTERESIS, 0, b"",
  613. MSG_HYSTERESIS)
  614. if not reply:
  615. print("Failed to fetch hysteresis. No reply.")
  616. return False
  617. reply = remote.getPayload(reply)
  618. xy_mbar = reply[0] | (reply[1] << 8)
  619. z_mbar = reply[2] | (reply[3] << 8)
  620. self.xy.hystSpin.setValue(float(xy_mbar) / 1000)
  621. self.z.hystSpin.setValue(float(z_mbar) / 1000)
  622. # Get the config flags
  623. flags = remote.configFlagsFetch()
  624. if flags is None:
  625. print("Failed to fetch config flags. No reply.")
  626. return False
  627. if flags[0] & (1 << CFG_FLAG_AUTOADJUST_ENABLE):
  628. self.xy.autoCheckbox.setCheckState(Qt.Checked)
  629. if flags[1] & (1 << CFG_FLAG_AUTOADJUST_ENABLE):
  630. self.z.autoCheckbox.setCheckState(Qt.Checked)
  631. return True
  632. def parseCurrentPressureMsg(self, msg):
  633. msg = remote.getPayload(msg)
  634. xy_mbar = msg[0] | (msg[1] << 8)
  635. z_mbar = msg[2] | (msg[3] << 8)
  636. self.xy.gauge.setValue(float(xy_mbar) / 1000)
  637. self.z.gauge.setValue(float(z_mbar) / 1000)
  638. logfile.logPressure(xy_mbar, self.xy.getDesiredPressure(),
  639. self.xy.getHysteresis(),
  640. z_mbar, self.z.getDesiredPressure(),
  641. self.z.getHysteresis())
  642. class MainWindow(QMainWindow):
  643. def __init__(self, parent=None):
  644. QMainWindow.__init__(self, parent)
  645. self.setWindowTitle(self.tr("Pneumatic pressure control"))
  646. mb = QMenuBar(self)
  647. ctlmen = QMenu(self.tr("Control"), mb)
  648. ctlmen.addAction(self.tr("Exit"), self.close)
  649. mb.addMenu(ctlmen)
  650. helpmen = QMenu(self.tr("Help"), mb)
  651. helpmen.addAction(self.tr("About"), self.about)
  652. mb.addMenu(helpmen)
  653. self.setMenuBar(mb)
  654. self.setStatusBar(StatusBar())
  655. self.setCentralWidget(MainWidget())
  656. self.resize(400, 350)
  657. def initializeState(self):
  658. self.centralWidget().initializeState()
  659. def shutdown(self):
  660. self.centralWidget().shutdown()
  661. def about(self):
  662. QMessageBox.information(self, self.tr("About"),
  663. self.tr("Pneumatic pressure control\n"
  664. "Copyright (c) 2008-2019 Michael Buesch"))
  665. def main():
  666. global remote
  667. global mainwnd
  668. global app
  669. global logfile
  670. mainwnd = None
  671. app = QApplication(sys.argv)
  672. parseArgs()
  673. logfile = LogFile(opt_logfile)
  674. mainwnd = MainWindow()
  675. remote = RemoteProtocol(opt_ttyfile)
  676. mainwnd.initializeState()
  677. mainwnd.show()
  678. result = app.exec_()
  679. mainwnd.shutdown()
  680. exit(result)
  681. if __name__ == "__main__":
  682. main()