Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions pyx2cscope/examples/export_import_variables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Example of importing and exporting variable.

The script initializes the X2CScope class with a specified serial port,
retrieves specific variables, reads their values, export them to a yaml file
and also export all the variables available as a pickle binary file.

We define a new instance of X2Cscope, and we reload the variables over the exported
files instead of the elf file.
"""
import os

from pyx2cscope.x2cscope import X2CScope
from pyx2cscope.variable.variable_factory import FileType

# initialize the X2CScope class with serial port, by default baud rate is 115200
com_port = "COM32"
x2c_scope = X2CScope(port=com_port)
# instead of loading directly the elf file, we can import it after instantiating the X2CScope class
x2c_scope.import_variables(r"..\..\tests\data\qspin_foc_same54.elf")

# Collect some variables, i.e.: from QSPIN on SAME54 MCLV-48V-300W
angle_reference = x2c_scope.get_variable("mcFocI_ModuleData_gds.dOutput.elecAngle")
speed_measured = x2c_scope.get_variable("mcFocI_ModuleData_gds.dOutput.elecSpeed")

# Read the value of the "motor.apiData.velocityMeasured" variable from the target
print(speed_measured.get_value())

# you can export only these two variables as yaml file (plain text)
x2c_scope.export_variables(filename="my_two_variables", items=[angle_reference, speed_measured])
# you can export all the variables available. For a different file format, define 'ext' variable, i.e. pickle (binary)
x2c_scope.export_variables(filename="my_variables", ext=FileType.PICKLE)

# disconnect x2cscope so we can reconnect with another instance
x2c_scope.disconnect()

# Instantiate a different X2Cscope to ensure we have an empty variable list, i.e. x2c
x2c = X2CScope(port=com_port)
# instead of loading the elf file, we load our exported file with all variable
x2c.import_variables("my_variables.pkl")
# or we can load only our two variables
# x2c.import_variables("my_two_variables.yml")
# or we can load our elf file again
# x2c.import_variables(filename=r"..\..\tests\data\qspin_foc_same54.elf")

# Collect some variables, i.e.: from QSPIN on SAME54 MCLV-48V-300W
angle_ref2 = x2c.get_variable("mcFocI_ModuleData_gds.dOutput.elecAngle")
speed_ref2 = x2c.get_variable("mcFocI_ModuleData_gds.dOutput.elecSpeed")

# Read the value of the "speed_ref2" variable from the target
print(speed_ref2.get_value())

# housekeeping
os.remove("my_variables.pkl")
os.remove("my_two_variables.yml")

2 changes: 1 addition & 1 deletion pyx2cscope/parser/elf_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

The module is designed to be extended by specific implementations for different ELF file formats.
"""

from abc import ABC, abstractmethod

from dataclasses import dataclass
from typing import Dict, List, Optional

Expand Down
132 changes: 110 additions & 22 deletions pyx2cscope/variable/variable_factory.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
"""Variable Factory returns the respective variable type according to the variable type found at the elf file."""

import logging
import os
import pickle
import yaml
from enum import Enum
from dataclasses import asdict

from mchplnet.lnet import LNet
from pyx2cscope.parser.elf_parser import DummyParser
from pyx2cscope.parser.elf_parser import DummyParser, VariableInfo
from pyx2cscope.parser.generic_parser import GenericParser
from pyx2cscope.variable.variable import (
Variable,
Expand All @@ -18,6 +23,25 @@
VariableUint64,
)

class FileType(Enum):
"""Enumeration of supported file types for import/export operations."""
YAML = ".yml"
PICKLE = ".pkl"
ELF = ".elf"

def variable_info_repr(dumper, data):
"""Helper function to yaml file deserializer. Do not call this function."""
return dumper.represent_mapping('!VariableInfo', asdict(data))

# Custom constructor for VariableInfo
def variable_info_constructor(loader, node):
"""Helper function to yaml file deserializer. Do not call this function."""
values = loader.construct_mapping(node)
return VariableInfo(**values)

# adding constructor and representation of VariableInfo to yaml module.
yaml.add_representer(VariableInfo, variable_info_repr)
yaml.add_constructor('!VariableInfo', variable_info_constructor)

class VariableFactory:
"""A factory class for creating variable objects based on ELF file parsing.
Expand Down Expand Up @@ -64,6 +88,84 @@ def set_elf_file(self, elf_path: str):
parser = GenericParser
self.parser = parser(elf_path)

def set_lnet_interface(self, lnet: LNet):
"""Set the LNet interface to be used for data communication.

Args:
lnet (LNet): the LNet interface
"""
self.l_net = lnet

def _build_export_file_name(self, filename: str = None, ext: FileType= FileType.YAML):
if filename is None:
if self.parser.elf_path is not None:
# get the elf_file name without extension
filename = os.path.splitext(os.path.basename(self.parser.elf_path))[0]
else:
filename = "variables_list"

return os.path.splitext(filename)[0] + ext.value

def export_variables(self, filename: str = None, ext: FileType = FileType.YAML, items=None):
"""Store the variables registered on the elf file to a pickle file.

