mirror of
https://github.com/Ralim/IronOS.git
synced 2025-07-23 04:13:01 +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.
|
||||
|
||||
## 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
|
||||
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')
|
||||
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
If the user has provided a settings binary dump, we load it and update the settings data
|
||||
If the user has requested to edit the settings, we provide a menu to edit each setting
|
||||
IronOS Settings Editor - Refactored
|
||||
|
||||
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
|
||||
from typing import List
|
||||
import os
|
||||
import sys
|
||||
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]:
|
||||
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')
|
||||
parser.add_argument('output_file_path', help='Path where the generated C++ file should be written')
|
||||
def parse_arguments() -> tuple[str, str, str]:
|
||||
"""Parse command line arguments for the settings generator
|
||||
|
||||
Returns:
|
||||
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:]}")
|
||||
if len(sys.argv) < 4:
|
||||
if len(sys.argv) < 3:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check if settings definitions file exists
|
||||
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()
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
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 __init__(self, min, max, increment, default):
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.increment = increment
|
||||
self.default = default
|
||||
def convert_settings_to_cpp(settings) -> str:
|
||||
"""Convert settings to C++ code for inclusion in a template
|
||||
|
||||
Args:
|
||||
settings: Either a Settings object or a dictionary with settings data
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
Returns:
|
||||
String containing formatted C++ code for settings table
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
"""
|
||||
Load the settings definitions yaml file, this is used to then generate the settings_gen.cpp file.
|
||||
"""
|
||||
def main():
|
||||
"""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)
|
||||
cpp_code = convert_settings_to_cpp(settings_data)
|
||||
with open(SETTINGS_TEMPLATE_PATH, 'r') as f:
|
||||
template_content = f.read()
|
||||
# Write the generated C++ code to the output file
|
||||
# Make sure the directory exists
|
||||
os.makedirs(os.path.dirname(SETTINGS_OUTPUT_PATH), exist_ok=True)
|
||||
with open(SETTINGS_OUTPUT_PATH, 'w') as f:
|
||||
f.write(template_content.replace("$SETTINGSTABLE", cpp_code))
|
||||
# Initialize settings
|
||||
settings = Settings()
|
||||
|
||||
# Load settings definitions from YAML
|
||||
print(f"Loading settings definitions from {settings_definitions_path}")
|
||||
try:
|
||||
settings.load_from_yaml(settings_definitions_path)
|
||||
except Exception as e:
|
||||
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