mirror of
https://github.com/Ralim/IronOS.git
synced 2025-07-23 20:30:38 +02:00
WiP on editing settings
This commit is contained in:
@@ -8,3 +8,19 @@ Utility scripts are provided to work with this file.
|
|||||||
This is only supported with devices that allow reading the device memory back out over USB. This **DOES NOT** work if your device shows up as a USB storage device when in programming mode.
|
This is only supported with devices that allow reading the device memory back out over USB. This **DOES NOT** work if your device shows up as a USB storage device when in programming mode.
|
||||||
|
|
||||||
## Writing settings in one go to a device
|
## Writing settings in one go to a device
|
||||||
|
|
||||||
|
You can use the edit_settings.py script to generate a .bin or .hex file that can be written to the device.
|
||||||
|
If your device supports reading out the current memory, you can load your existing settings from a file you can dump from the device.
|
||||||
|
|
||||||
|
### Main Files
|
||||||
|
|
||||||
|
- `edit_settings.py` - Editing binary settings files
|
||||||
|
- `generate_settings.py` - C++ Code generation used in build
|
||||||
|
|
||||||
|
### Library Structure (`lib/` directory)
|
||||||
|
|
||||||
|
- `settings_types.py` - Common types, constants, and imports
|
||||||
|
- `settings_util.py` - Utility functions like `get_base_address` and `resolve_expression`
|
||||||
|
- `settings_model.py` - Core data models (`SettingsEntry` and `Settings` classes)
|
||||||
|
- `settings_parser.py` - Functions for parsing settings and expressions
|
||||||
|
- `settings_cli.py` - Command-line interface handling
|
||||||
|
266
source/Settings/config_parser.py
Normal file
266
source/Settings/config_parser.py
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Dict, Optional, List, Set
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigParser:
|
||||||
|
"""
|
||||||
|
Parser for IronOS configuration.h files based on the specified model.
|
||||||
|
|
||||||
|
Extracts #define values from the appropriate BSP folder's configuration.h file,
|
||||||
|
handling model-specific sections with #ifdef blocks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Mapping from model string to BSP folder name
|
||||||
|
MODEL_TO_FOLDER = {
|
||||||
|
"TS100": "Miniware",
|
||||||
|
"TS80": "Miniware",
|
||||||
|
"TS80P": "Miniware",
|
||||||
|
"TS101": "Miniware",
|
||||||
|
"Pinecil": "Pinecil",
|
||||||
|
"Pinecilv2": "Pinecilv2",
|
||||||
|
"S60": "Sequre",
|
||||||
|
"S60P": "Sequre",
|
||||||
|
"MHP30": "MHP30",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, model: str, base_path: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Initialize the parser with the model name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: The model name (e.g., "TS100", "Pinecilv2")
|
||||||
|
base_path: Optional path to the IronOS source root, defaults to "../Core/BSP"
|
||||||
|
relative to this file's location
|
||||||
|
"""
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
# Validate model
|
||||||
|
if model not in self.MODEL_TO_FOLDER:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unknown model: {model}. Supported models: {', '.join(self.MODEL_TO_FOLDER.keys())}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.folder = self.MODEL_TO_FOLDER[model]
|
||||||
|
|
||||||
|
# Determine base path
|
||||||
|
if base_path is None:
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
base_path = os.path.join(current_dir, "..", "Core", "BSP")
|
||||||
|
self.base_path = base_path
|
||||||
|
|
||||||
|
# Compute the path to the configuration file
|
||||||
|
self.config_path = os.path.join(base_path, self.folder, "configuration.h")
|
||||||
|
if not os.path.exists(self.config_path):
|
||||||
|
raise FileNotFoundError(f"Configuration file not found: {self.config_path}")
|
||||||
|
|
||||||
|
def _preprocess_content(self, content: str) -> str:
|
||||||
|
"""
|
||||||
|
Preprocess the content by removing comments and handling line continuations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The raw file content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Preprocessed content with comments removed and line continuations handled
|
||||||
|
"""
|
||||||
|
# Remove C-style comments
|
||||||
|
content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
|
||||||
|
# Remove C++-style comments
|
||||||
|
content = re.sub(r"//.*?$", "", content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
# Handle line continuations
|
||||||
|
content = re.sub(r"\\\s*\n", " ", content)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _extract_defines(self, content: str) -> Dict[str, Optional[str]]:
|
||||||
|
"""
|
||||||
|
Extract all #define directives from the content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The preprocessed file content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping define names to their values
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
define_pattern = re.compile(
|
||||||
|
r"#define\s+(\w+)(?:\s+(.+?))?(?:\s+//.*)?$", re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
for match in define_pattern.finditer(content):
|
||||||
|
key = match.group(1)
|
||||||
|
value = match.group(2)
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
value = value.strip()
|
||||||
|
if not value: # Empty value after stripping
|
||||||
|
value = None
|
||||||
|
|
||||||
|
result[key] = value
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _is_valid_value(self, value: Optional[str]) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the define value is valid for inclusion.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: The define value to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the value is numeric, False otherwise
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Try to parse as an integer or float
|
||||||
|
try:
|
||||||
|
int(value, 0) # Base 0 handles 0x for hex, etc.
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
float(value)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _filter_defines(self, defines: Dict[str, Optional[str]]) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Filter defines to include only those with numeric values.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
defines: Dictionary of all defines
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with only numeric defines, converted to integers
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for key, value in defines.items():
|
||||||
|
if self._is_valid_value(value):
|
||||||
|
try:
|
||||||
|
# Try to convert to int (for hex, binary, etc.)
|
||||||
|
result[key] = int(value, 0)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
# If that fails, try float and then convert to int
|
||||||
|
result[key] = int(float(value))
|
||||||
|
except ValueError:
|
||||||
|
# If all conversions fail, skip this value
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_model_specific_blocks(self, content: str) -> List[tuple]:
|
||||||
|
"""
|
||||||
|
Extract model-specific blocks from the content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The preprocessed file content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of tuples with (model_name, block_content)
|
||||||
|
"""
|
||||||
|
blocks = []
|
||||||
|
model_ifdef_pattern = re.compile(
|
||||||
|
r"#ifdef\s+MODEL_(\w+)(.*?)(?:#else.*?)?#endif", re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
for match in model_ifdef_pattern.finditer(content):
|
||||||
|
model_name = match.group(1)
|
||||||
|
block_content = match.group(2)
|
||||||
|
blocks.append((model_name, block_content))
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
def parse(self) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Parse the configuration file for the specified model.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of parsed #define values (name -> numeric value)
|
||||||
|
"""
|
||||||
|
# Read the configuration file
|
||||||
|
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Preprocess the content
|
||||||
|
preprocessed = self._preprocess_content(content)
|
||||||
|
|
||||||
|
# Extract all defines from the main content
|
||||||
|
all_defines = self._extract_defines(preprocessed)
|
||||||
|
|
||||||
|
# Get model-specific blocks
|
||||||
|
model_blocks = self._get_model_specific_blocks(preprocessed)
|
||||||
|
|
||||||
|
# Process model-specific blocks
|
||||||
|
handled_keys = set()
|
||||||
|
for block_model, block_content in model_blocks:
|
||||||
|
# If this block is for our model or we're in a Miniware model
|
||||||
|
if block_model == self.model or (
|
||||||
|
self.folder == "Miniware"
|
||||||
|
and f"MODEL_{self.model}" == f"MODEL_{block_model}"
|
||||||
|
):
|
||||||
|
|
||||||
|
# Extract defines from this block
|
||||||
|
block_defines = self._extract_defines(block_content)
|
||||||
|
|
||||||
|
# Add to all_defines, these take precedence
|
||||||
|
for key, value in block_defines.items():
|
||||||
|
all_defines[key] = value
|
||||||
|
handled_keys.add(key)
|
||||||
|
|
||||||
|
# Remove keys that were in other model-specific blocks but not for our model
|
||||||
|
for block_model, block_content in model_blocks:
|
||||||
|
if block_model != self.model and not (
|
||||||
|
self.folder == "Miniware"
|
||||||
|
and f"MODEL_{self.model}" == f"MODEL_{block_model}"
|
||||||
|
):
|
||||||
|
block_defines = self._extract_defines(block_content)
|
||||||
|
for key in block_defines:
|
||||||
|
if key not in handled_keys and key in all_defines:
|
||||||
|
del all_defines[key]
|
||||||
|
|
||||||
|
# Filter defines to only include numeric values
|
||||||
|
numeric_defines = self._filter_defines(all_defines)
|
||||||
|
|
||||||
|
return numeric_defines
|
||||||
|
|
||||||
|
|
||||||
|
def parse_config(model: str, base_path: Optional[str] = None) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Parse the configuration for the specified model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: The model string (e.g., "TS100", "Pinecilv2")
|
||||||
|
base_path: Optional path to the IronOS source root
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of configuration values
|
||||||
|
"""
|
||||||
|
parser = ConfigParser(model, base_path)
|
||||||
|
return parser.parse()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python config_parser.py MODEL_NAME [BASE_PATH]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
model = sys.argv[1]
|
||||||
|
base_path = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = parse_config(model, base_path)
|
||||||
|
print(json.dumps(config, indent=2))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
@@ -1,32 +1,18 @@
|
|||||||
#! python3
|
#!/usr/bin/env python3
|
||||||
import yaml
|
|
||||||
from typing import List
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsEntry:
|
|
||||||
def __init__(self, min, max, increment, default):
|
|
||||||
self.min = min
|
|
||||||
self.max = max
|
|
||||||
self.increment = increment
|
|
||||||
self.default = default
|
|
||||||
|
|
||||||
|
|
||||||
class Settings:
|
|
||||||
settings:List[SettingsEntry] = []
|
|
||||||
|
|
||||||
|
|
||||||
def load_settings(file_path):
|
|
||||||
with open(file_path, 'r') as f:
|
|
||||||
data = yaml.safe_load(f)
|
|
||||||
settings_obj = Settings()
|
|
||||||
settings_obj.settings = data['settings']
|
|
||||||
return settings_obj
|
|
||||||
|
|
||||||
|
|
||||||
settings_data = load_settings( 'settings.yaml')
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
If the user has provided a settings binary dump, we load it and update the settings data
|
IronOS Settings Editor - Refactored
|
||||||
If the user has requested to edit the settings, we provide a menu to edit each setting
|
|
||||||
|
A tool to edit and generate settings binary files for IronOS.
|
||||||
|
This is a refactored version of the original edit_settings.py,
|
||||||
|
with functionality split into separate modules for better maintainability.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from lib import run_editing_settings_file_cli
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
run_editing_settings_file_cli()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nOperation cancelled by user")
|
||||||
|
sys.exit(1)
|
||||||
|
@@ -1,79 +1,146 @@
|
|||||||
#! python3
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
IronOS Settings Generator - Refactored
|
||||||
|
|
||||||
|
A tool to generate C++ code from settings definitions for IronOS.
|
||||||
|
This is a refactored version that uses the shared library modules.
|
||||||
|
"""
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from typing import List
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
|
from typing import List
|
||||||
|
|
||||||
SETTINGS_TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "settings_gen.cpp.template")
|
# Import from the lib package
|
||||||
|
from lib.settings_model import Settings
|
||||||
|
from lib.settings_types import DEFAULT_YAML_PATH
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
SETTINGS_TEMPLATE_PATH = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "settings_gen.cpp.template"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments()->tuple[str, str, str]:
|
def parse_arguments() -> tuple[str, str, str]:
|
||||||
parser = argparse.ArgumentParser(description='Processes the settings definitions and makes a compilable C++ file.')
|
"""Parse command line arguments for the settings generator
|
||||||
parser.add_argument('model_code', help='Model code identifier (e.g., TS101, Pinecilv2)')
|
|
||||||
parser.add_argument('settings_definitions', help='Path to the settings YAML definition file')
|
Returns:
|
||||||
parser.add_argument('output_file_path', help='Path where the generated C++ file should be written')
|
tuple: (model_code, settings_definitions_path, output_file_path)
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Processes the settings definitions and makes a compilable C++ file."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"model_code", help="Model code identifier (e.g., TS101, Pinecilv2)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"settings_definitions",
|
||||||
|
help="Path to the settings YAML definition file",
|
||||||
|
default=DEFAULT_YAML_PATH,
|
||||||
|
nargs="?",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"output_file_path", help="Path where the generated C++ file should be written"
|
||||||
|
)
|
||||||
|
|
||||||
print(f"Parsing command line arguments... {sys.argv[1:]}")
|
print(f"Parsing command line arguments... {sys.argv[1:]}")
|
||||||
if len(sys.argv) < 4:
|
if len(sys.argv) < 3:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Check if settings definitions file exists
|
# Check if settings definitions file exists
|
||||||
if not os.path.isfile(args.settings_definitions):
|
if not os.path.isfile(args.settings_definitions):
|
||||||
print(f"Error: Settings definition file '{args.settings_definitions}' does not exist.")
|
print(
|
||||||
|
f"Error: Settings definition file '{args.settings_definitions}' does not exist."
|
||||||
|
)
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return (args.model_code, args.settings_definitions, args.output_file_path)
|
return (args.model_code, args.settings_definitions, args.output_file_path)
|
||||||
|
|
||||||
# Parse the command line arguments
|
|
||||||
(MODEL_CODE,SETTINGS_DEFINITIONS_PATH,SETTINGS_OUTPUT_PATH) = parse_arguments()
|
|
||||||
|
|
||||||
class SettingsEntry:
|
def convert_settings_to_cpp(settings) -> str:
|
||||||
def __init__(self, min, max, increment, default):
|
"""Convert settings to C++ code for inclusion in a template
|
||||||
self.min = min
|
|
||||||
self.max = max
|
|
||||||
self.increment = increment
|
|
||||||
self.default = default
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
settings: Either a Settings object or a dictionary with settings data
|
||||||
|
|
||||||
class Settings:
|
Returns:
|
||||||
settings:List[SettingsEntry] = []
|
String containing formatted C++ code for settings table
|
||||||
|
"""
|
||||||
|
|
||||||
def load_settings(file_path):
|
|
||||||
with open(file_path, 'r') as f:
|
|
||||||
data = yaml.safe_load(f)
|
|
||||||
settings_obj = Settings()
|
|
||||||
settings_obj.settings = data['settings']
|
|
||||||
return settings_obj
|
|
||||||
|
|
||||||
def save_settings(settings_obj, file_path):
|
|
||||||
with open(file_path, 'w') as f:
|
|
||||||
yaml.dump(settings_obj.__dict__, f, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_settings_to_cpp(settings_obj):
|
|
||||||
cpp_code = ""
|
cpp_code = ""
|
||||||
for setting in settings_obj.settings:
|
|
||||||
|
|
||||||
cpp_code += f" {{ {setting['min']:>22}, {setting['max']:>70}, {setting['increment']:>18}, {setting['default']:>29}}}, // {setting['name']}\r\n"
|
# Handle both our Settings object format and the original dictionary format
|
||||||
|
if hasattr(settings, "entries"):
|
||||||
|
# New format: Settings object with entries attribute
|
||||||
|
for entry in settings.entries:
|
||||||
|
cpp_code += f" {{ {entry.min:>22}, {entry.max:>70}, {entry.increment:>18}, {entry.default:>29}}}, // {entry.name}\r\n"
|
||||||
|
else:
|
||||||
|
# Original format: Dictionary with 'settings' key
|
||||||
|
for setting in settings.settings:
|
||||||
|
cpp_code += f" {{ {setting['min']:>22}, {setting['max']:>70}, {setting['increment']:>18}, {setting['default']:>29}}}, // {setting['name']}\r\n"
|
||||||
|
|
||||||
return cpp_code
|
return cpp_code
|
||||||
|
|
||||||
|
|
||||||
"""
|
def main():
|
||||||
Load the settings definitions yaml file, this is used to then generate the settings_gen.cpp file.
|
"""Main function to run the settings generator"""
|
||||||
"""
|
# Parse command line arguments
|
||||||
|
(model_code, settings_definitions_path, settings_output_path) = parse_arguments()
|
||||||
|
|
||||||
settings_data = load_settings( SETTINGS_DEFINITIONS_PATH)
|
# Initialize settings
|
||||||
cpp_code = convert_settings_to_cpp(settings_data)
|
settings = Settings()
|
||||||
with open(SETTINGS_TEMPLATE_PATH, 'r') as f:
|
|
||||||
template_content = f.read()
|
# Load settings definitions from YAML
|
||||||
# Write the generated C++ code to the output file
|
print(f"Loading settings definitions from {settings_definitions_path}")
|
||||||
# Make sure the directory exists
|
try:
|
||||||
os.makedirs(os.path.dirname(SETTINGS_OUTPUT_PATH), exist_ok=True)
|
settings.load_from_yaml(settings_definitions_path)
|
||||||
with open(SETTINGS_OUTPUT_PATH, 'w') as f:
|
except Exception as e:
|
||||||
f.write(template_content.replace("$SETTINGSTABLE", cpp_code))
|
print(f"Error loading settings definitions: {e}")
|
||||||
|
# Fall back to the original loading method if the new one fails
|
||||||
|
try:
|
||||||
|
print("Trying alternative loading method...")
|
||||||
|
# Load using the original method from generate_settings.py
|
||||||
|
with open(settings_definitions_path, "r") as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
settings = type("Settings", (), {})()
|
||||||
|
settings.settings = data["settings"]
|
||||||
|
print("Successfully loaded settings using alternative method.")
|
||||||
|
except Exception as nested_e:
|
||||||
|
print(f"All loading methods failed: {nested_e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Convert settings to C++ code
|
||||||
|
cpp_code = convert_settings_to_cpp(settings)
|
||||||
|
|
||||||
|
# Load template content
|
||||||
|
try:
|
||||||
|
with open(SETTINGS_TEMPLATE_PATH, "r") as f:
|
||||||
|
template_content = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading template file: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Write the generated C++ code to the output file
|
||||||
|
try:
|
||||||
|
# Make sure the directory exists
|
||||||
|
os.makedirs(os.path.dirname(settings_output_path), exist_ok=True)
|
||||||
|
|
||||||
|
# Write the output file
|
||||||
|
with open(settings_output_path, "w") as f:
|
||||||
|
f.write(template_content.replace("$SETTINGSTABLE", cpp_code))
|
||||||
|
|
||||||
|
print(f"Successfully generated C++ code at {settings_output_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error writing output file: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nOperation cancelled by user")
|
||||||
|
sys.exit(1)
|
||||||
|
34
source/Settings/lib/__init__.py
Normal file
34
source/Settings/lib/__init__.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
IronOS Settings Management Package
|
||||||
|
|
||||||
|
This package contains modules for managing IronOS settings:
|
||||||
|
- settings_types: Common types and constants
|
||||||
|
- settings_util: Utility functions
|
||||||
|
- settings_model: Data models for settings
|
||||||
|
- settings_parser: Functions for parsing settings
|
||||||
|
- settings_cli: Command-line interface
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .settings_types import DEFAULT_YAML_PATH, HEX_SUPPORT
|
||||||
|
from .settings_util import get_base_address, resolve_expression
|
||||||
|
from .settings_model import SettingsEntry, Settings
|
||||||
|
from .settings_parser import process_default_values
|
||||||
|
from .settings_cli import (
|
||||||
|
parse_arguments,
|
||||||
|
handle_input_file,
|
||||||
|
run_editing_settings_file_cli,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DEFAULT_YAML_PATH",
|
||||||
|
"HEX_SUPPORT",
|
||||||
|
"get_base_address",
|
||||||
|
"resolve_expression",
|
||||||
|
"SettingsEntry",
|
||||||
|
"Settings",
|
||||||
|
"process_default_values",
|
||||||
|
"parse_arguments",
|
||||||
|
"handle_input_file",
|
||||||
|
"run_editing_settings_file_cli",
|
||||||
|
]
|
206
source/Settings/lib/settings_cli.py
Normal file
206
source/Settings/lib/settings_cli.py
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
settings_cli.py - Command line interface for IronOS settings management
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
# Import local modules
|
||||||
|
from .settings_types import DEFAULT_YAML_PATH, HEX_SUPPORT
|
||||||
|
from .settings_model import Settings
|
||||||
|
from .settings_util import get_base_address
|
||||||
|
from .settings_parser import process_default_values
|
||||||
|
|
||||||
|
# Import the config_parser module from parent directory
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
try:
|
||||||
|
from config_parser import parse_config
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
def parse_config(model):
|
||||||
|
print(
|
||||||
|
f"Warning: config_parser module not found, BSP configuration for model {model} not available"
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_arguments():
|
||||||
|
"""Parse command line arguments"""
|
||||||
|
parser = argparse.ArgumentParser(description="Edit IronOS settings")
|
||||||
|
parser.add_argument(
|
||||||
|
"-i", "--input", help="Input binary or hex settings file (optional)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output",
|
||||||
|
help="Output binary settings file or hex file (use .hex extension for Intel HEX format)",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--definitions",
|
||||||
|
help="Settings definitions YAML file",
|
||||||
|
default=DEFAULT_YAML_PATH,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-m",
|
||||||
|
"--model",
|
||||||
|
help="Device model ID (required for Intel HEX output to set correct base address)",
|
||||||
|
choices=[
|
||||||
|
"TS100",
|
||||||
|
"TS80",
|
||||||
|
"TS80P",
|
||||||
|
"TS101",
|
||||||
|
"S60",
|
||||||
|
"S60P",
|
||||||
|
"Pinecil",
|
||||||
|
"Pinecilv2",
|
||||||
|
"MHP30",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-n",
|
||||||
|
"--non-interactive",
|
||||||
|
help="Non-interactive mode (uses default values)",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--use-bsp-defaults",
|
||||||
|
help="Use values from BSP configuration.h for non-numeric settings",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
parser.add_argument("--debug", help="Enable debug output", action="store_true")
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_input_file(args, settings) -> Tuple[Settings, int]:
|
||||||
|
"""Load settings from input file if provided and return base address
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: Command line arguments
|
||||||
|
settings: Settings object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The detected base address from the input file (0 if not available)
|
||||||
|
"""
|
||||||
|
input_base_address = 0
|
||||||
|
bsp_config = None
|
||||||
|
|
||||||
|
# If model is provided, load the BSP configuration
|
||||||
|
if args.model:
|
||||||
|
try:
|
||||||
|
print(f"Loading BSP configuration for model {args.model}")
|
||||||
|
bsp_config = parse_config(args.model)
|
||||||
|
print(f"Loaded {len(bsp_config)} configuration values from BSP")
|
||||||
|
|
||||||
|
# Add common default values that might be missing from the BSP config
|
||||||
|
if "QC_VOLTAGE_MAX" not in bsp_config:
|
||||||
|
bsp_config["QC_VOLTAGE_MAX"] = 120
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading BSP configuration: {e}")
|
||||||
|
print("Will use YAML defaults instead")
|
||||||
|
bsp_config = None
|
||||||
|
|
||||||
|
# If input file is provided, load settings from it
|
||||||
|
if args.input:
|
||||||
|
file_type = "hex" if args.input.lower().endswith(".hex") else "binary"
|
||||||
|
print(f"Loading settings from {file_type} file {args.input}")
|
||||||
|
success, input_base_address = settings.load_from_binary(args.input)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
print("Using default values from settings definitions")
|
||||||
|
process_default_values(
|
||||||
|
settings, bsp_config, args.debug if hasattr(args, "debug") else False
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("No input file provided, using default values from settings definitions")
|
||||||
|
process_default_values(
|
||||||
|
settings, bsp_config, args.debug if hasattr(args, "debug") else False
|
||||||
|
)
|
||||||
|
|
||||||
|
return (settings, input_base_address)
|
||||||
|
|
||||||
|
|
||||||
|
def run_editing_settings_file_cli():
|
||||||
|
"""Main function to run the CLI"""
|
||||||
|
args = parse_arguments()
|
||||||
|
|
||||||
|
# Check if settings definitions file exists
|
||||||
|
if not os.path.isfile(args.definitions):
|
||||||
|
print(f"Error: Settings definition file '{args.definitions}' does not exist.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Initialize settings
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
# Load settings definitions from YAML
|
||||||
|
print(f"Loading settings definitions from {args.definitions}")
|
||||||
|
try:
|
||||||
|
settings.load_from_yaml(args.definitions)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading settings definitions: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Initialize base_address
|
||||||
|
base_address = 0
|
||||||
|
input_base_address = 0
|
||||||
|
|
||||||
|
# Handle input file and process defaults
|
||||||
|
(settings, input_base_address) = handle_input_file(args, settings)
|
||||||
|
|
||||||
|
# Determine the base address to use for output
|
||||||
|
# Priority: 1. Model-specified base address, 2. Input hex file base address, 3. Default (0)
|
||||||
|
if args.model:
|
||||||
|
base_address = get_base_address(args.model)
|
||||||
|
print(f"Using base address 0x{base_address:08X} for model {args.model}")
|
||||||
|
elif input_base_address > 0:
|
||||||
|
base_address = input_base_address
|
||||||
|
print(f"Using base address 0x{base_address:08X} from input file")
|
||||||
|
|
||||||
|
# If we have a model, try to get SETTINGS_START_PAGE from BSP config
|
||||||
|
if args.model and not args.input and base_address == 0:
|
||||||
|
try:
|
||||||
|
bsp_config = parse_config(args.model)
|
||||||
|
if "SETTINGS_START_PAGE" in bsp_config:
|
||||||
|
# Use the settings page address from BSP if available
|
||||||
|
base_address = bsp_config["SETTINGS_START_PAGE"]
|
||||||
|
print(f"Using SETTINGS_START_PAGE from BSP: 0x{base_address:08X}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to get flash address from BSP: {e}")
|
||||||
|
|
||||||
|
# Edit settings if not in non-interactive mode
|
||||||
|
if not args.non_interactive:
|
||||||
|
settings.edit_all_settings()
|
||||||
|
else:
|
||||||
|
print("Running in non-interactive mode, using loaded/default values")
|
||||||
|
|
||||||
|
# Check if output is hex and we need intelhex module
|
||||||
|
if args.output.lower().endswith(".hex"):
|
||||||
|
if not HEX_SUPPORT:
|
||||||
|
print(
|
||||||
|
"Error: Output file has .hex extension but intelhex module is not installed."
|
||||||
|
)
|
||||||
|
print("Install it with 'pip install intelhex' to generate Intel HEX files.")
|
||||||
|
print(
|
||||||
|
"Please change the output file extension to .bin or install the IntelHex module."
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.model and input_base_address == 0:
|
||||||
|
print("Warning: No base address available for HEX output.")
|
||||||
|
print(
|
||||||
|
"Please specify a model with the --model option or use an input hex file with a valid base address."
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Save settings to binary or hex file
|
||||||
|
print(f"\nSaving settings to {args.output}")
|
||||||
|
if not settings.save_to_binary(args.output, base_address):
|
||||||
|
print("Failed to save settings")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Settings saved successfully")
|
298
source/Settings/lib/settings_model.py
Executable file
298
source/Settings/lib/settings_model.py
Executable file
@@ -0,0 +1,298 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
settings_model.py - Settings data models for IronOS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
import struct
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import List, Dict, Optional, Tuple, Any, Union
|
||||||
|
|
||||||
|
# Import local modules
|
||||||
|
from .settings_types import HEX_SUPPORT, IntelHex
|
||||||
|
from .settings_util import resolve_expression
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsEntry:
|
||||||
|
"""Represents a single settings entry definition"""
|
||||||
|
|
||||||
|
def __init__(self, min_value, max_value, increment, default, name):
|
||||||
|
self.min = min_value
|
||||||
|
self.max = max_value
|
||||||
|
self.increment = increment
|
||||||
|
self.default = default
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}: {self.default} (min: {self.min}, max: {self.max}, increment: {self.increment})"
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
"""Manages a collection of settings entries and their values"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.entries: List[SettingsEntry] = []
|
||||||
|
self.values: List[int] = []
|
||||||
|
|
||||||
|
def load_from_yaml(self, file_path: str) -> None:
|
||||||
|
"""Load settings definitions from YAML file"""
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
self.entries = []
|
||||||
|
self.values = []
|
||||||
|
|
||||||
|
for setting in data["settings"]:
|
||||||
|
# Some values in the YAML might use expressions referencing other values
|
||||||
|
# We'll keep them as strings for now and resolve them later
|
||||||
|
entry = SettingsEntry(
|
||||||
|
setting["min"],
|
||||||
|
setting["max"],
|
||||||
|
setting["increment"],
|
||||||
|
setting["default"],
|
||||||
|
setting["name"],
|
||||||
|
)
|
||||||
|
self.entries.append(entry)
|
||||||
|
|
||||||
|
# Try to convert default value to int if possible
|
||||||
|
default_value = setting["default"]
|
||||||
|
if isinstance(default_value, int):
|
||||||
|
self.values.append(default_value)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.values.append(int(default_value))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
self.values.append(default_value)
|
||||||
|
|
||||||
|
def load_from_binary(self, file_path: str) -> Tuple[bool, int]:
|
||||||
|
"""Load settings from a binary or hex file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Path to the binary or hex file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, base_address)
|
||||||
|
success: True if settings were loaded successfully
|
||||||
|
base_address: The base address of the settings in the flash memory (0 if not applicable)
|
||||||
|
"""
|
||||||
|
# Check file extension to determine format
|
||||||
|
is_hex_file = file_path.lower().endswith(".hex")
|
||||||
|
|
||||||
|
if is_hex_file and not HEX_SUPPORT:
|
||||||
|
print(
|
||||||
|
"Error: Cannot load .hex file because intelhex module is not installed."
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"Install it with 'pip install intelhex' to work with Intel HEX files."
|
||||||
|
)
|
||||||
|
return False, 0
|
||||||
|
|
||||||
|
# Read the file
|
||||||
|
try:
|
||||||
|
if is_hex_file:
|
||||||
|
ih = IntelHex(file_path)
|
||||||
|
# Find the address range of data in the hex file
|
||||||
|
start_addr = ih.minaddr()
|
||||||
|
end_addr = ih.maxaddr()
|
||||||
|
|
||||||
|
if end_addr - start_addr < 64:
|
||||||
|
print(
|
||||||
|
f"Warning: Hex file contains very little data ({end_addr - start_addr + 1} bytes)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract the binary data from the hex file
|
||||||
|
binary_data = ih.tobinstr(
|
||||||
|
start=start_addr, size=end_addr - start_addr + 1
|
||||||
|
)
|
||||||
|
base_address = start_addr
|
||||||
|
else:
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
binary_data = f.read()
|
||||||
|
base_address = 0
|
||||||
|
|
||||||
|
# Check if file size is correct
|
||||||
|
expected_size = len(self.entries) * 2 # 2 bytes per setting
|
||||||
|
if len(binary_data) < expected_size:
|
||||||
|
print(
|
||||||
|
f"Warning: File size ({len(binary_data)} bytes) is smaller than expected ({expected_size} bytes)"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"File may be truncated or corrupted. Will read as many settings as possible."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse settings values
|
||||||
|
for i in range(min(len(self.entries), len(binary_data) // 2)):
|
||||||
|
# Read 16-bit value (little-endian)
|
||||||
|
value = struct.unpack("<H", binary_data[i * 2 : i * 2 + 2])[0]
|
||||||
|
|
||||||
|
# Apply bounds checking
|
||||||
|
min_val = self.entries[i].min
|
||||||
|
max_val = self.entries[i].max
|
||||||
|
|
||||||
|
# If min/max are strings, try to convert to integers
|
||||||
|
if isinstance(min_val, str):
|
||||||
|
try:
|
||||||
|
min_val = int(min_val)
|
||||||
|
except ValueError:
|
||||||
|
min_val = 0
|
||||||
|
|
||||||
|
if isinstance(max_val, str):
|
||||||
|
try:
|
||||||
|
max_val = int(max_val)
|
||||||
|
except ValueError:
|
||||||
|
max_val = 65535
|
||||||
|
|
||||||
|
if value < min_val:
|
||||||
|
print(
|
||||||
|
f"Warning: Setting {self.entries[i].name} value {value} is below minimum {min_val}, clamping"
|
||||||
|
)
|
||||||
|
value = min_val
|
||||||
|
elif value > max_val:
|
||||||
|
print(
|
||||||
|
f"Warning: Setting {self.entries[i].name} value {value} is above maximum {max_val}, clamping"
|
||||||
|
)
|
||||||
|
value = max_val
|
||||||
|
|
||||||
|
self.values[i] = value
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Successfully loaded {min(len(self.entries), len(binary_data) // 2)} settings from {file_path}"
|
||||||
|
)
|
||||||
|
return True, base_address
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading settings from file: {e}")
|
||||||
|
return False, 0
|
||||||
|
|
||||||
|
def save_to_binary(self, file_path: str, base_address: int = 0) -> bool:
|
||||||
|
"""Save settings to a binary or hex file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Path to the output file
|
||||||
|
base_address: Base address for the settings in flash memory (used only for hex files)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if settings were saved successfully
|
||||||
|
"""
|
||||||
|
# Make sure all values are resolved to integers
|
||||||
|
for i in range(len(self.values)):
|
||||||
|
if not isinstance(self.values[i], int):
|
||||||
|
print(
|
||||||
|
f"Error: Setting {self.entries[i].name} value '{self.values[i]}' is not an integer"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create binary data
|
||||||
|
binary_data = bytearray()
|
||||||
|
for value in self.values:
|
||||||
|
# Pack as 16-bit little-endian
|
||||||
|
binary_data.extend(struct.pack("<H", value))
|
||||||
|
|
||||||
|
# Check file extension to determine format
|
||||||
|
is_hex_file = file_path.lower().endswith(".hex")
|
||||||
|
|
||||||
|
if is_hex_file and not HEX_SUPPORT:
|
||||||
|
print(
|
||||||
|
"Error: Cannot save as .hex file because intelhex module is not installed."
|
||||||
|
)
|
||||||
|
print("Install it with 'pip install intelhex' to create Intel HEX files.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if is_hex_file:
|
||||||
|
# Create a new Intel HEX object
|
||||||
|
ih = IntelHex()
|
||||||
|
|
||||||
|
# Add the binary data at the specified base address
|
||||||
|
for i, byte_val in enumerate(binary_data):
|
||||||
|
ih[base_address + i] = byte_val
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
ih.write_hex_file(file_path)
|
||||||
|
else:
|
||||||
|
# Save directly as binary
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(binary_data)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving settings to file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def edit_all_settings(self) -> None:
|
||||||
|
"""Interactive editor for all settings"""
|
||||||
|
print("\nEditing settings (press Enter to keep current value):")
|
||||||
|
for i, entry in enumerate(self.entries):
|
||||||
|
value = self.values[i]
|
||||||
|
|
||||||
|
# Format current value, min and max for display
|
||||||
|
if isinstance(value, int):
|
||||||
|
current = str(value)
|
||||||
|
else:
|
||||||
|
current = f"'{value}' (unresolved)"
|
||||||
|
|
||||||
|
# Get the raw min/max/increment values for display
|
||||||
|
min_val = entry.min
|
||||||
|
max_val = entry.max
|
||||||
|
increment = entry.increment
|
||||||
|
|
||||||
|
# Format prompt with range and increment (if not 1)
|
||||||
|
range_text = f"[{min_val}-{max_val}]"
|
||||||
|
if increment != 1:
|
||||||
|
range_text = f"{range_text} step {increment}"
|
||||||
|
prompt = f"{i+1}. {entry.name} ({current}) {range_text}: "
|
||||||
|
|
||||||
|
# Get user input
|
||||||
|
while True:
|
||||||
|
user_input = input(prompt)
|
||||||
|
|
||||||
|
# Empty input = keep current value
|
||||||
|
if not user_input:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Try to parse input as integer
|
||||||
|
try:
|
||||||
|
new_value = int(user_input)
|
||||||
|
|
||||||
|
# Check if value is in range
|
||||||
|
# Convert min/max to integers for validation
|
||||||
|
min_int = min_val
|
||||||
|
max_int = max_val
|
||||||
|
inc_int = increment
|
||||||
|
|
||||||
|
if isinstance(min_int, str):
|
||||||
|
try:
|
||||||
|
min_int = int(min_int)
|
||||||
|
except ValueError:
|
||||||
|
min_int = 0
|
||||||
|
|
||||||
|
if isinstance(max_int, str):
|
||||||
|
try:
|
||||||
|
max_int = int(max_int)
|
||||||
|
except ValueError:
|
||||||
|
max_int = 65535
|
||||||
|
|
||||||
|
if isinstance(inc_int, str):
|
||||||
|
try:
|
||||||
|
inc_int = int(inc_int)
|
||||||
|
except ValueError:
|
||||||
|
inc_int = 1
|
||||||
|
|
||||||
|
if new_value < min_int or new_value > max_int:
|
||||||
|
print(f"Value must be between {min_int} and {max_int}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if value respects the increment step
|
||||||
|
if inc_int > 1:
|
||||||
|
# Check if value is min_int + n*inc_int
|
||||||
|
if (new_value - min_int) % inc_int != 0:
|
||||||
|
print(f"Value must be {min_int} + n*{inc_int}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Value is valid, update it
|
||||||
|
self.values[i] = new_value
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
print("Invalid input, please enter a number")
|
128
source/Settings/lib/settings_parser.py
Normal file
128
source/Settings/lib/settings_parser.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
settings_parser.py - Functions for parsing settings values and expressions
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
# Import from local modules
|
||||||
|
from .settings_model import Settings
|
||||||
|
from .settings_util import resolve_expression
|
||||||
|
|
||||||
|
|
||||||
|
def process_default_values(
|
||||||
|
settings: Settings, bsp_config: Optional[Dict[str, Any]] = None, debug: bool = False
|
||||||
|
) -> None:
|
||||||
|
"""Process and resolve default values that are expressions or refer to BSP configs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
settings: Settings object with entries and values
|
||||||
|
bsp_config: BSP configuration values (optional)
|
||||||
|
debug: Print debug information
|
||||||
|
"""
|
||||||
|
# Create a dictionary of values to use for resolving expressions
|
||||||
|
# First add all values that are already integers
|
||||||
|
values_dict = {}
|
||||||
|
for i, value in enumerate(settings.values):
|
||||||
|
if isinstance(value, int):
|
||||||
|
values_dict[settings.entries[i].name] = value
|
||||||
|
|
||||||
|
# If we have BSP config, add those values too
|
||||||
|
if bsp_config:
|
||||||
|
values_dict.update(bsp_config)
|
||||||
|
if debug:
|
||||||
|
print(f"Added {len(bsp_config)} values from BSP config")
|
||||||
|
|
||||||
|
# Handle special cases and defaults for BSP config values
|
||||||
|
special_cases = {
|
||||||
|
"BoostTemperature": lambda cfg: cfg.get("BOOST_TEMP", 450) if cfg else 450,
|
||||||
|
"SleepTemperature": lambda cfg: cfg.get("SLEEP_TEMP", 150) if cfg else 150,
|
||||||
|
"SleepTimeout": lambda cfg: cfg.get("SLEEP_TIMEOUT", 10) if cfg else 10,
|
||||||
|
"ShutdownTimeout": lambda cfg: cfg.get("SHUTDOWN_TIMEOUT", 10) if cfg else 10,
|
||||||
|
"MotionSensitivity": lambda cfg: cfg.get("SENSITIVITY", 0) if cfg else 0,
|
||||||
|
"TemperatureUnit": lambda cfg: (
|
||||||
|
0 if cfg and cfg.get("TEMP_UNIT", "C") == "C" else 1
|
||||||
|
),
|
||||||
|
"DisplayRotation": lambda cfg: cfg.get("ORIENTATION", 0) if cfg else 0,
|
||||||
|
"CooldownBlink": lambda cfg: (
|
||||||
|
1 if cfg and cfg.get("COOLING_BLINK", "enabled") == "enabled" else 0
|
||||||
|
),
|
||||||
|
"ScrollingSpeed": lambda cfg: cfg.get("SCROLLSPEED", 0) if cfg else 0,
|
||||||
|
"LockingMode": lambda cfg: cfg.get("LOCK_MODE", 0) if cfg else 0,
|
||||||
|
"MinVolCell": lambda cfg: cfg.get("VOLTAGE_MIN", 30) if cfg else 30,
|
||||||
|
"QCIdleVoltage": lambda cfg: cfg.get("QC_VOLTAGE", 90) if cfg else 90,
|
||||||
|
"PDNegTimeout": lambda cfg: cfg.get("PD_TIMEOUT", 5) if cfg else 5,
|
||||||
|
"AnimLoop": lambda cfg: (
|
||||||
|
1 if cfg and cfg.get("ANIMATION_LOOP", "enabled") == "enabled" else 0
|
||||||
|
),
|
||||||
|
"AnimSpeed": lambda cfg: cfg.get("ANIMATION_SPEED", 40) if cfg else 40,
|
||||||
|
"AutoStart": lambda cfg: (
|
||||||
|
0 if cfg and cfg.get("AUTOSTART_MODE", "none") == "none" else 1
|
||||||
|
),
|
||||||
|
"ShutdownTime": lambda cfg: cfg.get("AUTO_SHUTDOWN_TIME", 30) if cfg else 30,
|
||||||
|
"CalibrateInfo": lambda cfg: (
|
||||||
|
1 if cfg and cfg.get("TIP_CALIBRATION_INFO", "on") == "on" else 0
|
||||||
|
),
|
||||||
|
"PowerPulse": lambda cfg: (
|
||||||
|
1 if cfg and cfg.get("POWER_PULSE", "enabled") == "enabled" else 0
|
||||||
|
),
|
||||||
|
"PowerPulseWait": lambda cfg: cfg.get("POWER_PULSE_WAIT", 2) if cfg else 2,
|
||||||
|
"PowerPulseDuration": lambda cfg: (
|
||||||
|
cfg.get("POWER_PULSE_DURATION", 1) if cfg else 1
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve special cases if BSP config is available
|
||||||
|
for i, entry in enumerate(settings.entries):
|
||||||
|
if entry.name in special_cases and not isinstance(settings.values[i], int):
|
||||||
|
settings.values[i] = special_cases[entry.name](bsp_config)
|
||||||
|
if debug:
|
||||||
|
print(f"Applied special case for {entry.name}: {settings.values[i]}")
|
||||||
|
values_dict[entry.name] = settings.values[i]
|
||||||
|
|
||||||
|
# Now resolve remaining expressions
|
||||||
|
changed = True
|
||||||
|
max_passes = 10 # Limit the number of passes to avoid infinite loops with circular dependencies
|
||||||
|
pass_count = 0
|
||||||
|
|
||||||
|
while changed and pass_count < max_passes:
|
||||||
|
changed = False
|
||||||
|
pass_count += 1
|
||||||
|
if debug:
|
||||||
|
print(f"Pass {pass_count} resolving expressions")
|
||||||
|
|
||||||
|
for i, value in enumerate(settings.values):
|
||||||
|
if not isinstance(value, int):
|
||||||
|
try:
|
||||||
|
resolved = resolve_expression(value, values_dict, debug)
|
||||||
|
if debug:
|
||||||
|
print(
|
||||||
|
f"Resolved {settings.entries[i].name} from '{value}' to {resolved}"
|
||||||
|
)
|
||||||
|
settings.values[i] = resolved
|
||||||
|
values_dict[settings.entries[i].name] = resolved
|
||||||
|
changed = True
|
||||||
|
except Exception as e:
|
||||||
|
if debug:
|
||||||
|
print(
|
||||||
|
f"Failed to resolve {settings.entries[i].name} = '{value}': {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if any values are still unresolved
|
||||||
|
unresolved = []
|
||||||
|
for i, value in enumerate(settings.values):
|
||||||
|
if not isinstance(value, int):
|
||||||
|
unresolved.append(f"{settings.entries[i].name} = '{value}'")
|
||||||
|
|
||||||
|
if unresolved:
|
||||||
|
print("\nWarning: Could not resolve some expressions:")
|
||||||
|
for expr in unresolved:
|
||||||
|
print(f" {expr}")
|
||||||
|
print("\nUsing default value of 0 for unresolved settings")
|
||||||
|
|
||||||
|
# Set unresolved values to 0
|
||||||
|
for i, value in enumerate(settings.values):
|
||||||
|
if not isinstance(value, int):
|
||||||
|
settings.values[i] = 0
|
24
source/Settings/lib/settings_types.py
Normal file
24
source/Settings/lib/settings_types.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
settings_types.py - Common types and constants for IronOS settings management
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
# Try to import IntelHex, which is optional
|
||||||
|
try:
|
||||||
|
from intelhex import IntelHex
|
||||||
|
|
||||||
|
HEX_SUPPORT = True
|
||||||
|
except ImportError:
|
||||||
|
IntelHex = None
|
||||||
|
HEX_SUPPORT = False
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
DEFAULT_YAML_PATH = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "settings.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Type aliases
|
||||||
|
SettingsDict = Dict[str, Any]
|
90
source/Settings/lib/settings_util.py
Normal file
90
source/Settings/lib/settings_util.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
settings_util.py - Utility functions for IronOS settings management
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_address(model_code: str) -> int:
|
||||||
|
"""Get the base address for the given model code
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_code: The model code of the device (e.g., 'TS100', 'Pinecilv2')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The base address for the settings in flash memory
|
||||||
|
"""
|
||||||
|
base_addresses = {
|
||||||
|
"TS100": 0,
|
||||||
|
"TS80": 0,
|
||||||
|
"TS80P": 0,
|
||||||
|
"TS101": 0,
|
||||||
|
"Pinecil": 0,
|
||||||
|
"Pinecilv2": 0x23000000 + (1023 * 1024),
|
||||||
|
"S60": 0,
|
||||||
|
"S60P": 0,
|
||||||
|
"MHP30": 0,
|
||||||
|
}
|
||||||
|
# If the model code is not found, exit with an error
|
||||||
|
if model_code not in base_addresses:
|
||||||
|
print(f"Error: Model code '{model_code}' is not recognized.")
|
||||||
|
sys.exit(1)
|
||||||
|
return base_addresses[model_code]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_expression(
|
||||||
|
expression: str, values: Dict[str, Any], debug: bool = False
|
||||||
|
) -> int:
|
||||||
|
"""Resolve a mathematical expression with variable substitution
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expression: String expression like "100 + x / 2"
|
||||||
|
values: Dictionary of variable values
|
||||||
|
debug: Print debug information
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resolved integer value
|
||||||
|
"""
|
||||||
|
if isinstance(expression, (int, float)):
|
||||||
|
return int(expression)
|
||||||
|
|
||||||
|
if not isinstance(expression, str):
|
||||||
|
raise ValueError(f"Invalid expression type: {type(expression)}")
|
||||||
|
|
||||||
|
# Replace variable references with their values
|
||||||
|
result_expr = expression
|
||||||
|
|
||||||
|
# Find all variable references in the expression
|
||||||
|
var_refs = re.findall(r"[A-Za-z_][A-Za-z0-9_]*", expression)
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
print(f"Expression: {expression}")
|
||||||
|
print(f"Found variables: {var_refs}")
|
||||||
|
|
||||||
|
# Replace each variable with its value
|
||||||
|
for var in var_refs:
|
||||||
|
if var in values:
|
||||||
|
# Make sure we replace whole words only (not parts of other words)
|
||||||
|
# Using word boundaries in regex
|
||||||
|
result_expr = re.sub(r"\b" + var + r"\b", str(values[var]), result_expr)
|
||||||
|
if debug:
|
||||||
|
print(f"Replaced {var} with {values[var]}")
|
||||||
|
else:
|
||||||
|
if debug:
|
||||||
|
print(f"Warning: Variable {var} not found in values dictionary")
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
print(f"Final expression: {result_expr}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Evaluate the expression
|
||||||
|
# Using eval is generally not recommended for security reasons,
|
||||||
|
# but in this controlled environment with no user input, it's acceptable
|
||||||
|
result = eval(result_expr)
|
||||||
|
return int(result)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error evaluating expression '{expression}' -> '{result_expr}': {e}")
|
||||||
|
return 0
|
89
source/Settings/test_config_parser.py
Normal file
89
source/Settings/test_config_parser.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from config_parser import parse_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_parser():
|
||||||
|
"""
|
||||||
|
Test the configuration parser on all supported models
|
||||||
|
"""
|
||||||
|
# List of models to test
|
||||||
|
models = [
|
||||||
|
"TS100",
|
||||||
|
"TS80",
|
||||||
|
"TS80P",
|
||||||
|
"TS101",
|
||||||
|
"Pinecil",
|
||||||
|
"Pinecilv2",
|
||||||
|
"S60",
|
||||||
|
"S60P",
|
||||||
|
"MHP30",
|
||||||
|
]
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
failed_models = []
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
print(f"Testing model: {model}")
|
||||||
|
try:
|
||||||
|
# Try to parse the configuration for this model
|
||||||
|
config = parse_config(model)
|
||||||
|
|
||||||
|
# Print number of config items found
|
||||||
|
print(f" Found {len(config)} configuration items")
|
||||||
|
|
||||||
|
# Print a few sample values if available
|
||||||
|
if config:
|
||||||
|
sample_keys = list(config.keys())[:5]
|
||||||
|
print(" Sample values:")
|
||||||
|
for key in sample_keys:
|
||||||
|
print(f" {key}: {config[key]}")
|
||||||
|
|
||||||
|
# Check for key configuration parameters
|
||||||
|
important_keys = [
|
||||||
|
"SOLDERING_TEMP",
|
||||||
|
"SLEEP_TEMP",
|
||||||
|
"BOOST_TEMP",
|
||||||
|
"PID_POWER_LIMIT",
|
||||||
|
"MAX_POWER_LIMIT",
|
||||||
|
]
|
||||||
|
|
||||||
|
missing_keys = [key for key in important_keys if key not in config]
|
||||||
|
if missing_keys:
|
||||||
|
print(f" Warning: Missing important keys: {', '.join(missing_keys)}")
|
||||||
|
|
||||||
|
success_count += 1
|
||||||
|
print(" Success!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Failed: {str(e)}")
|
||||||
|
failed_models.append((model, str(e)))
|
||||||
|
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
print(f"\nSummary: {success_count}/{len(models)} models parsed successfully")
|
||||||
|
if failed_models:
|
||||||
|
print("Failed models:")
|
||||||
|
for model, error in failed_models:
|
||||||
|
print(f" {model}: {error}")
|
||||||
|
else:
|
||||||
|
print("All models parsed successfully!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# If a specific model is provided as command line argument, test only that one
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
model = sys.argv[1]
|
||||||
|
try:
|
||||||
|
config = parse_config(model)
|
||||||
|
print(json.dumps(config, indent=2))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing {model}: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
# Otherwise, run the full test suite
|
||||||
|
test_parser()
|
174
source/Settings/test_edit_settings.py
Normal file
174
source/Settings/test_edit_settings.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
#! python3
|
||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import struct
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add parent directory to path to import edit_settings module
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from edit_settings import SettingsEntry, Settings
|
||||||
|
|
||||||
|
|
||||||
|
class TestSettingsEntry(unittest.TestCase):
|
||||||
|
def test_settings_entry_init(self):
|
||||||
|
"""Test SettingsEntry initialization"""
|
||||||
|
entry = SettingsEntry(10, 100, 5, 20, "TestSetting")
|
||||||
|
self.assertEqual(entry.min, 10)
|
||||||
|
self.assertEqual(entry.max, 100)
|
||||||
|
self.assertEqual(entry.increment, 5)
|
||||||
|
self.assertEqual(entry.default, 20)
|
||||||
|
self.assertEqual(entry.name, "TestSetting")
|
||||||
|
|
||||||
|
def test_settings_entry_str(self):
|
||||||
|
"""Test SettingsEntry string representation"""
|
||||||
|
entry = SettingsEntry(10, 100, 5, 20, "TestSetting")
|
||||||
|
self.assertIn("TestSetting", str(entry))
|
||||||
|
self.assertIn("20", str(entry))
|
||||||
|
self.assertIn("10", str(entry))
|
||||||
|
self.assertIn("100", str(entry))
|
||||||
|
self.assertIn("5", str(entry))
|
||||||
|
|
||||||
|
|
||||||
|
class TestSettings(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures"""
|
||||||
|
self.settings = Settings()
|
||||||
|
|
||||||
|
# Create a temporary YAML file for testing
|
||||||
|
self.temp_yaml = tempfile.NamedTemporaryFile(
|
||||||
|
delete=False, suffix=".yaml", mode="w"
|
||||||
|
)
|
||||||
|
self.temp_yaml.write(
|
||||||
|
"""
|
||||||
|
settings:
|
||||||
|
- default: 20
|
||||||
|
increment: 5
|
||||||
|
max: 100
|
||||||
|
min: 10
|
||||||
|
name: Setting1
|
||||||
|
- default: 500
|
||||||
|
increment: 10
|
||||||
|
max: 1000
|
||||||
|
min: 0
|
||||||
|
name: Setting2
|
||||||
|
- default: 1
|
||||||
|
increment: 1
|
||||||
|
max: 1
|
||||||
|
min: 0
|
||||||
|
name: Setting3
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.temp_yaml.close()
|
||||||
|
|
||||||
|
# Create a temporary binary file for testing
|
||||||
|
self.temp_binary = tempfile.NamedTemporaryFile(
|
||||||
|
delete=False, suffix=".bin", mode="wb"
|
||||||
|
)
|
||||||
|
# Write three uint16_t values in little-endian: 30, 600, 0
|
||||||
|
self.temp_binary.write(struct.pack("<HHH", 30, 600, 0))
|
||||||
|
self.temp_binary.close()
|
||||||
|
|
||||||
|
# Output file path for testing save operations
|
||||||
|
self.output_binary = tempfile.NamedTemporaryFile(
|
||||||
|
delete=False, suffix=".bin"
|
||||||
|
).name
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Tear down test fixtures"""
|
||||||
|
os.unlink(self.temp_yaml.name)
|
||||||
|
os.unlink(self.temp_binary.name)
|
||||||
|
if os.path.exists(self.output_binary):
|
||||||
|
os.unlink(self.output_binary)
|
||||||
|
|
||||||
|
def test_load_from_yaml(self):
|
||||||
|
"""Test loading settings from YAML file"""
|
||||||
|
self.settings.load_from_yaml(self.temp_yaml.name)
|
||||||
|
|
||||||
|
# Check that entries were loaded correctly
|
||||||
|
self.assertEqual(len(self.settings.entries), 3)
|
||||||
|
self.assertEqual(self.settings.entries[0].name, "Setting1")
|
||||||
|
self.assertEqual(self.settings.entries[0].default, 20)
|
||||||
|
self.assertEqual(self.settings.entries[1].name, "Setting2")
|
||||||
|
self.assertEqual(self.settings.entries[1].min, 0)
|
||||||
|
self.assertEqual(self.settings.entries[2].name, "Setting3")
|
||||||
|
self.assertEqual(self.settings.entries[2].max, 1)
|
||||||
|
|
||||||
|
# Check that values were set to defaults
|
||||||
|
self.assertEqual(len(self.settings.values), 3)
|
||||||
|
self.assertEqual(self.settings.values[0], 20)
|
||||||
|
self.assertEqual(self.settings.values[1], 500)
|
||||||
|
self.assertEqual(self.settings.values[2], 1)
|
||||||
|
|
||||||
|
def test_load_from_binary(self):
|
||||||
|
"""Test loading settings from binary file"""
|
||||||
|
# First load YAML to set up entries
|
||||||
|
self.settings.load_from_yaml(self.temp_yaml.name)
|
||||||
|
|
||||||
|
# Then load binary values
|
||||||
|
result = self.settings.load_from_binary(self.temp_binary.name)
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
# Check that values were updated from binary
|
||||||
|
self.assertEqual(self.settings.values[0], 30) # Changed from default 20
|
||||||
|
self.assertEqual(self.settings.values[1], 600) # Changed from default 500
|
||||||
|
self.assertEqual(self.settings.values[2], 0) # Changed from default 1
|
||||||
|
|
||||||
|
def test_load_from_nonexistent_binary(self):
|
||||||
|
"""Test loading settings from a nonexistent binary file"""
|
||||||
|
self.settings.load_from_yaml(self.temp_yaml.name)
|
||||||
|
result = self.settings.load_from_binary("nonexistent_file.bin")
|
||||||
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
# Values should still be defaults
|
||||||
|
self.assertEqual(self.settings.values[0], 20)
|
||||||
|
self.assertEqual(self.settings.values[1], 500)
|
||||||
|
self.assertEqual(self.settings.values[2], 1)
|
||||||
|
|
||||||
|
def test_save_to_binary(self):
|
||||||
|
"""Test saving settings to binary file"""
|
||||||
|
# Set up settings
|
||||||
|
self.settings.load_from_yaml(self.temp_yaml.name)
|
||||||
|
self.settings.values = [25, 550, 1] # Custom values
|
||||||
|
|
||||||
|
# Save to binary
|
||||||
|
result = self.settings.save_to_binary(self.output_binary)
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
# Check the file exists
|
||||||
|
self.assertTrue(os.path.exists(self.output_binary))
|
||||||
|
|
||||||
|
# Read the binary data and verify
|
||||||
|
with open(self.output_binary, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
self.assertEqual(len(data), 6) # 3 settings * 2 bytes
|
||||||
|
values = struct.unpack("<HHH", data)
|
||||||
|
self.assertEqual(values[0], 25)
|
||||||
|
self.assertEqual(values[1], 550)
|
||||||
|
self.assertEqual(values[2], 1)
|
||||||
|
|
||||||
|
def test_save_to_binary_error_handling(self):
|
||||||
|
"""Test error handling when saving to an invalid location"""
|
||||||
|
self.settings.load_from_yaml(self.temp_yaml.name)
|
||||||
|
# Try to save to a location that doesn't exist
|
||||||
|
result = self.settings.save_to_binary("/nonexistent/directory/file.bin")
|
||||||
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSettingsEditingFunctional(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
These tests would normally involve mocking user input to test edit_all_settings.
|
||||||
|
However, since mocking stdin is complex and potentially brittle, we'll skip those tests.
|
||||||
|
In a real project, you might use a library like unittest.mock to patch input() or
|
||||||
|
restructure the code to allow for dependency injection of user input.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
119
source/Settings/test_generate_settings.py
Executable file
119
source/Settings/test_generate_settings.py
Executable file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for generate_settings.py
|
||||||
|
|
||||||
|
This script tests the functionality of the settings generator
|
||||||
|
by creating a temporary YAML file and comparing the output with expected results.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# Add the parent directory to the path so we can import from lib
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
# Import the modules we're testing
|
||||||
|
try:
|
||||||
|
from lib.settings_model import Settings
|
||||||
|
from generate_settings import convert_settings_to_cpp
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Error importing modules: {e}")
|
||||||
|
print("Make sure you're running this from the Settings directory")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenerateSettings(unittest.TestCase):
|
||||||
|
"""Test suite for the generate_settings.py script"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Create temporary directories and files for testing"""
|
||||||
|
self.temp_dir = tempfile.mkdtemp(prefix="ironos_test_")
|
||||||
|
self.yaml_path = os.path.join(self.temp_dir, "test_settings.yaml")
|
||||||
|
self.cpp_path = os.path.join(self.temp_dir, "test_settings.cpp")
|
||||||
|
self.template_path = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "settings_gen.cpp.template"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a simple test YAML file
|
||||||
|
with open(self.yaml_path, "w") as f:
|
||||||
|
f.write(
|
||||||
|
"""settings:
|
||||||
|
- name: TestSetting1
|
||||||
|
min: 0
|
||||||
|
max: 100
|
||||||
|
increment: 1
|
||||||
|
default: 50
|
||||||
|
- name: TestSetting2
|
||||||
|
min: 10
|
||||||
|
max: 200
|
||||||
|
increment: 5
|
||||||
|
default: 100
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Clean up temporary files and directories"""
|
||||||
|
shutil.rmtree(self.temp_dir)
|
||||||
|
|
||||||
|
def test_load_yaml_and_convert(self):
|
||||||
|
"""Test loading YAML and converting it to C++ code using the library"""
|
||||||
|
settings = Settings()
|
||||||
|
settings.load_from_yaml(self.yaml_path)
|
||||||
|
|
||||||
|
# Verify settings were loaded correctly
|
||||||
|
self.assertEqual(len(settings.entries), 2)
|
||||||
|
self.assertEqual(settings.entries[0].name, "TestSetting1")
|
||||||
|
self.assertEqual(settings.entries[0].min, 0)
|
||||||
|
self.assertEqual(settings.entries[0].max, 100)
|
||||||
|
self.assertEqual(settings.entries[0].increment, 1)
|
||||||
|
self.assertEqual(settings.entries[0].default, 50)
|
||||||
|
|
||||||
|
# Convert to C++ code
|
||||||
|
cpp_code = convert_settings_to_cpp(settings)
|
||||||
|
|
||||||
|
# Verify the C++ code contains expected content
|
||||||
|
self.assertIn("0", cpp_code)
|
||||||
|
self.assertIn("100", cpp_code)
|
||||||
|
self.assertIn("1", cpp_code)
|
||||||
|
self.assertIn("50", cpp_code)
|
||||||
|
self.assertIn("TestSetting1", cpp_code)
|
||||||
|
self.assertIn("TestSetting2", cpp_code)
|
||||||
|
|
||||||
|
def test_script_execution(self):
|
||||||
|
"""Test executing the script as a subprocess"""
|
||||||
|
# Only run if the template file exists
|
||||||
|
if not os.path.exists(self.template_path):
|
||||||
|
self.skipTest("Template file not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get path to generate_settings.py
|
||||||
|
script_path = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "generate_settings.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run the script
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, script_path, "TestModel", self.yaml_path, self.cpp_path],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the script executed successfully
|
||||||
|
self.assertEqual(result.returncode, 0, f"Script failed with: {result.stderr}")
|
||||||
|
|
||||||
|
# Verify the output file was created
|
||||||
|
self.assertTrue(os.path.exists(self.cpp_path))
|
||||||
|
|
||||||
|
# Check the content of the output file
|
||||||
|
with open(self.cpp_path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
self.assertIn("TestSetting1", content)
|
||||||
|
self.assertIn("TestSetting2", content)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Reference in New Issue
Block a user