Args:
filename (str): The path and name of the file to store data to. Defaults to 'elf_file_name.yml'.
ext (FileType): The file extension type to be used (yml or pkl, elf is not supported for export).
items (List): A list of variable names or variables to export. Export all variables if empty.
"""
if ext is FileType.ELF:
raise ValueError("Elf file is not yet supported as export format...")
filename = self._build_export_file_name(filename, ext)

export_dict = {}
if items:
for item in items:
variable_name = item.name if isinstance(item, Variable) else item
export_dict[variable_name] = self.parser.variable_map.get(variable_name)
else:
export_dict = self.parser.variable_map

if ext is FileType.PICKLE:
with open(filename, 'wb') as file:
pickle.dump(export_dict, file)
if ext is FileType.YAML:
with open(filename, 'w') as file:
yaml.dump(export_dict, file)

logging.debug(f"Dictionary stored to {filename}")

def import_variables(self, filename: str):
"""Import and load variables registered on the file.

Currently supported files are Elf (.elf), Pickle (.pkl), and Yaml (.yml).
The flush parameter defaults to true and clears all previous loaded variables. This flag is
intended to be used when adding single variables to the parser.

Args:
filename (str): The name of the file and its path.
"""
if not os.path.exists(filename):
raise ValueError(f"File does not exist at given path: {filename}")
try:
ext = FileType(os.path.splitext(filename)[1])
except ValueError:
raise ValueError(f"File extension not supported. Supported ones are: {[f.value for f in FileType]}")

# clear any previous loaded variable
self.parser.variable_map.clear()

if ext is FileType.ELF:
self.parser = GenericParser(filename)
if ext is FileType.PICKLE:
with open(filename, 'rb') as file:
self.parser.variable_map = pickle.load(file)
if ext is FileType.YAML:
with open(filename, 'r') as file:
self.parser.variable_map = yaml.load(file, Loader=yaml.FullLoader)

logging.debug(f"Variables loaded from {filename}")

def get_var_list(self) -> list[str]:
"""Get a list of variable names available in the ELF file.

Expand All @@ -83,29 +185,15 @@ def get_variable(self, name: str) -> Variable | None:
"""
try:
variable_info = self.parser.get_var_info(name)
return self._get_variable_instance(
variable_info.address,
variable_info.type,
variable_info.array_size,
variable_info.name,
)
return self._get_variable_instance(variable_info)
except Exception as e:
logging.error(f"Error while getting variable '{name}' : {str(e)}")

def _get_variable_instance(
self,
address: int,
var_type: str,
array_size: int,
name: str,
) -> Variable:
def _get_variable_instance(self, var_info: VariableInfo) -> Variable:
"""Create a variable object based on the provided address, type, and name.

Args:
address (int): Address of the variable in the MCU memory.
var_type (VarTypes): Type of the variable.
array_size (int): the size of the array, in case of an array, 0 otherwise.
name (str): Name of the variable.
var_info (VariableInfo): details about the variable as name, address, type, array_size, etc.

returns:
Variable: Variable object based on the provided information.
Expand All @@ -129,7 +217,7 @@ def _get_variable_instance(
VariableUint16
if self.device_info.uc_width == self.device_info.MACHINE_16
else VariableUint32
), # TODO v 0.2.0
),
"short": VariableInt16,
"short int": VariableInt16,
"short unsigned int": VariableUint16,
Expand All @@ -144,9 +232,9 @@ def _get_variable_instance(
}

