Skip to content

Commit 8486915

Browse files
committed
- ensure that only future events are replayed (note that the set of future events may change on moving the background profile and in consequence an event can be replayed again)
- ramping replay ramps from either the last foreground event set or last background event passed, which ever is in closer distance, to the next background event - Santoker protocol uses received header for sending commands
1 parent cceb733 commit 8486915

File tree

8 files changed

+213
-107
lines changed

8 files changed

+213
-107
lines changed

src/artisanlib/canvas.py

+102-69
Large diffs are not rendered by default.

src/artisanlib/devices.py

+52-18
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from artisanlib.util import deltaLabelUTF8, setDeviceDebugLogLevel, argb_colorname2rgba_colorname, rgba_colorname2argb_colorname, toInt
3030
from artisanlib.dialogs import ArtisanResizeablDialog
3131
from artisanlib.widgets import MyContentLimitedQComboBox, MyQComboBox, MyQDoubleSpinBox, wait_cursor
32-
from artisanlib.scale import Scale, SUPPORTED_SCALES, ScaleSpecs
32+
from artisanlib.scale import SUPPORTED_SCALES, ScaleSpecs
3333

3434

3535
_log: Final[logging.Logger] = logging.getLogger(__name__)
@@ -65,6 +65,13 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
6565
self.org_santokerBLE = self.aw.santokerBLE
6666
self.org_kaleidoSerial = self.aw.kaleidoSerial
6767

68+
self.org_scale1_model = self.aw.scale1_model
69+
self.org_scale1_name = self.aw.scale1_name
70+
self.org_scale1_id = self.aw.scale1_id
71+
self.org_scale2_model = self.aw.scale2_model
72+
self.org_scale2_name = self.aw.scale2_name
73+
self.org_scale2_id = self.aw.scale2_id
74+
6875
################ TAB 1 WIDGETS
6976
#ETcurve
7077
self.ETcurve = QCheckBox(QApplication.translate('CheckBox', 'ET'))
@@ -1446,8 +1453,6 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
14461453

14471454
self.scale1_devices:ScaleSpecs = [] # discovered scale1 devices
14481455
self.scale2_devices:ScaleSpecs = [] # discovered scale2 devices
1449-
self.scale1:Optional[Scale] = None
1450-
self.scale2:Optional[Scale] = None
14511456

14521457
scale1ModelLabel = QLabel(QApplication.translate('Label','Model'))
14531458
self.scale1ModelComboBox = QComboBox()
@@ -1471,6 +1476,9 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
14711476
self.scale1NameComboBox.currentIndexChanged.connect(self.scale1NameChanged)
14721477
self.scale1ScanButton.clicked.connect(self.scanScale1)
14731478

1479+
if self.aw.scale1_name and self.aw.scale1_id:
1480+
self.updateScale1devices([(self.aw.scale1_name, self.aw.scale1_id)])
1481+
14741482
scale1Grid = QGridLayout()
14751483
scale1Grid.addWidget(scale1ModelLabel,0,0)
14761484
scale1Grid.addWidget(self.scale1ModelComboBox,0,1)
@@ -1497,11 +1505,17 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
14971505
self.scale2ScanButton.setEnabled(False)
14981506
elif self.aw.scale2_model < len(SUPPORTED_SCALES):
14991507
self.scale2ModelComboBox.setCurrentIndex(self.aw.scale2_model + 1)
1500-
if self.aw.scale2_name is not None:
1508+
if self.aw.scale2_name is None:
1509+
self.scale2NameComboBox.setEnabled(False)
1510+
else:
15011511
self.scale2NameComboBox.setEnabled(True)
15021512
self.scale2ModelComboBox.currentIndexChanged.connect(self.scale2ModelChanged)
1513+
self.scale2NameComboBox.currentIndexChanged.connect(self.scale2NameChanged)
15031514
self.scale2ScanButton.clicked.connect(self.scanScale2)
15041515

