Skip to content

Commit 5b12546

Browse files
alireza787bclaude
andcommitted
feat: FollowerConfigManager — unified Global + Override config pattern for followers
Implements the same proven architecture as SafetyManager for follower operational config: Follower.General defaults → sparse Follower.FollowerOverrides → legacy per-section fallback (deprecated) → hardcoded fallback. Thread-safe singleton with O(1) cached lookups and per-param provenance reporting. Backend changes: - New FollowerConfigManager singleton (src/classes/follower_config_manager.py) - 38 unit tests covering resolution hierarchy, caching, provenance, callbacks - All 8 followers migrated from per-section config reads to FCM calls - YawRateSmoother adopted in mc_velocity_position + mc_velocity_distance - config_default.yaml restructured: shared params → Follower.General, sparse overrides in Follower.FollowerOverrides, per-follower sections keep unique only - API endpoints: GET /api/follower/config/general, /api/follower/config/{name} - Config backend fixes: remove dead deepdiff import, fix restore_backup reload, fix revert_to_default _config_raw sync, remove noisy validation warning - Removed orphaned GimbalTrackerSettings section - parameters.py: Follower in _GROUPED_SECTIONS, FCM init/reload integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 128339f commit 5b12546

15 files changed

+1253
-308
lines changed

configs/config_default.yaml

Lines changed: 92 additions & 201 deletions
Large diffs are not rendered by default.

src/classes/config_service.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333

3434
# Use ruamel.yaml for round-trip YAML (comment preservation)
3535
from ruamel.yaml import YAML
36-
from deepdiff import DeepDiff
3736

3837
logger = logging.getLogger(__name__)
3938

@@ -461,10 +460,8 @@ def validate_value(self, section: str, param: str, value: Any) -> ValidationResu
461460
f"Value {value} is above recommended maximum {rec_max}"
462461
)
463462

464-
# Check if value is different from default
465-
default_value = self.get_default_parameter(section, param)
466-
if value != default_value:
467-
warnings.append("Value differs from default")
463+
# Note: "differs from default" is informational provenance, not a warning.
464+
# The UI can show this via the default_value field in parameter metadata.
468465

