config_manager.py
CrazyFly/python/utils/config_manager.py
"""
Configuration Management System for Quadrotor Control System
===========================================================
This module provides a comprehensive configuration management system
for the quadrotor control system, supporting multiple formats, validation,
hot-reloading, and hierarchical configuration.
Key Features:
- Multiple configuration formats (JSON, YAML, INI, TOML)
- Configuration validation and schema enforcement
- Hot-reloading of configuration changes
- Hierarchical configuration with inheritance
- Environment variable support
- Configuration encryption for sensitive data
- Configuration versioning and migration
- Default configuration generation
- Configuration export and import
The configuration manager enables flexible and robust configuration
management for all system components.
Author: [Your Name]
Date: [Current Date]
License: MIT
"""
import json
import yaml
import configparser
import toml
import os
import time
import threading
from typing import Dict, List, Tuple, Optional, Any, Union, Callable
from dataclasses import dataclass, field, asdict
from enum import Enum
from pathlib import Path
import logging
from datetime import datetime
import hashlib
import base64
from cryptography.fernet import Fernet
import copy
import jsonschema
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ConfigFormat(Enum):
"""Enumeration of configuration formats."""
JSON = "json"
YAML = "yaml"
INI = "ini"
TOML = "toml"
class ConfigSection(Enum):
"""Enumeration of configuration sections."""
SYSTEM = "system"
CONTROL = "control"
SENSORS = "sensors"
COMMUNICATION = "communication"
GUI = "gui"
LOGGING = "logging"
SAFETY = "safety"
PERFORMANCE = "performance"
@dataclass
class ConfigSchema:
"""
Configuration schema definition.
Attributes:
section (ConfigSection): Configuration section
properties (Dict[str, Any]): Schema properties
required (List[str]): Required properties
default_values (Dict[str, Any]): Default values
"""
section: ConfigSection
properties: Dict[str, Any] = field(default_factory=dict)
required: List[str] = field(default_factory=list)
default_values: Dict[str, Any] = field(default_factory=dict)
@dataclass
class ConfigEntry:
"""
Configuration entry.
Attributes:
key (str): Configuration key
value (Any): Configuration value
section (ConfigSection): Configuration section
description (str): Description of the configuration
data_type (str): Data type of the value
validation_rules (Dict[str, Any]): Validation rules
encrypted (bool): Whether the value is encrypted
"""
key: str
value: Any
section: ConfigSection
description: str = ""
data_type: str = "string"
validation_rules: Dict[str, Any] = field(default_factory=dict)
encrypted: bool = False
class ConfigValidator:
"""
Configuration validator.
This class provides methods for validating configuration
data against schemas and custom validation rules.
"""
def __init__(self):
"""Initialize configuration validator."""
self.schemas: Dict[ConfigSection, ConfigSchema] = {}
self.custom_validators: Dict[str, Callable] = {}
# Setup default schemas
self._setup_default_schemas()
logger.info("Configuration validator initialized")
def _setup_default_schemas(self):
"""Setup default configuration schemas."""
# System configuration schema
system_schema = ConfigSchema(
section=ConfigSection.SYSTEM,
properties={
"system_name": {"type": "string", "minLength": 1},
"version": {"type": "string", "pattern": r"^\d+\.\d+\.\d+$"},
"debug_mode": {"type": "boolean"},
"log_level": {"type": "string", "enum": ["DEBUG", "INFO", "WARNING", "ERROR"]},
"max_threads": {"type": "integer", "minimum": 1, "maximum": 100},
"update_rate": {"type": "number", "minimum": 1.0, "maximum": 1000.0}
},
required=["system_name", "version", "debug_mode"],
default_values={
"system_name": "CrazyFly",
"version": "1.0.0",
"debug_mode": False,
"log_level": "INFO",
"max_threads": 8,
"update_rate": 100.0
}
)
self.schemas[ConfigSection.SYSTEM] = system_schema
# Control configuration schema
control_schema = ConfigSchema(
section=ConfigSection.CONTROL,
properties={
"control_mode": {"type": "string", "enum": ["PID", "L1", "MPC", "HYBRID"]},
"position_kp": {"type": "number", "minimum": 0.0, "maximum": 100.0},
"position_ki": {"type": "number", "minimum": 0.0, "maximum": 100.0},
"position_kd": {"type": "number", "minimum": 0.0, "maximum": 100.0},
"velocity_kp": {"type": "number", "minimum": 0.0, "maximum": 100.0},
"velocity_ki": {"type": "number", "minimum": 0.0, "maximum": 100.0},
"velocity_kd": {"type": "number", "minimum": 0.0, "maximum": 100.0},
"attitude_kp": {"type": "number", "minimum": 0.0, "maximum": 100.0},
"attitude_ki": {"type": "number", "minimum": 0.0, "maximum": 100.0},
"attitude_kd": {"type": "number", "minimum": 0.0, "maximum": 100.0},
"max_thrust": {"type": "number", "minimum": 0.0, "maximum": 1.0},
"min_thrust": {"type": "number", "minimum": 0.0, "maximum": 1.0}
},
required=["control_mode"],
default_values={
"control_mode": "PID",
"position_kp": 2.0,
"position_ki": 0.1,
"position_kd": 1.0,
"velocity_kp": 1.5,
"velocity_ki": 0.05,
"velocity_kd": 0.8,
"attitude_kp": 3.0,
"attitude_ki": 0.2,
"attitude_kd": 1.5,
"max_thrust": 0.9,
"min_thrust": 0.1
}
)
self.schemas[ConfigSection.CONTROL] = control_schema
# Sensors configuration schema
sensors_schema = ConfigSchema(
section=ConfigSection.SENSORS,
properties={
"vicon_enabled": {"type": "boolean"},
"vicon_host": {"type": "string"},
"vicon_port": {"type": "integer", "minimum": 1, "maximum": 65535},
"imu_enabled": {"type": "boolean"},
"imu_update_rate": {"type": "number", "minimum": 1.0, "maximum": 1000.0},
"gps_enabled": {"type": "boolean"},
"gps_update_rate": {"type": "number", "minimum": 1.0, "maximum": 10.0},
"barometer_enabled": {"type": "boolean"},
"magnetometer_enabled": {"type": "boolean"}
},
required=[],
default_values={
"vicon_enabled": True,
"vicon_host": "192.168.1.100",
"vicon_port": 801,
"imu_enabled": True,
"imu_update_rate": 100.0,
"gps_enabled": False,
"gps_update_rate": 1.0,
"barometer_enabled": True,
"magnetometer_enabled": True
}
)
self.schemas[ConfigSection.SENSORS] = sensors_schema
# Communication configuration schema
communication_schema = ConfigSchema(
section=ConfigSection.COMMUNICATION,
properties={
"udp_enabled": {"type": "boolean"},
"udp_host": {"type": "string"},
"udp_port": {"type": "integer", "minimum": 1, "maximum": 65535},
"serial_enabled": {"type": "boolean"},
"serial_port": {"type": "string"},
"serial_baudrate": {"type": "integer", "enum": [9600, 19200, 38400, 57600, 115200]},
"crazyflie_enabled": {"type": "boolean"},
"crazyflie_address": {"type": "string"}
},
required=[],
default_values={
"udp_enabled": True,
"udp_host": "127.0.0.1",
"udp_port": 8080,
"serial_enabled": False,
"serial_port": "/dev/ttyUSB0",
"serial_baudrate": 115200,
"crazyflie_enabled": True,
"crazyflie_address": "radio://0/80/2M/E7E7E7E7E7"
}
)
self.schemas[ConfigSection.COMMUNICATION] = communication_schema
def add_schema(self, schema: ConfigSchema):
"""
Add a configuration schema.
Args:
schema (ConfigSchema): Schema to add
"""
self.schemas[schema.section] = schema
logger.debug(f"Added schema for section: {schema.section.value}")
def add_custom_validator(self, name: str, validator_func: Callable):
"""
Add a custom validator function.
Args:
name (str): Validator name
validator_func (Callable): Validator function
"""
self.custom_validators[name] = validator_func
logger.debug(f"Added custom validator: {name}")
def validate_section(self, section: ConfigSection, data: Dict[str, Any]) -> Tuple[bool, List[str]]:
"""
Validate a configuration section.
Args:
section (ConfigSection): Configuration section
data (Dict[str, Any]): Configuration data
Returns:
Tuple[bool, List[str]]: (is_valid, error_messages)
"""
errors = []
if section not in self.schemas:
errors.append(f"No schema defined for section: {section.value}")
return False, errors
schema = self.schemas[section]
# Check required fields
for required_field in schema.required:
if required_field not in data:
errors.append(f"Required field missing: {required_field}")
# Validate properties
for key, value in data.items():
if key in schema.properties:
prop_schema = schema.properties[key]
is_valid, prop_errors = self._validate_property(key, value, prop_schema)
if not is_valid:
errors.extend(prop_errors)
return len(errors) == 0, errors
def _validate_property(self, key: str, value: Any, schema: Dict[str, Any]) -> Tuple[bool, List[str]]:
"""
Validate a single property.
Args:
key (str): Property key
value (Any): Property value
schema (Dict[str, Any]): Property schema
Returns:
Tuple[bool, List[str]]: (is_valid, error_messages)
"""
errors = []
# Type validation
expected_type = schema.get("type")
if expected_type:
if expected_type == "string" and not isinstance(value, str):
errors.append(f"{key}: Expected string, got {type(value).__name__}")
elif expected_type == "integer" and not isinstance(value, int):
errors.append(f"{key}: Expected integer, got {type(value).__name__}")
elif expected_type == "number" and not isinstance(value, (int, float)):
errors.append(f"{key}: Expected number, got {type(value).__name__}")
elif expected_type == "boolean" and not isinstance(value, bool):
errors.append(f"{key}: Expected boolean, got {type(value).__name__}")
# Range validation
if "minimum" in schema and value < schema["minimum"]:
errors.append(f"{key}: Value {value} below minimum {schema['minimum']}")
if "maximum" in schema and value > schema["maximum"]:
errors.append(f"{key}: Value {value} above maximum {schema['maximum']}")
# Length validation
if "minLength" in schema and isinstance(value, str) and len(value) < schema["minLength"]:
errors.append(f"{key}: String length {len(value)} below minimum {schema['minLength']}")
if "maxLength" in schema and isinstance(value, str) and len(value) > schema["maxLength"]:
errors.append(f"{key}: String length {len(value)} above maximum {schema['maxLength']}")
# Enum validation
if "enum" in schema and value not in schema["enum"]:
errors.append(f"{key}: Value {value} not in allowed values {schema['enum']}")
# Pattern validation
if "pattern" in schema and isinstance(value, str):
import re
if not re.match(schema["pattern"], value):
errors.append(f"{key}: Value {value} does not match pattern {schema['pattern']}")
return len(errors) == 0, errors
class ConfigEncryption:
"""
Configuration encryption utility.
This class provides methods for encrypting sensitive
configuration data.
"""
def __init__(self, key: str = None):
"""
Initialize configuration encryption.
Args:
key (str): Encryption key (if None, generates a new key)
"""
if key is None:
self.key = Fernet.generate_key()
else:
self.key = key.encode() if isinstance(key, str) else key
self.cipher = Fernet(self.key)
logger.info("Configuration encryption initialized")
def encrypt_value(self, value: str) -> str:
"""
Encrypt a configuration value.
Args:
value (str): Value to encrypt
Returns:
str: Encrypted value (base64 encoded)
"""
encrypted = self.cipher.encrypt(value.encode())
return base64.b64encode(encrypted).decode()
def decrypt_value(self, encrypted_value: str) -> str:
"""
Decrypt a configuration value.
Args:
encrypted_value (str): Encrypted value (base64 encoded)
Returns:
str: Decrypted value
"""
encrypted_bytes = base64.b64decode(encrypted_value.encode())
decrypted = self.cipher.decrypt(encrypted_bytes)
return decrypted.decode()
def get_key(self) -> str:
"""
Get the encryption key.
Returns:
str: Encryption key (base64 encoded)
"""
return base64.b64encode(self.key).decode()
class ConfigurationManager:
"""
Main configuration manager.
This class provides comprehensive configuration management
functionality with support for multiple formats and hot-reloading.
"""
def __init__(self, config_dir: str = "config", encryption_key: str = None):
"""
Initialize configuration manager.
Args:
config_dir (str): Configuration directory
encryption_key (str): Encryption key for sensitive data
"""
self.config_dir = Path(config_dir)
self.config_dir.mkdir(parents=True, exist_ok=True)
# Initialize components
self.validator = ConfigValidator()
self.encryption = ConfigEncryption(encryption_key)
# Configuration storage
self.config_data: Dict[ConfigSection, Dict[str, Any]] = {}
self.config_files: Dict[ConfigSection, Path] = {}
self.config_format = ConfigFormat.JSON
# Hot-reloading
self.watch_thread = None
self.watching = False
self.file_hashes: Dict[Path, str] = {}
self.callbacks: List[Callable] = []
# Load default configurations
self._load_default_configs()
logger.info(f"Configuration manager initialized with directory: {self.config_dir}")
def _load_default_configs(self):
"""Load default configurations for all sections."""
for section in ConfigSection:
schema = self.validator.schemas.get(section)
if schema:
self.config_data[section] = schema.default_values.copy()
self.config_files[section] = self.config_dir / f"{section.value}.{self.config_format.value}"
def set_config_format(self, format: ConfigFormat):
"""
Set the configuration file format.
Args:
format (ConfigFormat): Configuration format
"""
self.config_format = format
logger.info(f"Configuration format set to: {format.value}")
def load_config(self, section: ConfigSection, file_path: str = None) -> bool:
"""
Load configuration from file.
Args:
section (ConfigSection): Configuration section
file_path (str): Configuration file path (optional)
Returns:
bool: True if loaded successfully
"""
if file_path is None:
file_path = self.config_files[section]
else:
file_path = Path(file_path)
try:
if not file_path.exists():
logger.warning(f"Configuration file not found: {file_path}")
return False
with open(file_path, 'r') as f:
if self.config_format == ConfigFormat.JSON:
data = json.load(f)
elif self.config_format == ConfigFormat.YAML:
data = yaml.safe_load(f)
elif self.config_format == ConfigFormat.INI:
parser = configparser.ConfigParser()
parser.read(file_path)
data = {}
for key, value in parser[section.value].items():
# Try to convert to appropriate type
try:
if value.lower() in ['true', 'false']:
data[key] = value.lower() == 'true'
elif '.' in value:
data[key] = float(value)
else:
data[key] = int(value)
except ValueError:
data[key] = value
elif self.config_format == ConfigFormat.TOML:
data = toml.load(f)
if section.value in data:
data = data[section.value]
else:
data = {}
# Validate configuration
is_valid, errors = self.validator.validate_section(section, data)
if not is_valid:
logger.error(f"Configuration validation errors for {section.value}: {errors}")
return False
# Update configuration
self.config_data[section] = data
self.config_files[section] = file_path
# Update file hash for hot-reloading
self.file_hashes[file_path] = self._get_file_hash(file_path)
logger.info(f"Loaded configuration for section: {section.value}")
return True
except Exception as e:
logger.error(f"Failed to load configuration for {section.value}: {e}")
return False
def save_config(self, section: ConfigSection, file_path: str = None) -> bool:
"""
Save configuration to file.
Args:
section (ConfigSection): Configuration section
file_path (str): Configuration file path (optional)
Returns:
bool: True if saved successfully
"""
if file_path is None:
file_path = self.config_files[section]
else:
file_path = Path(file_path)
try:
# Ensure directory exists
file_path.parent.mkdir(parents=True, exist_ok=True)
data = self.config_data.get(section, {})
with open(file_path, 'w') as f:
if self.config_format == ConfigFormat.JSON:
json.dump(data, f, indent=2)
elif self.config_format == ConfigFormat.YAML:
yaml.dump(data, f, default_flow_style=False)
elif self.config_format == ConfigFormat.INI:
parser = configparser.ConfigParser()
parser[section.value] = {str(k): str(v) for k, v in data.items()}
parser.write(f)
elif self.config_format == ConfigFormat.TOML:
toml.dump({section.value: data}, f)
# Update file hash
self.file_hashes[file_path] = self._get_file_hash(file_path)
logger.info(f"Saved configuration for section: {section.value}")
return True
except Exception as e:
logger.error(f"Failed to save configuration for {section.value}: {e}")
return False
def get_config(self, section: ConfigSection, key: str = None, default: Any = None) -> Any:
"""
Get configuration value.
Args:
section (ConfigSection): Configuration section
key (str): Configuration key (if None, returns entire section)
default (Any): Default value if key not found
Returns:
Any: Configuration value or entire section
"""
if section not in self.config_data:
return default
if key is None:
return self.config_data[section]
return self.config_data[section].get(key, default)
def set_config(self, section: ConfigSection, key: str, value: Any,
encrypt: bool = False) -> bool:
"""
Set configuration value.
Args:
section (ConfigSection): Configuration section
key (str): Configuration key
value (Any): Configuration value
encrypt (bool): Whether to encrypt the value
Returns:
bool: True if set successfully
"""
if section not in self.config_data:
self.config_data[section] = {}
# Encrypt value if requested
if encrypt and isinstance(value, str):
value = self.encryption.encrypt_value(value)
# Validate the new configuration
temp_data = self.config_data[section].copy()
temp_data[key] = value
is_valid, errors = self.validator.validate_section(section, temp_data)
if not is_valid:
logger.error(f"Configuration validation errors: {errors}")
return False
# Update configuration
self.config_data[section][key] = value
# Save to file
self.save_config(section)
logger.debug(f"Set configuration {section.value}.{key} = {value}")
return True
def get_encrypted_config(self, section: ConfigSection, key: str) -> str:
"""
Get and decrypt a configuration value.
Args:
section (ConfigSection): Configuration section
key (str): Configuration key
Returns:
str: Decrypted value
"""
encrypted_value = self.get_config(section, key)
if encrypted_value and isinstance(encrypted_value, str):
try:
return self.encryption.decrypt_value(encrypted_value)
except Exception as e:
logger.error(f"Failed to decrypt value for {section.value}.{key}: {e}")
return ""
return ""
def set_encrypted_config(self, section: ConfigSection, key: str, value: str) -> bool:
"""
Set and encrypt a configuration value.
Args:
section (ConfigSection): Configuration section
key (str): Configuration key
value (str): Value to encrypt and set
Returns:
bool: True if set successfully
"""
return self.set_config(section, key, value, encrypt=True)
def start_watching(self):
"""Start watching configuration files for changes."""
if self.watching:
logger.warning("Already watching configuration files")
return
self.watching = True
self.watch_thread = threading.Thread(target=self._watch_files, daemon=True)
self.watch_thread.start()
logger.info("Started watching configuration files")
def stop_watching(self):
"""Stop watching configuration files."""
self.watching = False
if self.watch_thread and self.watch_thread.is_alive():
self.watch_thread.join(timeout=5.0)
logger.info("Stopped watching configuration files")
def _watch_files(self):
"""Watch configuration files for changes."""
while self.watching:
for section, file_path in self.config_files.items():
if file_path.exists():
current_hash = self._get_file_hash(file_path)
if file_path in self.file_hashes and current_hash != self.file_hashes[file_path]:
logger.info(f"Configuration file changed: {file_path}")
self.load_config(section, str(file_path))
self._notify_callbacks(section)
time.sleep(1.0) # Check every second
def _get_file_hash(self, file_path: Path) -> str:
"""Get file hash for change detection."""
try:
with open(file_path, 'rb') as f:
return hashlib.md5(f.read()).hexdigest()
except Exception:
return ""
def add_change_callback(self, callback: Callable):
"""
Add a callback for configuration changes.
Args:
callback (Callable): Callback function
"""
self.callbacks.append(callback)
logger.debug("Added configuration change callback")
def _notify_callbacks(self, section: ConfigSection):
"""Notify callbacks of configuration changes."""
for callback in self.callbacks:
try:
callback(section, self.config_data[section])
except Exception as e:
logger.error(f"Error in configuration change callback: {e}")
def generate_default_config(self, section: ConfigSection, file_path: str = None) -> bool:
"""
Generate default configuration file.
Args:
section (ConfigSection): Configuration section
file_path (str): Output file path (optional)
Returns:
bool: True if generated successfully
"""
schema = self.validator.schemas.get(section)
if not schema:
logger.error(f"No schema defined for section: {section.value}")
return False
# Use default values from schema
self.config_data[section] = schema.default_values.copy()
# Save to file
return self.save_config(section, file_path)
def export_config(self, output_format: ConfigFormat, output_path: str = None) -> bool:
"""
Export all configurations to a different format.
Args:
output_format (ConfigFormat): Output format
output_path (str): Output file path (optional)
Returns:
bool: True if exported successfully
"""
if output_path is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = self.config_dir / f"export_{timestamp}.{output_format.value}"
else:
output_path = Path(output_path)
try:
# Combine all configurations
all_config = {}
for section, data in self.config_data.items():
all_config[section.value] = data
with open(output_path, 'w') as f:
if output_format == ConfigFormat.JSON:
json.dump(all_config, f, indent=2)
elif output_format == ConfigFormat.YAML:
yaml.dump(all_config, f, default_flow_style=False)
elif output_format == ConfigFormat.TOML:
toml.dump(all_config, f)
elif output_format == ConfigFormat.INI:
parser = configparser.ConfigParser()
for section_name, section_data in all_config.items():
parser[section_name] = {str(k): str(v) for k, v in section_data.items()}
parser.write(f)
logger.info(f"Exported configuration to {output_path}")
return True
except Exception as e:
logger.error(f"Failed to export configuration: {e}")
return False
def get_encryption_key(self) -> str:
"""
Get the encryption key.
Returns:
str: Encryption key (base64 encoded)
"""
return self.encryption.get_key()
# Example usage
if __name__ == "__main__":
"""
Example usage of the configuration manager.
This demonstrates how to set up and use the configuration
manager for managing quadrotor system settings.
"""
# Create configuration manager
config_manager = ConfigurationManager("config")
# Set configuration format
config_manager.set_config_format(ConfigFormat.JSON)
# Generate default configurations
for section in ConfigSection:
config_manager.generate_default_config(section)
# Load configurations
for section in ConfigSection:
config_manager.load_config(section)
# Set some configuration values
config_manager.set_config(ConfigSection.CONTROL, "position_kp", 2.5)
config_manager.set_config(ConfigSection.CONTROL, "position_ki", 0.15)
config_manager.set_config(ConfigSection.SYSTEM, "debug_mode", True)
# Set encrypted configuration
config_manager.set_encrypted_config(ConfigSection.COMMUNICATION, "api_key", "secret_key_123")
# Get configuration values
position_kp = config_manager.get_config(ConfigSection.CONTROL, "position_kp")
debug_mode = config_manager.get_config(ConfigSection.SYSTEM, "debug_mode")
api_key = config_manager.get_encrypted_config(ConfigSection.COMMUNICATION, "api_key")
print(f"Position KP: {position_kp}")
print(f"Debug Mode: {debug_mode}")
print(f"API Key: {api_key}")
# Add change callback
def on_config_change(section, data):
print(f"Configuration changed for {section.value}: {data}")
config_manager.add_change_callback(on_config_change)
# Start watching for changes
config_manager.start_watching()
# Export configuration
config_manager.export_config(ConfigFormat.YAML, "config_export.yaml")
print("Configuration management example completed!")
# Stop watching
config_manager.stop_watching()
Bài viết liên quan
real_time_controller.cpp
real_time_controller.cpp — cpp source code from the CrazyFly learning materials (CrazyFly/cpp/high_freq_control/real_time_controller.cpp).
Đọc bài viết →four_layer_pid.m
four_layer_pid.m — objectivec source code from the CrazyFly learning materials (CrazyFly/matlab/analysis/four_layer_pid.m).
Đọc bài viết →hybrid_controller.m
hybrid_controller.m — objectivec source code from the CrazyFly learning materials (CrazyFly/matlab/analysis/hybrid_controller.m).
Đọc bài viết →l1_adaptive_model.m
l1_adaptive_model.m — objectivec source code from the CrazyFly learning materials (CrazyFly/matlab/analysis/l1_adaptive_model.m).
Đọc bài viết →parameter_optimizer.m
parameter_optimizer.m — objectivec source code from the CrazyFly learning materials (CrazyFly/matlab/analysis/parameter_optimizer.m).
Đọc bài viết →performance_analyzer.m
performance_analyzer.m — objectivec source code from the CrazyFly learning materials (CrazyFly/matlab/analysis/performance_analyzer.m).
Đọc bài viết →