1516+
if self.aw.scale2_name and self.aw.scale2_id:
1517+
self.updateScale2devices([(self.aw.scale2_name, self.aw.scale2_id)])
1518+
15051519
scale2Grid = QGridLayout()
15061520
scale2Grid.addWidget(scale2ModelLabel,0,0)
15071521
scale2Grid.addWidget(self.scale2ModelComboBox,0,1)
@@ -1692,14 +1706,17 @@ def scale1ModelChanged(self, i:int) -> None:
16921706
@pyqtSlot(int)
16931707
def scale1NameChanged(self, i:int) -> None:
16941708
if 0 <= i < len(self.scale1_devices) and self.aw.scale1_model is not None:
1695-
if self.scale1 is not None:
1696-
self.scale1.disconnect()
1697-
self.scale1 = self.aw.scale_manager.get_scale(self.aw.scale1_model, self.scale1_devices[i][1])
1698-
if self.scale1 is not None:
1699-
self.scale1.connect()
1709+
self.aw.scale1_name = self.scale1_devices[i][0]
1710+
self.aw.scale1_id = self.scale1_devices[i][1]
1711+
scale = self.aw.scale_manager.get_scale(self.aw.scale1_model, self.scale1_devices[i][1])
1712+
self.aw.scale_manager.set_scale1(scale)
1713+
if scale is not None:
1714+
scale.connect()
17001715
# i == -1 if self.scale1NameComboBox is empty!
1701-
elif self.scale1 is not None:
1702-
self.scale1.disconnect()
1716+
else:
1717+
scale1 = self.aw.scale_manager.get_scale1()
1718+
if scale1 is not None:
1719+
scale1.disconnect()
17031720

17041721
def updateScale1devices(self, devices:ScaleSpecs) -> None:
17051722
self.scale1_devices = devices
@@ -1734,14 +1751,17 @@ def scale2ModelChanged(self, i:int) -> None:
17341751
@pyqtSlot(int)
17351752
def scale2NameChanged(self, i:int) -> None:
17361753
if 0 <= i < len(self.scale2_devices) and self.aw.scale2_model is not None:
1737-
if self.scale2 is not None:
1738-
self.scale2.disconnect()
1739-
self.scale2 = self.aw.scale_manager.get_scale(self.aw.scale2_model, self.scale2_devices[i][1])
1740-
if self.scale2 is not None:
1741-
self.scale2.connect()
1754+
self.aw.scale2_name = self.scale2_devices[i][0]
1755+
self.aw.scale2_id = self.scale2_devices[i][1]
1756+
scale = self.aw.scale_manager.get_scale(self.aw.scale2_model, self.scale2_devices[i][1])
1757+
self.aw.scale_manager.set_scale2(scale)
1758+
if scale is not None:
1759+
scale.connect()
17421760
# i == -1 if self.scale2NameComboBox is empty!
1743-
elif self.scale2 is not None:
1744-
self.scale2.disconnect()
1761+
else:
1762+
scale2 = self.aw.scale_manager.get_scale2()
1763+
if scale2 is not None:
1764+
scale2.disconnect()
17451765

17461766
def updateScale2devices(self, devices:ScaleSpecs) -> None:
17471767
self.scale2_devices = devices
@@ -2725,13 +2745,19 @@ def setextracolor(self, ll:int, i:int) -> None:
27252745
_t, _e, exc_tb = sys.exc_info()
27262746
self.aw.qmc.adderror((QApplication.translate('Error Message', 'Exception:') + ' setextracolor(): {0}').format(str(e)),getattr(exc_tb, 'tb_lineno', '?'))
27272747

2748+
# close is called from OK and CANCEL
27282749
def close(self) -> bool:
27292750
self.closeHelp()
27302751
settings = QSettings()
27312752
#save window geometry
27322753
settings.setValue('DeviceAssignmentGeometry',self.saveGeometry())
27332754
self.aw.DeviceAssignmentDlg_activeTab = self.TabWidget.currentIndex()
27342755
# self.aw.closeEventSettings() # save all app settings
2756+
2757+
if not self.aw.schedule_window:
2758+
# we disconnect all scales again if scheduler is not active
2759+
self.aw.scale_manager.disconnect_all()
2760+
27352761
return True
27362762

27372763
@pyqtSlot()
@@ -2743,6 +2769,14 @@ def cancelEvent(self) -> None:
27432769
self.aw.santokerSerial = self.org_santokerSerial
27442770
self.aw.santokerBLE = self.org_santokerBLE
27452771
self.aw.kaleidoSerial = self.org_kaleidoSerial
2772+
2773+
self.aw.scale1_model = self.org_scale1_model
2774+
self.aw.scale1_name = self.org_scale1_name
2775+
self.aw.scale1_id = self.org_scale1_id
2776+
self.aw.scale2_model = self.org_scale2_model
2777+
self.aw.scale2_name = self.org_scale2_name
2778+
self.aw.scale2_id = self.org_scale2_id
2779+
27462780
self.reject()
27472781