469466
# Check reboot requirement
470467
if param_schema.get('reboot_required', False):
@@ -668,9 +665,15 @@ def revert_to_default(
668665
default_section = self.get_default(section)
669666
if default_section:
670667
self._config[section] = default_section.copy()
668+
# Keep _config_raw in sync for round-trip YAML
669+
if self._config_raw is not None and section in self._config_raw:
670+
for key, value in default_section.items():
671+
self._config_raw[section][key] = value
671672
else:
672673
# Revert everything
673674
self._config = self._default.copy()
675+
# Reload _config_raw from default file for full round-trip sync
676+
self._config_raw = None
674677

675678
logger.info(f"Reverted to default: section={section}, param={param}")
676679
return True

src/classes/fastapi_handler.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,10 @@ def define_routes(self):
419419
self.app.get("/api/safety/limits/{follower_name}")(self.get_follower_safety_limits)
420420
# Note: /api/safety/vehicle-profiles removed in v4.0.0 (was deprecated in v3.6.0)
421421

422+
# Follower configuration API endpoints (v6.1.0+)
423+
self.app.get("/api/follower/config/general")(self.get_follower_config_general)
424+
self.app.get("/api/follower/config/{follower_name}")(self.get_follower_config_effective)
425+
422426
# Enhanced safety/config endpoints (v5.0.0+)
423427
self.app.get("/api/config/effective-limits")(self.get_effective_limits)
424428
self.app.get("/api/config/sections/relevant")(self.get_relevant_sections)
@@ -4541,6 +4545,55 @@ def get_group_source(param_names):
45414545

45424546
# Note: get_vehicle_profiles() removed in v4.0.0 (was deprecated in v3.6.0)
45434547

4548+
# ==================== Follower Config API Endpoints (v6.1.0+) ====================
4549+
4550+
async def get_follower_config_general(self):
4551+
"""
4552+
Get Follower.General configuration values.
4553+
4554+
Returns:
4555+
dict: General follower config and available followers with overrides
4556+
"""
4557+
try:
4558+
from classes.follower_config_manager import get_follower_config_manager
4559+
fcm = get_follower_config_manager()
4560+
4561+
return JSONResponse(content={
4562+
'available': True,
4563+
'general': fcm._general,
4564+
'follower_overrides': fcm._overrides,
4565+
'available_followers': fcm.get_available_followers(),
4566+
'timestamp': time.time()
4567+
})
4568+
except Exception as e:
4569+
self.logger.error(f"Error getting follower config general: {e}")
4570+
raise HTTPException(status_code=500, detail=str(e))
4571+
4572+
async def get_follower_config_effective(self, follower_name: str):
4573+
"""
4574+
Get per-parameter provenance for a specific follower.
4575+
4576+
Args:
4577+
follower_name: Name of the follower (e.g., 'MC_VELOCITY_CHASE')
4578+
4579+
Returns:
4580+
dict: Per-param effective value, source, override status
4581+
"""
4582+
try:
4583+
from classes.follower_config_manager import get_follower_config_manager
4584+
fcm = get_follower_config_manager()
4585+
4586+
summary = fcm.get_effective_config_summary(follower_name)
4587+
4588+
return JSONResponse(content={
4589+
'follower_name': follower_name,
4590+
'params': summary,
4591+
'timestamp': time.time()
4592+
})
4593+
except Exception as e:
4594+
self.logger.error(f"Error getting follower config for {follower_name}: {e}")
4595+
raise HTTPException(status_code=500, detail=str(e))
4596+
45444597
# ==================== Enhanced Safety/Config API Endpoints (v5.0.0+) ====================
45454598

45464599
async def get_effective_limits(self, follower_name: str = None):
@@ -4607,8 +4660,8 @@ async def get_relevant_sections(self, follower_mode: str = None):
46074660
'mc_velocity_distance': ['Follower', 'MC_VELOCITY_DISTANCE', 'Safety', 'PID', 'Tracking', 'OSD'],
46084661
'mc_velocity_ground': ['Follower', 'MC_VELOCITY_GROUND', 'Safety', 'PID', 'Tracking', 'OSD'],
46094662
'mc_attitude_rate': ['Follower', 'MC_ATTITUDE_RATE', 'Safety', 'PID', 'Tracking', 'OSD'],
4610-
'gm_velocity_chase': ['Follower', 'GM_VELOCITY_CHASE', 'Safety', 'GimbalTracker', 'GimbalTrackerSettings', 'PID', 'Tracking', 'Gimbal', 'OSD'],
4611-
'gm_velocity_vector': ['Follower', 'GM_VELOCITY_VECTOR', 'Safety', 'GimbalTracker', 'GimbalTrackerSettings', 'PID', 'Tracking', 'Gimbal', 'OSD'],
4663+
'gm_velocity_chase': ['Follower', 'GM_VELOCITY_CHASE', 'Safety', 'GimbalTracker', 'PID', 'Tracking', 'Gimbal', 'OSD'],
4664+
'gm_velocity_vector': ['Follower', 'GM_VELOCITY_VECTOR', 'Safety', 'GimbalTracker', 'PID', 'Tracking', 'Gimbal', 'OSD'],
46124665
'fw_attitude_rate': ['Follower', 'FW_ATTITUDE_RATE', 'Safety', 'PID', 'Tracking', 'OSD'],
46134666
}
46144667

@@ -5471,6 +5524,13 @@ async def restore_config_backup(self, backup_id: str):
54715524
service = self._get_config_service()
54725525
success = service.restore_backup(backup_id)
54735526

5527+
# Reload Parameters + managers so runtime reflects the restored config
5528+
if success:
5529+
try:
5530+
Parameters.reload_config()
5531+
except Exception as e:
5532+
self.logger.error(f"Failed to reload after backup restore: {e}")
5533+
54745534
return JSONResponse(content={
54755535
'success': success,
54765536
'backup_id': backup_id,

0 commit comments

Comments
 (0)