From f29aba5fd873636ae7a3a11dc6659dc9e370f886 Mon Sep 17 00:00:00 2001 From: MAKOMO Date: Sun, 20 Oct 2024 20:06:34 +0200 Subject: [PATCH] adds option to start the RampSoak time on PID ON --- src/artisanlib/canvas.py | 8 +++---- src/artisanlib/main.py | 2 ++ src/artisanlib/pid_control.py | 41 ++++++++++++++++++++++++++++++----- src/artisanlib/pid_dialogs.py | 16 +++++++++++++- src/artisanlib/santoker.py | 6 +++-- src/artisanlib/santoker_r.py | 7 ++++-- wiki/ReleaseHistory.md | 1 + 7 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/artisanlib/canvas.py b/src/artisanlib/canvas.py index 4a0555a70..b386b3f3e 100644 --- a/src/artisanlib/canvas.py +++ b/src/artisanlib/canvas.py @@ -531,7 +531,7 @@ def __init__(self, parent:QWidget, dpi:int, locale:str, aw:'ApplicationWindow') # default delay between readings in milliseconds self.default_delay: Final[int] = 2000 # default 2s self.delay:int = self.default_delay - self.min_delay: Final[int] = 250 #500 # 1000 # Note that a 0.25s min delay puts a lot of performance pressure on the app + self.min_delay: Final[int] = 100 #250 # 1000 # Note that already a 0.25s min delay puts a lot of performance pressure on the app # extra event sampling interval in milliseconds. If 0, then extra sampling commands are sent "in sync" with the standard sampling commands self.extra_event_sampling_delay:int = 0 # sync, 0.5s, 1.0s, 1.5s,.., 5s => 0, 500, 1000, 1500, .. # 0, 500, 1000, 1500, ... @@ -15853,11 +15853,9 @@ def backgroundXTat(self, n:int, seconds:float, relative:bool = False, smoothed:b if n % 2 == 0: # even tempBX = self.stemp1BX if smoothed else self.temp1BX - # odd - elif smoothed: - tempBX = self.stemp2BX else: - tempBX = self.temp2BX + # odd + tempBX = self.stemp2BX if smoothed else self.temp2BX c = n // 2 if len(tempBX)>c: temp = tempBX[c] diff --git a/src/artisanlib/main.py b/src/artisanlib/main.py index e9df5309b..644269ee8 100644 --- a/src/artisanlib/main.py +++ b/src/artisanlib/main.py @@ -17304,6 +17304,7 @@ def settingsLoad(self, filename:Optional[str] = None, theme:bool = False, machin #restore TC4/Arduino PID settings settings.beginGroup('ArduinoPID') self.pidcontrol.pidOnCHARGE = bool(toBool(settings.value('pidOnCHARGE',self.pidcontrol.pidOnCHARGE))) + self.pidcontrol.RStimeAfterCHARGE = bool(toBool(settings.value('RStimeAfterCHARGE',self.pidcontrol.RStimeAfterCHARGE))) self.pidcontrol.loadpidfrombackground = bool(toBool(settings.value('loadpidfrombackground',self.pidcontrol.loadpidfrombackground))) self.pidcontrol.createEvents = bool(toBool(settings.value('createEvents',self.pidcontrol.createEvents))) self.pidcontrol.loadRampSoakFromProfile = bool(toBool(settings.value('loadRampSoakFromProfile',self.pidcontrol.loadRampSoakFromProfile))) @@ -18963,6 +18964,7 @@ def saveAllSettings(self, settings:QSettings, default_settings:Optional[Dict[str #save pid settings (only key and value[0]) settings.beginGroup('ArduinoPID') self.settingsSetValue(settings, default_settings, 'pidOnCHARGE',self.pidcontrol.pidOnCHARGE, read_defaults) + self.settingsSetValue(settings, default_settings, 'RStimeAfterCHARGE',self.pidcontrol.RStimeAfterCHARGE, read_defaults) self.settingsSetValue(settings, default_settings, 'loadpidfrombackground',self.pidcontrol.loadpidfrombackground, read_defaults) self.settingsSetValue(settings, default_settings, 'createEvents',self.pidcontrol.createEvents, read_defaults) self.settingsSetValue(settings, default_settings, 'loadRampSoakFromProfile',self.pidcontrol.loadRampSoakFromProfile, read_defaults) diff --git a/src/artisanlib/pid_control.py b/src/artisanlib/pid_control.py index b799878d2..43572122c 100644 --- a/src/artisanlib/pid_control.py +++ b/src/artisanlib/pid_control.py @@ -471,6 +471,7 @@ def setpidPXR(self, var:str, v:float) -> None: self.aw.qmc.adderror(message) def calcSV(self, tx:float) -> Optional[float]: + # tx is the timestamp recorded, NOT the time displayed to the user after CHARGE if self.aw.qmc.background: # Follow Background mode if self.aw.qmc.swapETBT: # we observe the BT @@ -1134,11 +1135,11 @@ def fujiCrc16(string:bytes) -> int: ################################################################################### class PIDcontrol: - __slots__ = [ 'aw', 'pidActive', 'sv', 'pidOnCHARGE', 'loadpidfrombackground', 'createEvents', 'loadRampSoakFromProfile', 'loadRampSoakFromBackground', 'svLen', 'svLabel', + __slots__ = [ 'aw', 'pidActive', 'sv', 'pidOnCHARGE', 'RStimeAfterCHARGE', 'loadpidfrombackground', 'createEvents', 'loadRampSoakFromProfile', 'loadRampSoakFromBackground', 'svLen', 'svLabel', 'svValues', 'svRamps', 'svSoaks', 'svActions', 'svBeeps', 'svDescriptions','svTriggeredAlarms', 'RSLen', 'RS_svLabels', 'RS_svValues', 'RS_svRamps', 'RS_svSoaks', 'RS_svActions', 'RS_svBeeps', 'RS_svDescriptions', 'svSlider', 'svButtons', 'svMode', 'svLookahead', 'dutySteps', 'svSliderMin', 'svSliderMax', 'svValue', 'dutyMin', 'dutyMax', 'pidKp', 'pidKi', 'pidKd', 'pOnE', 'pidSource', 'pidCycle', 'pidPositiveTarget', 'pidNegativeTarget', 'invertControl', - 'sv_smoothing_factor', 'sv_decay_weights', 'previous_svs', 'time_pidON', 'current_ramp_segment', 'current_soak_segment', 'ramp_soak_engaged', + 'sv_smoothing_factor', 'sv_decay_weights', 'previous_svs', 'time_pidON', 'source_reading_pidON', 'current_ramp_segment', 'current_soak_segment', 'ramp_soak_engaged', 'RS_total_time', 'slider_force_move', 'positiveTargetRangeLimit', 'positiveTargetMin', 'positiveTargetMax', 'negativeTargetRangeLimit', 'negativeTargetMin', 'negativeTargetMax', 'derivative_filter'] @@ -1148,6 +1149,7 @@ def __init__(self, aw:'ApplicationWindow') -> None: self.sv:Optional[float] = None # the last sv send to the Arduino # self.pidOnCHARGE:bool = False + self.RStimeAfterCHARGE = True # if True RS time is taken from CHARGE if FALSE it is the time after the PID was last started self.loadpidfrombackground = False # if True, p-i-d parameters pidKp, pidKi, pidKd, pidSource, pOnE and svLookahead are set from the background profile self.createEvents:bool = False self.loadRampSoakFromProfile:bool = False @@ -1211,6 +1213,7 @@ def __init__(self, aw:'ApplicationWindow') -> None: self.previous_svs:List[float] = [] # time @ PID ON self.time_pidON:float = 0 # in monitoring mode, ramp-soak times are interpreted w.r.t. the time after the PID was turned on and not the time after CHARGE as during recording + self.source_reading_pidON:float = 0 # the reading of the selected source on PID ON (to be used as start point for the first RAMP/SOAK pattern) self.current_ramp_segment:int = 0 # the RS segment currently active. Note that this is 1 based, 0 indicates that no segment has started yet self.current_soak_segment:int = 0 # the RS segment currently active. Note that this is 1 based, 0 indicates that no segment has started yet self.ramp_soak_engaged:int = 1 # set to 0, disengaged, after the RS pattern was processed fully @@ -1343,7 +1346,9 @@ def pidModeInit(self) -> None: self.RS_total_time = self.RStotalTime(self.svRamps,self.svSoaks) self.svTriggeredAlarms = [False]*self.svLen - if self.aw.qmc.flagstart or len(self.aw.qmc.on_timex)<1: + if self.aw.qmc.flagstart: + self.time_pidON = self.aw.qmc.timex[-1] + elif len(self.aw.qmc.on_timex)<1: self.time_pidON = 0 else: self.time_pidON = self.aw.qmc.on_timex[-1] @@ -1351,6 +1356,23 @@ def pidModeInit(self) -> None: # turn the timer LCD color blue if in RS mode and not recording self.aw.setTimerColor('rstimer') + # remember current pidSource reading + self.source_reading_pidON = 0 + if self.pidSource == 1: # we observe the BT + self.source_reading_pidON = self.aw.qmc.temp2[-1] + elif self.pidSource == 2: # we observe the ET + self.source_reading_pidON = self.aw.qmc.temp1[-1] + elif self.pidSource>2: # we observe an extra curve + n = self.pidSource-3 + c = n // 2 + if n % 2 == 0: + tempX = self.aw.qmc.extratemp1 if self.aw.qmc.flagstart else self.aw.qmc.on_extratemp1 + else: + tempX = self.aw.qmc.extratemp2 if self.aw.qmc.flagstart else self.aw.qmc.on_extratemp2 + if len(tempX)>c: + self.source_reading_pidON = tempX[c][-1] + + # the internal software PID should be configured on ON, but not be activated yet to warm it up def confSoftwarePID(self) -> None: if self.externalPIDControl() not in [1, 2, 4] and not(self.aw.qmc.device == 19 and self.aw.qmc.PIDbuttonflag) and self.aw.qmc.Controlbuttonflag: @@ -1498,7 +1520,7 @@ def svRampSoak(self, t:float) -> Optional[float]: segment_end_time = 0 # the (end) time of the segments prev_segment_end_time = 0 # the (end) time of the previous segment segment_start_sv = 0. # the (target) sv of the segment - prev_segment_start_sv = 0. # the (target) sv of the previous segment + prev_segment_start_sv = self.source_reading_pidON # the (target) sv of the previous segment; initialized to the reading of the pid source on PID ON for i, v in enumerate(self.svValues): # Ramp if self.svRamps[i] != 0: @@ -1554,10 +1576,17 @@ def smooth_sv(self, sv:float) -> float: # returns None if in manual mode or no other sv (via ramp/soak or follow mode) defined def calcSV(self, tx:float) -> Optional[float]: + # tx is the timestamp recorded, NOT the time displayed to the user after CHARGE if self.svMode == 1: # Ramp/Soak mode - # actual time (after CHARGE) on recording and time after PID ON on monitoring: - return self.svRampSoak(tx - self.time_pidON) + # actual time (after CHARGE) on recording (if CHARGE and RStimeAfterCHARGE) and time after PID ON (on monitoring or if RStimeAfterCHARGE): + time = tx + if not self.aw.qmc.flagstart or not self.RStimeAfterCHARGE: + time = time - self.time_pidON + elif self.RStimeAfterCHARGE and self.aw.qmc.timeindex[0] > -1: + # after CHARGE + time = time - self.aw.qmc.timex[self.aw.qmc.timeindex[0]] + return self.svRampSoak(time) if self.svMode == 2 and self.aw.qmc.background: # Follow Background mode if self.aw.qmc.device == 19 and self.externalPIDControl(): # in case we run TC4 with the PIDfirmware diff --git a/src/artisanlib/pid_dialogs.py b/src/artisanlib/pid_dialogs.py index b2b65e44f..8a9a85853 100644 --- a/src/artisanlib/pid_dialogs.py +++ b/src/artisanlib/pid_dialogs.py @@ -779,7 +779,19 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) -> flagsLayout.addWidget(self.loadRampSoakFromBackground) flagsLayout.addStretch() - + time_label = QLabel(QApplication.translate('Label', 'Time starts at')) + self.radioTimeAfterCHARGE = QRadioButton(QApplication.translate('Label','CHARGE')) + self.radioTimeAfterPIDON = QRadioButton(QApplication.translate('Button','PID ON')) + if self.aw.pidcontrol.RStimeAfterCHARGE: + self.radioTimeAfterCHARGE.setChecked(True) + else: + self.radioTimeAfterPIDON.setChecked(True) + radioButtonsLayout = QHBoxLayout() + radioButtonsLayout.addStretch() + radioButtonsLayout.addWidget(time_label) + radioButtonsLayout.addWidget(self.radioTimeAfterCHARGE) + radioButtonsLayout.addWidget(self.radioTimeAfterPIDON) + radioButtonsLayout.addStretch() buttonLayout = QHBoxLayout() buttonLayout.addStretch() @@ -792,6 +804,7 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) -> tab2Layout.addLayout(buttonLayout) tab2Layout.addStretch() + tab2Layout.addLayout(radioButtonsLayout) tab2Layout.addLayout(flagsLayout) @@ -1180,6 +1193,7 @@ def close(self) -> bool: self.aw.pidcontrol.setPID(kp,ki,kd,source,cycle,pOnE) # self.aw.pidcontrol.pidOnCHARGE = self.startPIDonCHARGE.isChecked() + self.aw.pidcontrol.RStimeAfterCHARGE = self.radioTimeAfterCHARGE.isChecked() self.aw.pidcontrol.loadpidfrombackground = self.loadPIDfromBackground.isChecked() self.aw.pidcontrol.createEvents = self.createEvents.isChecked() self.aw.pidcontrol.loadRampSoakFromProfile = self.loadRampSoakFromProfile.isChecked() diff --git a/src/artisanlib/santoker.py b/src/artisanlib/santoker.py index 4c7217c1c..011d04bb2 100644 --- a/src/artisanlib/santoker.py +++ b/src/artisanlib/santoker.py @@ -215,9 +215,11 @@ def register_reading(self, target:bytes, data:bytes) -> None: if target == self.BOARD: self._board = value / 10.0 elif target == self.BT: - self._bt = value / 10.0 + BT = value / 10.0 + self._bt = (BT if self._bt == -1 else (2*BT + self._bt)/3) elif target == self.ET: - self._et = value / 10.0 + ET = value / 10.0 + self._et = (ET if self._et == -1 else (2*ET + self._et)/3) elif target == self.BT_ROR: self._bt_ror = value / 10.0 elif target == self.ET_ROR: diff --git a/src/artisanlib/santoker_r.py b/src/artisanlib/santoker_r.py index e94329397..55fedd4b9 100644 --- a/src/artisanlib/santoker_r.py +++ b/src/artisanlib/santoker_r.py @@ -78,8 +78,11 @@ def notify_callback(self, _sender:'BleakGATTCharacteristic', data:bytearray) -> if self._logging: _log.debug('notify: %s', data) if len(data) > 3: - self._BT = int.from_bytes(data[0:2], 'big') / 10 - self._ET = int.from_bytes(data[2:4], 'big') / 10 + BT = int.from_bytes(data[0:2], 'big') / 10 + ET = int.from_bytes(data[2:4], 'big') / 10 + # BLE delivers a new reading about every 0.5sec which we average + self._BT = (BT if self._BT == -1 else (2*BT + self._BT)/3) + self._ET = (ET if self._ET == -1 else (2*ET + self._ET)/3) def getBT(self) -> float: return self._BT diff --git a/wiki/ReleaseHistory.md b/wiki/ReleaseHistory.md index 745b98c28..95d176e37 100644 --- a/wiki/ReleaseHistory.md +++ b/wiki/ReleaseHistory.md @@ -18,6 +18,7 @@ v3.0.3 - adds [Loring](https://artisan-scope.org/machines/loring/) 'auto' setup which picks up CHARGE and DROP events set at the machine - adds control function to [Diedrich DR](https://artisan-scope.org/machines/diedrich/) machine setup and adds [Diedrich CR](https://artisan-scope.org/machines/diedrich/) machine setup - adds support for [Phidget Stepper Motor Controllers](https://artisan-scope.org/devices/phidgets/#47-stepper-motor-control) ([Discussion #891](../../../discussions/891) and [PR #1715](../../../pull/1715)) + - adds option to start the RampSoak timer on PID ON instead on CHARGE ([Discussion #1720](../../../discussions/1720)) * CHANGES - automatically start of the scheduler on connected to [artisan.plus](https://artisan.plus) if there are incompleted scheduled items