27482782
@pyqtSlot()

src/artisanlib/main.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -13453,11 +13453,6 @@ def loadbackgroundRedraw(self, filename:str) -> None:
1345313453
self.qmc.background = not self.qmc.hideBgafterprofileload
1345413454
self.qmc.timealign(redraw=False)
1345513455
self.qmc.redraw()
13456-
if self.qmc.backgroundPlaybackEvents:
13457-
# first turn playback off to clean previous disabled events
13458-
self.qmc.turn_playback_event_OFF()
13459-
# turn on again after background load to ignore already passed events
13460-
self.qmc.turn_playback_event_ON()
1346113456

1346213457
except Exception as e: # pylint: disable=broad-except
1346313458
_log.exception(e)
@@ -20508,6 +20503,10 @@ def stopActivities(self) -> None:
2050820503
except Exception as e: # pylint: disable=broad-except
2050920504
_log.exception(e)
2051020505
self.qmc.stopPhidgetManager()
20506+
try:
20507+
self.scale_manager.disconnect_all()
20508+
except Exception as e: # pylint: disable=broad-except
20509+
_log.exception(e)
2051120510

2051220511
# returns True if confirmed, False if canceled by the user
2051320512
def closeApp(self) -> bool:

src/artisanlib/pid_control.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1646,7 +1646,7 @@ def setDutySteps(self, dutySteps:int) -> None:
16461646

16471647

16481648
def setSV(self, sv:float, move:bool = True, init:bool = False) -> None:
1649-
_log.debug('PRINT setSV(%s,%s,%s)',sv,move,init)
1649+
# _log.debug('PRINT setSV(%s,%s,%s)',sv,move,init)
16501650
# if not move:
16511651
# self.aw.sendmessage(QApplication.translate("Message","SV set to %s"%sv))
16521652
if self.externalPIDControl() == 1:

src/artisanlib/santoker.py

+20-10
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def register_reading(self, target:bytes, data:bytes) -> None:
209209
#if self._logging:
210210
value:int
211211
# convert data into the integer data
212-
if target in (self.BT_ROR, self.ET_ROR):
212+
if target in {self.BT_ROR, self.ET_ROR}:
213213
# first for bits of the RoR data contain the sign of the value
214214
if len(data) != 2:
215215
return
@@ -220,16 +220,16 @@ def register_reading(self, target:bytes, data:bytes) -> None:
220220
value = - value
221221
else:
222222
value = int.from_bytes(data, 'big')
223-
if self._logging:
224-
_log.debug('register_reading(%s,%s)',target,value)
223+
# if self._logging:
224+
# _log.debug('register_reading(%s,%s)',target,value)
225225
if target == self.BOARD:
226226
self._board = value / 10.0
227-
elif target in (self.BT, self.OLD_BT):
227+
elif target in {self.BT, self.OLD_BT}:
228228
BT = value / 10.0
229229
self._bt = (BT if self._bt == -1 else (2*BT + self._bt)/3)
230230
if self._logging:
231231
_log.debug('BT: %s',self._bt)
232-
elif target in (self.ET, self.OLD_ET):
232+
elif target in {self.ET, self.OLD_ET}:
233233
ET = value / 10.0
234234
self._et = (ET if self._et == -1 else (2*ET + self._et)/3)
235235
if self._logging:
@@ -287,10 +287,10 @@ def register_reading(self, target:bytes, data:bytes) -> None:
287287
except Exception as e: # pylint: disable=broad-except
288288
_log.exception(e)
289289
self._DROP = b
290-
elif self._logging and target in {self.MIN_POWER, self.MAX_POWER, self.BT_CALIB, self.ET_CALIB}:
291-
_log.debug('unsupported data target %s', target)
292-
elif self._logging:
293-
_log.debug('unknown data target %s', target)
290+
# elif self._logging and target in {self.MIN_POWER, self.MAX_POWER, self.BT_CALIB, self.ET_CALIB}:
291+
# _log.debug('unsupported data target %s', target)
292+
# elif self._logging:
293+
# _log.debug('unknown data target %s', target)
294294

295295

