From be4736e87947f82b6ff53b4b66c4feb12e374bde Mon Sep 17 00:00:00 2001 From: MAKOMO Date: Fri, 3 Nov 2023 21:34:37 +0100 Subject: [PATCH] - upgrades matplotlib to 3.8.1 - downgrades modbusport from async to sync - fixes regression which could lead to wrong temperature mode selection - improves performance of decay_average() - aligns background and foreground smoothing during recording (Issue 1279) --- src/artisanlib/canvas.py | 228 +++++++++-------- src/artisanlib/main.py | 5 +- src/artisanlib/modbusport.py | 285 ++++++++-------------- src/includes/Machines/Ambex/YM.aset | 2 +- src/includes/Machines/Cogen/Series_C.aset | 2 +- src/requirements.txt | 2 +- 6 files changed, 236 insertions(+), 288 deletions(-) diff --git a/src/artisanlib/canvas.py b/src/artisanlib/canvas.py index f611233d8..9820c5a9e 100644 --- a/src/artisanlib/canvas.py +++ b/src/artisanlib/canvas.py @@ -281,7 +281,8 @@ class tgraphcanvas(FigureCanvas): 'filterDropOut_replaceRoR_period', 'filterDropOut_spikeRoR_period', 'filterDropOut_tmin_C_default', 'filterDropOut_tmax_C_default', 'filterDropOut_tmin_F_default', 'filterDropOut_tmax_F_default', 'filterDropOut_spikeRoR_dRoR_limit_C_default', 'filterDropOut_spikeRoR_dRoR_limit_F_default', 'filterDropOuts', 'filterDropOut_tmin', 'filterDropOut_tmax', 'filterDropOut_spikeRoR_dRoR_limit', 'minmaxLimits', - 'dropSpikes', 'dropDuplicates', 'dropDuplicatesLimit', 'liveMedianRoRfilter', 'liveMedianETfilter', 'liveMedianBTfilter', 'interpolatemax', 'swapETBT', 'wheelflag', 'wheelnames', 'segmentlengths', 'segmentsalpha', + 'dropSpikes', 'dropDuplicates', 'dropDuplicatesLimit', 'median_filter_factor', 'liveMedianETRoRfilter', 'liveMedianBTRoRfilter', + 'liveMedianETfilter', 'liveMedianBTfilter', 'interpolatemax', 'swapETBT', 'wheelflag', 'wheelnames', 'segmentlengths', 'segmentsalpha', 'wheellabelparent', 'wheelcolor', 'wradii', 'startangle', 'projection', 'wheeltextsize', 'wheelcolorpattern', 'wheeledge', 'wheellinewidth', 'wheellinecolor', 'wheeltextcolor', 'wheelconnections', 'wheelx', 'wheelz', 'wheellocationx', 'wheellocationz', 'wheelaspect', 'samplingSemaphore', 'updateGraphicsSemaphore', 'profileDataSemaphore', 'messagesemaphore', 'errorsemaphore', 'serialsemaphore', 'seriallogsemaphore', @@ -1936,9 +1937,12 @@ def __init__(self, parent:QWidget, dpi:int, locale:str, aw:'ApplicationWindow') self.dropDuplicates:bool = False self.dropDuplicatesLimit:float = 0.3 - self.liveMedianETfilter:LiveMedian = LiveMedian(3) - self.liveMedianBTfilter:LiveMedian = LiveMedian(3) - self.liveMedianRoRfilter:LiveMedian = LiveMedian(5) # the offline filter uses a window length of 5, introducing some delay, compared to the medfilt() in offline mode which does not introduce any delay + # self.median_filter_factor: factor used for MedianFilter on both, temperature and RoR curves + self.median_filter_factor:Final[int] = 5 # k=3 is conservative seems not to catch all spikes in all cases; k=5 and k=7 seems to be ok; 13 might be the maximum; k must be odd! + self.liveMedianETfilter:LiveMedian = LiveMedian(self.median_filter_factor) + self.liveMedianBTfilter:LiveMedian = LiveMedian(self.median_filter_factor) + self.liveMedianETRoRfilter:LiveMedian = LiveMedian(self.median_filter_factor) + self.liveMedianBTRoRfilter:LiveMedian = LiveMedian(self.median_filter_factor) self.interpolatemax:Final[int] = 3 # maximal number of dropped readings (-1) that will be interpolated @@ -3392,32 +3396,32 @@ def inputFilter(self, timex, tempx, time, temp, BT=False): # to linear time based on tx and the current sampling interval # -1 and None values are skipped/ignored def decay_average(self, tx_in,temp_in,decay_weights): - if len(tx_in) != len(temp_in): + if len(decay_weights)<2 or len(tx_in) != len(temp_in): if len(temp_in)>0: return temp_in[-1] return -1 - # remove items where temp[i]=None to fulfil precond. of numpy.interp - tx = [] - temp = [] - for i, tempin in enumerate(temp_in): - if tempin not in [None, -1] and not numpy.isnan(tempin): - tx.append(tx_in[i]) - temp.append(tempin) - if len(temp) == 0: + l = min(len(decay_weights),len(temp_in)) + # take trail of length l and remove items where temp[i]=None to fulfil precond. of numpy.interp + tx_org = [] + temp_trail = [] + for x, tp in zip(tx_in[-l:],temp_in[-l:]): # we only iterate over l-elements + if tp not in [None, -1]: + tx_org.append(x) + temp_trail.append(tp) + if len(temp_trail) == 0: + # no valid values return -1 - # - l = min(len(decay_weights),len(temp)) + l = len(temp_trail) # might be shorter than before + # len(tx)=len(temp) here and it is guaranteed that len(tx_org)=len(temp_trail) = l d = self.delay / 1000. - tx_org = tx[-l:] # as len(tx)=len(temp) here, it is guaranteed that len(tx_org)=l # we create a linearly spaced time array starting from the newest timestamp in sampling interval distance tx_lin = numpy.flip(numpy.arange(tx_org[-1],tx_org[-1]-l*d,-d), axis=0) # by construction, len(tx_lin)=len(tx_org)=l - temp_trail = temp[-l:] # by construction, len(temp_trail)=len(tx_lin)=len(tx_org)=l temp_trail_re = numpy.interp(tx_lin, tx_org, temp_trail) # resample data into that linear spaced time try: return numpy.average(temp_trail_re[-len(decay_weights):],axis=0,weights=decay_weights[-l:]) # len(decay_weights)>len(temp_trail_re)=l is possible except Exception: # pylint: disable=broad-except # in case something goes very wrong we at least return the standard average over temp, this should always work as len(tx)=len(temp) - return numpy.average(tx,temp) + return numpy.average(tx_org,temp_trail) # returns true after BT passed the TP def checkTPalarmtime(self): @@ -3707,8 +3711,8 @@ def sample_processing(self, local_flagstart:bool, temp1_readings:List[float], te else: st2 = -1 - # we apply a minimal live median spike filter minimizing the delay by choosing a window smaller than in the offline medfilt - if self.filterDropOuts and self.delay <= 2000: + # we apply a minimal live median spike filter + if self.filterDropOuts: if st1 is not None and st1 != -1: st1 = self.liveMedianETfilter(st1) if st1 is not None and st2 != -1: @@ -3828,9 +3832,9 @@ def sample_processing(self, local_flagstart:bool, temp1_readings:List[float], te # we apply a minimal live median spike filter minimizing the delay by choosing a window smaller than in the offline medfilt if self.filterDropOuts and self.delay <= 2000: if self.rateofchange1 is not None and self.rateofchange1 != -1: - self.rateofchange1 = self.liveMedianRoRfilter(self.rateofchange1) + self.rateofchange1 = self.liveMedianETRoRfilter(self.rateofchange1) if self.rateofchange2 is not None and self.rateofchange2 != -1: - self.rateofchange2 = self.liveMedianRoRfilter(self.rateofchange2) + self.rateofchange2 = self.liveMedianBTRoRfilter(self.rateofchange2) sample_unfiltereddelta1.append(self.rateofchange1) sample_unfiltereddelta2.append(self.rateofchange2) @@ -4302,7 +4306,7 @@ def updateLCDs(self, time:Optional[float], temp1:List[float], temp2:List[float], @pyqtSlot() def updategraphics(self) -> None: # QApplication.processEvents() # without this we see some flickers (canvas redraws) on using multiple button event actions on macOS!? - gotlock = self.aw.qmc.updateGraphicsSemaphore.tryAcquire(1,150) # we try to catch a lock if available but we do not wait, if we fail we just skip this redraw round (prevents stacking of waiting calls); we maximally wait 150ms which should be enough on modern machines + gotlock = self.aw.qmc.updateGraphicsSemaphore.tryAcquire(1,200) # we try to catch a lock if available but we do not wait, if we fail we just skip this redraw round (prevents stacking of waiting calls); we maximally wait 200ms which should be enough on modern machines if not gotlock: _log.info('updategraphics(): failed to get updateGraphicsSemaphore lock') else: @@ -6309,7 +6313,9 @@ def formtime(self, x:float, _pos:Optional[int]) -> str: # returns True if nothing to save, discard or save was selected and False if canceled by the user def checkSaved(self,allow_discard:bool = True) -> bool: #prevents deleting accidentally a finished roast - if self.safesaveflag and len(self.timex) > 3: + flag = self.safesaveflag + self.safesaveflag = False + if flag and len(self.timex) > 3: if allow_discard: string = QApplication.translate('Message','Save profile?') buttons = QMessageBox.StandardButton.Discard|QMessageBox.StandardButton.Save|QMessageBox.StandardButton.Cancel @@ -6317,6 +6323,7 @@ def checkSaved(self,allow_discard:bool = True) -> bool: string = QApplication.translate('Message','Save profile?') buttons = QMessageBox.StandardButton.Save|QMessageBox.StandardButton.Cancel reply = QMessageBox.warning(self.aw, QApplication.translate('Message','Profile unsaved'), string, buttons) + self.safesaveflag = flag if reply == QMessageBox.StandardButton.Save: return bool(self.aw.fileSave(self.aw.curFile)) #if accepted, calls fileClean() and thus turns safesaveflag = False if reply == QMessageBox.StandardButton.Discard: @@ -6833,10 +6840,12 @@ def smooth(self, x, y, window_len=15, window='hanning'): # re-sample, filter and smooth slice # takes numpy arrays a (time) and b (temp) of the same length and returns a numpy array representing the processed b values + # delta: True if b is a RoR signal # precondition: (self.filterDropOuts or window_len>2) - def smooth_slice(self, a, b, - window_len=7, window='hanning',decay_weights=None,decay_smoothing=False, - re_sample=True,back_sample=True,a_lin=None): + def smooth_slice(self, a:'npt.NDArray[numpy.floating]', b:'npt.NDArray[numpy.floating]', + window_len:int = 7, window:str = 'hanning', decay_weights:Optional[List[int]] = None, decay_smoothing:bool = False, + re_sample:bool = True, back_sample:bool = True, a_lin:Optional['npt.NDArray[numpy.floating]'] = None, + delta:bool=False) -> 'npt.NDArray[numpy.floating]': # 1. re-sample if re_sample: @@ -6847,42 +6856,46 @@ def smooth_slice(self, a, b, b = numpy.interp(a_mod, a, b) # resample data to linear spaced time else: a_mod = a - res = b # just in case the precondition (self.filterDropOuts or window_len>2) does not hold + res:List[float] = b.tolist() # just in case the precondition (self.filterDropOuts or window_len>2) does not hold # 2. filter spikes if self.filterDropOuts: try: - b = self.medfilt(b,5) # k=3 seems not to catch all spikes in all cases; k=5 and k=7 seems to be ok; 13 might be the maximum; k must be odd! + if delta and self.flagon: + online_medfilt = LiveMedian(self.median_filter_factor) + b = numpy.array(list(map(online_medfilt, b))) + else: + b = self.medfilt(b, self.median_filter_factor) # scipyernative which performs equal, but produces larger artefacts at the borders and for intermediate NaN values for k>3 # from scipy.signal import medfilt as scipy_medfilt # b = scipy_medfilt(b,3) - res = b + res = b.tolist() except Exception as e: # pylint: disable=broad-except _log.exception(e) - res = b + res = b.tolist() # 3. smooth data if window_len>2: if decay_smoothing: # decay smoothing + decay_weights_internal:'npt.NDArray[numpy.integer]' if decay_weights is None: - decay_weights = numpy.arange(1,window_len+1) + decay_weights_internal = numpy.arange(1,window_len+1) else: window_len = len(decay_weights) - # invariant: window_len = len(decay_weights) - if decay_weights.sum() == 0: - res = b + decay_weights_internal = numpy.array(decay_weights) + # invariant: window_len = len(decay_weights_internal) + if decay_weights_internal.sum() == 0: + res = b.tolist() else: res = [] # ignore -1 readings in averaging and ensure a good ramp for i, v in enumerate(b): seq = b[max(0,i-window_len + 1):i+1] -# # we need to suppress -1 drop out values from this -# seq = list(filter(lambda item: item != -1,seq)) # -1 drop out values in b have already been replaced by numpy.nan above - - w = decay_weights[max(0,window_len-len(seq)):] # preCond: len(decay_weights)=window_len and len(seq) <= window_len; postCond: len(w)=len(seq) + w = decay_weights_internal[max(0,window_len-len(seq)):] # preCond: len(decay_weights_internal)=window_len and len(seq) <= window_len; postCond: len(w)=len(seq) if len(w) == 0: - res.append(v) # we don't average if there is are no weights (e.g. if the original seq did only contain -1 values and got empty) + # we don't average if there is are no weights (e.g. if the original seq did only contain -1 values and got empty) + res.append(v) else: - res.append(numpy.average(seq,weights=w)) # works only if len(seq) = len(w) + res.append(numpy.average(seq,axis=0,weights=w)) # works only if len(seq) = len(w) # postCond: len(res) = len(b) else: # optimal smoothing (the default) @@ -6890,11 +6903,11 @@ def smooth_slice(self, a, b, if win_len != 1: # at the lowest level we turn smoothing completely off res = self.smooth(a_mod,b,win_len,window) else: - res = b + res = b.tolist() # 4. sample back if re_sample and back_sample: - res = numpy.interp(a, a_mod, res) # re-sampled back to original timestamps - return res + res = numpy.interp(a, a_mod, res).tolist() # re-sampled back to original timestamps + return numpy.array(res) # takes lists a (time array) and b (temperature array) containing invalid segments of -1/None values and returns a list with all segments of valid values smoothed # a: list of timestamps @@ -6902,11 +6915,12 @@ def smooth_slice(self, a, b, # re_sample: if true re-sample readings to a linear spaced time before smoothing # back_sample: if true results are back-sampled to original timestamps given in "a" after smoothing # a_lin: pre-computed linear spaced timestamps of equal length than a + # delta: True if b is a RoR signal # NOTE: result can contain NaN items on places where the input array contains the error element -1 # result is a numpy array or the b as numpy array with drop out readings -1 replaced by NaN def smooth_list(self, aa:Union['npt.NDArray[numpy.floating]', Sequence[float]], b:List[float], window_len:int = 7, window:str = 'hanning', decay_weights:Optional[List[int]] = None, decay_smoothing:bool = False, fromIndex:int = -1, toIndex:int = 0, - re_sample:bool = True, back_sample:bool = True, a_lin:Optional['npt.NDArray[numpy.floating]'] = None) -> 'npt.NDArray[numpy.floating]': + re_sample:bool = True, back_sample:bool = True, a_lin:Optional['npt.NDArray[numpy.floating]'] = None, delta:bool=False) -> 'npt.NDArray[numpy.floating]': if len(aa) > 1 and len(aa) == len(b) and (self.filterDropOuts or window_len>2): #pylint: disable=E1103 # 1. truncate @@ -6932,7 +6946,7 @@ def smooth_list(self, aa:Union['npt.NDArray[numpy.floating]', Sequence[float]], b_smoothed.append(numpy.full(s.stop - s.start, numpy.nan, dtype=numpy.double)) else: # a slice with proper data - b_smoothed.append(self.smooth_slice(a[s], mb[s], window_len, window, decay_weights, decay_smoothing, re_sample, back_sample, a_lin)) + b_smoothed.append(self.smooth_slice(a[s], mb[s], window_len, window, decay_weights, decay_smoothing, re_sample, back_sample, a_lin, delta)) b_smoothed.append(numpy.full(len(a)-toIndex, numpy.nan, dtype=numpy.double)) # append the final segment to the list of resulting segments return numpy.concatenate(b_smoothed) bb = numpy.array(b, dtype=numpy.floating) @@ -7380,7 +7394,7 @@ def computeDeltas(self, timex:'npt.NDArray[numpy.floating]', temp:Optional[Union user_filter = deltaFilter else: user_filter = int(round(deltaFilter/2.)) - delta1 = self.smooth_list(timex,z1,window_len=user_filter,decay_smoothing=(not optimalSmoothing),a_lin=timex_lin) + delta1 = self.smooth_list(timex,z1,window_len=user_filter,decay_smoothing=(not optimalSmoothing),a_lin=timex_lin,delta=True) # cut out the part after DROP and before CHARGE and remove values beyond the RoRlimit return [ @@ -7728,20 +7742,20 @@ def smoothETBT(self,smooth,recomputeAllDeltas,sampling,decay_smoothing_p): if self.flagon: # we don't smooth, but remove the dropouts self.stemp1 = temp1_nogaps else: - self.stemp1 = list(self.smooth_list(self.timex,temp1_nogaps,window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=timex_lin)) + self.stemp1 = list(self.smooth_list(self.timex,temp1_nogaps,window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=timex_lin,delta=False)) if smooth or len(self.stemp2) != len(self.timex): if self.flagon: # we don't smooth, but remove the dropouts self.stemp2 = temp2_nogaps else: - self.stemp2 = list(self.smooth_list(self.timex,temp2_nogaps,window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=timex_lin)) + self.stemp2 = list(self.smooth_list(self.timex,temp2_nogaps,window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=timex_lin,delta=False)) #populate delta ET (self.delta1) and delta BT (self.delta2) # calculated here to be available for parsepecialeventannotations(). the curve are plotted later. if (recomputeAllDeltas or (self.DeltaETflag and self.delta1 == []) or (self.DeltaBTflag and self.delta2 == [])) and not self.flagstart: # during recording we don't recompute the deltas - cf = self.curvefilter #*2 # we smooth twice as heavy for PID/RoR calculation as for normal curve smoothing + cf = self.curvefilter decay_smoothing_p = not self.optimalSmoothing or sampling or self.flagon - t1 = self.smooth_list(self.timex,temp1_nogaps,window_len=cf,decay_smoothing=decay_smoothing_p,a_lin=timex_lin) - t2 = self.smooth_list(self.timex,temp2_nogaps,window_len=cf,decay_smoothing=decay_smoothing_p,a_lin=timex_lin) + t1 = self.smooth_list(self.timex,temp1_nogaps,window_len=cf,decay_smoothing=decay_smoothing_p,a_lin=timex_lin,delta=False) + t2 = self.smooth_list(self.timex,temp2_nogaps,window_len=cf,decay_smoothing=decay_smoothing_p,a_lin=timex_lin,delta=False) # we start RoR computation 10 readings after CHARGE to avoid this initial peak if self.timeindex[0]>-1: RoR_start = min(self.timeindex[0]+10, len(self.timex)-1) @@ -7767,9 +7781,11 @@ def smoothETBTBkgnd(self,recomputeAllDeltas,decay_smoothing_p): timeB_lin = None # we populate temporary smoothed ET/BT data arrays +# CHANGE ML: cf = self.curvefilter #*2 # we smooth twice as heavy for PID/RoR calculation as for normal curve smoothing - st1 = self.smooth_list(self.timeB,fill_gaps(self.temp1B),window_len=cf,decay_smoothing=decay_smoothing_p,a_lin=timeB_lin) - st2 = self.smooth_list(self.timeB,fill_gaps(self.temp2B),window_len=cf,decay_smoothing=decay_smoothing_p,a_lin=timeB_lin) +# cf = (1 if self.flagon else self.curvefilter) # no curve smoothing during recording + st1 = self.smooth_list(self.timeB,fill_gaps(self.temp1B),window_len=cf,decay_smoothing=decay_smoothing_p,a_lin=timeB_lin,delta=False) + st2 = self.smooth_list(self.timeB,fill_gaps(self.temp2B),window_len=cf,decay_smoothing=decay_smoothing_p,a_lin=timeB_lin,delta=False) # we start RoR computation 10 readings after CHARGE to avoid this initial peak if self.timeindexB[0]>-1: RoRstart = min(self.timeindexB[0]+10, len(self.timeB)-1) @@ -8200,8 +8216,9 @@ def redraw(self, recomputeAllDeltas=True, smooth=True, sampling=False, takelock= tb_lin = numpy.linspace(tb[0],tb[-1],len(tb)) else: tb_lin = None - self.stemp1B = self.smooth_list(tb,fill_gaps(t1),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=tb_lin) - self.stemp2B = self.smooth_list(tb,fill_gaps(t2),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=tb_lin) + # no smoothing (curvefilter = 1) if sampling! + self.stemp1B = self.smooth_list(tb,fill_gaps(t1),window_len=(1 if self.flagon else self.curvefilter),decay_smoothing=decay_smoothing_p,a_lin=tb_lin,delta=False) + self.stemp2B = self.smooth_list(tb,fill_gaps(t2),window_len=(1 if self.flagon else self.curvefilter),decay_smoothing=decay_smoothing_p,a_lin=tb_lin,delta=False) self.l_background_annotations = [] #check to see if there is both a profile loaded and a background loaded @@ -8233,7 +8250,7 @@ def redraw(self, recomputeAllDeltas=True, smooth=True, sampling=False, takelock= else: trans = self.ax.transData if smooth: - self.stemp1BX[n3] = self.smooth_list(tx,fill_gaps(self.temp1BX[n3]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=tx_lin) + self.stemp1BX[n3] = self.smooth_list(tx,fill_gaps(self.temp1BX[n3]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=tx_lin,delta=False) stemp3B = self.stemp1BX[n3] else: if self.temp2Bdelta[n3] and self.delta_ax is not None: @@ -8241,7 +8258,7 @@ def redraw(self, recomputeAllDeltas=True, smooth=True, sampling=False, takelock= else: trans = self.ax.transData if smooth: - self.stemp2BX[n3] = self.smooth_list(tx,fill_gaps(self.temp2BX[n3]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=tx_lin) + self.stemp2BX[n3] = self.smooth_list(tx,fill_gaps(self.temp2BX[n3]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=tx_lin,delta=False) stemp3B = self.stemp2BX[n3] if not self.backgroundShowFullflag: if not self.autotimex or self.autotimexMode == 0: @@ -8281,7 +8298,7 @@ def redraw(self, recomputeAllDeltas=True, smooth=True, sampling=False, takelock= else: trans = self.ax.transData if smooth: - self.stemp1BX[n4] = self.smooth_list(tx,fill_gaps(self.temp1BX[n4]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=tx_lin) + self.stemp1BX[n4] = self.smooth_list(tx,fill_gaps(self.temp1BX[n4]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=tx_lin,delta=False) stemp4B = self.stemp1BX[n4] else: if self.temp2Bdelta[n4] and self.delta_ax is not None: @@ -8289,7 +8306,7 @@ def redraw(self, recomputeAllDeltas=True, smooth=True, sampling=False, takelock= else: trans = self.ax.transData if smooth: - self.stemp2BX[n4] = self.smooth_list(tx,fill_gaps(self.temp2BX[n4]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=tx_lin) + self.stemp2BX[n4] = self.smooth_list(tx,fill_gaps(self.temp2BX[n4]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=tx_lin,delta=False) stemp4B = self.stemp2BX[n4] if not self.backgroundShowFullflag: if not self.autotimex or self.autotimexMode == 0: @@ -9506,7 +9523,7 @@ def redraw(self, recomputeAllDeltas=True, smooth=True, sampling=False, takelock= try: if self.aw.extraCurveVisibility1[i]: if not self.flagon and (smooth or len(self.extrastemp1[i]) != len(self.extratimex[i])): - self.extrastemp1[i] = self.smooth_list(self.extratimex[i],fill_gaps(self.extratemp1[i]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=timexi_lin).tolist() + self.extrastemp1[i] = self.smooth_list(self.extratimex[i],fill_gaps(self.extratemp1[i]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=timexi_lin,delta=False).tolist() else: # we don't smooth, but remove the dropouts self.extrastemp1[i] = fill_gaps(self.extratemp1[i]) if self.aw.extraDelta1[i] and self.delta_ax is not None: @@ -9539,7 +9556,7 @@ def redraw(self, recomputeAllDeltas=True, smooth=True, sampling=False, takelock= try: if self.aw.extraCurveVisibility2[i]: if not self.flagon and (smooth or len(self.extrastemp2[i]) != len(self.extratimex[i])): - self.extrastemp2[i] = self.smooth_list(self.extratimex[i],fill_gaps(self.extratemp2[i]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=timexi_lin).tolist() + self.extrastemp2[i] = self.smooth_list(self.extratimex[i],fill_gaps(self.extratemp2[i]),window_len=self.curvefilter,decay_smoothing=decay_smoothing_p,a_lin=timexi_lin,delta=False).tolist() else: self.extrastemp2[i] = fill_gaps(self.extratemp2[i]) if self.aw.extraDelta2[i] and self.delta_ax is not None: @@ -11292,6 +11309,12 @@ def resetTimer(self): @pyqtSlot() def OnMonitor(self): try: + self.generateNoneTempHints() + self.block_update = True # block the updating of the bitblit canvas (unblocked at the end of this function to avoid multiple redraws) + res = self.reset(False,False,sampling=True,keepProperties=True) + if not res: # reset canceled + return + if self.aw.simulator is None: self.startPhidgetManager() # collect ambient data if any @@ -11308,13 +11331,6 @@ def OnMonitor(self): # warm up software PID (write current p-i-d settings,..) self.aw.pidcontrol.confSoftwarePID() - self.generateNoneTempHints() - self.block_update = True # block the updating of the bitblit canvas (unblocked at the end of this function to avoid multiple redraws) - res = self.reset(False,False,sampling=True,keepProperties=True) - if not res: # reset canceled - self.OffMonitor() - return - if not bool(self.aw.simulator): if self.device == 53: # connect HOTTOP @@ -11387,8 +11403,6 @@ def OnMonitor(self): _, _, exc_tb = sys.exc_info() self.adderror((QApplication.translate('Error Message', 'Exception:') + ' Bluetooth BLE support not available {0}').format(str(ex)),getattr(exc_tb, 'tb_lineno', '?')) - - self.aw.initializedMonitoringExtraDeviceStructures() #reset alarms @@ -11399,7 +11413,7 @@ def OnMonitor(self): self.TPalarmtimeindex = None self.flagon = True - self.redraw(True,sampling=True,smooth=self.optimalSmoothing) # we need to re-smooth with standard smoothing if ON and optimal-smoothing is ticked + self.redraw(True,sampling=True,smooth=True) # we need to re-smooth background with no curve-smoothing and standard instead of optimal-smoothing on ON if self.designerflag: return @@ -11414,7 +11428,6 @@ def OnMonitor(self): except Exception as e: # pylint: disable=broad-except _log.exception(e) -# QApplication.processEvents() if self.aw.simulator: self.aw.buttonONOFF.setStyleSheet(self.aw.pushbuttonstyles_simulator['ON']) else: @@ -11455,6 +11468,8 @@ def OnMonitor(self): _log.exception(ex) _, _, exc_tb = sys.exc_info() self.adderror((QApplication.translate('Error Message', 'Exception:') + ' OnMonitor() {0}').format(str(ex)),getattr(exc_tb, 'tb_lineno', '?')) + finally: + self.block_update = False # unblock the updating of the bitblit canvas # OffMonitorCloseDown is called after the sampling loop stopped @pyqtSlot() @@ -11597,42 +11612,43 @@ def OffMonitorCloseDown(self): def OffMonitor(self): _log.info('MODE: OFF MONITOR') - try: - # first activate "Stopping Mode" to ensure that sample() is not resetting the timer now (independent of the flagstart state) - - self.aw.buttonONOFF.setEnabled(False) - ge:Optional[QGraphicsEffect] = self.aw.buttonONOFF.graphicsEffect() - if ge is not None: - ge.setEnabled(False) - self.aw.buttonSTARTSTOP.setEnabled(False) - ge = self.aw.buttonSTARTSTOP.graphicsEffect() - if ge is not None: - ge.setEnabled(False) - self.aw.buttonSTARTSTOP.setEnabled(False) - - self.aw.buttonCONTROL.setEnabled(False) - ge = self.aw.buttonCONTROL.graphicsEffect() - if ge is not None: - ge.setEnabled(False) - - # stop Recorder if still running - if self.flagstart: - self.OffRecorder(autosave=False, enableButton=False) # we autosave after the monitor is turned off to get all the data in the generated PDF! - + if self.flagon: try: - # trigger event action before disconnecting from devices - if self.extrabuttonactions[1] != 18: # Artisan Commands are executed after the OFFMonitor action is fully executed as they might trigger another buttons - self.aw.eventactionx(self.extrabuttonactions[1],self.extrabuttonactionstrings[1]) - except Exception as e: # pylint: disable=broad-except - _log.exception(e) + # first activate "Stopping Mode" to ensure that sample() is not resetting the timer now (independent of the flagstart state) + + self.aw.buttonONOFF.setEnabled(False) + ge:Optional[QGraphicsEffect] = self.aw.buttonONOFF.graphicsEffect() + if ge is not None: + ge.setEnabled(False) + self.aw.buttonSTARTSTOP.setEnabled(False) + ge = self.aw.buttonSTARTSTOP.graphicsEffect() + if ge is not None: + ge.setEnabled(False) + self.aw.buttonSTARTSTOP.setEnabled(False) + + self.aw.buttonCONTROL.setEnabled(False) + ge = self.aw.buttonCONTROL.graphicsEffect() + if ge is not None: + ge.setEnabled(False) + + # stop Recorder if still running + if self.flagstart: + self.OffRecorder(autosave=False, enableButton=False) # we autosave after the monitor is turned off to get all the data in the generated PDF! - self.threadserver.terminatingSignal.connect(self.OffMonitorCloseDown) - self.flagon = False + try: + # trigger event action before disconnecting from devices + if self.extrabuttonactions[1] != 18: # Artisan Commands are executed after the OFFMonitor action is fully executed as they might trigger another buttons + self.aw.eventactionx(self.extrabuttonactions[1],self.extrabuttonactionstrings[1]) + except Exception as e: # pylint: disable=broad-except + _log.exception(e) - except Exception as ex: # pylint: disable=broad-except - _log.exception(ex) - _, _, exc_tb = sys.exc_info() - self.adderror((QApplication.translate('Error Message', 'Exception:') + ' OffMonitor() {0}').format(str(ex)),getattr(exc_tb, 'tb_lineno', '?')) + self.threadserver.terminatingSignal.connect(self.OffMonitorCloseDown) + self.flagon = False + + except Exception as ex: # pylint: disable=broad-except + _log.exception(ex) + _, _, exc_tb = sys.exc_info() + self.adderror((QApplication.translate('Error Message', 'Exception:') + ' OffMonitor() {0}').format(str(ex)),getattr(exc_tb, 'tb_lineno', '?')) def getAmbientData(self): _log.debug('getAmbientData()') diff --git a/src/artisanlib/main.py b/src/artisanlib/main.py index 96ff239ff..40cfc0f1c 100644 --- a/src/artisanlib/main.py +++ b/src/artisanlib/main.py @@ -17826,7 +17826,8 @@ def saveAllSettings(self, settings:QSettings, default_settings:Optional[Dict[str pass #save mode - settings.setValue('Mode',self.qmc.mode) # 'Mode' is always stored as it is used to discriminate the ViewerSettings (see _settingsCopied) + if not read_defaults: + settings.setValue('Mode',self.qmc.mode) # 'Mode' is always stored as it is used to discriminate the ViewerSettings (see _settingsCopied) if filename is not None and not read_defaults: # only add those on exporting settings (those are never read by Artisan) @@ -18923,10 +18924,10 @@ def closeApp(self): flagKeepON = self.qmc.flagKeepON self.qmc.flagKeepON = False # temporarily turn keepOn off self.stopActivities() - self.qmc.flagKeepON = flagKeepON if unsaved_changes: # in case we have unsaved changes and the user decided to discard those, we first reset to have the correct settings (like axis limits) saved self.qmc.reset(redraw=False,soundOn=False,sampling=False,keepProperties=False,fireResetAction=False) + self.qmc.flagKeepON = flagKeepON if QApplication.queryKeyboardModifiers() != Qt.KeyboardModifier.AltModifier: self.closeEventSettings() # it takes quite some time to write the >1000 setting items # gc.collect() # this takes quite some time diff --git a/src/artisanlib/modbusport.py b/src/artisanlib/modbusport.py index 1247a57a5..ed8c07768 100644 --- a/src/artisanlib/modbusport.py +++ b/src/artisanlib/modbusport.py @@ -18,7 +18,6 @@ import sys import time import logging -import asyncio from typing import Optional, List, Dict, Tuple, Union, Any, TYPE_CHECKING from typing import Final # Python <=3.7 @@ -100,19 +99,17 @@ def getBinaryPayloadDecoderFromRegisters(registers:List[int], byteorderLittle:bo # pymodbus version class modbusport: - """This class handles the communications with all the modbus devices""" + """ this class handles the communications with all the modbus devices""" __slots__ = [ 'aw', 'modbus_serial_read_delay', 'modbus_serial_extra_read_delay', 'modbus_serial_write_delay', 'maxCount', 'readRetries', 'default_comport', 'comport', 'baudrate', 'bytesize', 'parity', 'stopbits', 'timeout', 'IP_timeout', 'IP_retries', 'serial_readRetries', 'PID_slave_ID', 'PID_SV_register', 'PID_p_register', 'PID_i_register', 'PID_d_register', 'PID_ON_action', 'PID_OFF_action', 'channels', 'inputSlaves', 'inputRegisters', 'inputFloats', 'inputBCDs', 'inputFloatsAsInt', 'inputBCDsAsInt', 'inputSigned', 'inputCodes', 'inputDivs', 'inputModes', 'optimizer', 'fetch_max_blocks', 'fail_on_cache_miss', 'disconnect_on_error', 'acceptable_errors', 'reset_socket', 'activeRegisters', 'readingsCache', 'SVmultiplier', 'PIDmultiplier', - 'byteorderLittle', 'wordorderLittle', 'master', 'COMsemaphore', 'default_host', 'host', 'port', 'type', 'lastReadResult', 'commError', '_loop' ] + 'byteorderLittle', 'wordorderLittle', 'master', 'COMsemaphore', 'default_host', 'host', 'port', 'type', 'lastReadResult', 'commError' ] def __init__(self, aw:'ApplicationWindow') -> None: self.aw = aw - self._loop: Optional[asyncio.AbstractEventLoop] = None # the asyncio loop - self.modbus_serial_read_delay :Final[float] = 0.035 # in seconds self.modbus_serial_extra_read_delay :float = 0.0 # in seconds (user configurable) self.modbus_serial_write_delay :Final[float] = 0.080 # in seconds @@ -195,7 +192,7 @@ def __init__(self, aw:'ApplicationWindow') -> None: def sleepBetween(self, write:bool = False) -> None: if write: pass # handled in MODBUS lib -# if self.type in [3,4]: # TCP or UDP +# if self.type in {3,4}: # TCP or UDP # pass # else: # time.sleep(self.modbus_serial_write_delay) @@ -211,29 +208,7 @@ def address2register(addr:Union[float, int], code:int = 3) -> int: return int(addr) - 30001 def isConnected(self) -> bool: - return self._loop is not None and not self._loop.is_closed() and self.master is not None and bool(self.master.connected) - -# async def cancel_tasks(self) -> None: -# tasks: List[asyncio.Task] = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] -# for task in tasks: -# task.cancel() -# await asyncio.gather(*tasks) -# -# def stop_asyncio_loop(self) -> None: -# if self._loop is not None: -# if not self._loop.is_closed(): -# if self._loop.is_running(): -# future = asyncio.run_coroutine_threadsafe(self.cancel_tasks(), self._loop) -# try: -# return future.result(0.2) # wait 0.2sec for cancellation of all tasks -# except TimeoutError: -# # the coroutine took too long, cancelling the task... -# future.cancel() -# except Exception as ex: # pylint: disable=broad-except -# _log.error(ex) -# self._loop.call_soon_threadsafe(self._loop.stop) # this just halts the loop -# self._loop.close() -# self._loop = None + return self.master is not None and bool(self.master.connected) # and bool(self.master.socket) def disconnect(self) -> None: try: @@ -243,7 +218,6 @@ def disconnect(self) -> None: self.clearReadingsCache() self.aw.sendmessage(QApplication.translate('Message', 'MODBUS disconnected')) del self.master -# self.stop_asyncio_loop() # we just keep the loop running except Exception as e: # pylint: disable=broad-except _log.exception(e) self.master = None @@ -270,16 +244,13 @@ def reconnect() -> None: def connect(self) -> None: if not self.isConnected(): - if self._loop is None or self._loop.is_closed(): - self._loop = asyncio.new_event_loop() - asyncio.set_event_loop(self._loop) self.commError = 0 try: # as in the following the port is None, no port is opened on creation of the (py)serial object if self.type == 1: # Serial ASCII - from pymodbus.client import AsyncModbusSerialClient + from pymodbus.client import ModbusSerialClient from pymodbus.transaction import ModbusAsciiFramer - self.master = AsyncModbusSerialClient( + self.master = ModbusSerialClient( framer=ModbusAsciiFramer, #method='ascii', # deprecated in pymodbus 3.x port=self.comport, @@ -297,9 +268,9 @@ def connect(self) -> None: timeout=min((self.aw.qmc.delay/2000), self.timeout)) # the timeout should not be larger than half of the sampling interval self.readRetries = self.serial_readRetries elif self.type == 2: # Serial Binary - from pymodbus.client import AsyncModbusSerialClient # @Reimport + from pymodbus.client import ModbusSerialClient # @Reimport from pymodbus.transaction import ModbusBinaryFramer - self.master = AsyncModbusSerialClient( + self.master = ModbusSerialClient( framer=ModbusBinaryFramer, #method='binary', # deprecated in pymodbus 3.x port=self.comport, @@ -317,9 +288,9 @@ def connect(self) -> None: timeout=min((self.aw.qmc.delay/2000), self.timeout)) # the timeout should not be larger than half of the sampling interval self.readRetries = self.serial_readRetries elif self.type == 3: # TCP - from pymodbus.client import AsyncModbusTcpClient + from pymodbus.client import ModbusTcpClient try: - self.master = AsyncModbusTcpClient( + self.master = ModbusTcpClient( host=self.host, port=self.port, retries=2, # number of send retries @@ -334,14 +305,14 @@ def connect(self) -> None: ) self.readRetries = self.IP_retries except Exception: # pylint: disable=broad-except - self.master = AsyncModbusTcpClient( + self.master = ModbusTcpClient( host=self.host, port=self.port, ) elif self.type == 4: # UDP - from pymodbus.client import AsyncModbusUdpClient + from pymodbus.client import ModbusUdpClient try: - self.master = AsyncModbusUdpClient( + self.master = ModbusUdpClient( host=self.host, port=self.port, retries=2, # number of send retries @@ -356,14 +327,14 @@ def connect(self) -> None: ) self.readRetries = self.IP_retries except Exception: # pylint: disable=broad-except # older versions of pymodbus don't support the retries, timeout nor the retry_on_empty arguments - self.master = AsyncModbusUdpClient( + self.master = ModbusUdpClient( host=self.host, port=self.port, ) else: # Serial RTU - from pymodbus.client import AsyncModbusSerialClient # @Reimport + from pymodbus.client import ModbusSerialClient # @Reimport from pymodbus.transaction import ModbusRtuFramer - self.master = AsyncModbusSerialClient( + self.master = ModbusSerialClient( framer=ModbusRtuFramer, #method='rtu', # deprecated in pymodbus 3.x port=self.comport, @@ -380,10 +351,9 @@ def connect(self) -> None: on_reconnect_callback=self.reconnect, timeout=min((self.aw.qmc.delay/2000), self.timeout)) # the timeout should not be larger than half of the sampling interval self.readRetries = self.serial_readRetries - _log.debug('connect_async(): connecting') + _log.debug('connect(): connecting') time.sleep(.2) # avoid possible hickups on startup - connect_task = self._loop.create_task(self.master.connect()) - self._loop.run_until_complete(connect_task) + self.master.connect() if self.isConnected(): self.updateActiveRegisters() self.clearReadingsCache() @@ -456,82 +426,76 @@ def invalidResult(res:Any, count:int) -> Tuple[bool, bool]: return False, False def readActiveRegisters(self) -> None: - _log.debug('readActiveRegisters()') if not self.optimizer: return - self.connect() - if self.isConnected(): - assert self._loop is not None - task = self._loop.create_task(self.readActiveRegistersAsync()) - self._loop.run_until_complete(task) - - async def readActiveRegistersAsync(self) -> None: error_disconnect = False # set to True if a serious error requiring a disconnect was detected try: - _log.debug('readActiveRegistersAsync_internal()') + _log.debug('readActiveRegisters()') #### lock shared resources ##### self.COMsemaphore.acquire(1) - assert self.master is not None - self.clearReadingsCache() - for code, slaves in self.activeRegisters.items(): - for slave, registers in slaves.items(): - registers_sorted = sorted(registers) - sequences:List[Tuple[int,int]] - if self.fetch_max_blocks: - sequences = [(registers_sorted[0],registers_sorted[-1])] - else: - # split in successive sequences - gaps = [[s, er] for s, er in zip(registers_sorted, registers_sorted[1:]) if s+1 < er] - edges = iter(registers_sorted[:1] + sum(gaps, []) + registers_sorted[-1:]) - sequences = list(zip(edges, edges)) # list of pairs of the form (start-register,end-register) - just_send:bool = False - for seq in sequences: - retry:int = self.readRetries - register:int = seq[0] - count:int = seq[1]-seq[0] + 1 - if 0 < count <= self.maxCount: - res:Optional['ModbusResponse'] = None - if just_send: - self.sleepBetween() # we start with a sleep, as it could be that just a send command happened before the semaphore was caught - just_send = True - tx:float = time.time() - while True: - _log.debug('readActive(%d,%d,%d,%d)', slave, code, register, count) - try: - # we cache only MODBUS function 3 and 4 (not 1 and 2!) - if code == 3: - res = await self.master.read_holding_registers(register,count,slave=slave) # type: ignore - elif code == 4: - res = await self.master.read_input_registers(register,count,slave=slave) # type: ignore - except Exception as e: # pylint: disable=broad-except - _log.info('readActive(%d,%d,%d,%d)', slave, code, register, count) - _log.debug(e) - res = None - error, disconnect = self.invalidResult(res,count) - if error: - error_disconnect = error_disconnect or disconnect - if retry > 0: - retry = retry - 1 - time.sleep(0.020) - _log.debug('retry') - else: + self.connect() + if self.isConnected(): + assert self.master is not None + self.clearReadingsCache() + for code, slaves in self.activeRegisters.items(): + for slave, registers in slaves.items(): + registers_sorted = sorted(registers) + sequences:List[Tuple[int,int]] + if self.fetch_max_blocks: + sequences = [(registers_sorted[0],registers_sorted[-1])] + else: + # split in successive sequences + gaps = [[s, er] for s, er in zip(registers_sorted, registers_sorted[1:]) if s+1 < er] + edges = iter(registers_sorted[:1] + sum(gaps, []) + registers_sorted[-1:]) + sequences = list(zip(edges, edges)) # list of pairs of the form (start-register,end-register) + just_send:bool = False + for seq in sequences: + retry:int = self.readRetries + register:int = seq[0] + count:int = seq[1]-seq[0] + 1 + if 0 < count <= self.maxCount: + res:Optional['ModbusResponse'] = None + if just_send: + self.sleepBetween() # we start with a sleep, as it could be that just a send command happened before the semaphore was caught + just_send = True + tx:float = time.time() + while True: + _log.debug('readActive(%d,%d,%d,%d)', slave, code, register, count) + try: + # we cache only MODBUS function 3 and 4 (not 1 and 2!) + if code == 3: + res = self.master.read_holding_registers(register,count,slave=slave) + elif code == 4: + res = self.master.read_input_registers(register,count,slave=slave) + except Exception as e: # pylint: disable=broad-except + _log.info('readActive(%d,%d,%d,%d)', slave, code, register, count) + _log.debug(e) res = None - raise Exception('Exception response') # pylint: disable=broad-exception-raised - else: - break - - #note: logged chars should be unicode not binary - if self.aw.seriallogflag and res is not None and hasattr(res, 'registers'): - if self.type < 3: # serial MODBUS - ser_str = f'MODBUS readActiveregisters : {self.formatMS(tx,time.time())}ms => {self.comport},{self.baudrate},{self.bytesize},{self.parity},{self.stopbits},{self.timeout} || Slave = {slave} || Register = {register} || Code = {code} || Rx# = {len(res.registers)}' - else: # IP MODBUS - ser_str = f'MODBUS readActiveregisters : {self.formatMS(tx,time.time())}ms => {self.host}:{self.port} || Slave = {slave} || Register = {register} || Code = {code} || Rx# = {len(res.registers)}' - _log.debug(ser_str) - self.aw.addserial(ser_str) - - if res is not None and hasattr(res, 'registers'): - self.clearCommError() - self.cacheReadings(code,slave,register,res.registers) + error, disconnect = self.invalidResult(res,count) + if error: + error_disconnect = error_disconnect or disconnect + if retry > 0: + retry = retry - 1 + time.sleep(0.020) + _log.debug('retry') + else: + res = None + raise Exception('Exception response') # pylint: disable=broad-exception-raised + else: + break + + #note: logged chars should be unicode not binary + if self.aw.seriallogflag and res is not None and hasattr(res, 'registers'): + if self.type < 3: # serial MODBUS + ser_str = f'MODBUS readActiveregisters : {self.formatMS(tx,time.time())}ms => {self.comport},{self.baudrate},{self.bytesize},{self.parity},{self.stopbits},{self.timeout} || Slave = {slave} || Register = {register} || Code = {code} || Rx# = {len(res.registers)}' + else: # IP MODBUS + ser_str = f'MODBUS readActiveregisters : {self.formatMS(tx,time.time())}ms => {self.host}:{self.port} || Slave = {slave} || Register = {register} || Code = {code} || Rx# = {len(res.registers)}' + _log.debug(ser_str) + self.aw.addserial(ser_str) + + if res is not None and hasattr(res, 'registers'): + self.clearCommError() + self.cacheReadings(code,slave,register,res.registers) except Exception as ex: # pylint: disable=broad-except _log.debug(ex) @@ -557,10 +521,8 @@ def writeCoils(self, slave:int, register:int, values:List[bool]) -> None: self.COMsemaphore.acquire(1) self.connect() if self.isConnected(): - assert self._loop is not None assert self.master is not None - task = self._loop.create_task(self.master.write_coils(int(register),list(values),slave=int(slave))) # type: ignore - self._loop.run_until_complete(task) + self.master.write_coils(int(register),list(values),slave=int(slave)) time.sleep(.3) # avoid possible hickups on startup except Exception as ex: # pylint: disable=broad-except _log.info('writeCoils(%d,%d,%s)', slave, register, values) @@ -583,10 +545,8 @@ def writeCoil(self, slave:int ,register:int ,value:bool) -> None: self.COMsemaphore.acquire(1) self.connect() if self.isConnected(): - assert self._loop is not None assert self.master is not None - task = self._loop.create_task(self.master.write_coil(int(register),value,slave=int(slave))) # type: ignore - self._loop.run_until_complete(task) + self.master.write_coil(int(register),value,slave=int(slave)) time.sleep(.3) # avoid possible hickups on startup except Exception as ex: # pylint: disable=broad-except _log.info('writeCoil(%d,%d,%s) failed', slave, register, value) @@ -625,10 +585,8 @@ def writeSingleRegister(self, slave:int, register:int, value:float) -> None: self.COMsemaphore.acquire(1) self.connect() if self.isConnected(): - assert self._loop is not None assert self.master is not None - task = self._loop.create_task(self.master.write_register(int(register),int(round(value)),slave=int(slave))) # type: ignore - self._loop.run_until_complete(task) + self.master.write_register(int(register),int(round(value)),slave=int(slave)) time.sleep(.03) # avoid possible hickups on startup except Exception as ex: # pylint: disable=broad-except _log.info('writeSingleRegister(%d,%d,%s) failed', slave, register, value) @@ -654,10 +612,8 @@ def maskWriteRegister(self, slave:int, register:int, and_mask:int, or_mask:int) self.COMsemaphore.acquire(1) self.connect() if self.isConnected(): - assert self._loop is not None assert self.master is not None - task = self._loop.create_task(self.master.mask_write_register(int(register),int(and_mask),int(or_mask),slave=int(slave))) # type: ignore - self._loop.run_until_complete(task) + self.master.mask_write_register(int(register),int(and_mask),int(or_mask),slave=int(slave)) time.sleep(.03) except Exception as ex: # pylint: disable=broad-except _log.info('maskWriteRegister(%d,%d,%s,%s) failed', slave, register, and_mask, or_mask) @@ -692,10 +648,8 @@ def writeRegisters(self, slave:int, register:int, values:Union[List[int], int]) self.COMsemaphore.acquire(1) self.connect() if self.isConnected(): - assert self._loop is not None assert self.master is not None - task = self._loop.create_task(self.master.write_registers(int(register),values,slave=int(slave))) # type: ignore - self._loop.run_until_complete(task) + self.master.write_registers(int(register),values,slave=int(slave)) time.sleep(.03) except Exception as ex: # pylint: disable=broad-except _log.info('writeRegisters(%d,%d,%s) failed', slave, register, values) @@ -720,14 +674,12 @@ def writeWord(self, slave:int, register:int, value:float) -> None: self.COMsemaphore.acquire(1) self.connect() if self.isConnected(): - assert self._loop is not None assert self.master is not None builder = getBinaryPayloadBuilder(self.byteorderLittle,self.wordorderLittle) builder.add_32bit_float(float(value)) payload = builder.build() #payload:List[int] = [int.from_bytes(b,("little" if self.byteorderLittle else "big")) for b in builder.build()] - task = self._loop.create_task(self.master.write_registers(int(register),payload,slave=int(slave),skip_encode=True)) # type: ignore # pyright: ignore [reportGeneralTypeIssues] # Argument of type "list[bytes]" cannot be assigned to parameter "values" of type "List[int] | int" in function "write_registers" - self._loop.run_until_complete(task) + self.master.write_registers(int(register),payload,slave=int(slave),skip_encode=True) # type: ignore # pyright: ignore [reportGeneralTypeIssues] # Argument of type "list[bytes]" cannot be assigned to parameter "values" of type "List[int] | int" in function "write_registers" time.sleep(.03) except Exception as ex: # pylint: disable=broad-except _log.info('writeWord(%d,%d,%s) failed', slave, register, value) @@ -749,7 +701,6 @@ def writeBCD(self, slave:int, register:int, value:int) -> None: #### lock shared resources ##### self.COMsemaphore.acquire(1) if self.isConnected(): - assert self._loop is not None assert self.master is not None self.connect() builder = getBinaryPayloadBuilder(self.byteorderLittle,self.wordorderLittle) @@ -757,8 +708,7 @@ def writeBCD(self, slave:int, register:int, value:int) -> None: builder.add_16bit_uint(r) payload = builder.build() #payload:List[int] = [int.from_bytes(b,("little" if self.byteorderLittle else "big")) for b in builder.build()] - task = self._loop.create_task(self.master.write_registers(int(register),payload,slave=int(slave),skip_encode=True)) # type:ignore # pyright: ignore [reportGeneralTypeIssues] # Argument of type "list[bytes]" cannot be assigned to parameter "values" of type "List[int] | int" in function "write_registers" - self._loop.run_until_complete(task) + self.master.write_registers(int(register),payload,slave=int(slave),skip_encode=True) # type:ignore # pyright: ignore [reportGeneralTypeIssues] # Argument of type "list[bytes]" cannot be assigned to parameter "values" of type "List[int] | int" in function "write_registers" time.sleep(.03) except Exception as ex: # pylint: disable=broad-except _log.info('writeBCD(%d,%d,%s) failed', slave, register, value) @@ -783,14 +733,12 @@ def writeLong(self, slave:int, register:int, value:int) -> None: self.COMsemaphore.acquire(1) self.connect() if self.isConnected(): - assert self._loop is not None assert self.master is not None builder = getBinaryPayloadBuilder(self.byteorderLittle,self.wordorderLittle) builder.add_32bit_int(int(value)) payload = builder.build() #payload:List[int] = [int.from_bytes(b,("little" if self.byteorderLittle else "big")) for b in builder.build()] - task = self._loop.create_task(self.master.write_registers(int(register),payload,slave=int(slave),skip_encode=True)) # type: ignore # pyright: ignore [reportGeneralTypeIssues] # Argument of type "list[bytes]" cannot be assigned to parameter "values" of type "List[int] | int" in function "write_registers" - self._loop.run_until_complete(task) + self.master.write_registers(int(register),payload,slave=int(slave),skip_encode=True) # type: ignore # pyright: ignore [reportGeneralTypeIssues] # Argument of type "list[bytes]" cannot be assigned to parameter "values" of type "List[int] | int" in function "write_registers" time.sleep(.03) except Exception as ex: # pylint: disable=broad-except _log.info('writeLong(%d,%d,%s) failed', slave, register, value) @@ -833,15 +781,13 @@ def readFloat(self, slave:int, register:int,code:int=3, force:bool=False) -> Opt self.connect() res: Optional[ModbusResponse] if self.isConnected(): - assert self._loop is not None assert self.master is not None while True: try: if code==3: - task = self._loop.create_task(self.master.read_holding_registers(int(register),2,slave=int(slave))) # type: ignore + res = self.master.read_holding_registers(int(register),2,slave=int(slave)) else: - task = self._loop.create_task(self.master.read_input_registers(int(register),2,slave=int(slave))) # type: ignore - res = self._loop.run_until_complete(task) + res = self.master.read_input_registers(int(register),2,slave=int(slave)) except Exception as ex: # pylint: disable=broad-except _log.debug(ex) res = None @@ -917,15 +863,13 @@ def readBCD(self, slave:int, register:int, code:int = 3, force:bool = False) -> self.connect() res: Optional[ModbusResponse] if self.isConnected(): - assert self._loop is not None assert self.master is not None while True: try: if code==3: - task = self._loop.create_task(res = self.master.read_holding_registers(int(register),2,slave=int(slave))) # type: ignore + res = self.master.read_holding_registers(int(register),2,slave=int(slave)) else: - task = self._loop.create_task(res = self.master.read_input_registers(int(register),2,slave=int(slave))) # type: ignore - res = self._loop.run_until_complete(task) + res = self.master.read_input_registers(int(register),2,slave=int(slave)) except Exception as ex: # pylint: disable=broad-except _log.debug(ex) res = None @@ -979,17 +923,15 @@ def peekSingleRegister(self, slave:int, register:int, code:int = 3) -> Optional[ try: self.connect() if self.isConnected(): - assert self._loop is not None assert self.master is not None if code==1: - task = self._loop.create_task(self.master.read_coils(int(register),1,slave=int(slave))) # type: ignore + res = self.master.read_coils(int(register),1,slave=int(slave)) elif code==2: - task = self._loop.create_task(self.master.read_discrete_inputs(int(register),1,slave=int(slave))) # type: ignore + res = self.master.read_discrete_inputs(int(register),1,slave=int(slave)) elif code==4: - task = self._loop.create_task(self.master.read_input_registers(int(register),1,slave=int(slave))) # type: ignore + res = self.master.read_input_registers(int(register),1,slave=int(slave)) else: # code==3 - task = self._loop.create_task(self.master.read_holding_registers(int(register),1,slave=int(slave))) # type: ignore - res = self._loop.run_until_complete(task) + res = self.master.read_holding_registers(int(register),1,slave=int(slave)) except Exception as ex: # pylint: disable=broad-except _log.info('peekSingleRegister(%d,%d,%d) failed', slave, register, code) _log.debug(ex) @@ -1043,19 +985,17 @@ def readSingleRegister(self, slave:int, register:int, code:int = 3, force:bool = self.connect() res: Optional[ModbusResponse] if self.isConnected(): - assert self._loop is not None assert self.master is not None while True: try: if code==1: - task = self._loop.create_task(self.master.read_coils(int(register),1,slave=int(slave))) # type: ignore + res = self.master.read_coils(int(register),1,slave=int(slave)) elif code==2: - task = self._loop.create_task(self.master.read_discrete_inputs(int(register),1,slave=int(slave))) # type: ignore + res = self.master.read_discrete_inputs(int(register),1,slave=int(slave)) elif code==4: - task = self._loop.create_task(self.master.read_input_registers(int(register),1,slave=int(slave))) # type: ignore + res = self.master.read_input_registers(int(register),1,slave=int(slave)) else: # code==3 - task = self._loop.create_task(self.master.read_holding_registers(int(register),1,slave=int(slave))) # type: ignore - res = self._loop.run_until_complete(task) + res = self.master.read_holding_registers(int(register),1,slave=int(slave)) except Exception as ex: # pylint: disable=broad-except _log.debug(ex) res = None @@ -1141,18 +1081,12 @@ def readInt32(self, slave:int, register:int, code:int = 3,force:bool = False, si self.connect() res: Optional[ModbusResponse] if self.isConnected(): - assert self._loop is not None assert self.master is not None while True: - try: - if code==3: - task = self._loop.create_task(self.master.read_holding_registers(int(register),2,slave=int(slave))) # type: ignore - else: - task = self._loop.create_task(self.master.read_input_registers(int(register),2,slave=int(slave))) # type: ignore - res = self._loop.run_until_complete(task) - except Exception as ex: # pylint: disable=broad-except - _log.debug(ex) - res = None + if code==3: + res = self.master.read_holding_registers(int(register),2,slave=int(slave)) + else: + res = self.master.read_input_registers(int(register),2,slave=int(slave)) error, disconnect = self.invalidResult(res,2) if error: error_disconnect = error_disconnect or disconnect @@ -1227,17 +1161,14 @@ def readBCDint(self, slave:int, register:int, code:int = 3, force:bool = False) self.connect() res: Optional[ModbusResponse] if self.isConnected(): - assert self._loop is not None assert self.master is not None while True: try: if code==3: - task = self._loop.create_task(self.master.read_holding_registers(int(register),1,slave=int(slave))) # type: ignore + res = self.master.read_holding_registers(int(register),1,slave=int(slave)) else: - task = self._loop.create_task(self.master.read_input_registers(int(register),1,slave=int(slave))) # type: ignore - res = self._loop.run_until_complete(task) - except Exception as ex: # pylint: disable=broad-except - _log.debug(ex) + res = self.master.read_input_registers(int(register),1,slave=int(slave)) + except Exception: # pylint: disable=broad-except res = None error, disconnect = self.invalidResult(res,1) if error: diff --git a/src/includes/Machines/Ambex/YM.aset b/src/includes/Machines/Ambex/YM.aset index baa23198b..cbd67c1ed 100644 --- a/src/includes/Machines/Ambex/YM.aset +++ b/src/includes/Machines/Ambex/YM.aset @@ -33,7 +33,7 @@ input2code=3 input2div=0 input2float=true input2mode=F -input2register=450 +input2register=440 input2slave=1 input3FloatsAsInt=false input3bcd=false diff --git a/src/includes/Machines/Cogen/Series_C.aset b/src/includes/Machines/Cogen/Series_C.aset index 55ba0fa82..a41a790f1 100644 --- a/src/includes/Machines/Cogen/Series_C.aset +++ b/src/includes/Machines/Cogen/Series_C.aset @@ -68,7 +68,7 @@ baudrate=115200 bytesize=8 comport=COM5 fetch_max_blocks=false -host=127.0.0.1 +host=192.168.1.42 input1BCDsAsInt=false input1FloatsAsInt=false input1Signed=false diff --git a/src/requirements.txt b/src/requirements.txt index feeb80023..a93274f6e 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -60,7 +60,7 @@ keyring==24.2.0 prettytable==3.9.0 lxml==4.9.3 matplotlib==3.7.3; python_version < '3.9' # last Python 3.8 release -matplotlib==3.8.0; python_version >= '3.9' +matplotlib==3.8.1; python_version >= '3.9' jinja2==3.1.2 aiohttp==3.8.6 aiohttp_jinja2==1.5.1