try:
var_type = var_type.lower().replace("_", "")
return type_factory[var_type](self.l_net, address, array_size, name)
var_type = var_info.type.lower().replace("_", "")
return type_factory[var_type](self.l_net, var_info.address, var_info.array_size, var_info.name)
except IndexError:
raise Exception(
raise ValueError(
f"Type {var_type} not found. Cannot select the right variable representation."
)
36 changes: 24 additions & 12 deletions pyx2cscope/x2cscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from mchplnet.services.frame_load_parameter import LoadScopeData
from mchplnet.services.scope import ScopeChannel, ScopeTrigger
from pyx2cscope.variable.variable import Variable
from pyx2cscope.variable.variable_factory import VariableFactory
from pyx2cscope.variable.variable_factory import VariableFactory, FileType

# Configure logging for debugging and tracking
logging.basicConfig(
Expand Down Expand Up @@ -75,12 +75,12 @@ class X2CScope:
requesting data, and processing received data.

Attributes:
elf_file (str): Path to the ELF file.
interface (InterfaceABC): Interface object for communication.
lnet (LNet): LNet object for low-level network operations.
variable_factory (VariableFactory): Factory to create Variable objects.
scope_setup: Configuration for the scope setup.
convert_list (dict): Dictionary to store variable conversion functions.
uc_width (int): the processor architecture 2: 16 bit, 4: 32 bit.
"""

def __init__(self, elf_file: str = None, interface: InterfaceABC = None, **kwargs):
Expand All @@ -94,7 +94,6 @@ def __init__(self, elf_file: str = None, interface: InterfaceABC = None, **kwarg
i_type = interface if interface is not None else InterfaceType.SERIAL
self.interface = InterfaceFactory.get_interface(interface_type=i_type, **kwargs)
self.lnet = LNet(self.interface)
self.elf_file = elf_file
self.variable_factory = VariableFactory(self.lnet, elf_file)
self.scope_setup = self.lnet.get_scope_setup()
self.convert_list = {}
Expand All @@ -108,15 +107,7 @@ def set_interface(self, interface: InterfaceABC):
"""
self.lnet = LNet(interface)
self.scope_setup = self.lnet.get_scope_setup()
self.variable_factory = VariableFactory(self.lnet, self.elf_file)

def set_elf_file(self, elf_file: str):
"""Set the ELF file for the scope.

Args:
elf_file (str): Path to the ELF file.
"""
self.variable_factory.set_elf_file(elf_file)
self.variable_factory.set_lnet_interface(self.lnet)

def connect(self):
"""Establish a connection with the scope interface."""
Expand Down Expand Up @@ -145,6 +136,27 @@ def get_variable(self, name: str) -> Variable:
"""
return self.variable_factory.get_variable(name)

def export_variables(self, filename: str = None, ext: FileType = FileType.YAML, items=None):
"""Store the variables registered on the elf file to a pickle file.

Args:
filename (str): The path and name of the file to store data to. Defaults to 'elf_file_name.yml'.
ext (FileType): The file extension type to be used (elf, yml, pkl, etc.)
items (List): A list of variable names or variables to export. Export all variables if empty.
"""
self.variable_factory.export_variables(filename, ext, items)

def import_variables(self, filename: str):
"""Import and load variables registered on the file.

Currently supported files are Elf (.elf), Pickle (.pkl), and Yaml (.yml).
This method clears any loaded variable.

Args:
filename (str): The name of the file and its path.
"""
self.variable_factory.import_variables(filename)

def add_scope_channel(self, variable: Variable, trigger: bool = False) -> int:
"""Add a variable as a scope channel.

Expand Down
43 changes: 42 additions & 1 deletion tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os

from pyx2cscope.x2cscope import X2CScope
from pyx2cscope.variable.variable_factory import FileType
from tests import data
from tests.utils.serial_stub import fake_serial

Expand Down Expand Up @@ -56,4 +57,44 @@ def test_nested_array_variable_32(self, mocker, array_size_test=3):
variable = x2c_scope.get_variable("mcFocI_ModuleData_gds.dOutput.duty")
assert variable is not None, "variable name not found"
assert variable.is_array() == True, "variable should be an array"
assert len(variable) == array_size_test, "array has wrong length"
assert len(variable) == array_size_test, "array has wrong length"

def test_variable_export_import(self, mocker):
"""Given a valid 32 bit elf file, check if export and import functions for variables are working."""
fake_serial(mocker, 32)
x2c_scope = X2CScope(port="COM14")
# try to import elf file instead of loading directly from the constructor
x2c_scope.import_variables(self.elf_file_32)
variable = x2c_scope.get_variable("mcFocI_ModuleData_gds.dOutput.elecSpeed")
# store all variables with custom name and default yaml file format
x2c_scope.export_variables(filename="my_variables")
assert os.path.exists("my_variables.yml") == True, "custom export yaml file name not found"
# store all variables with default name and default file format
x2c_scope.export_variables()
assert os.path.exists("qspin_foc_same54.yml") == True, "default export yaml file name not found"
# store a pickle file with only one variable
x2c_scope.export_variables("my_single_variable", ext=FileType.PICKLE, items=[variable])
assert os.path.exists("my_single_variable.pkl") == True, "default export pickle file name not found"
x2c_scope.disconnect()

# load generated yaml file and try
x2c_reloaded = X2CScope(port="COM14")
x2c_reloaded.import_variables(filename="my_variables.yml")
variable_reloaded = x2c_scope.get_variable("mcFocI_ModuleData_gds.dOutput.elecSpeed")
assert variable.name == variable_reloaded.name, "variables don't have the same name"
assert variable.address == variable_reloaded.address, "variables don't have the same address"
assert variable.array_size == variable_reloaded.array_size, "variables don't have the same array size"

# load generated pickle file with single variable
x2c_reloaded = X2CScope(port="COM14")
x2c_reloaded.import_variables(filename="my_single_variable.pkl")
variable_reloaded = x2c_scope.get_variable("mcFocI_ModuleData_gds.dOutput.elecSpeed")
assert len(x2c_reloaded.variable_factory.parser.variable_map) == 1, "import loaded more than 1 variable"
assert variable.name == variable_reloaded.name, "variables don't have the same name"
assert variable.address == variable_reloaded.address, "variables don't have the same address"
assert variable.array_size == variable_reloaded.array_size, "variables don't have the same array size"

# house keeping -> delete generated files
os.remove("my_variables.yml")
os.remove("qspin_foc_same54.yml")
os.remove("my_single_variable.pkl")
Loading