Skip to content

Commit 3a0cc58

Browse files
committed
Code Cleanup
1 parent 86cc779 commit 3a0cc58

File tree

5 files changed

+260
-169
lines changed

5 files changed

+260
-169
lines changed

Software/web-server/config_manager.py

Lines changed: 105 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
import json
1010
import logging
1111
import os
12+
import fcntl
13+
import copy
1214
from pathlib import Path
15+
from threading import RLock
1316
from typing import Any, Dict, List, Optional, Tuple
1417

1518
logger = logging.getLogger(__name__)
@@ -19,12 +22,14 @@ class ConfigurationManager:
1922
"""Manages PiTrac configuration with JSON-based system"""
2023

2124
def __init__(self):
25+
self._lock = RLock()
26+
2227
self._raw_metadata = self._load_raw_metadata()
2328
sys_paths = self._raw_metadata.get("systemPaths", {})
24-
29+
2530
def expand_path(path_str: str) -> Path:
2631
return Path(path_str.replace("~", str(Path.home())))
27-
32+
2833
# Configuration paths for three-tier system
2934
self.user_settings_path = expand_path(sys_paths.get("userSettingsPath", {}).get("default", "~/.pitrac/config/user_settings.json"))
3035
self.calibration_data_path = expand_path("~/.pitrac/config/calibration_data.json")
@@ -62,12 +67,19 @@ def _load_restart_required_params(self) -> set:
6267

6368
def reload(self) -> None:
6469
"""Reload configuration from metadata, calibration data, and user settings"""
65-
self.user_settings = self._load_json(self.user_settings_path)
66-
self.calibration_data = self._load_json(self.calibration_data_path)
67-
# Build merged config from metadata defaults + calibration + user overrides
70+
with self._lock:
71+
self.user_settings = self._load_json(self.user_settings_path)
72+
self.calibration_data = self._load_json(self.calibration_data_path)
73+
# Build merged config from metadata defaults + calibration + user overrides
74+
self.merged_config = self._build_config_from_metadata()
75+
self.restart_required_params = self._load_restart_required_params()
76+
logger.info(f"Loaded configuration: {len(self.calibration_data)} calibration fields, {len(self.user_settings)} user overrides")
77+
78+
def _rebuild_merged_config(self) -> None:
79+
"""Rebuild merged config from current state (internal use, assumes lock is held)"""
80+
# This method is called from within locked methods, so no lock needed here
6881
self.merged_config = self._build_config_from_metadata()
6982
self.restart_required_params = self._load_restart_required_params()
70-
logger.info(f"Loaded configuration: {len(self.calibration_data)} calibration fields, {len(self.user_settings)} user overrides")
7183

7284
def _load_json(self, path: Path) -> Dict[str, Any]:
7385
"""Load JSON file safely"""
@@ -82,13 +94,21 @@ def _load_json(self, path: Path) -> Dict[str, Any]:
8294
return {}
8395

8496
def _save_json(self, path: Path, data: Dict[str, Any]) -> bool:
85-
"""Save JSON file with proper formatting"""
97+
"""Save JSON file with proper formatting and file locking"""
8698
try:
8799
path.parent.mkdir(parents=True, exist_ok=True)
88100

101+
data_copy = copy.deepcopy(data)
102+
89103
temp_path = path.with_suffix(".tmp")
90104
with open(temp_path, "w") as f:
91-
json.dump(data, f, indent=2, sort_keys=True)
105+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
106+
try:
107+
json.dump(data_copy, f, indent=2, sort_keys=True)
108+
f.flush()
109+
os.fsync(f.fileno())
110+
finally:
111+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
92112

