Skip to content

Commit 112c226

Browse files
committed
prepares for v4.2
1 parent de9dc95 commit 112c226

75 files changed

Lines changed: 69522 additions & 68835 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Version History
6363

6464
| Version | Date | Comment |
6565
|---------|------:|---------|
66+
| [v4.2.0](https://artisan-scope.org/release/Artisan-v4.2/) | Jun&nbsp;30,&nbsp;2026 | Adds support for [ROEST](https://artisan-scope.org/machines/roest/), [Orbiter](https://artisan-scope.org/machines/orbiter/), [Pratter](https://artisan-scope.org/machines/pratter/), [Probat G/UG 2026](https://artisan-scope.org/machines/probat/), [Neuhaus Neotec](https://artisan-scope.org/machines/neuhaus/), [North Roasters](https://artisan-scope.org/machines/north/), and the [MQTT protocol](https://artisan-scope.org/devices/mqtt/). <br>:heart: <b>Release Sponsor: [LABEL!STEN](https://labelisten.com/)</b> :heart: |
6667
| [v4.0.2](https://github.com/artisan-roaster-scope/artisan/releases/tag/v4.0.2) | Feb&nbsp;7,&nbsp;2026 | Bug fixes |
6768
| [v4.0.0](https://github.com/artisan-roaster-scope/artisan/releases/tag/v4.0.0) | Jan&nbsp;28,&nbsp;2026 | Adds UI modes, adds support for [Lebrew RoastSee NEXT](https://lebrewtech.com/products/roastsee-next) and [Acaia Pyxis Black (2025)](https://eu.acaia.co/products/pyxis-black), 2DOF software PID implementation with interpolated gain scheduling, bumpless transfer and filtering, general stability and performance improvements.<br>:heart: <b>Release Sponsor: [artisan.plus](https://artisan.plus/)</b> :heart: |
6869
| [v3.4.0](https://github.com/artisan-roaster-scope/artisan/releases/tag/v3.4.0) | Oct&nbsp;31,&nbsp;2025 | Adds support for [Kraffe](https://artisan-scope.org/machines/kraffe/), [Berto](https://artisan-scope.org/machines/berto/), [Nordic PLC](https://artisan-scope.org/machines/nordic/), [Prisma](https://artisan-scope.org/machines/prisma/), additional [Easyster](https://artisan-scope.org/machines/easyster/) and [Proaster](https://artisan-scope.org/machines/proaster/) models, the latest [Cogen](https://artisan-scope.org/machines/cogen/) and [Toper](https://artisan-scope.org/machines/toper/) roasters, and updates setups for [Sweet Coffee Italia](https://artisan-scope.org/machines/sci/) and [Joper](https://joper-roasters.com/) machines. Adds support for [Tasi TA612C 4-channel](https://artisan-scope.org/devices/meters/) and [Shelly energy meters](https://artisan-scope.org/devices/shelly/), as well as the ability to import Cropster profiles through drag-and-drop. Enables multi-day coffee fermentation experiments. Adds Czech and improved Chinese, Korean, and Spanish localizations. Updates the internal PID algorithm with advanced algorithmic and the app icon for the glass era (last version featuring legacy builds supporting macOS 10.13+ and Window 8).<br>:heart: <b>Release Sponsor: [artisan.plus](https://artisan.plus/)</b> :heart: |

src/artisanlib/acaia.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from artisanlib.ble_port import ClientBLE
3131
from artisanlib.async_comm import AsyncComm, AsyncIterable, IteratorReader
3232
from artisanlib.scale import Scale, ScaleSpecs, STATE_ACTION
33-
from artisanlib.util import float2float
33+
from artisanlib.util import float2float, round_base
3434
from artisanlib.atypes import SerialSettings
3535

3636
_log: Final[logging.Logger] = logging.getLogger(__name__)
@@ -226,6 +226,8 @@ class ACAIA_TIMER(IntEnum):
226226

227227
HEARTBEAT_FREQUENCY:Final[int] = 5 # send the heartbeat every 5 sec
228228

229+
RELAY_STREAMING:Final[bool] = True
230+
229231

230232
# ISP versions -> device model (CSMO series)
231233
ISP_VERSION_CSMO_L = 35 # CSMO-L, large scale (100kg)
@@ -248,7 +250,7 @@ class AcaiaProtocol:
248250

249251
__slots__ = [ '_send', '_weight_changed_handler', '_battery_changed_handler', '_tare_pressed_handler', '_logging', 'stable_only',
250252
'decimals', 'heartbeat_frequency', 'scale_class', 'id_sent', 'info_msg_received', 'device_identified', 'fast_notifications_sent', 'slow_notifications_sent',
251-
'weight', 'stable_weight', 'battery', 'isp_version', 'firmware', 'unit', 'max_weight', 'readability', 'repeatability', 'auto_off_timer', 'has_leds' ]
253+
'stable_weight', 'battery', 'isp_version', 'firmware', 'unit', 'max_weight', 'readability', 'repeatability', 'auto_off_timer', 'has_leds' ]
252254

253255

254256
def __init__(self,
@@ -284,7 +286,6 @@ def __init__(self,
284286
self.slow_notifications_sent:bool = False # after first reading is received we step down to slow readings again
285287

286288
# readings
287-
self.weight:float|None = None
288289
self.stable_weight:float|None = None
289290
self.battery:int|None = None
290291
self.isp_version:int|None = None
@@ -472,7 +473,6 @@ def reset_parser(self) -> None:
472473
self.device_identified = False
473474
self.fast_notifications_sent = False
474475
self.slow_notifications_sent = False
475-
self.weight = None
476476
self.stable_weight = None
477477
self.battery = None
478478
self.isp_version = None
@@ -536,26 +536,27 @@ def decode_weight(self, payload:bytes) -> tuple[float|None, bool]:
536536
except Exception: # pylint: disable=broad-except
537537
return None, False
538538

539-
540539
def update_weight(self, value:float|None, stable:bool|None = False) -> None:
541-
_log.debug('PRINT update_weight(%s,%s)', value, stable)
540+
## _log.debug("PRINT update_weight(%s,%s)", value, stable)
542541
if value is not None and (not self.stable_only or stable):
543-
# if self.repeatability > 1:
544-
# # we round to full 10g
545-
# value = round(value, -1)
542+
if self.repeatability > 1:
543+
# round to full 10g
544+
#value = round(value, -1)
545+
# round to full 5g
546+
value = round_base(value, 5)
546547
# convert the weight in g delivered with one decimal to an int
547548
value_rounded:float = float2float(value, self.decimals)
548549
if stable and value_rounded != self.stable_weight:
549550
# if value is fresh and reading is stable
551+
## _log.debug("PRINT new stable weight: %s", value_rounded)
552+
self._weight_changed_handler(value_rounded, True)
550553
self.stable_weight = value_rounded
551-
self.weight = value_rounded # we also update the last weight value to prevent it to be send again
552-
_log.debug('PRINT new stable weight: %s', self.stable_weight)
553-
self._weight_changed_handler(self.stable_weight, bool(stable))
554-
elif not stable and value_rounded != self.weight:
554+
elif not stable:
555+
## _log.debug("PRINT new non-stable weight: %s", value_rounded)
556+
self._weight_changed_handler(value_rounded, False)
555557
self.stable_weight = None # non-stable weights invalidate the last stable weight to ensure a sequence of equal stable weights is reported if interleaved with non-stable weights
556-
self.weight = value_rounded
557-
_log.debug('PRINT new non-stable weight: %s', self.weight)
558-
self._weight_changed_handler(self.weight, bool(stable))
558+
## else:
559+
## _log.debug("PRINT stable weight ignored")
559560

560561
# returns length of consumed data or -1 on error
561562
def parse_weight_event(self, payload:bytes) -> int:
@@ -1001,7 +1002,7 @@ def changes_notifications(self) -> None:
10011002

10021003
def slow_notifications(self) -> None:
10031004
_log.debug('slow notifications: %s', self.scale_class)
1004-
if self.scale_class == SCALE_CLASS.RELAY:
1005+
if self.scale_class == SCALE_CLASS.RELAY and not RELAY_STREAMING:
10051006
self.send_event(
10061007
bytes([ # pairs of key/setting
10071008
0, # weight id

src/artisanlib/devices.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,9 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
15871587
self.scale1ModelComboBox.setToolTip(QApplication.translate('Tooltip','Choose the model of your scale'))
15881588
self.scale1ModelComboBox.setMinimumWidth(150)
15891589
self.scale1ModelComboBox.addItems([''] + [m for (m,_) in SUPPORTED_SCALES])
1590+
self.scale1GreenOnlyCheckBox = QCheckBox(QApplication.translate('Label','Greens only'))
1591+
self.scale1GreenOnlyCheckBox.setChecked(self.aw.scale1_dedicated_for_green_only)
1592+
self.scale1GreenOnlyCheckBox.setToolTip(QApplication.translate('Tooltip','Reserve scale 1 for green beans'))
15901593
self.scale1NameLabel = QLabel(QApplication.translate('Label','Name'))
15911594
self.scale1NameComboBox = QComboBox()
15921595
self.scale1NameComboBox.setToolTip(QApplication.translate('Tooltip','Choose your scale'))
@@ -1633,6 +1636,7 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
16331636
scale1Grid = QGridLayout()
16341637
scale1Grid.addWidget(scale1ModelLabel,0,0)
16351638
scale1Grid.addWidget(self.scale1ModelComboBox,0,1)
1639+
scale1Grid.addWidget(self.scale1GreenOnlyCheckBox,0,2,1,4,Qt.AlignmentFlag.AlignRight)
16361640
scale1Grid.addWidget(self.scale1NameLabel,1,0)
16371641
scale1Grid.addWidget(self.scale1NameComboBox,1,1)
16381642
scale1Grid.addWidget(self.scale1EditButton,1,2)
@@ -1660,6 +1664,9 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
16601664
self.scale2ModelComboBox.setToolTip(QApplication.translate('Tooltip','Choose the model of your scale'))
16611665
self.scale2ModelComboBox.setMinimumWidth(150)
16621666
self.scale2ModelComboBox.addItems([''] + [m for (m,_) in SUPPORTED_SCALES])
1667+
self.scale2RoastedOnlyCheckBox = QCheckBox(QApplication.translate('Label','Roasted only'))
1668+
self.scale2RoastedOnlyCheckBox.setChecked(self.aw.scale2_dedicated_for_roasted_only)
1669+
self.scale2RoastedOnlyCheckBox.setToolTip(QApplication.translate('Tooltip','Reserve scale 2 for roasted coffee'))
16631670
self.scale2NameLabel = QLabel(QApplication.translate('Label','Name'))
16641671
self.scale2NameComboBox = QComboBox()
16651672
self.scale2NameComboBox.setToolTip(QApplication.translate('Tooltip','Choose your scale'))
@@ -1708,6 +1715,7 @@ def __init__(self, parent:QWidget, aw:'ApplicationWindow', activeTab:int = 0) ->
17081715
scale2Grid = QGridLayout()
17091716
scale2Grid.addWidget(scale2ModelLabel,0,0)
17101717
scale2Grid.addWidget(self.scale2ModelComboBox,0,1)
1718+
scale2Grid.addWidget(self.scale2RoastedOnlyCheckBox,0,2,1,4,Qt.AlignmentFlag.AlignRight)
17111719
scale2Grid.addWidget(self.scale2NameLabel,1,0)
17121720
scale2Grid.addWidget(self.scale2NameComboBox,1,1)
17131721
scale2Grid.addWidget(self.scale2EditButton,1,2)
@@ -2113,15 +2121,18 @@ def scale1ModelChanged(self, i:int) -> None:
21132121
self.scale1NameComboBox.setEnabled(False)
21142122
self.scale1EditButton.setEnabled(False)
21152123
if i > 0 and len(SUPPORTED_SCALES) > i-1 and len(SUPPORTED_SCALES[i-1]) > 0:
2124+
self.aw.scale1_name = None
2125+
self.scale1NameComboBox.clear()
2126+
self.update_scale1_weight(None)
21162127
self.aw.scale1_model = i-1
21172128
self.scale1ScanButton.setEnabled(True)
21182129
self.updateScale1NameLabel(SUPPORTED_SCALES[i-1][1])
21192130
else:
21202131
self.aw.scale1_name = None
2121-
self.aw.scale1_model = None
21222132
self.scale1NameComboBox.clear()
2123-
self.scale1ScanButton.setEnabled(False)
21242133
self.update_scale1_weight(None)
2134+
self.aw.scale1_model = None
2135+
self.scale1ScanButton.setEnabled(False)
21252136
self.updateScale1NameLabel(0)
21262137

21272138
@pyqtSlot(int)
@@ -2251,15 +2262,18 @@ def scale2ModelChanged(self, i:int) -> None:
22512262
self.scale2NameComboBox.setEnabled(False)
22522263
self.scale2EditButton.setEnabled(False)
22532264
if i > 0 and len(SUPPORTED_SCALES) > i-1 and len(SUPPORTED_SCALES[i-1]) > 0:
2265+
self.aw.scale2_name = None
2266+
self.scale2NameComboBox.clear()
2267+
self.update_scale2_weight(None)
22542268
self.aw.scale2_model = i-1
22552269
self.scale2ScanButton.setEnabled(True)
22562270
self.updateScale2NameLabel(SUPPORTED_SCALES[i-1][1])
22572271
else:
22582272
self.aw.scale2_name = None
2259-
self.aw.scale2_model = None
22602273
self.scale2NameComboBox.clear()
2261-
self.scale2ScanButton.setEnabled(False)
22622274
self.update_scale2_weight(None)
2275+
self.aw.scale2_model = None
2276+
self.scale2ScanButton.setEnabled(False)
22632277
self.updateScale1NameLabel(0)
22642278

22652279
@pyqtSlot(int)
@@ -2455,13 +2469,6 @@ def roastedContainerChanged(self, i:int) -> None:
24552469
self.aw.container2_idx = i - 3
24562470
# update displayed scale weight
24572471
self.updateRoastedContainerWeight()
2458-
# # we need to update availability, as roasted scale is only available if roasted container weight is set
2459-
# self.aw.scale_manager.update_availability(force=True)
2460-
# # if green display is ON, roasted display can only be turned ON if roasted container is selected
2461-
# if self.aw.taskWebDisplayGreenActive and self.aw.container2_idx == -1:
2462-
# self.taskWebDisplayRoasted(False)
2463-
# self.taskWebDisplayRoastedFlag.setDisabled(self.aw.taskWebDisplayGreenActive)# and self.aw.container2_idx == -1)
2464-
# self.taskWebDisplayRoastedPort.setDisabled(self.aw.taskWebDisplayGreenActive)# and self.aw.container2_idx == -1)
24652472

24662473
def updateRoastedContainerWeight(self) -> None:
24672474
weight = self.aw.qmc.get_container_weight(self.aw.container2_idx)
@@ -3533,6 +3540,9 @@ def okEvent(self) -> None: # pyright: ignore [reportGeneralTypeIssues] # Code is
35333540
self.aw.two_bucket_mode = self.dual_bucket_mode.isChecked()
35343541
self.aw.green_task_precision = self.greenTaskPrecision.value()
35353542

3543+
self.aw.scale1_dedicated_for_green_only = self.scale1GreenOnlyCheckBox.isChecked()
3544+
self.aw.scale2_dedicated_for_roasted_only = self.scale2RoastedOnlyCheckBox.isChecked()
3545+
35363546
if self.pidButton.isChecked():
35373547
#type index[0]: 0 = PXG, 1 = PXR, 2 = DTA
35383548
#if str(self.controlpidtypeComboBox.currentText()) == 'Fuji PXG':

src/artisanlib/main.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
except Exception: # pylint: disable=broad-except
9595
pass
9696

97-
# write logtrace to Console on OS X:
97+
# write logtrace to Console on macOS:
9898
#try:
9999
#..
100100
#except Exception as e: # pylint: disable=broad-except
@@ -1467,7 +1467,7 @@ class ApplicationWindow(QMainWindow):
14671467
'taskWebDisplayGreenActive', 'taskWebDisplayGreenPort', 'taskWebDisplayRoastedActive', 'taskWebDisplayRoastedPort',
14681468
'taskWebDisplayRoastedIndexPath', 'taskWebDisplayRoastedWebSocketPath', 'taskWebDisplayGreen_server', 'taskWebDisplayRoasted_server',
14691469
'custom_scale_ids', 'custom_scale_names',
1470-
'scale_manager', 'scale1_model', 'scale1_name', 'scale1_id', 'container1_idx', 'two_bucket_mode', 'green_task_precision', 'scale2_model', 'scale2_name', 'scale2_id', 'container2_idx',
1470+
'scale_manager', 'scale1_model', 'scale1_name', 'scale1_id', 'container1_idx', 'two_bucket_mode', 'green_task_precision', 'scale2_model', 'scale2_name', 'scale2_id', 'container2_idx', 'scale1_dedicated_for_green_only', 'scale2_dedicated_for_roasted_only',
14711471
'WebLCDsAlerts', 'EventsDlg_activeTab', 'graphColorDlg_activeTab', 'PID_DlgControl_activeTab', 'CurveDlg_activeTab', 'editGraphDlg_activeTab',
14721472
'backgroundDlg_activeTab', 'DeviceAssignmentDlg_activeTab', 'AlarmDlg_activeTab', 'schedule_activeTab', 'StatisticsDlg_activeTab', 'resetqsettings', 'settingspath', 'wheelpath', 'profilepath',
14731473
'userprofilepath', 'printer', 'main_widget', 'defaultdpi', 'dpi', 'qmc', 'HottopControlActive', 'AsyncSamplingTimer', 'wheeldialog',
@@ -1678,6 +1678,8 @@ def __init__(self, parent:QWidget|None = None, *, locale:str, WebEngineSupport:b
16781678
self.scale1_id:str|None = None # the id, eg. the BT address (like "24:71:89:cc:09:05")
16791679
self.container1_idx:int = -1 # -1: no container set; otherwise index into selected qmc.container_names/qmc.container_weights
16801680
self.two_bucket_mode:bool = False # if True, the TaskManager allows to split green task weight into two buckets
1681+
self.scale1_dedicated_for_green_only:bool = False
1682+
self.scale2_dedicated_for_roasted_only:bool = False
16811683
self.green_task_precision:float = 10 # precision in percent (range [0.1 - 10%]; if set to 0 all "non-overlapping" weights are accepted)
16821684
# scale2: just for green
16831685
self.scale2_model:int|None = None
@@ -19460,6 +19462,8 @@ def settingsLoad(self, filename:str|None = None, theme:bool = False, machine:boo
1946019462
self.scale1_id = None
1946119463
self.container1_idx = toInt(settings.value('container1_idx',int(self.container1_idx)))
1946219464
self.two_bucket_mode = toBool(settings.value('two_bucket_mode',int(self.two_bucket_mode)))
19465+
self.scale1_dedicated_for_green_only = toBool(settings.value('scale1_dedicated_for_green_only',int(self.scale1_dedicated_for_green_only)))
19466+
self.scale2_dedicated_for_roasted_only = toBool(settings.value('scale2_dedicated_for_roasted_only',int(self.scale2_dedicated_for_roasted_only)))
1946319467
self.green_task_precision = toFloat(settings.value('green_task_precision', self.green_task_precision))
1946419468
if settings.contains('scale2_model'):
1946519469
try:
@@ -21139,6 +21143,8 @@ def saveAllSettings(self, settings:QSettings, default_settings:dict[str, Any]|No
2113921143
self.settingsSetValue(settings, default_settings, 'scale1_id',self.scale1_id, read_defaults)
2114021144
self.settingsSetValue(settings, default_settings, 'container1_idx',self.container1_idx, read_defaults)
2114121145
self.settingsSetValue(settings, default_settings, 'two_bucket_mode',self.two_bucket_mode, read_defaults)
21146+
self.settingsSetValue(settings, default_settings, 'scale1_dedicated_for_green_only',self.scale1_dedicated_for_green_only, read_defaults)
21147+
self.settingsSetValue(settings, default_settings, 'scale2_dedicated_for_roasted_only',self.scale2_dedicated_for_roasted_only, read_defaults)
2114221148
self.settingsSetValue(settings, default_settings, 'green_task_precision',self.green_task_precision, read_defaults)
2114321149
self.settingsSetValue(settings, default_settings, 'scale2_model',self.scale2_model, read_defaults)
2114421150
self.settingsSetValue(settings, default_settings, 'scale2_name',self.scale2_name, read_defaults)

0 commit comments

Comments
 (0)