296296
# asyncio read implementation
@@ -302,7 +302,17 @@ async def read_msg(self, stream: Union[asyncio.StreamReader, IteratorReader]) ->
302302
# check for the second header byte
303303
# if await stream.readexactly(1) != self.HEADER[1:2]:
304304
# return
305-
if await stream.readexactly(1) not in (self.HEADER_BT[1:2], self.HEADER_WIFI[1:2]): # we always accept both headers, the one for WiFi and the one for BT
305+
# if await stream.readexactly(1) not in {self.HEADER_BT[1:2], self.HEADER_WIFI[1:2]}: # we always accept both headers, the one for WiFi and the one for BT
306+
# return
307+
# we accept both headers, BT and WiFi and adjust the self.HEADER to be used on sending messages accordingly
308+
# as it seems that some machines connected via bluetooth still send data using the WiFi header and expect that header also on read
309+
# so we adjust to the header we receive and use that on sending our messages as well
310+
snd_header_byte = await stream.readexactly(1)
311+
if snd_header_byte == self.HEADER_BT[1:2]:
312+
self.HEADER = self.HEADER_BT
313+
elif snd_header_byte == self.HEADER_WIFI[1:2]:
314+
self.HEADER = self.HEADER_WIFI
315+
else:
306316
return
307317
# read the data target (BT, ET,..)
308318
target = await stream.readexactly(1)

src/artisanlib/scale.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
('Acaia', 0) # 0
3434
]
3535

36-
ScaleSpec = Tuple[str,str]
36+
ScaleSpec = Tuple[str,str] # scale name, scale id (eg. ble address)
3737
ScaleSpecs = List[ScaleSpec]
3838

3939

@@ -59,6 +59,12 @@ def disconnect(self) -> None:
5959

6060
class ScaleManager:
6161

62+
__slots__ = [ 'scale1', 'scale2' ]
63+
64+
def __init__(self) -> None:
65+
self.scale1: Optional[Scale] = None
66+
self.scale2: Optional[Scale] = None
67+
6268
# returns list of discovered devices as (name, address) tuples matching selected scale model
6369
@staticmethod
6470
def scan_for_scales(model:int) -> ScaleSpecs:
@@ -86,3 +92,27 @@ def get_scale(model:int, address:str) -> Optional[Scale]:
8692
scale.set_address(address)
8793
return scale
8894
return None
95+
96+
def set_scale1(self, scale:Optional[Scale]) -> None:
97+
if self.scale1 is not None:
98+
self.scale1.disconnect()
99+
self.scale1 = scale
100+
101+
def get_scale1(self) -> Optional[Scale]:
102+
return self.scale1
103+
104+
def set_scale2(self, scale:Optional[Scale]) -> None:
105+
if self.scale2 is not None:
106+
self.scale2.disconnect()
107+
self.scale2 = scale
108+
109+
def get_scale2(self) -> Optional[Scale]:
110+
return self.scale2
111+
112+
def disconnect_all(self) -> None:
113+
if self.scale1 is not None:
114+
self.scale1.disconnect()
115+
self.scale1 = None
116+
if self.scale2 is not None:
117+
self.scale2.disconnect()
118+
self.scale2 = None

src/includes/Machines/Santoker/Cube_Bluetooth_PID.aset

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ pidKp=1.1
150150
pidNegativeTarget=0
151151
pidOnCHARGE=true
152152
pidPositiveTarget=4
153-
pidSource=1
153+
pidSource=2
154154
positiveTargetMax=100
155155
positiveTargetMin=0
156156
positiveTargetRangeLimit=false

src/requirements-dev.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ types-docutils>=0.21.0.20241128
1313
lxml-stubs>=0.5.1
1414
mypy==1.15.0
1515
pyright==1.1.396
16-
ruff>=0.10.0
16+
ruff>=0.11.0
1717
pylint==3.3.5
1818
pre-commit>=4.1.0
1919
pytest>=8.3.5
@@ -25,7 +25,7 @@ pytest-cov==6.0.0
2525
#pytest-bdd==6.1.1
2626
#pytest-benchmark==4.0.0
2727
#pytest-mock==3.11.1
28-
hypothesis>=6.129.1
28+
hypothesis>=6.129.3
2929
coverage>=7.6.12
3030
coverage-badge==1.1.2
3131
codespell==2.4.1

0 commit comments

Comments
 (0)