Skip to content

Commit e740c85

Browse files
committed
v1.19.0: Cycle statistics, stop logging fix
1 parent 1554a2f commit e740c85

File tree

12 files changed

+859
-48
lines changed

12 files changed

+859
-48
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
## [1.19.0] - 2025-11-26
8+
9+
### Added
10+
- **Cycle Statistics**: Persistent stats tracking (operations, cycle times, yield) with rolling averages
11+
- **Stats Display**: "Show Stats" button in operator view, "Cycle Statistics..." in Settings menu
12+
- **Job Tracking**: Stats tracked per-job with "Last Job" comparison
13+
14+
### Changed
15+
- **Stop Logging**: No longer errors when no active logging sessions
16+
- **Menus**: Removed "Operator Views" from Devices menu (use double-click instead)
17+
718
## [1.18.0] - 2025-11-26
819

920
### Added

_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.18.0"
1+
__version__ = "1.19.0"

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Runtime dependencies
22
pyserial>=3.5 # For USB serial communication
3+
psutil>=5.9.0 # For system stats (uptime, etc.)
34

45
# Note: Barcode/QR scanner support uses keyboard input emulation
56
# Most USB/Bluetooth scanners work as HID devices, no extra dependencies needed

src/app.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ def __init__(self, root, startup_file=None):
276276

277277
# --- Serial Number System ---
278278
initialize_serial_manager()
279+
from .serial_number import get_serial_manager
280+
self.shared_gui_refs['serial_manager'] = get_serial_manager()
279281
print("[SYSTEM] Serial number system initialized")
280282

281283
# --- Populate device-specific shared refs ---
@@ -298,6 +300,39 @@ def __init__(self, root, startup_file=None):
298300
pass # Ignore if logger not available
299301

300302
self.load_last_script()
303+
self.launch_startup_operator_views()
304+
305+
def launch_startup_operator_views(self):
306+
"""Launch operator views that are configured to show on startup."""
307+
from src.device.views import get_startup_operator_views, show_operator_view
308+
from src.config import load_config
309+
310+
def _launch_views():
311+
try:
312+
config_data = load_config()
313+
startup_devices = get_startup_operator_views(config_data)
314+
315+
for device_name in startup_devices:
316+
# Check if device exists and is connected
317+
device_data = self.device_manager.devices.get(device_name)
318+
if device_data:
319+
device_state = self.device_manager.get_device_state(device_name)
320+
if device_state and device_state.get('connected'):
321+
print(f"[OPERATOR VIEW] Launching operator view for {device_name}")
322+
script_runner = self.shared_gui_refs.get('script_runner')
323+
show_operator_view(self.root, device_name, device_data,
324+
self.shared_gui_refs, script_runner)
325+
else:
326+
print(f"[OPERATOR VIEW] Skipping {device_name} - device not connected")
327+
else:
328+
print(f"[OPERATOR VIEW] Skipping {device_name} - device not found")
329+
except Exception as e:
330+
print(f"[OPERATOR VIEW] Error launching startup views: {e}")
331+
import traceback
332+
traceback.print_exc()
333+
334+
# Launch after a delay to ensure devices are connected
335+
self.root.after(1000, _launch_views)
301336

302337
def initialize_shared_variables(self):
303338
# This method's logic has been moved into __init__ to resolve a startup dependency issue.

src/device/registry.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def discover_devices(self):
109109

110110
if os.path.exists(config_path):
111111
try:
112-
with open(config_path, 'r') as f:
112+
with open(config_path, 'r', encoding='utf-8') as f:
113113
config = json.load(f)
114114
device_name = config.get('device_name') or config.get('name')
115115
except Exception as e:
@@ -139,33 +139,41 @@ def _load_device_from_path(self, device_name, definition_path):
139139
scripting_commands = {}
140140
json_path = os.path.join(definition_path, 'commands.json')
141141
if os.path.exists(json_path):
142-
with open(json_path, 'r') as f:
142+
with open(json_path, 'r', encoding='utf-8') as f:
143143
scripting_commands = json.load(f)
144144

145145
# Load telemetry schema
146146
telemetry_data = {}
147147
schema_path = os.path.join(definition_path, 'telemetry.json')
148148
if os.path.exists(schema_path):
149-
with open(schema_path, 'r') as f:
149+
with open(schema_path, 'r', encoding='utf-8') as f:
150150
telemetry_data = json.load(f)
151151