93113
temp_path.replace(path)
94114
logger.info(f"Saved configuration to {path}")
@@ -150,21 +170,23 @@ def get_config(self, key: Optional[str] = None) -> Any:
150170
Returns:
151171
Configuration value or None if not found
152172
"""
153-
if key is None:
154-
return self.get_merged_with_metadata_defaults()
155-
156-
value = self.get_merged_with_metadata_defaults()
157-
for part in key.split("."):
158-
if isinstance(value, dict) and part in value:
159-
value = value[part]
160-
else:
161-
return None
173+
with self._lock:
174+
if key is None:
175+
return self.get_merged_with_metadata_defaults()
176+
177+
value = self.get_merged_with_metadata_defaults()
178+
for part in key.split("."):
179+
if isinstance(value, dict) and part in value:
180+
value = value[part]
181+
else:
182+
return None
162183

163-
return value
184+
return value
164185

165186
def get_merged_with_metadata_defaults(self) -> Dict[str, Any]:
166187
"""Get merged config (already includes metadata defaults)"""
167-
return self.merged_config.copy()
188+
with self._lock:
189+
return copy.deepcopy(self.merged_config)
168190

169191
def get_default(self, key: Optional[str] = None) -> Any:
170192
"""Get default value from metadata"""
@@ -203,7 +225,8 @@ def get_all_defaults_with_metadata(self) -> Dict[str, Any]:
203225

204226
def get_user_settings(self) -> Dict[str, Any]:
205227
"""Get only user overrides"""
206-
return self.user_settings.copy()
228+
with self._lock:
229+
return copy.deepcopy(self.user_settings)
207230

208231
def set_config(self, key: str, value: Any) -> Tuple[bool, str, bool]:
209232
"""Set configuration value
@@ -215,51 +238,55 @@ def set_config(self, key: str, value: Any) -> Tuple[bool, str, bool]:
215238
Returns:
216239
Tuple of (success, message, requires_restart)
217240
"""
218-
default_value = self.get_default(key)
219-
is_calibration = self._is_calibration_field(key)
241+
with self._lock:
242+
default_value = self.get_default(key)
243+
is_calibration = self._is_calibration_field(key)
244+
245+
if value == default_value:
246+
if is_calibration:
247+
calibration_copy = copy.deepcopy(self.calibration_data)
248+
if self._delete_from_dict(calibration_copy, key):
249+
if self._save_json(self.calibration_data_path, calibration_copy):
250+
self.calibration_data = calibration_copy
251+
self._rebuild_merged_config()
252+
return (
253+
True,
254+
f"Reset calibration {key} to default value",
255+
key in self.restart_required_params,
256+
)
257+
else:
258+
settings_copy = copy.deepcopy(self.user_settings)
259+
if self._delete_from_dict(settings_copy, key):
260+
if self._save_json(self.user_settings_path, settings_copy):
261+
self.user_settings = settings_copy
262+
self._rebuild_merged_config()
263+
return (
264+
True,
265+
f"Reset {key} to default value",
266+
key in self.restart_required_params,
267+
)
268+
return True, "Value already at default", False
220269

221-
# If resetting to default
222-
if value == default_value:
223-
# Remove from appropriate storage
224270
if is_calibration:
225-
if self._delete_from_dict(self.calibration_data, key):
226-
self._save_json(self.calibration_data_path, self.calibration_data)
227-
self.reload()
228-
return (
229-
True,
230-
f"Reset calibration {key} to default value",
231-
key in self.restart_required_params,
232-
)
271+
calibration_copy = copy.deepcopy(self.calibration_data)
272+
if self._set_in_dict(calibration_copy, key, value):
273+
if self._save_json(self.calibration_data_path, calibration_copy):
274+
self.calibration_data = calibration_copy
275+
self._rebuild_merged_config()
276+
requires_restart = key in self.restart_required_params
277+
return True, f"Set calibration {key} = {value}", requires_restart
278+
return False, "Failed to save calibration data", False
233279
else:
234-
if self._delete_from_dict(self.user_settings, key):
235-
self._save_json(self.user_settings_path, self.user_settings)
236-
self.reload()
237-
return (
238-
True,
239-
f"Reset {key} to default value",
240-
key in self.restart_required_params,
241-
)
242-
return True, "Value already at default", False
243-
244-
# Setting a new value
245-
if is_calibration:
246-
# Save to calibration data
247-
if self._set_in_dict(self.calibration_data, key, value):
248-
if self._save_json(self.calibration_data_path, self.calibration_data):
249-
self.reload()
250-
requires_restart = key in self.restart_required_params
251-
return True, f"Set calibration {key} = {value}", requires_restart
252-
return False, "Failed to save calibration data", False
253-
else:
254-
# Save to user settings
255-
if self._set_in_dict(self.user_settings, key, value):
256-
if self._save_json(self.user_settings_path, self.user_settings):
257-
self.reload()
258-
requires_restart = key in self.restart_required_params
259-
return True, f"Set {key} = {value}", requires_restart
260-
return False, "Failed to save configuration", False
280+
settings_copy = copy.deepcopy(self.user_settings)
281+
if self._set_in_dict(settings_copy, key, value):
282+
if self._save_json(self.user_settings_path, settings_copy):
283+
self.user_settings = settings_copy
284+
self._rebuild_merged_config()
285+
requires_restart = key in self.restart_required_params
286+
return True, f"Set {key} = {value}", requires_restart
287+
return False, "Failed to save configuration", False
261288

262-
return False, "Failed to set value", False
289+
return False, "Failed to set value", False
263290

264291
def _set_in_dict(self, d: Dict[str, Any], key: str, value: Any) -> bool:
265292
"""Set value in nested dictionary using dot notation"""
@@ -295,13 +322,23 @@ def _delete_from_dict(self, d: Dict[str, Any], key: str) -> bool:
295322

296323
return False
297324

298-
def _cleanup_empty_dicts(self, d: Dict[str, Any]) -> None:
299-
"""Remove empty nested dictionaries"""
325+
def _cleanup_empty_dicts(self, d: Dict[str, Any], max_depth: int = 100, current_depth: int = 0) -> None:
326+
"""Remove empty nested dictionaries
327+
328+
Args:
329+
d: Dictionary to clean up
330+
max_depth: Maximum recursion depth (default 100)
331+
current_depth: Current recursion depth
332+
"""
333+
if current_depth >= max_depth:
334+
logger.warning(f"Maximum recursion depth {max_depth} reached in _cleanup_empty_dicts")
335+
return
336+
300337
keys_to_delete = []
301338

302339
for key, value in d.items():
303340
if isinstance(value, dict):
304-
self._cleanup_empty_dicts(value)
341+
self._cleanup_empty_dicts(value, max_depth, current_depth + 1)
305342
if not value: # Empty dict
306343
keys_to_delete.append(key)
307344

@@ -353,10 +390,10 @@ def validate_config(self, key: str, value: Any) -> Tuple[bool, str]:
353390
Returns:
354391
Tuple of (is_valid, error_message)
355392
"""
356-
# Use load_configurations_metadata() to get metadata with dynamic options
357-
metadata = self.load_configurations_metadata()
358-
settings_metadata = metadata.get("settings", {})
359-
validation_rules = metadata.get("validationRules", {})
393+
with self._lock:
394+
metadata = self.load_configurations_metadata()
395+
settings_metadata = metadata.get("settings", {})
396+
validation_rules = metadata.get("validationRules", {})
360397

