# S7 Parser Utilities - Technical Documentation

## Table of Contents
1. [Introduction](#introduction)
2. [Utility Functions](#utility-functions)
   - [find_working_directory](#find_working_directory)
   - [custom_json_serializer](#custom_json_serializer)
   - [flatten_db_structure](#flatten_db_structure)
   - [format_address_for_display](#format_address_for_display)
   - [access_by_hierarchy_path](#access_by_hierarchy_path)
   - [format_data_type_for_source](#format_data_type_for_source)
3. [Usage Examples](#usage-examples)

## Introduction

This document provides technical documentation for the utility functions used in the S7 parser system. These functions facilitate working with Siemens S7 data structures, addressing, and serialization/deserialization processes.

## Utility Functions

### find_working_directory

#### Purpose
Retrieves the configured working directory from the script configuration.

#### Signature
```python
def find_working_directory() -> str:
```

#### Returns
- `str`: The absolute path to the configured working directory.

#### Behavior
1. Loads the configuration using `load_configuration()`
2. Retrieves the "working_directory" setting
3. Exits with error if no working directory is specified
4. Returns the working directory path

#### Usage Example
```python
working_dir = find_working_directory()
print(f"Using working directory: {working_dir}")
```

#### Notes
- This function depends on `load_configuration()` from `backend.script_utils`
- The working directory should be defined in the script configuration JSON file

---

### custom_json_serializer

#### Purpose
Custom JSON serializer that handles S7 data structures that are not natively JSON-serializable.

#### Signature
```python
def custom_json_serializer(obj: Any) -> Any:
```

#### Parameters
- `obj` (Any): Object to be serialized to JSON

#### Returns
- Serializable version of the object compatible with JSON format

#### Behavior
1. Ignores `OffsetContext` objects (returns `None`)
2. Converts `ArrayDimension` objects to dictionaries
3. Handles `VariableInfo` and other objects with `__dict__` attribute
4. Properly manages special properties like `current_element_values`
5. Raises `TypeError` for objects that cannot be serialized

#### Usage Example
```python
# Serialize a parsed S7 object to JSON
json_output = json.dumps(parsed_result, default=custom_json_serializer, indent=2)
with open("output.json", "w", encoding='utf-8') as f:
    f.write(json_output)
```

#### Notes
- Removes empty lists and None values to keep JSON output clean
- Special handling for `is_udt_expanded_member` flag
- Preserves format of array element values with their offsets

---

### flatten_db_structure

#### Purpose
Completely flattens a hierarchical DB/UDT structure, expanding all nested variables, UDTs, and array elements for easier processing.

#### Signature
```python
def flatten_db_structure(db_info: Dict[str, Any]) -> List[Dict[str, Any]]:
```

#### Parameters
- `db_info` (Dict): The DB or UDT structure to flatten

#### Returns
- `List[Dict]`: A list of flattened variables with full paths and hierarchy path references

#### Behavior
1. Recursively processes the hierarchical structure of a DB/UDT
2. Expands nested structures, UDT instances, and array elements
3. Maintains offset information and creates full path strings
4. Adds hierarchical path pointers for direct access to original structure
5. Assigns element type designations based on variable characteristics
6. Returns a list sorted by byte.bit offset for consistent ordering

#### Key Properties Added to Each Variable
- `full_path`: Complete path to the variable (e.g., `"MyStruct.MyArray[1]"`)
- `is_array_element`: Boolean indicating if this is an array element
- `element_type`: Type classification ("SIMPLE_VAR", "ARRAY", "ARRAY_ELEMENT", "STRUCT", "UDT_INSTANCE")
- `address_display`: Formatted display version of the address
- `_hierarchy_path`: Internal path references for direct access to the original structure
- `_array_index`: For array elements, stores the index for direct access

#### Usage Example
```python
# Get a flattened view of a DB
flat_variables = flatten_db_structure(my_db)

# Find variables with specific element types
simple_vars = [var for var in flat_variables if var.get("element_type") == "SIMPLE_VAR"]

# Create an offset-to-variable map for quick lookup
variables_by_offset = {var["byte_offset"]: var for var in flat_variables}
```

#### Notes
- Prevents duplicate processing of expanded UDT members
- Handles array elements including their specific offsets
- Preserves all original attributes of each variable
- Critical for functions that need to process or match variables by offset

---

### format_address_for_display

#### Purpose
Formats byte offsets into readable S7 address notation, correctly handling bit addresses for BOOL types.

#### Signature
```python
def format_address_for_display(byte_offset: float, bit_size: int = 0) -> str:
```

#### Parameters
- `byte_offset` (float): The byte offset with the decimal part representing the bit offset
- `bit_size` (int, optional): Size in bits (>0 for BOOL types). Defaults to 0.

#### Returns
- `str`: A formatted address string in S7 notation

#### Behavior
1. For bit variables (BOOLs), formats as "X.Y" where X is the byte and Y is the bit
2. For byte-aligned variables, returns the byte number as an integer
3. Preserves decimal notation only when necessary

#### Usage Example
```python
# Format a BOOL address
bool_address = format_address_for_display(12.5, 1)  # Returns "12.5"

# Format a regular variable address 
int_address = format_address_for_display(24.0, 0)   # Returns "24"
```

#### Notes
- Particularly important for BOOL variables and array elements
- Converts integer byte offsets (e.g., 10.0) to simple integers ("10")
- The bit part of BOOL addresses is rounded to account for potential floating-point imprecision

---

### access_by_hierarchy_path

#### Purpose
Directly accesses a variable in the hierarchical structure using the hierarchy path references created by `flatten_db_structure`.

#### Signature
```python
def access_by_hierarchy_path(root_obj: Dict[str, Any], hierarchy_path: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
```

#### Parameters
- `root_obj` (Dict): The root object (usually a DB) containing the hierarchy
- `hierarchy_path` (List[Dict]): List of hierarchical steps to navigate

#### Returns
- The variable object if found, `None` if not accessible

#### Behavior
1. Starts at the root object provided
2. Follows each step in the hierarchy path
3. Each step contains a type ("members" or "children") and an index
4. Returns the object found at the end of the path
5. Returns None if any step in the path is invalid

#### Usage Example
```python
# Get flattened variables with hierarchy paths
flat_vars = flatten_db_structure(my_db)

# Find a specific variable
target_var = next(var for var in flat_vars if var["full_path"] == "MyStruct.MyField")

# Access the original variable directly in the hierarchy
original_var = access_by_hierarchy_path(my_db, target_var["_hierarchy_path"])

# Now you can modify the original variable
if original_var:
    original_var["current_value"] = "New Value"
```

#### Notes
- Critical for updating values in the original structure
- Much more efficient than manually traversing the hierarchy
- Works because hierarchy paths are preserved across deep copies
- Provides error checking for invalid paths

---

### format_data_type_for_source

#### Purpose
Formats a variable's data type information as it would appear in S7 source code.

#### Signature
```python
def format_data_type_for_source(var_info: Dict[str, Any]) -> str:
```

#### Parameters
- `var_info` (Dict): Variable information dictionary

#### Returns
- `str`: Formatted data type string as it would appear in S7 source code

#### Behavior
1. Uses the UDT source name if available, otherwise uses the data type
2. Adds ARRAY declarations with dimensions for array variables
3. Adds size specifications for STRING types
4. Returns the complete type declaration as it would appear in source code

#### Usage Example
```python
# Format a simple INT variable
simple_var = {"data_type": "INT"}
type_str = format_data_type_for_source(simple_var)  # Returns "INT"

# Format an array of REALs
array_var = {
    "data_type": "REAL", 
    "array_dimensions": [{"lower_bound": 1, "upper_bound": 10}]
}
type_str = format_data_type_for_source(array_var)  # Returns "ARRAY [1..10] OF REAL"

# Format a STRING variable
string_var = {"data_type": "STRING", "string_length": 32}
type_str = format_data_type_for_source(string_var)  # Returns "STRING[32]"
```

#### Notes
- Essential for generating S7 source code declarations
- Handles UDT references, arrays, and string length specifications
- Properly formats complex types like arrays of UDTs or strings

## Usage Examples

### Complete Example: Processing and Updating a DB

```python
# Load JSON data
with open("my_db.json", "r", encoding="utf-8") as f:
    db_data = json.load(f)

# 1. Flatten the structure for easier processing
flat_vars = flatten_db_structure(db_data["dbs"][0])

# 2. Display all variables with their addresses
for var in flat_vars:
    address = var.get("address_display", format_address_for_display(var["byte_offset"], var.get("bit_size", 0)))
    data_type = format_data_type_for_source(var)
    print(f"{address}: {var['full_path']} ({data_type})")

# 3. Update a specific variable in the original structure
target_var = next(var for var in flat_vars if var["full_path"] == "Settings.Timeout")
original_var = access_by_hierarchy_path(db_data["dbs"][0], target_var["_hierarchy_path"])
if original_var:
    original_var["current_value"] = "5000"  # Update the value
    print(f"Updated {target_var['full_path']} to 5000")

# 4. Save the modified structure back to JSON
with open("updated_db.json", "w", encoding="utf-8") as f:
    json.dump(db_data, f, default=custom_json_serializer, indent=2)
```

### Example: Creating a Filtered View of Variables

```python
# Load DB data
working_dir = find_working_directory()
input_file = os.path.join(working_dir, "my_db.json")
with open(input_file, "r", encoding="utf-8") as f:
    db_data = json.load(f)

# Get flattened structure
flat_vars = flatten_db_structure(db_data["dbs"][0])

# Filter by element type
simple_vars = [var for var in flat_vars if var.get("element_type") == "SIMPLE_VAR"]
array_elements = [var for var in flat_vars if var.get("element_type") == "ARRAY_ELEMENT"]
structs = [var for var in flat_vars if var.get("element_type") == "STRUCT"]

# Create address-based lookup for quick access
vars_by_address = {}
for var in flat_vars:
    if var.get("element_type") in ["SIMPLE_VAR", "ARRAY_ELEMENT"]:
        vars_by_address[var["byte_offset"]] = var

# Look up a variable by address
target_address = 24.0
if target_address in vars_by_address:
    target_var = vars_by_address[target_address]
    print(f"Variable at address {target_address}: {target_var['full_path']}")
    
    # Access and modify the original variable in the hierarchy
    original_var = access_by_hierarchy_path(db_data["dbs"][0], target_var["_hierarchy_path"])
    if original_var:
        print(f"Current value: {original_var.get('current_value')}")
```