152152
# Load events
153153
events_data = {}
154154
events_path = os.path.join(definition_path, 'events.json')
155155
if os.path.exists(events_path):
156-
with open(events_path, 'r') as f:
156+
with open(events_path, 'r', encoding='utf-8') as f:
157157
events_data = json.load(f)
158158

159-
# Load Python modules (gui, simulator)
159+
# Load views
160+
views_data = {}
161+
views_path = os.path.join(definition_path, 'views.json')
162+
if os.path.exists(views_path):
163+
with open(views_path, 'r', encoding='utf-8') as f:
164+
views_data = json.load(f)
165+
166+
# Load Python modules (gui, simulator, operator_view, parser)
160167
gui_module = self._load_module_from_path(device_name, 'gui', definition_path)
161168
simulator_module = self._load_module_from_path(device_name, 'simulator', definition_path)
169+
operator_view_module = self._load_module_from_path(device_name, 'operator_view', definition_path)
162170
parser_module = self._load_module_from_path(device_name, 'parser', definition_path)
163171

164172
# Load warnings from JSON (always from definition folder)
165173
warnings_data = {}
166174
warnings_path = os.path.join(definition_path, 'warnings.json')
167175
if os.path.exists(warnings_path):
168-
with open(warnings_path, 'r') as f:
176+
with open(warnings_path, 'r', encoding='utf-8') as f:
169177
warnings_data = json.load(f)
170178

171179
# Store device data (use old key names for backward compatibility)
@@ -179,8 +187,15 @@ def _load_device_from_path(self, device_name, definition_path):
179187
'telemetry_data': telemetry_data,
180188
'events_data': events_data,
181189
'warnings': warnings_data,
190+
'views_data': views_data,
182191
'config': {}, # Keep for consistent structure
183192
'status_var': None, # Will be created during GUI init
193+
'modules': {
194+
'gui': gui_module,
195+
'simulator': simulator_module,
196+
'operator_view': operator_view_module,
197+
'parser': parser_module,
198+
},
184199
}
185200

186201
return True
@@ -205,28 +220,28 @@ def reload_single_device(self, device_name):
205220
# Reload commands.json
206221
json_path = os.path.join(device_path, 'commands.json')
207222
if os.path.exists(json_path):
208-
with open(json_path, 'r') as f:
223+
with open(json_path, 'r', encoding='utf-8') as f:
209224
self.devices[device_name]['scripting_commands'] = json.load(f)
210225
self.log(f"Reloaded commands.json for {device_name}")
211226

212227
# Reload telemetry.json
213228
schema_path = os.path.join(device_path, 'telemetry.json')
214229
if os.path.exists(schema_path):
215-
with open(schema_path, 'r') as f:
230+
with open(schema_path, 'r', encoding='utf-8') as f:
216231
self.devices[device_name]['telemetry_data'] = json.load(f)
217232
self.log(f"Reloaded telemetry.json for {device_name}")
218233

219234
# Reload events.json
220235
events_path = os.path.join(device_path, 'events.json')
221236
if os.path.exists(events_path):
222-
with open(events_path, 'r') as f:
237+
with open(events_path, 'r', encoding='utf-8') as f:
223238
self.devices[device_name]['events_data'] = json.load(f)
224239
self.log(f"Reloaded events.json for {device_name}")
225240

226241
# Reload warnings.json
227242
warnings_path = os.path.join(device_path, 'warnings.json')
228243
if os.path.exists(warnings_path):
229-
with open(warnings_path, 'r') as f:
244+
with open(warnings_path, 'r', encoding='utf-8') as f:
230245
self.devices[device_name]['warnings'] = json.load(f)
231246
self.log(f"Reloaded warnings.json for {device_name}")
232247

@@ -341,7 +356,7 @@ def get_device_config(self, device_name):
341356
config_path = os.path.join(device_path, 'config.json')
342357
if os.path.exists(config_path):
343358
try:
344-
with open(config_path, 'r') as f:
359+
with open(config_path, 'r', encoding='utf-8') as f:
345360
return json.load(f)
346361
except Exception as e:
347362
self.log(f"Failed to read config.json for {device_name}: {e}")

0 commit comments

Comments
 (0)