|
54 | 54 |
|
55 | 55 | if TYPE_CHECKING: |
56 | 56 | from artisanlib.comm import serialport # pylint: disable=unused-import |
| 57 | + from artisanlib.power import SleepInhibitor, WakeDetector # pylint: disable=unused-import |
| 58 | + from artisanlib.async_comm import AsyncComm # pylint: disable=unused-import |
| 59 | + from artisanlib.kaleido import KaleidoPort # pylint: disable=unused-import |
57 | 60 | from artisanlib.atypes import ProfileData, BTU # pylint: disable=unused-import |
58 | 61 | from artisanlib.main import ApplicationWindow # pylint: disable=unused-import |
59 | 62 | from plus.stock import Blend # pylint: disable=unused-import |
|
126 | 129 |
|
127 | 130 | _log: Final[logging.Logger] = logging.getLogger(__name__) |
128 | 131 |
|
| 132 | +# interval in milliseconds in which Artisan checks for a system suspension while sampling |
| 133 | +_wake_check_interval: Final[int] = 5000 |
| 134 | + |
129 | 135 |
|
130 | 136 |
|
131 | 137 | type Interp1dKind = Literal['linear', 'nearest', 'nearest-up', 'zero', 'slinear', 'quadratic', 'cubic', 'previous', 'next'] |
@@ -287,7 +293,7 @@ class tgraphcanvas(QObject): |
287 | 293 | 'extraname1', 'extraname2', 'extramathexpression1', 'extramathexpression2', 'extralinestyles1', 'extralinestyles2', 'extradrawstyles1', 'extradrawstyles2', |
288 | 294 | 'extralinewidths1', 'extralinewidths2', 'extramarkers1', 'extramarkers2', 'extramarkersizes1', 'extramarkersizes2', 'devicetablecolumnwidths', 'energytablecolumnwidths', 'extraNoneTempHint1', |
289 | 295 | 'extraNoneTempHint2', 'plotcurves', 'plotcurvecolor', 'overlapList', 'tight_layout_params', 'cupping_tight_layout_params', 'fig', 'ax', 'delta_ax', 'legendloc', 'legendloc_pos', 'onclick_cid', |
290 | | - 'oncpick_cid', 'ondraw_cid', 'onmove_cid', 'rateofchange1', 'rateofchange2', 'flagon', 'flagstart', 'flagKeepON', 'flagOpenCompleted', 'flagsampling', 'flagsamplingthreadrunning', |
| 296 | + 'oncpick_cid', 'ondraw_cid', 'onmove_cid', 'rateofchange1', 'rateofchange2', 'flagon', 'flagstart', 'flagKeepON', 'flagOpenCompleted', 'flagKeepAwake', 'sleep_inhibitor', 'wake_detector', 'wake_timer', 'flagsampling', 'flagsamplingthreadrunning', |
291 | 297 | 'manuallogETflag', 'zoom_follow', 'zoom_follow_onET', 'alignEvent', 'compareAlignEvent', 'compareEvents', 'compareET', 'compareBT', 'compareDeltaET', 'compareDeltaBT', 'compareMainEvents', 'compareBBP', 'compareRoast', 'compareExtraCurves1', 'compareExtraCurves2', |
292 | 298 | 'replayType', 'replayDropType', 'replayedBackgroundEvents', 'beepedBackgroundEvents', 'roastpropertiesflag', 'roastpropertiesAutoOpenFlag', 'roastpropertiesAutoOpenDropFlag', |
293 | 299 | 'title', 'title_show_always', 'ambientTemp', 'ambientTempSource', 'ambientHumiditySource', 'ambientPressureSource', 'ambient_temperature_device', 'ambient_pressure', 'ambient_pressure_device', 'ambient_humidity', |
@@ -923,6 +929,10 @@ def __init__(self, parent:QWidget, dpi:int, locale:str, aw:'ApplicationWindow') |
923 | 929 | self.flagstart:bool = False # Artisan logging/recording |
924 | 930 | self.flagKeepON:bool = False # turn Artisan ON again after pressing OFF during recording |
925 | 931 | self.flagOpenCompleted:bool = False # after completing a recording with OFF, send the saved profile to be opened in the ArtisanViewer |
| 932 | + self.flagKeepAwake:bool = True # if True the computer is prevented from entering standby (idle system sleep) while Artisan is ON |
| 933 | + self.sleep_inhibitor:SleepInhibitor|None = None # holds the system sleep inhibition while Artisan is ON (see flagKeepAwake) |
| 934 | + self.wake_detector:WakeDetector|None = None # detects system suspensions while Artisan is ON to reconnect the machine on wake |
| 935 | + self.wake_timer:QTimer|None = None # drives the wake_detector while Artisan is ON |
926 | 936 | self.flagsampling:bool = False # if True, Artisan is still in the sampling phase and one has to wait for its end to turn OFF |
927 | 937 | self.flagsamplingthreadrunning:bool = False |
928 | 938 | #log flag that tells to log ET when using device 18 (manual mode) |
@@ -12929,6 +12939,88 @@ def resetTimer(self) -> None: |
12929 | 12939 | if self.samplingSemaphore.available() < 1: |
12930 | 12940 | self.samplingSemaphore.release(1) |
12931 | 12941 |
|
| 12942 | + # prevents the computer from entering standby (idle system sleep) while Artisan is sampling. |
| 12943 | + # A system sleep tears down the USB/Bluetooth link to the machine and machines watching the |
| 12944 | + # communication (like Kaleido) react on such a connection loss (eg. by starting to cool). |
| 12945 | + # NOTE: the display is still allowed to sleep, only the system has to stay awake. A laptop |
| 12946 | + # closing its lid still sleeps (clamshell sleep) as this cannot be inhibited by an application. |
| 12947 | + def preventSleep(self) -> None: |
| 12948 | + if not self.flagKeepAwake: |
| 12949 | + return |
| 12950 | + try: |
| 12951 | + if self.sleep_inhibitor is None: |
| 12952 | + from artisanlib.power import SleepInhibitor |
| 12953 | + self.sleep_inhibitor = SleepInhibitor() |
| 12954 | + self.sleep_inhibitor.inhibit() |
| 12955 | + except Exception as e: # pylint: disable=broad-except |
| 12956 | + _log.exception(e) |
| 12957 | + |
| 12958 | + # releases the system sleep inhibition acquired by preventSleep() |
| 12959 | + def allowSleep(self) -> None: |
| 12960 | + try: |
| 12961 | + if self.sleep_inhibitor is not None: |
| 12962 | + self.sleep_inhibitor.release() |
| 12963 | + except Exception as e: # pylint: disable=broad-except |
| 12964 | + _log.exception(e) |
| 12965 | + |
| 12966 | + # a system suspension cannot be prevented in all cases (a laptop closing its lid or a user |
| 12967 | + # explicitly sending the machine to sleep). As a machine connection established before a |
| 12968 | + # suspension is dead afterwards, without the transport necessarily reporting an error or |
| 12969 | + # running into a timeout, we watch out for suspensions while sampling to reconnect on wake. |
| 12970 | + def startWakeDetection(self) -> None: |
| 12971 | + try: |
| 12972 | + if self.wake_detector is None: |
| 12973 | + from artisanlib.power import WakeDetector |
| 12974 | + self.wake_detector = WakeDetector() |
| 12975 | + self.wake_detector.start() |
| 12976 | + if self.wake_timer is None: |
| 12977 | + self.wake_timer = QTimer() |
| 12978 | + self.wake_timer.timeout.connect(self.checkWake) |
| 12979 | + self.wake_timer.start(_wake_check_interval) |
| 12980 | + except Exception as e: # pylint: disable=broad-except |
| 12981 | + _log.exception(e) |
| 12982 | + |
| 12983 | + def stopWakeDetection(self) -> None: |
| 12984 | + try: |
| 12985 | + if self.wake_timer is not None: |
| 12986 | + self.wake_timer.stop() |
| 12987 | + if self.wake_detector is not None: |
| 12988 | + self.wake_detector.stop() |
| 12989 | + except Exception as e: # pylint: disable=broad-except |
| 12990 | + _log.exception(e) |
| 12991 | + |
| 12992 | + @pyqtSlot() |
| 12993 | + def checkWake(self) -> None: |
| 12994 | + try: |
| 12995 | + if self.wake_detector is None: |
| 12996 | + return |
| 12997 | + suspension:float = self.wake_detector.check() |
| 12998 | + if suspension > 0: |
| 12999 | + _log.warning('system resumed after a suspension of %.0fs; reconnecting the machine ' |
| 13000 | + '(NOTE: the roast timer does not advance while the system is suspended)', suspension) |
| 13001 | + self.reconnectMachine() |
| 13002 | + self.aw.sendmessage(QApplication.translate('Message','System resumed. Reconnecting...')) |
| 13003 | + except Exception as e: # pylint: disable=broad-except |
| 13004 | + _log.exception(e) |
| 13005 | + |
| 13006 | + # forces a reconnect of those machine connections which offer this operation and returns the |
| 13007 | + # number of connections that were asked to reconnect. |
| 13008 | + # NOTE: the classic serial devices re-open their port automatically on the next failing read |
| 13009 | + # and the BLE devices are reconnected by their disconnect callbacks, thus both are not |
| 13010 | + # handled here |
| 13011 | + def reconnectMachine(self) -> int: |
| 13012 | + reconnects:int = 0 |
| 13013 | + machines:list[AsyncComm|KaleidoPort|None] = [self.aw.hottop, self.aw.santoker, self.aw.mugma, |
| 13014 | + self.aw.orbiter, self.aw.kaleido] |
| 13015 | + for machine in machines: |
| 13016 | + if machine is not None: |
| 13017 | + try: |
| 13018 | + if machine.reconnect(): |
| 13019 | + reconnects += 1 |
| 13020 | + except Exception as e: # pylint: disable=broad-except |
| 13021 | + _log.exception(e) |
| 13022 | + return reconnects |
| 13023 | + |
12932 | 13024 | @pyqtSlot() |
12933 | 13025 | def OnMonitor(self) -> None: |
12934 | 13026 | try: |
@@ -13102,6 +13194,8 @@ def OnMonitor(self) -> None: |
13102 | 13194 | self.TPalarmtimeindex = None |
13103 | 13195 |
|
13104 | 13196 | self.flagon = True |
| 13197 | + self.preventSleep() # keep the computer awake to not lose the connection to the machine |
| 13198 | + self.startWakeDetection() # reconnect the machine if the system was suspended nevertheless |
13105 | 13199 | self.redraw(True,re_smooth_foreground=False, re_smooth_background=True) # there is now foreground at this point; we need to re-smooth background with no curve-smoothing and standard instead of optimal-smoothing on ON |
13106 | 13200 |
|
13107 | 13201 | if self.designerflag: |
@@ -13279,6 +13373,8 @@ def OffMonitorCloseDown(self, respectAlwaysON:bool, wasRecording:bool) -> None: |
13279 | 13373 | except Exception as e: # pylint: disable=broad-except |
13280 | 13374 | _log.exception(e) |
13281 | 13375 | QTimer.singleShot(5,self.disconnectProbes) |
| 13376 | + self.stopWakeDetection() |
| 13377 | + self.allowSleep() # the machine is disconnected, the computer may sleep again |
13282 | 13378 | # reset the canvas color when it was set by an alarm but never reset |
13283 | 13379 | if 'canvas_alt' in self.palette: |
13284 | 13380 | self.palette['canvas'] = self.palette['canvas_alt'] |
|
0 commit comments