Carregar ficheiros para "src/core"
This commit is contained in:
131
src/core/file_comparison.py
Normal file
131
src/core/file_comparison.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
from typing import List, Tuple, Dict
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from .file_handler import ECUFile, MapData
|
||||||
|
from .edc15_maps import EDC15MapDetector
|
||||||
|
|
||||||
|
class FileComparison:
|
||||||
|
def __init__(self, file1: ECUFile, file2: ECUFile):
|
||||||
|
self.file1 = file1
|
||||||
|
self.file2 = file2
|
||||||
|
self.differences = []
|
||||||
|
self.map_differences = {}
|
||||||
|
|
||||||
|
def compare_files(self) -> bool:
|
||||||
|
"""
|
||||||
|
Compare two ECU files and find differences
|
||||||
|
Returns True if differences were found
|
||||||
|
"""
|
||||||
|
if not self.file1.data or not self.file2.data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Compare file sizes
|
||||||
|
if len(self.file1.data) != len(self.file2.data):
|
||||||
|
self.differences.append(("File Size",
|
||||||
|
len(self.file1.data),
|
||||||
|
len(self.file2.data)))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Compare byte by byte
|
||||||
|
diff_positions = []
|
||||||
|
for i, (b1, b2) in enumerate(zip(self.file1.data, self.file2.data)):
|
||||||
|
if b1 != b2:
|
||||||
|
diff_positions.append((i, b1, b2))
|
||||||
|
|
||||||
|
if diff_positions:
|
||||||
|
self.differences.extend(diff_positions)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def compare_maps(self) -> Dict[str, np.ndarray]:
|
||||||
|
"""
|
||||||
|
Compare maps between two files
|
||||||
|
Returns dictionary of map differences
|
||||||
|
"""
|
||||||
|
# Detect maps in both files
|
||||||
|
detector1 = EDC15MapDetector(self.file1.data)
|
||||||
|
detector2 = EDC15MapDetector(self.file2.data)
|
||||||
|
|
||||||
|
maps1 = detector1.find_potential_maps()
|
||||||
|
maps2 = detector2.find_potential_maps()
|
||||||
|
|
||||||
|
# Compare maps at same addresses
|
||||||
|
for map1 in maps1:
|
||||||
|
for map2 in maps2:
|
||||||
|
if map1.address == map2.address:
|
||||||
|
map_data1 = detector1.extract_map(map1)
|
||||||
|
map_data2 = detector2.extract_map(map2)
|
||||||
|
|
||||||
|
if not np.array_equal(map_data1.data, map_data2.data):
|
||||||
|
diff = map_data2.data - map_data1.data
|
||||||
|
self.map_differences[map1.name] = {
|
||||||
|
'difference': diff,
|
||||||
|
'original': map_data1,
|
||||||
|
'modified': map_data2
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.map_differences
|
||||||
|
|
||||||
|
def get_difference_summary(self) -> str:
|
||||||
|
"""
|
||||||
|
Generate a human-readable summary of differences
|
||||||
|
"""
|
||||||
|
summary = []
|
||||||
|
|
||||||
|
if not self.differences and not self.map_differences:
|
||||||
|
return "No differences found between the files."
|
||||||
|
|
||||||
|
if self.differences:
|
||||||
|
summary.append(f"Found {len(self.differences)} byte differences:")
|
||||||
|
for i, (pos, val1, val2) in enumerate(self.differences[:10]):
|
||||||
|
summary.append(f" Offset 0x{pos:X}: {val1:02X} -> {val2:02X}")
|
||||||
|
if len(self.differences) > 10:
|
||||||
|
summary.append(f" ... and {len(self.differences)-10} more differences")
|
||||||
|
|
||||||
|
if self.map_differences:
|
||||||
|
summary.append(f"\nFound {len(self.map_differences)} modified maps:")
|
||||||
|
for map_name, diff_data in self.map_differences.items():
|
||||||
|
diff = diff_data['difference']
|
||||||
|
changes = np.count_nonzero(diff)
|
||||||
|
max_change = np.max(np.abs(diff))
|
||||||
|
summary.append(f" {map_name}: {changes} changed values, "
|
||||||
|
f"max difference: {max_change:.2f}")
|
||||||
|
|
||||||
|
return "\n".join(summary)
|
||||||
|
|
||||||
|
def export_report(self, filepath: str) -> bool:
|
||||||
|
"""Export comparison report to file"""
|
||||||
|
try:
|
||||||
|
with open(filepath, 'w') as f:
|
||||||
|
# Write header
|
||||||
|
f.write("ECU File Comparison Report\n")
|
||||||
|
f.write("=" * 30 + "\n\n")
|
||||||
|
|
||||||
|
# Write file information
|
||||||
|
f.write("File 1: " + os.path.basename(self.file1.file_path) + "\n")
|
||||||
|
f.write("File 2: " + os.path.basename(self.file2.file_path) + "\n\n")
|
||||||
|
|
||||||
|
# Write differences
|
||||||
|
f.write("Map Differences\n")
|
||||||
|
f.write("-" * 20 + "\n\n")
|
||||||
|
|
||||||
|
for map_name, diff_data in self.map_differences.items():
|
||||||
|
f.write(f"Map: {map_name}\n")
|
||||||
|
f.write(f"Total differences: {np.count_nonzero(diff_data['difference'])}\n")
|
||||||
|
f.write("Changes:\n")
|
||||||
|
|
||||||
|
diff = diff_data['difference']
|
||||||
|
rows, cols = diff.shape
|
||||||
|
for i in range(rows):
|
||||||
|
for j in range(cols):
|
||||||
|
if diff[i, j] != 0:
|
||||||
|
orig = diff_data['original'].data[i, j]
|
||||||
|
mod = diff_data['modified'].data[i, j]
|
||||||
|
f.write(f" Position ({i},{j}): {orig:.2f} -> {mod:.2f}\n")
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False
|
||||||
234
src/core/file_handler.py
Normal file
234
src/core/file_handler.py
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from .edc15p_maps import MapDefinition, get_map_definition, list_maps_by_group, EDC15P_MAPS
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MapData:
|
||||||
|
"""Map data structure"""
|
||||||
|
name: str
|
||||||
|
map_id: str
|
||||||
|
data: np.ndarray
|
||||||
|
rows: int
|
||||||
|
cols: int
|
||||||
|
x_axis: np.ndarray
|
||||||
|
y_axis: np.ndarray
|
||||||
|
address: str
|
||||||
|
units: str = ""
|
||||||
|
description: str = ""
|
||||||
|
group: str = ""
|
||||||
|
|
||||||
|
# Additional properties for map manager
|
||||||
|
data_format: str = "16-bit Hi-Lo"
|
||||||
|
display_mode: str = "Original" # Original, Difference, or Percent
|
||||||
|
is_signed: bool = False
|
||||||
|
swap_axis: bool = False
|
||||||
|
display_hex: bool = False
|
||||||
|
is_reciprocal: bool = False
|
||||||
|
value_min: float = 0.0
|
||||||
|
value_max: float = 0.0
|
||||||
|
factor: float = 1.0
|
||||||
|
offset: float = 0.0
|
||||||
|
precision: int = 0
|
||||||
|
formula: str = ""
|
||||||
|
|
||||||
|
# X-axis properties
|
||||||
|
x_name: str = "RPM"
|
||||||
|
x_address: str = ""
|
||||||
|
x_data_source: str = "EPROM"
|
||||||
|
x_data_format: str = "16-bit Hi-Lo"
|
||||||
|
x_step: int = 0
|
||||||
|
x_factor: float = 1.0
|
||||||
|
x_offset: float = 0.0
|
||||||
|
x_precision: int = 0
|
||||||
|
x_is_signed: bool = False
|
||||||
|
x_is_inverted: bool = False
|
||||||
|
x_display_hex: bool = False
|
||||||
|
x_is_reciprocal: bool = False
|
||||||
|
x_formula: str = ""
|
||||||
|
|
||||||
|
# Y-axis properties
|
||||||
|
y_name: str = "FUEL"
|
||||||
|
y_address: str = ""
|
||||||
|
y_data_source: str = "EPROM"
|
||||||
|
y_data_format: str = "16-bit Hi-Lo"
|
||||||
|
y_step: int = 0
|
||||||
|
y_factor: float = 1.0
|
||||||
|
y_offset: float = 0.0
|
||||||
|
y_precision: int = 0
|
||||||
|
y_is_signed: bool = False
|
||||||
|
y_is_inverted: bool = False
|
||||||
|
y_display_hex: bool = False
|
||||||
|
y_is_reciprocal: bool = False
|
||||||
|
y_formula: str = ""
|
||||||
|
|
||||||
|
def get_value(self, row: int, col: int) -> float:
|
||||||
|
"""Get value at specified coordinates"""
|
||||||
|
return float(self.data[row, col])
|
||||||
|
|
||||||
|
def set_value(self, row: int, col: int, value: float):
|
||||||
|
"""Set value at specified coordinates"""
|
||||||
|
self.data[row, col] = value
|
||||||
|
|
||||||
|
class ECUFile:
|
||||||
|
"""Class to handle ECU file operations"""
|
||||||
|
|
||||||
|
def __init__(self, filepath: str):
|
||||||
|
self.filepath = filepath
|
||||||
|
self.backup_path = f"{filepath}.backup"
|
||||||
|
self.content = None
|
||||||
|
self.maps: Dict[str, MapData] = {} # Key is map_id
|
||||||
|
self.modified = False
|
||||||
|
|
||||||
|
def read_file(self) -> bool:
|
||||||
|
"""Read and parse the ECU file"""
|
||||||
|
try:
|
||||||
|
with open(self.filepath, 'rb') as f:
|
||||||
|
self.content = f.read()
|
||||||
|
|
||||||
|
# Create backup if it doesn't exist
|
||||||
|
if not os.path.exists(self.backup_path):
|
||||||
|
shutil.copy2(self.filepath, self.backup_path)
|
||||||
|
|
||||||
|
# Extract maps
|
||||||
|
self._extract_map_data()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_raw_content(self) -> Optional[bytes]:
|
||||||
|
"""Get raw binary content of the file"""
|
||||||
|
return self.content
|
||||||
|
|
||||||
|
def read_map_data(self, map_id: str, map_def: MapDefinition) -> Tuple[np.ndarray, bool]:
|
||||||
|
"""Read map data from binary file using map definition"""
|
||||||
|
try:
|
||||||
|
# Convert hex address to integer
|
||||||
|
address = int(map_def.address, 16)
|
||||||
|
|
||||||
|
# Calculate total bytes needed (2 bytes per value - 16 bit)
|
||||||
|
total_bytes = map_def.rows * map_def.cols * 2
|
||||||
|
|
||||||
|
# Ensure we have enough data
|
||||||
|
if address + total_bytes > len(self.content):
|
||||||
|
return np.zeros((map_def.rows, map_def.cols)), False
|
||||||
|
|
||||||
|
# Read raw bytes from file
|
||||||
|
raw_data = self.content[address:address + total_bytes]
|
||||||
|
|
||||||
|
# Convert bytes to 16-bit integers (little-endian)
|
||||||
|
data = np.frombuffer(raw_data, dtype=np.uint16, count=map_def.rows * map_def.cols)
|
||||||
|
data = data.reshape(map_def.rows, map_def.cols)
|
||||||
|
|
||||||
|
# Verify data shape
|
||||||
|
if data.shape != (map_def.rows, map_def.cols):
|
||||||
|
return np.zeros((map_def.rows, map_def.cols)), False
|
||||||
|
|
||||||
|
return data, True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return np.zeros((map_def.rows, map_def.cols)), False
|
||||||
|
|
||||||
|
def _extract_map_data(self):
|
||||||
|
"""Extract real map data from ECU file"""
|
||||||
|
if not self.content:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get all map definitions by group
|
||||||
|
groups = list_maps_by_group()
|
||||||
|
for group_name, maps in groups.items():
|
||||||
|
for map_def in maps:
|
||||||
|
try:
|
||||||
|
# Find map ID
|
||||||
|
map_id = next((k for k, v in EDC15P_MAPS.items() if v == map_def), None)
|
||||||
|
if not map_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Read map data
|
||||||
|
data, success = self.read_map_data(map_id, map_def)
|
||||||
|
if not success:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create MapData object with all necessary information
|
||||||
|
self.maps[map_id] = MapData(
|
||||||
|
name=map_def.name,
|
||||||
|
map_id=map_id,
|
||||||
|
data=data,
|
||||||
|
rows=map_def.rows,
|
||||||
|
cols=map_def.cols,
|
||||||
|
x_axis=np.array(map_def.x_axis),
|
||||||
|
y_axis=np.array(map_def.y_axis),
|
||||||
|
address=map_def.address,
|
||||||
|
units=map_def.units,
|
||||||
|
description=map_def.description,
|
||||||
|
group=map_def.group
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
def _update_file_content(self):
|
||||||
|
"""Update file content with modified maps"""
|
||||||
|
if not self.content:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get all map definitions
|
||||||
|
for map_id, map_data in self.maps.items():
|
||||||
|
try:
|
||||||
|
# Get address
|
||||||
|
address = int(map_data.address, 16)
|
||||||
|
|
||||||
|
# Convert data to bytes (16-bit little-endian)
|
||||||
|
raw_data = map_data.data.astype(np.uint16).tobytes()
|
||||||
|
|
||||||
|
# Update content
|
||||||
|
self.content = (
|
||||||
|
self.content[:address] +
|
||||||
|
raw_data +
|
||||||
|
self.content[address + len(raw_data):]
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
continue
|
||||||
|
|
||||||
|
def save_file(self, filepath: Optional[str] = None) -> bool:
|
||||||
|
"""Save the file content"""
|
||||||
|
try:
|
||||||
|
# Update content with modified maps
|
||||||
|
self._update_file_content()
|
||||||
|
|
||||||
|
# Use provided filepath or default to original
|
||||||
|
save_path = filepath or self.filepath
|
||||||
|
|
||||||
|
with open(save_path, 'wb') as f:
|
||||||
|
f.write(self.content)
|
||||||
|
|
||||||
|
self.modified = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def restore_backup(self) -> bool:
|
||||||
|
"""Restore from backup file"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(self.backup_path):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Read backup content
|
||||||
|
with open(self.backup_path, 'rb') as f:
|
||||||
|
self.content = f.read()
|
||||||
|
|
||||||
|
# Re-extract maps
|
||||||
|
self.maps.clear()
|
||||||
|
self._extract_map_data()
|
||||||
|
|
||||||
|
# Save restored content
|
||||||
|
return self.save_file()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False
|
||||||
27
src/core/project_manager.py
Normal file
27
src/core/project_manager.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
class ProjectManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.current_project = None
|
||||||
|
|
||||||
|
def create_project(self, name, path):
|
||||||
|
"""
|
||||||
|
Create a new ECU project
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def open_project(self, path):
|
||||||
|
"""
|
||||||
|
Open an existing ECU project
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def save_project(self):
|
||||||
|
"""
|
||||||
|
Save current project
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def validate_file(self, file_path):
|
||||||
|
"""
|
||||||
|
Validate ECU file integrity
|
||||||
|
"""
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user