361398
if key in settings_metadata:
362399
setting_info = settings_metadata[key]
@@ -648,11 +685,7 @@ def get_categories(self) -> Dict[str, Dict[str, List[str]]]:
648685
category = setting_info.get("category", "Advanced")
649686

650687
# Determine if this is a basic or advanced setting
651-
# Use showInBasic for backward compatibility, but prefer subcategory field
652688
subcategory = setting_info.get("subcategory", "advanced")
653-
if subcategory not in ["basic", "advanced"]:
654-
# Fallback to showInBasic for backward compatibility
655-
subcategory = setting_info.get("subcategory", "advanced")
656689

657690
if category in categories:
658691
categories[category][subcategory].append(key)

Software/web-server/configurations.json

Lines changed: 6 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -419,47 +419,6 @@
419419
"passedTo": "camera2",
420420
"envVariable": "PITRAC_SLOT2_LENS_TYPE"
421421
},
422-
"cameras.slot1.index": {
423-
"category": "Cameras",
424-
"subcategory": "basic",
425-
"basicSubcategory": "Hardware",
426-
"displayName": "Camera 1 Hardware Index",
427-
"description": "Hardware camera index for Camera 1 (0-based, use 'rpicam-hello --list-cameras' to see available cameras)",
428-
"type": "select",
429-
"options": {
430-
"0": "Camera 0",
431-
"1": "Camera 1",
432-
"2": "Camera 2",
433-
"3": "Camera 3"
434-
},
435-
"default": "0",
436-
"requiresRestart": true,
437-
"passedVia": "environment",
438-
"passedTo": "camera1",
439-
"envVariable": "PITRAC_SLOT1_CAMERA_INDEX"
440-
},
441-
"cameras.slot2.index": {
442-
"category": "Cameras",
443-
"subcategory": "basic",
444-
"basicSubcategory": "Hardware",
445-
"displayName": "Camera 2 Hardware Index",
446-
"description": "Hardware camera index for Camera 2 (0-based, use 'rpicam-hello --list-cameras' to see available cameras) - Only used in single Pi mode",
447-
"visibleWhen": {
448-
"system.mode": "single"
449-
},
450-
"type": "select",
451-
"options": {
452-
"0": "Camera 0",
453-
"1": "Camera 1",
454-
"2": "Camera 2",
455-
"3": "Camera 3"
456-
},
457-
"default": "1",
458-
"requiresRestart": true,
459-
"passedVia": "environment",
460-
"passedTo": "camera2",
461-
"envVariable": "PITRAC_SLOT2_CAMERA_INDEX"
462-
},
463422
"logging.level": {
464423
"category": "Logging",
465424
"subcategory": "basic",
@@ -578,7 +537,7 @@
578537
"min": 0.5,
579538
"max": 16.0,
580539
"step": 0.1,
581-
"default": 1.0,
540+
"default": 6.0,
582541
"requiresRestart": true,
583542
"passedVia": "json",
584543
"passedTo": "both"
@@ -596,7 +555,7 @@
596555
"min": 0.5,
597556
"max": 16.0,
598557
"step": 0.1,
599-
"default": 1.0,
558+
"default": 6.0,
600559
"requiresRestart": true,
601560
"passedVia": "json",
602561
"passedTo": "both"
@@ -1194,7 +1153,7 @@
11941153
"min": 0.5,
11951154
"max": 16.0,
11961155
"step": 0.1,
1197-
"default": 1.5,
1156+
"default": 4.0,
11981157
"requiresRestart": true,
11991158
"passedVia": "json",
12001159
"passedTo": "both"
@@ -1973,7 +1932,7 @@
19731932
"type": "number",
19741933
"min": 50,
19751934
"max": 300,
1976-
"default": 130,
1935+
"default": 120,
19771936
"requiresRestart": false,
19781937
"passedVia": "json",
19791938
"passedTo": "both"
@@ -1987,7 +1946,7 @@
19871946
"min": 0.5,
19881947
"max": 3.0,
19891948
"step": 0.1,
1990-
"default": 1.7,
1949+
"default": 1.5,
19911950
"requiresRestart": false,
19921951
"passedVia": "json",
19931952
"passedTo": "both"
@@ -2456,7 +2415,7 @@
24562415
"type": "number",
24572416
"min": 30,
24582417
"max": 150,
2459-
"default": 60,
2418+
"default": 70,
24602419
"requiresRestart": false,
24612420
"passedVia": "json",
24622421
"passedTo": "both"

Software/web-server/listeners.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,12 @@ def _extract_message_data(self, frame: Any) -> bytes:
111111
body = body.encode("latin-1")
112112
logger.debug("Encoded Camera2 image as latin-1")
113113
# Mark this as an image message for special processing
114-
self._handle_image_message(body, content_length)
115-
return # Don't process as regular msgpack
114+
#self._handle_image_message(body, content_length)
115+
return b""
116116
except UnicodeEncodeError as e:
117117
logger.warning(f"Failed to encode Camera2 image as latin-1: {e}")
118118
logger.info("Skipping corrupted Camera2 image")
119-
return
119+
return b""
120120
elif content_length and content_length > len(body) * 1.5:
121121
logger.info(f"Detected large binary data (content-length={content_length}, string_length={len(body)})")
122122
# Other binary data
@@ -126,7 +126,7 @@ def _extract_message_data(self, frame: Any) -> bytes:
126126
except UnicodeEncodeError as e:
127127
logger.warning(f"Failed to encode binary data as latin-1: {e}")
128128
logger.info(f"Skipping corrupted binary message #{self.message_count}")
129-
return
129+
return b""
130130
else:
131131
# Regular text string - encode as UTF-8
132132
try:

0 commit comments

Comments
 (0)