diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f5dcb33 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# Avlos – Agent and contributor guide + +This file gives AI and contributors consistent context for the Avlos codebase. For user-facing docs, see [README.md](README.md) and [docs/index.rst](docs/index.rst). + +## What Avlos is + +Avlos is a code generator: a single YAML device specification is turned into C firmware code, C++ client code, RST documentation, and CAN DBC. It is used for embedded protocols (e.g. [Tinymovr](https://tinymovr.com)). + +## Key directories + +| Directory | Purpose | +|-----------|---------| +| `avlos/definitions/` | Node types (RemoteNode, RemoteAttribute, RemoteFunction, RemoteEnum, RemoteBitmask, RootNode) and Marshmallow schemas | +| `avlos/generators/` | Generator modules (`generator_c.py`, `generator_cpp.py`, etc.) and [filters.py](avlos/generators/filters.py) (e.g. `avlos_endpoints(instance)`) | +| `avlos/templates/` | Jinja templates for C, C++, RST, DBC | +| `avlos/datatypes.py` | `DataType` enum and C name/size maps | +| `tests/definition/` | YAML specs and [avlos_config.yaml](tests/definition/avlos_config.yaml) | +| `tests/outputs/` | Generated outputs (often gitignored) | + +## Conventions + +- **Device spec**: YAML with `name` and `remote_attributes` (nested). Endpoints have `dtype`, `getter_name` / `setter_name` / `caller_name`, and `arguments` for callables. +- **Config**: `generators..enabled`, `generators..paths.`. Paths are relative to the config file directory. +- **C generator**: The same endpoint order is used everywhere (enums, header, impl, metadata). Use `instance | endpoints` for the canonical list. + +## Extending the C generator + +- Add optional path keys in config (do not add them to `required_paths` if they are optional). +- In `generator_c.process()`: validate and render new templates using the same `instance` and `instance | endpoints`; append generated file paths to the list that gets clang-format applied. + +## Testing + +- Unittest/pytest in `tests/`. C generator tests use `deserialize()` + `generator_c.process()` and often run cppcheck on generated C. Outputs go under `tests/outputs/`. + +## Docs + +- Sphinx in [docs/](docs/); [docs/index.rst](docs/index.rst) is the root. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..1ac1442 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,86 @@ +# Avlos architecture + +This document describes the structure and data flow of Avlos. For contributor and AI guidance, see [AGENTS.md](AGENTS.md). + +## High-level flow + +1. **Input**: A device spec YAML file and an Avlos config file (`avlos_config.yaml`). +2. **CLI or API**: The CLI (`avlos from file `) or programmatic use loads the YAML and calls the processor. See [avlos/cli.py](avlos/cli.py) and [avlos/processor.py](avlos/processor.py). +3. **Processor** ([avlos/processor.py](avlos/processor.py)): Loads the config, resolves all paths to absolute (relative to the config file directory), then calls each enabled generator’s `process(instance, module_config)`. +4. **Deserializer** ([avlos/deserializer.py](avlos/deserializer.py)): Builds the in-memory object tree via `RootNodeSchema`. The schema in [avlos/definitions/remote_node.py](avlos/definitions/remote_node.py) (`RemoteNodeSchema.post_load`) decides whether each node is a RemoteNode, RemoteFunction, RemoteAttribute, RemoteBitmask, or RemoteEnum, and assigns `ep_id` via [avlos/counter.py](avlos/counter.py). +5. **Generators**: Each generator receives the same `instance` (root of the tree). The C generator uses `avlos_endpoints(instance)` to get one flat list of endpoints; templates iterate that list to produce the enums header, endpoints header, endpoints implementation, and optionally the endpoint metadata files. + +```mermaid +flowchart LR + subgraph inputs [Inputs] + SpecYAML[device.yaml] + ConfigYAML[avlos_config.yaml] + end + subgraph avlos [Avlos] + CLI[CLI or API] + Proc[Processor] + Deser[Deserializer] + Gen[Generators] + end + subgraph outputs [Generated files] + Enums[tm_enums.h] + Header[fw_endpoints.h] + Impl[fw_endpoints.c] + Meta[metadata .h/.c optional] + end + SpecYAML --> CLI + ConfigYAML --> CLI + CLI --> Deser + Deser --> Proc + Proc --> Gen + Gen --> Enums + Gen --> Header + Gen --> Impl + Gen --> Meta +``` + +## Object model + +- **RootNode** ([avlos/definitions/remote_root_node.py](avlos/definitions/remote_root_node.py)): Root of the tree; extends RemoteNode. +- **RemoteNode** ([avlos/definitions/remote_node.py](avlos/definitions/remote_node.py)): Interior node with `remote_attributes` (ordered dict of children). Can contain more RemoteNodes or endpoint leaves. +- **Endpoint leaves** (same schema, different `post_load` result): + - **RemoteAttribute**: Has `dtype`, `getter_name`, `setter_name`. Used for read/write values. + - **RemoteFunction**: Has `caller_name`, `arguments` (list of name + dtype), `dtype` (return type). + - **RemoteEnum**: Has `options` (enum); exposes `dtype` as UINT8. + - **RemoteBitmask**: Has `flags` (bitmask); exposes `dtype` as UINT8. + +All endpoints have `ep_id`, `endpoint_function_name`, and a `dtype` (or equivalent). The **canonical traversal order** is given by `avlos_endpoints(root)` in [avlos/generators/filters.py](avlos/generators/filters.py): depth-first over `remote_attributes`, collecting any node that has `getter_name`, `setter_name`, or `caller_name`. + +### Endpoint kind (for metadata) + +When generating endpoint metadata, each endpoint is classified into one of: + +| Condition | Avlos_EndpointKind | +|-----------|---------------------| +| getter only, no setter, no caller | READ_ONLY | +| setter only, no getter, no caller | WRITE_ONLY | +| getter + setter, no caller | READ_WRITE | +| caller, `arguments` empty | CALL_NO_ARGS | +| caller, `arguments` non-empty | CALL_WITH_ARGS | + +```mermaid +flowchart LR + subgraph kinds [Endpoint kinds] + RO[READ_ONLY] + WO[WRITE_ONLY] + RW[READ_WRITE] + CNA[CALL_NO_ARGS] + CWA[CALL_WITH_ARGS] + end +``` + +## C generator in detail + +- **Required paths**: `output_enums`, `output_header`, `output_impl`. See [avlos/generators/generator_c.py](avlos/generators/generator_c.py). +- **Optional paths** (when both present, metadata is generated): `output_metadata_header`, `output_metadata_impl`. +- **Templates**: `tm_enums.h.jinja`, `fw_endpoints.h.jinja`, `fw_endpoints.c.jinja`, and optionally `avlos_endpoint_metadata.h.jinja`, `avlos_endpoint_metadata.c.jinja`. +- **Filters** (registered in `process()`): `endpoints`, `enum_eps`, `bitmask_eps`, `as_include`, and for metadata `avlos_ep_kind`, `avlos_metadata_dtype`. The endpoint list is identical for header, implementation, and metadata. + +## Config and paths + +Config is per generator under `generators.`. Paths are relative to the directory containing the config file; the processor converts them to absolute before calling the generator. New outputs (e.g. metadata) are added as new optional path keys; the C generator only generates metadata when both `output_metadata_header` and `output_metadata_impl` are set. diff --git a/README.md b/README.md index 4aae39e..94f72ab 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ The output config defines the output modules that will be used and their options paths: output_header: outputs/header.h output_impl: outputs/header.c + # optional: output_metadata_header + output_metadata_impl for type-aware UART/ASCII header_includes: - src/header.h impl_includes: diff --git a/avlos/generators/filters.py b/avlos/generators/filters.py index 0d4ee32..9b11f61 100644 --- a/avlos/generators/filters.py +++ b/avlos/generators/filters.py @@ -2,6 +2,25 @@ from copy import copy from typing import List +from avlos.datatypes import DataType + +# Avlos_Dtype enum names for metadata (reduced set for UART/ASCII parsing) +_AVLOS_DTYPE_MAP = { + DataType.VOID: "AVLOS_DTYPE_VOID", + DataType.BOOL: "AVLOS_DTYPE_BOOL", + DataType.UINT8: "AVLOS_DTYPE_UINT8", + DataType.INT8: "AVLOS_DTYPE_UINT8", + DataType.UINT16: "AVLOS_DTYPE_UINT32", + DataType.INT16: "AVLOS_DTYPE_INT32", + DataType.UINT32: "AVLOS_DTYPE_UINT32", + DataType.INT32: "AVLOS_DTYPE_INT32", + DataType.UINT64: "AVLOS_DTYPE_UINT32", + DataType.INT64: "AVLOS_DTYPE_INT32", + DataType.FLOAT: "AVLOS_DTYPE_FLOAT", + DataType.DOUBLE: "AVLOS_DTYPE_FLOAT", + DataType.STR: "AVLOS_DTYPE_STRING", +} + def avlos_endpoints(input) -> List: """ @@ -101,3 +120,50 @@ def capitalize_first(input: str) -> str: String with first character capitalized """ return input[0].upper() + input[1:] + + +def avlos_ep_kind(ep) -> str: + """ + Return the Avlos_EndpointKind enum name for an endpoint (for metadata generation). + + Args: + ep: Endpoint object (RemoteAttribute, RemoteFunction, RemoteEnum, or RemoteBitmask) + + Returns: + String like AVLOS_EP_KIND_READ_ONLY, AVLOS_EP_KIND_CALL_WITH_ARGS, etc. + """ + has_getter = getattr(ep, "getter_name", None) is not None + has_setter = getattr(ep, "setter_name", None) is not None + has_caller = getattr(ep, "caller_name", None) is not None + if has_caller: + num_args = len(getattr(ep, "arguments", None) or []) + if num_args == 0: + return "AVLOS_EP_KIND_CALL_NO_ARGS" + return "AVLOS_EP_KIND_CALL_WITH_ARGS" + if has_getter and has_setter: + return "AVLOS_EP_KIND_READ_WRITE" + if has_getter: + return "AVLOS_EP_KIND_READ_ONLY" + if has_setter: + return "AVLOS_EP_KIND_WRITE_ONLY" + return "AVLOS_EP_KIND_READ_ONLY" # fallback + + +def avlos_metadata_dtype(value) -> str: + """ + Map a DataType or an object with .dtype (endpoint or argument) to Avlos_Dtype enum name. + + Used for value_dtype and arg_dtypes in endpoint metadata. Narrowing (e.g. 64-bit to + 32-bit) is applied where the metadata enum set is smaller than DataType. + + Args: + value: Either a DataType enum member or an object with a .dtype attribute (endpoint, argument) + + Returns: + String like AVLOS_DTYPE_UINT32, AVLOS_DTYPE_FLOAT, etc. + """ + dtype = getattr(value, "dtype", value) + try: + return _AVLOS_DTYPE_MAP[dtype] + except KeyError: + return "AVLOS_DTYPE_UINT32" # safe fallback diff --git a/avlos/generators/generator_c.py b/avlos/generators/generator_c.py index c412b07..af35230 100644 --- a/avlos/generators/generator_c.py +++ b/avlos/generators/generator_c.py @@ -4,13 +4,60 @@ from jinja2 import Environment, PackageLoader, select_autoescape from avlos.formatting import format_c_code, is_clang_format_available -from avlos.generators.filters import as_include, avlos_bitmask_eps, avlos_endpoints, avlos_enum_eps +from avlos.generators.filters import ( + as_include, + avlos_bitmask_eps, + avlos_endpoints, + avlos_enum_eps, + avlos_ep_kind, + avlos_metadata_dtype, +) from avlos.validation import ValidationError, validate_all env = Environment(loader=PackageLoader("avlos"), autoescape=select_autoescape()) +def _generate_metadata_if_requested(instance, config): + """Generate endpoint metadata .h/.c when both paths are in config. Returns extra paths or [].""" + paths = config["paths"] + if "output_metadata_header" not in paths or "output_metadata_impl" not in paths: + return [] + meta_header_path = paths["output_metadata_header"] + meta_impl_path = paths["output_metadata_impl"] + metadata_header_basename = os.path.basename(meta_header_path) + os.makedirs(os.path.dirname(meta_header_path), exist_ok=True) + template_meta_h = env.get_template("avlos_endpoint_metadata.h.jinja") + with open(meta_header_path, "w") as f: + print(template_meta_h.render(), file=f) + os.makedirs(os.path.dirname(meta_impl_path), exist_ok=True) + template_meta_c = env.get_template("avlos_endpoint_metadata.c.jinja") + with open(meta_impl_path, "w") as f: + print( + template_meta_c.render( + instance=instance, + metadata_header_basename=metadata_header_basename, + ), + file=f, + ) + return [meta_header_path, meta_impl_path] + + def process(instance, config): + # Validate config has required paths + required_paths = ["output_enums", "output_header", "output_impl"] + if "paths" not in config: + raise ValidationError( + "Config validation failed: Missing 'paths' section in avlos config.\n" + "Please add a 'paths' section with: output_enums, output_header, output_impl" + ) + + missing_paths = [p for p in required_paths if p not in config["paths"]] + if missing_paths: + raise ValidationError( + f"Config validation failed: Missing required paths in avlos config: {', '.join(missing_paths)}\n" + f"Please add these paths to the 'paths' section of your avlos config file." + ) + # Validate before generation validation_errors = validate_all(instance) if validation_errors: @@ -21,6 +68,8 @@ def process(instance, config): env.filters["enum_eps"] = avlos_enum_eps env.filters["bitmask_eps"] = avlos_bitmask_eps env.filters["as_include"] = as_include + env.filters["avlos_ep_kind"] = avlos_ep_kind + env.filters["avlos_metadata_dtype"] = avlos_metadata_dtype template = env.get_template("tm_enums.h.jinja") os.makedirs(os.path.dirname(config["paths"]["output_enums"]), exist_ok=True) @@ -54,17 +103,16 @@ def process(instance, config): file=output_file, ) - # Post-process with clang-format if available - format_style = config.get("format_style", "LLVM") - - generated_files = [ + base_files = [ config["paths"]["output_enums"], config["paths"]["output_header"], config["paths"]["output_impl"], ] + generated_files = base_files + _generate_metadata_if_requested(instance, config) + # Post-process with clang-format if available + format_style = config.get("format_style", "LLVM") for file_path in generated_files: success = format_c_code(file_path, format_style) if not success and is_clang_format_available(): - # Only warn if clang-format is installed but failed print(f"Warning: clang-format failed for {file_path}", file=sys.stderr) diff --git a/avlos/generators/generator_cpp.py b/avlos/generators/generator_cpp.py index f87c61a..9cb3382 100644 --- a/avlos/generators/generator_cpp.py +++ b/avlos/generators/generator_cpp.py @@ -12,6 +12,21 @@ def process(instance, config): + # Validate config has required paths + required_paths = ["output_helpers", "output_header", "output_impl"] + if "paths" not in config: + raise ValidationError( + "Config validation failed: Missing 'paths' section in avlos config.\n" + "Please add a 'paths' section with: output_helpers, output_header, output_impl" + ) + + missing_paths = [p for p in required_paths if p not in config["paths"]] + if missing_paths: + raise ValidationError( + f"Config validation failed: Missing required paths in avlos config: {', '.join(missing_paths)}\n" + f"Please add these paths to the 'paths' section of your avlos config file." + ) + # Validate before generation validation_errors = validate_all(instance) if validation_errors: diff --git a/avlos/templates/avlos_endpoint_metadata.c.jinja b/avlos/templates/avlos_endpoint_metadata.c.jinja new file mode 100644 index 0000000..21ac28c --- /dev/null +++ b/avlos/templates/avlos_endpoint_metadata.c.jinja @@ -0,0 +1,34 @@ +/* +* This file was automatically generated using Avlos. +* https://github.com/tinymovr/avlos +* +* Any changes to this file will be overwritten when +* content is regenerated. +*/ + +#include "{{ metadata_header_basename }}" + +{% set eps = instance | endpoints %} +const Avlos_EndpointMeta avlos_endpoint_meta[] = { +{% for ep in eps %} + /* {{ ep.endpoint_function_name }} */ + [{{ ep.ep_id }}] = { + .kind = {{ ep | avlos_ep_kind }}, + .value_dtype = {{ ep | avlos_metadata_dtype }}, + .num_args = {% if ep | avlos_ep_kind == "AVLOS_EP_KIND_CALL_WITH_ARGS" %}{{ ep.arguments | length }}{% else %}0{% endif %}, + .arg_dtypes = { + {%- for i in range(4) %} + {%- if ep | avlos_ep_kind == "AVLOS_EP_KIND_CALL_WITH_ARGS" and i < (ep.arguments | length) %} + {{ ep.arguments[i] | avlos_metadata_dtype }} + {%- else %} + AVLOS_DTYPE_VOID + {%- endif %} + {%- if i < 3 %}, {% endif %} + {%- endfor %} + } + }{% if not loop.last %},{% endif %} + +{% endfor %} +}; + +const uint8_t avlos_endpoint_meta_count = sizeof(avlos_endpoint_meta) / sizeof(avlos_endpoint_meta[0]); diff --git a/avlos/templates/avlos_endpoint_metadata.h.jinja b/avlos/templates/avlos_endpoint_metadata.h.jinja new file mode 100644 index 0000000..45dcc63 --- /dev/null +++ b/avlos/templates/avlos_endpoint_metadata.h.jinja @@ -0,0 +1,43 @@ +/* +* This file was automatically generated using Avlos. +* https://github.com/tinymovr/avlos +* +* Any changes to this file will be overwritten when +* content is regenerated. +* +* Endpoint type metadata for type-aware UART/ASCII parsing and formatting. +* No dependency on UART; consumed by the firmware layer. +*/ + +#pragma once +#include + +typedef enum { + AVLOS_DTYPE_UINT8, + AVLOS_DTYPE_UINT32, + AVLOS_DTYPE_INT32, + AVLOS_DTYPE_FLOAT, + AVLOS_DTYPE_BOOL, + AVLOS_DTYPE_STRING, + AVLOS_DTYPE_VOID +} Avlos_Dtype; + +typedef enum { + AVLOS_EP_KIND_READ_ONLY, + AVLOS_EP_KIND_WRITE_ONLY, + AVLOS_EP_KIND_READ_WRITE, + AVLOS_EP_KIND_CALL_NO_ARGS, + AVLOS_EP_KIND_CALL_WITH_ARGS +} Avlos_EndpointKind; + +#define AVLOS_MAX_CALL_ARGS 4 + +typedef struct { + Avlos_EndpointKind kind; + Avlos_Dtype value_dtype; + uint8_t num_args; + Avlos_Dtype arg_dtypes[AVLOS_MAX_CALL_ARGS]; +} Avlos_EndpointMeta; + +extern const Avlos_EndpointMeta avlos_endpoint_meta[]; +extern const uint8_t avlos_endpoint_meta_count; diff --git a/avlos/templates/docs.rst.jinja b/avlos/templates/docs.rst.jinja index 9a4673a..f182753 100644 --- a/avlos/templates/docs.rst.jinja +++ b/avlos/templates/docs.rst.jinja @@ -1,6 +1,3 @@ - -.. _api-reference: - API REFERENCE ============= diff --git a/avlos/validation.py b/avlos/validation.py index 5a47777..cb5a4e4 100644 --- a/avlos/validation.py +++ b/avlos/validation.py @@ -3,9 +3,12 @@ Validates C identifiers, detects conflicts, and ensures consistency. """ +import logging import re from typing import List +_logger = logging.getLogger("avlos") + # C reserved words (C11 standard) C_RESERVED_WORDS = { "auto", @@ -86,9 +89,11 @@ def validate_c_identifier(name: str, context: str = "") -> None: if len(name) > 63: # C99 requires at least 63 significant characters for identifiers ctx = f" ({context})" if context else "" - print( - f"Warning: Identifier '{name}'{ctx} is very long ({len(name)} chars). " - f"Some compilers may truncate after 63 characters." + _logger.warning( + "Identifier '%s'%s is very long (%d chars). " "Some compilers may truncate after 63 characters.", + name, + ctx, + len(name), ) diff --git a/docs/config.rst b/docs/config.rst index ace34ee..208a0ef 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -74,6 +74,16 @@ The output config defines the output modules that will be used and their options There are three main generated files that are configured above: A header containing enums (`output_enums`), a header containing function declarations (`output_header`), and an implementation containing function definitions (`output_impl`). +Optional: Endpoint metadata (for type-aware UART/ASCII parsing) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you want the C generator to emit an endpoint type metadata table (so firmware can parse and format ASCII without a hand-maintained table), add both of these optional paths under ``paths``: + +- ``output_metadata_header``: Path for the generated metadata header (e.g. ``fw/avlos_endpoint_metadata.h``). +- ``output_metadata_impl``: Path for the generated metadata implementation (e.g. ``fw/avlos_endpoint_metadata.c``). + +When both are present, Avlos generates ``Avlos_EndpointMeta avlos_endpoint_meta[]`` and ``avlos_endpoint_meta_count`` in the same order as ``avlos_endpoints[]``. Each entry describes the endpoint kind (read-only, read-write, call with/without args), value type, and for callables the argument types. If either path is omitted, no metadata files are generated (backward compatible). + Of note is that no #include statements for the generated files are generated automatically. This is something that we decided in order to maximize compatibility to edge cases, but may be revised in future Avlos versions. CLI Usage diff --git a/example/avlos_config.yaml b/example/avlos_config.yaml index 5b21b96..7b42583 100644 --- a/example/avlos_config.yaml +++ b/example/avlos_config.yaml @@ -4,6 +4,9 @@ generators: paths: output_header: fw/protocol.h output_impl: fw/protocol.c + # Optional: emit endpoint type metadata for type-aware UART/ASCII (uncomment to enable) + # output_metadata_header: fw/avlos_endpoint_metadata.h + # output_metadata_impl: fw/avlos_endpoint_metadata.c header_includes: - header.h impl_includes: diff --git a/tests/definition/tinymovr_1_6_x.yaml b/tests/definition/tinymovr_1_6_x.yaml new file mode 100644 index 0000000..1e7d93c --- /dev/null +++ b/tests/definition/tinymovr_1_6_x.yaml @@ -0,0 +1,523 @@ + +name: tm +remote_attributes: + - name: protocol_hash + dtype: uint32 + getter_name: _avlos_get_proto_hash + summary: The Avlos protocol hash. + - name: uid + dtype: uint32 + getter_name: system_get_uid + summary: The unique device ID, unique to each PAC55xx chip produced. + - name: fw_version + dtype: string + getter_name: system_get_fw_version_string + summary: The firmware version. + - name: hw_revision + dtype: uint32 + getter_name: system_get_hw_revision + summary: The hardware revision. + - name: Vbus + dtype: float + unit: volt + meta: {dynamic: True} + getter_name: system_get_Vbus + summary: The measured bus voltage. + - name: Ibus + dtype: float + unit: ampere + meta: {dynamic: True} + getter_name: controller_get_Ibus_est + summary: The estimated bus current. Only estimates current drawn by motor. + - name: power + dtype: float + unit: watt + meta: {dynamic: True} + getter_name: controller_get_power_est + summary: The estimated power. Only estimates power drawn by motor. + - name: temp + dtype: float + unit: degC + meta: {dynamic: True} + getter_name: adc_get_mcu_temp + summary: The internal temperature of the PAC55xx MCU. + - name: calibrated + dtype: bool + meta: {dynamic: True} + getter_name: system_get_calibrated + summary: Whether the system has been calibrated. + - name: errors + flags: [UNDERVOLTAGE, DRIVER_FAULT, CHARGE_PUMP_FAULT_STAT, CHARGE_PUMP_FAULT, DRV10_DISABLE, DRV32_DISABLE, DRV54_DISABLE] + meta: {dynamic: True} + getter_name: system_get_errors + summary: Any system errors, as a bitmask + - name: save_config + summary: Save configuration to non-volatile memory. + caller_name: nvm_save_config + dtype: void + arguments: [] + - name: erase_config + summary: Erase the config stored in non-volatile memory and reset the device. + caller_name: nvm_erase + dtype: void + arguments: [] + meta: {reload_data: True} + - name: reset + summary: Reset the device. + caller_name: system_reset + dtype: void + arguments: [] + meta: {reload_data: True} + - name: enter_dfu + summary: Enter DFU mode. + caller_name: system_enter_dfu + dtype: void + arguments: [] + meta: {reload_data: True} + - name: scheduler + remote_attributes: + - name: errors + flags: [CONTROL_BLOCK_REENTERED] + meta: {dynamic: True} + getter_name: scheduler_get_errors + summary: Any scheduler errors, as a bitmask + - name: controller + remote_attributes: + - name: state + dtype: uint8 + meta: {dynamic: True} + getter_name: controller_get_state + setter_name: controller_set_state + summary: The state of the controller. + - name: mode + dtype: uint8 + meta: {dynamic: True} + getter_name: controller_get_mode + setter_name: controller_set_mode + summary: The control mode of the controller. + - name: warnings + meta: {dynamic: True} + flags: [VELOCITY_LIMITED, CURRENT_LIMITED, MODULATION_LIMITED] + getter_name: controller_get_warnings + summary: Any controller warnings, as a bitmask + - name: errors + meta: {dynamic: True} + flags: [CURRENT_LIMIT_EXCEEDED] + getter_name: controller_get_errors + summary: Any controller errors, as a bitmask + - name: position + remote_attributes: + - name: setpoint + dtype: float + unit: tick + getter_name: controller_get_pos_setpoint_user_frame + setter_name: controller_set_pos_setpoint_user_frame + summary: The position setpoint. + - name: p_gain + dtype: float + meta: {export: True} + getter_name: controller_get_pos_gain + setter_name: controller_set_pos_gain + summary: The proportional gain of the position controller. + - name: velocity + remote_attributes: + - name: setpoint + dtype: float + unit: tick/sec + getter_name: controller_get_vel_setpoint_user_frame + setter_name: controller_set_vel_setpoint_user_frame + summary: The velocity setpoint. + - name: limit + dtype: float + unit: tick/sec + meta: {export: True} + getter_name: controller_get_vel_limit + setter_name: controller_set_vel_limit + summary: The velocity limit. + - name: p_gain + dtype: float + meta: {export: True} + getter_name: controller_get_vel_gain + setter_name: controller_set_vel_gain + summary: The proportional gain of the velocity controller. + - name: i_gain + dtype: float + meta: {export: True} + getter_name: controller_get_vel_integrator_gain + setter_name: controller_set_vel_integrator_gain + summary: The integral gain of the velocity controller. + - name: deadband + dtype: float + unit: tick + meta: {export: True} + getter_name: controller_get_vel_integrator_deadband + setter_name: controller_set_vel_integrator_deadband + rst_target: integrator-deadband + summary: The deadband of the velocity integrator. A region around the position setpoint where the velocity integrator is not updated. + - name: increment + dtype: float + meta: {export: True} + getter_name: controller_get_vel_increment + setter_name: controller_set_vel_increment + summary: Max velocity setpoint increment (ramping) rate. Set to 0 to disable. + - name: current + remote_attributes: + - name: Iq_setpoint + dtype: float + unit: ampere + getter_name: controller_get_Iq_setpoint_user_frame + setter_name: controller_set_Iq_setpoint_user_frame + summary: The Iq setpoint. + - name: Id_setpoint + dtype: float + unit: ampere + meta: {dynamic: True} + getter_name: controller_get_Id_setpoint_user_frame + summary: The Id setpoint. + - name: Iq_limit + dtype: float + unit: ampere + getter_name: controller_get_Iq_limit + setter_name: controller_set_Iq_limit + summary: The Iq limit. + - name: Iq_estimate + dtype: float + unit: ampere + meta: {dynamic: True} + getter_name: controller_get_Iq_estimate_user_frame + summary: The Iq estimate. + - name: bandwidth + dtype: float + unit: Hz + meta: {export: True} + getter_name: controller_get_I_bw + setter_name: controller_set_I_bw + summary: The current controller bandwidth. + - name: Iq_p_gain + dtype: float + getter_name: controller_get_Iq_gain + summary: The current controller proportional gain. + - name: max_Ibus_regen + dtype: float + unit: ampere + getter_name: controller_get_max_Ibus_regen + setter_name: controller_set_max_Ibus_regen + summary: The max current allowed to be fed back to the power source before flux braking activates. + - name: max_Ibrake + dtype: float + unit: ampere + meta: {export: True} + getter_name: controller_get_max_Ibrake + setter_name: controller_set_max_Ibrake + summary: The max current allowed to be dumped to the motor windings during flux braking. Set to zero to deactivate flux braking. + - name: voltage + remote_attributes: + - name: Vq_setpoint + dtype: float + unit: volt + meta: {dynamic: True} + getter_name: controller_get_Vq_setpoint_user_frame + summary: The Vq setpoint. + - name: calibrate + summary: Calibrate the device. + caller_name: controller_calibrate + dtype: void + arguments: [] + - name: idle + summary: Set idle mode, disabling the driver. + caller_name: controller_idle + dtype: void + arguments: [] + - name: position_mode + summary: Set position control mode. + caller_name: controller_position_mode + dtype: void + arguments: [] + - name: velocity_mode + summary: Set velocity control mode. + caller_name: controller_velocity_mode + dtype: void + arguments: [] + - name: current_mode + summary: Set current control mode. + caller_name: controller_current_mode + dtype: void + arguments: [] + - name: set_pos_vel_setpoints + summary: Set the position and velocity setpoints in one go, and retrieve the position estimate + caller_name: controller_set_pos_vel_setpoints + dtype: float + arguments: + - name: pos_setpoint + dtype: float + unit: tick + - name: vel_setpoint + dtype: float + unit: tick + - name: comms + remote_attributes: + - name: can + remote_attributes: + - name: rate + dtype: uint32 + meta: {export: True} + getter_name: CAN_get_kbit_rate + setter_name: CAN_set_kbit_rate + rst_target: api-can-rate + summary: The baud rate of the CAN interface. + - name: id + dtype: uint32 + meta: {export: True, reload_data: True} + getter_name: CAN_get_ID + setter_name: CAN_set_ID + summary: The ID of the CAN interface. + - name: motor + remote_attributes: + - name: R + dtype: float + unit: ohm + meta: {dynamic: True, export: True} + getter_name: motor_get_phase_resistance + setter_name: motor_set_phase_resistance + summary: The motor Resistance value. + - name: L + dtype: float + unit: henry + meta: {dynamic: True, export: True} + getter_name: motor_get_phase_inductance + setter_name: motor_set_phase_inductance + summary: The motor Inductance value. + - name: pole_pairs + dtype: uint8 + meta: {dynamic: True, export: True} + getter_name: motor_get_pole_pairs + setter_name: motor_set_pole_pairs + summary: The motor pole pair count. + - name: type + options: [HIGH_CURRENT, GIMBAL] + meta: {export: True} + getter_name: motor_get_is_gimbal + setter_name: motor_set_is_gimbal + summary: The type of the motor. Either high current or gimbal. + - name: offset + dtype: float + meta: {export: True} + getter_name: motor_get_user_offset + setter_name: motor_set_user_offset + summary: User-defined offset of the motor. + - name: direction + dtype: int8 + meta: {export: True} + getter_name: motor_get_user_direction + setter_name: motor_set_user_direction + summary: User-defined direction of the motor. + - name: calibrated + dtype: bool + meta: {dynamic: True} + getter_name: motor_get_calibrated + summary: Whether the motor has been calibrated. + - name: I_cal + dtype: float + unit: ampere + meta: {export: True} + getter_name: motor_get_I_cal + setter_name: motor_set_I_cal + summary: The calibration current. + - name: errors + flags: [PHASE_RESISTANCE_OUT_OF_RANGE, PHASE_INDUCTANCE_OUT_OF_RANGE,INVALID_POLE_PAIRS] + meta: {dynamic: True} + getter_name: motor_get_errors + summary: Any motor/calibration errors, as a bitmask + # - name: phase_currents + # remote_attributes: + # - name: U + # dtype: float + # unit: ampere + # getter_name: motor_get_IU + # summary: Measured current in phase U. + # - name: V + # dtype: float + # unit: ampere + # getter_name: motor_get_IV + # summary: Measured current in phase V. + # - name: W + # dtype: float + # unit: ampere + # getter_name: motor_get_IW + # summary: Measured current in phase W. + - name: encoder + remote_attributes: + - name: position_estimate + dtype: float + unit: ticks + meta: {dynamic: True} + getter_name: observer_get_pos_estimate_user_frame + summary: The filtered encoder position estimate. + - name: velocity_estimate + dtype: float + unit: ticks/second + meta: {dynamic: True} + getter_name: observer_get_vel_estimate_user_frame + summary: The filtered encoder velocity estimate. + - name: type + options: [INTERNAL, HALL] + meta: {export: True} + getter_name: encoder_get_type + setter_name: encoder_set_type + summary: The encoder type. Either INTERNAL or HALL. + - name: bandwidth + dtype: float + unit: Hz + meta: {export: True} + getter_name: observer_get_bw + setter_name: observer_set_bw + summary: The encoder observer bandwidth. + - name: calibrated + dtype: bool + meta: {dynamic: True} + getter_name: encoder_get_calibrated + summary: Whether the encoder has been calibrated. + - name: errors + flags: [CALIBRATION_FAILED, READING_UNSTABLE] + meta: {dynamic: True} + getter_name: encoder_get_errors + summary: Any encoder errors, as a bitmask + - name: traj_planner + remote_attributes: + - name: max_accel + dtype: float + unit: ticks/s + meta: {export: True} + getter_name: planner_get_max_accel + setter_name: planner_set_max_accel + summary: The max allowed acceleration of the generated trajectory. + - name: max_decel + dtype: float + unit: ticks/second/second + meta: {export: True} + getter_name: planner_get_max_decel + setter_name: planner_set_max_decel + summary: The max allowed deceleration of the generated trajectory. + - name: max_vel + dtype: float + unit: ticks/second + meta: {export: True} + getter_name: planner_get_max_vel + setter_name: planner_set_max_vel + summary: The max allowed cruise velocity of the generated trajectory. + - name: t_accel + dtype: float + unit: second + meta: {export: True} + getter_name: planner_get_deltat_accel + setter_name: planner_set_deltat_accel + summary: In time mode, the acceleration time of the generated trajectory. + - name: t_decel + dtype: float + unit: second + meta: {export: True} + getter_name: planner_get_deltat_decel + setter_name: planner_set_deltat_decel + summary: In time mode, the deceleration time of the generated trajectory. + - name: t_total + dtype: float + unit: second + meta: {export: True} + getter_name: planner_get_deltat_total + setter_name: planner_set_deltat_total + summary: In time mode, the total time of the generated trajectory. + - name: move_to + summary: Move to target position respecting velocity and acceleration limits. + caller_name: planner_move_to_vlimit + dtype: void + arguments: + - name: pos_setpoint + dtype: float + unit: tick + - name: move_to_tlimit + summary: Move to target position respecting time limits for each sector. + caller_name: planner_move_to_tlimit + dtype: void + arguments: + - name: pos_setpoint + dtype: float + unit: tick + - name: errors + flags: [INVALID_INPUT, VCRUISE_OVER_LIMIT] + getter_name: planner_get_errors + summary: Any errors in the trajectory planner, as a bitmask + - name: homing + remote_attributes: + - name: velocity + dtype: float + unit: ticks/s + meta: {export: True} + getter_name: homing_planner_get_homing_velocity + setter_name: homing_planner_set_homing_velocity + summary: The velocity at which the motor performs homing. + - name: max_homing_t + dtype: float + unit: s + meta: {export: True} + getter_name: homing_planner_get_max_homing_t + setter_name: homing_planner_set_max_homing_t + summary: The maximum time the motor is allowed to travel before homing times out and aborts. + - name: retract_dist + dtype: float + unit: ticks + meta: {export: True} + getter_name: homing_planner_get_retract_distance + setter_name: homing_planner_set_retract_distance + summary: The retraction distance the motor travels after the endstop has been found. + - name: warnings + meta: {dynamic: True} + flags: [HOMING_TIMEOUT] + getter_name: homing_planner_get_warnings + summary: Any homing warnings, as a bitmask + - name: stall_detect + remote_attributes: + - name: velocity + dtype: float + unit: ticks/s + meta: {export: True} + getter_name: homing_planner_get_max_stall_vel + setter_name: homing_planner_set_max_stall_vel + summary: The velocity below which (and together with `stall_detect.delta_pos`) stall detection mode is triggered. + - name: delta_pos + dtype: float + unit: ticks + meta: {export: True} + getter_name: homing_planner_get_max_stall_delta_pos + setter_name: homing_planner_set_max_stall_delta_pos + summary: The velocity below which (and together with `stall_detect.delta_pos`) stall detection mode is triggered. + - name: t + dtype: float + unit: s + meta: {export: True} + getter_name: homing_planner_get_max_stall_t + setter_name: homing_planner_set_max_stall_t + summary: The time to remain in stall detection mode before the motor is considered stalled. + - name: home + summary: Perform the homing operation. + caller_name: homing_planner_home + dtype: void + arguments: [] + - name: watchdog + remote_attributes: + - name: enabled + dtype: bool + getter_name: Watchdog_get_enabled + setter_name: Watchdog_set_enabled + summary: Whether the watchdog is enabled or not. + - name: triggered + dtype: bool + meta: {dynamic: True} + getter_name: Watchdog_triggered + summary: Whether the watchdog has been triggered or not. + - name: timeout + dtype: float + unit: s + meta: {export: True} + getter_name: Watchdog_get_timeout_seconds + setter_name: Watchdog_set_timeout_seconds + summary: The watchdog timeout period. diff --git a/tests/definition/tinymovr_2_3_x.yaml b/tests/definition/tinymovr_2_3_x.yaml new file mode 100644 index 0000000..04c926a --- /dev/null +++ b/tests/definition/tinymovr_2_3_x.yaml @@ -0,0 +1,622 @@ + +name: tm +remote_attributes: + - name: protocol_hash + dtype: uint32 + getter_name: _avlos_get_proto_hash + summary: The Avlos protocol hash. + - name: uid + dtype: uint32 + getter_name: system_get_uid + summary: The unique device ID, unique to each PAC55xx chip produced. + - name: fw_version + dtype: string + getter_name: system_get_fw_version_string + summary: The firmware version. + - name: hw_revision + dtype: uint32 + getter_name: system_get_hw_revision + summary: The hardware revision. + - name: Vbus + dtype: float + unit: volt + meta: {dynamic: True} + getter_name: system_get_Vbus + summary: The measured bus voltage. + - name: Ibus + dtype: float + unit: ampere + meta: {dynamic: True} + getter_name: controller_get_Ibus_est + summary: The estimated bus current. Only estimates current drawn by motor. + - name: power + dtype: float + unit: watt + meta: {dynamic: True} + getter_name: controller_get_power_est + summary: The estimated power. Only estimates power drawn by motor. + - name: temp + dtype: float + unit: degC + meta: {dynamic: True} + getter_name: ADC_get_mcu_temp + summary: The internal temperature of the PAC55xx MCU. + - name: calibrated + dtype: bool + meta: {dynamic: True} + getter_name: system_get_calibrated + summary: Whether the system has been calibrated. + - name: errors + flags: [UNDERVOLTAGE] + meta: {dynamic: True} + getter_name: system_get_errors + summary: Any system errors, as a bitmask + - name: warnings + flags: [DRIVER_FAULT, CHARGE_PUMP_FAULT_STAT, CHARGE_PUMP_FAULT, DRV10_DISABLE, DRV32_DISABLE, DRV54_DISABLE] + meta: {dynamic: True} + getter_name: system_get_warnings + summary: Any system warnings, as a bitmask + - name: save_config + summary: Save configuration to non-volatile memory. + caller_name: nvm_save_config + dtype: void + arguments: [] + - name: erase_config + summary: Erase the config stored in non-volatile memory and reset the device. + caller_name: nvm_erase_and_reset + dtype: void + arguments: [] + meta: {reload_data: True} + - name: reset + summary: Reset the device. + caller_name: system_reset + dtype: void + arguments: [] + meta: {reload_data: True} + - name: enter_dfu + summary: Enter DFU mode. + caller_name: system_enter_dfu + dtype: void + arguments: [] + meta: {reload_data: True} + - name: config_size + summary: Size (in bytes) of the configuration object. + getter_name: system_get_config_size + dtype: uint32 + - name: scheduler + remote_attributes: + - name: load + summary: Processor load in ticks per PWM cycle. + getter_name: scheduler_get_load + meta: {dynamic: True} + dtype: uint32 + - name: warnings + flags: [CONTROL_BLOCK_REENTERED] + meta: {dynamic: True} + getter_name: scheduler_get_warnings + summary: Any scheduler warnings, as a bitmask + - name: controller + remote_attributes: + - name: state + options: [IDLE, CALIBRATE, CL_CONTROL] + meta: {dynamic: True} + getter_name: controller_get_state + setter_name: controller_set_state + summary: The state of the controller. + - name: mode + options: [CURRENT, VELOCITY, POSITION, TRAJECTORY, HOMING] + meta: {dynamic: True} + getter_name: controller_get_mode + setter_name: controller_set_mode + summary: The control mode of the controller. + - name: warnings + meta: {dynamic: True} + flags: [VELOCITY_LIMITED, CURRENT_LIMITED, MODULATION_LIMITED] + getter_name: controller_get_warnings + summary: Any controller warnings, as a bitmask + - name: errors + meta: {dynamic: True} + flags: [CURRENT_LIMIT_EXCEEDED, PRE_CL_I_SD_EXCEEDED] + getter_name: controller_get_errors + summary: Any controller errors, as a bitmask + - name: position + remote_attributes: + - name: setpoint + dtype: float + unit: tick + meta: {jog_step: 100} + getter_name: controller_get_pos_setpoint_user_frame + setter_name: controller_set_pos_setpoint_user_frame + summary: The position setpoint in the user reference frame. + - name: p_gain + dtype: float + meta: {export: True} + getter_name: controller_get_pos_gain + setter_name: controller_set_pos_gain + summary: The proportional gain of the position controller. + - name: velocity + remote_attributes: + - name: setpoint + dtype: float + unit: tick/sec + meta: {jog_step: 200} + getter_name: controller_get_vel_setpoint_user_frame + setter_name: controller_set_vel_setpoint_user_frame + summary: The velocity setpoint in the user reference frame. + - name: limit + dtype: float + unit: tick/sec + meta: {export: True} + getter_name: controller_get_vel_limit + setter_name: controller_set_vel_limit + summary: The velocity limit. + - name: p_gain + dtype: float + meta: {export: True} + getter_name: controller_get_vel_gain + setter_name: controller_set_vel_gain + summary: The proportional gain of the velocity controller. + - name: i_gain + dtype: float + meta: {export: True} + getter_name: controller_get_vel_integral_gain + setter_name: controller_set_vel_integral_gain + summary: The integral gain of the velocity controller. + - name: deadband + dtype: float + unit: tick + meta: {export: True} + getter_name: controller_get_vel_integral_deadband + setter_name: controller_set_vel_integral_deadband + rst_target: integrator-deadband + summary: The deadband of the velocity integrator. A region around the position setpoint where the velocity integrator is not updated. + - name: increment + dtype: float + meta: {export: True} + getter_name: controller_get_vel_increment + setter_name: controller_set_vel_increment + summary: Max velocity setpoint increment (ramping) rate. Set to 0 to disable. + - name: current + remote_attributes: + - name: Iq_setpoint + dtype: float + unit: ampere + meta: {jog_step: 0.005} + getter_name: controller_get_Iq_setpoint_user_frame + setter_name: controller_set_Iq_setpoint_user_frame + summary: The Iq setpoint in the user reference frame. + - name: Id_setpoint + dtype: float + unit: ampere + meta: {dynamic: True} + getter_name: controller_get_Id_setpoint_user_frame + summary: The Id setpoint in the user reference frame. + - name: Iq_limit + dtype: float + unit: ampere + getter_name: controller_get_Iq_limit + setter_name: controller_set_Iq_limit + summary: The Iq limit. + - name: Iq_estimate + dtype: float + unit: ampere + meta: {dynamic: True} + getter_name: controller_get_Iq_estimate_user_frame + summary: The Iq estimate in the user reference frame. + - name: bandwidth + dtype: float + unit: Hz + meta: {export: True} + getter_name: controller_get_I_bw + setter_name: controller_set_I_bw + summary: The current controller bandwidth. + - name: Iq_p_gain + dtype: float + getter_name: controller_get_Iq_gain + summary: The current controller proportional gain. + - name: max_Ibus_regen + dtype: float + unit: ampere + getter_name: controller_get_max_Ibus_regen + setter_name: controller_set_max_Ibus_regen + summary: The max current allowed to be fed back to the power source before flux braking activates. + - name: max_Ibrake + dtype: float + unit: ampere + meta: {export: True} + getter_name: controller_get_max_Ibrake + setter_name: controller_set_max_Ibrake + summary: The max current allowed to be dumped to the motor windings during flux braking. Set to zero to deactivate flux braking. + - name: voltage + remote_attributes: + - name: Vq_setpoint + dtype: float + unit: volt + meta: {dynamic: True} + getter_name: controller_get_Vq_setpoint_user_frame + summary: The Vq setpoint. + - name: calibrate + summary: Calibrate the device. + caller_name: controller_calibrate + dtype: void + arguments: [] + - name: idle + summary: Set idle mode, disabling the driver. + caller_name: controller_idle + dtype: void + arguments: [] + - name: position_mode + summary: Set position control mode. + caller_name: controller_position_mode + dtype: void + arguments: [] + - name: velocity_mode + summary: Set velocity control mode. + caller_name: controller_velocity_mode + dtype: void + arguments: [] + - name: current_mode + summary: Set current control mode. + caller_name: controller_current_mode + dtype: void + arguments: [] + - name: set_pos_vel_setpoints + summary: Set the position and velocity setpoints in the user reference frame in one go, and retrieve the position estimate + caller_name: controller_set_pos_vel_setpoints_user_frame + dtype: float + arguments: + - name: pos_setpoint + dtype: float + unit: tick + - name: vel_setpoint + dtype: float + unit: tick + - name: comms + remote_attributes: + - name: can + remote_attributes: + - name: rate + dtype: uint32 + meta: {export: True} + getter_name: CAN_get_kbit_rate + setter_name: CAN_set_kbit_rate + rst_target: api-can-rate + summary: The baud rate of the CAN interface. + - name: id + dtype: uint32 + meta: {export: True, reload_data: True} + getter_name: CAN_get_ID + setter_name: CAN_set_ID + summary: The ID of the CAN interface. + - name: heartbeat + dtype: bool + getter_name: CAN_get_send_heartbeat + setter_name: CAN_set_send_heartbeat + summary: Toggle sending of heartbeat messages. + - name: motor + remote_attributes: + - name: R + dtype: float + unit: ohm + meta: {dynamic: True, export: True} + getter_name: motor_get_phase_resistance + setter_name: motor_set_phase_resistance + summary: The motor Resistance value. + - name: L + dtype: float + unit: henry + meta: {dynamic: True, export: True} + getter_name: motor_get_phase_inductance + setter_name: motor_set_phase_inductance + summary: The motor Inductance value. + - name: pole_pairs + dtype: uint8 + meta: {dynamic: True, export: True} + getter_name: motor_get_pole_pairs + setter_name: motor_set_pole_pairs + summary: The motor pole pair count. + - name: type + options: [HIGH_CURRENT, GIMBAL] + meta: {export: True} + getter_name: motor_get_is_gimbal + setter_name: motor_set_is_gimbal + summary: The type of the motor. Either high current or gimbal. + - name: calibrated + dtype: bool + meta: {dynamic: True} + getter_name: motor_get_calibrated + summary: Whether the motor has been calibrated. + - name: I_cal + dtype: float + unit: ampere + meta: {export: True} + getter_name: motor_get_I_cal + setter_name: motor_set_I_cal + summary: The calibration current. + - name: errors + flags: [PHASE_RESISTANCE_OUT_OF_RANGE, PHASE_INDUCTANCE_OUT_OF_RANGE, POLE_PAIRS_CALCULATION_DID_NOT_CONVERGE, POLE_PAIRS_OUT_OF_RANGE, ABNORMAL_CALIBRATION_VOLTAGE] + meta: {dynamic: True} + getter_name: motor_get_errors + summary: Any motor/calibration errors, as a bitmask + - name: sensors + remote_attributes: + - name: user_frame + remote_attributes: + - name: position_estimate + dtype: float + unit: ticks + meta: {dynamic: True} + getter_name: user_frame_get_pos_estimate + summary: The filtered position estimate in the user reference frame. + - name: velocity_estimate + dtype: float + unit: ticks/second + meta: {dynamic: True} + getter_name: user_frame_get_vel_estimate + summary: The filtered velocity estimate in the user reference frame. + - name: offset + dtype: float + unit: ticks + getter_name: frame_user_to_position_sensor_get_offset + setter_name: frame_user_to_position_sensor_set_offset + summary: The user defined offset. + - name: multiplier + dtype: float + getter_name: frame_user_to_position_sensor_get_multiplier + setter_name: frame_user_to_position_sensor_set_multiplier + summary: The user defined multipler. + - name: setup + remote_attributes: + - name: onboard + remote_attributes: + - name: calibrated + dtype: bool + meta: {dynamic: True} + getter_name: sensor_onboard_get_is_calibrated + summary: Whether the sensor has been calibrated. + - name: errors + flags: [CALIBRATION_FAILED, READING_UNSTABLE] + meta: {dynamic: True} + getter_name: sensor_onboard_get_errors + summary: Any sensor errors, as a bitmask + - name: external_spi + remote_attributes: + - name: type + options: [MA7XX, AS5047, AMT22] + meta: {export: True} + getter_name: sensor_external_spi_get_type_avlos + setter_name: sensor_external_spi_set_type_avlos + summary: The type of the external sensor. + - name: rate + options: [1_5Mbps, 3Mbps, 6Mbps, 8Mbps, 12Mbps] + meta: {export: True, dynamic: True} + getter_name: sensor_external_spi_get_rate_avlos + setter_name: sensor_external_spi_set_rate_avlos + summary: The rate of the external sensor. + - name: calibrated + dtype: bool + meta: {dynamic: True} + getter_name: sensor_external_spi_get_is_calibrated + summary: Whether the sensor has been calibrated. + - name: errors + flags: [CALIBRATION_FAILED, READING_UNSTABLE] + meta: {dynamic: True} + getter_name: sensor_external_spi_get_errors + summary: Any sensor errors, as a bitmask + - name: hall + remote_attributes: + - name: calibrated + dtype: bool + meta: {dynamic: True} + getter_name: sensor_hall_get_is_calibrated + summary: Whether the sensor has been calibrated. + - name: errors + flags: [CALIBRATION_FAILED, READING_UNSTABLE] + meta: {dynamic: True} + getter_name: sensor_hall_get_errors + summary: Any sensor errors, as a bitmask + - name: select + remote_attributes: + - name: position_sensor + remote_attributes: + - name: connection + options: [ONBOARD, EXTERNAL_SPI, HALL] + meta: {export: True} + getter_name: position_sensor_get_connection + setter_name: position_sensor_set_connection + summary: The position sensor connection. Either ONBOARD, EXTERNAL_SPI or HALL. + - name: bandwidth + dtype: float + unit: Hz + meta: {export: True} + getter_name: position_observer_get_bandwidth + setter_name: position_observer_set_bandwidth + summary: The position sensor observer bandwidth. + - name: raw_angle + dtype: int32 + meta: {dynamic: True} + getter_name: sensor_position_get_raw_angle + summary: The raw position sensor angle. + - name: position_estimate + dtype: float + unit: ticks + meta: {dynamic: True} + getter_name: position_observer_get_pos_estimate + summary: The filtered position estimate in the position sensor reference frame. + - name: velocity_estimate + dtype: float + unit: ticks/second + meta: {dynamic: True} + getter_name: position_observer_get_vel_estimate + summary: The filtered velocity estimate in the position sensor reference frame. + - name: commutation_sensor + remote_attributes: + - name: connection + options: [ONBOARD, EXTERNAL_SPI, HALL] + meta: {export: True} + getter_name: commutation_sensor_get_connection + setter_name: commutation_sensor_set_connection + summary: The commutation sensor connection. Either ONBOARD, EXTERNAL_SPI or HALL. + - name: bandwidth + dtype: float + unit: Hz + meta: {export: True} + getter_name: commutation_observer_get_bandwidth + setter_name: commutation_observer_set_bandwidth + summary: The commutation sensor observer bandwidth. + - name: raw_angle + dtype: int32 + meta: {dynamic: True} + getter_name: sensor_commutation_get_raw_angle + summary: The raw commutation sensor angle. + - name: position_estimate + dtype: float + unit: ticks + meta: {dynamic: True} + getter_name: commutation_observer_get_pos_estimate + summary: The filtered position estimate in the commutation sensor reference frame. + - name: velocity_estimate + dtype: float + unit: ticks/second + meta: {dynamic: True} + getter_name: commutation_observer_get_vel_estimate + summary: The filtered velocity estimate in the commutation sensor reference frame. + - name: traj_planner + remote_attributes: + - name: max_accel + dtype: float + unit: ticks/s + meta: {export: True} + getter_name: planner_get_max_accel + setter_name: planner_set_max_accel + summary: The max allowed acceleration of the generated trajectory. + - name: max_decel + dtype: float + unit: ticks/second/second + meta: {export: True} + getter_name: planner_get_max_decel + setter_name: planner_set_max_decel + summary: The max allowed deceleration of the generated trajectory. + - name: max_vel + dtype: float + unit: ticks/second + meta: {export: True} + getter_name: planner_get_max_vel + setter_name: planner_set_max_vel + summary: The max allowed cruise velocity of the generated trajectory. + - name: t_accel + dtype: float + unit: second + meta: {export: True} + getter_name: planner_get_deltat_accel + setter_name: planner_set_deltat_accel + summary: In time mode, the acceleration time of the generated trajectory. + - name: t_decel + dtype: float + unit: second + meta: {export: True} + getter_name: planner_get_deltat_decel + setter_name: planner_set_deltat_decel + summary: In time mode, the deceleration time of the generated trajectory. + - name: t_total + dtype: float + unit: second + meta: {export: True} + getter_name: planner_get_deltat_total + setter_name: planner_set_deltat_total + summary: In time mode, the total time of the generated trajectory. + - name: move_to + summary: Move to target position in the user reference frame respecting velocity and acceleration limits. + caller_name: planner_move_to_vlimit + dtype: void + arguments: + - name: pos_setpoint + dtype: float + unit: tick + - name: move_to_tlimit + summary: Move to target position in the user reference frame respecting time limits for each sector. + caller_name: planner_move_to_tlimit + dtype: void + arguments: + - name: pos_setpoint + dtype: float + unit: tick + - name: errors + flags: [INVALID_INPUT, VCRUISE_OVER_LIMIT] + getter_name: planner_get_errors + summary: Any errors in the trajectory planner, as a bitmask + - name: homing + remote_attributes: + - name: velocity + dtype: float + unit: ticks/s + meta: {export: True} + getter_name: homing_planner_get_homing_velocity + setter_name: homing_planner_set_homing_velocity + summary: The velocity at which the motor performs homing. + - name: max_homing_t + dtype: float + unit: s + meta: {export: True} + getter_name: homing_planner_get_max_homing_t + setter_name: homing_planner_set_max_homing_t + summary: The maximum time the motor is allowed to travel before homing times out and aborts. + - name: retract_dist + dtype: float + unit: ticks + meta: {export: True} + getter_name: homing_planner_get_retract_distance + setter_name: homing_planner_set_retract_distance + summary: The retraction distance the motor travels after the endstop has been found. + - name: warnings + meta: {dynamic: True} + flags: [HOMING_TIMEOUT] + getter_name: homing_planner_get_warnings + summary: Any homing warnings, as a bitmask + - name: stall_detect + remote_attributes: + - name: velocity + dtype: float + unit: ticks/s + meta: {export: True} + getter_name: homing_planner_get_max_stall_vel + setter_name: homing_planner_set_max_stall_vel + summary: The velocity below which (and together with `stall_detect.delta_pos`) stall detection mode is triggered. + - name: delta_pos + dtype: float + unit: ticks + meta: {export: True} + getter_name: homing_planner_get_max_stall_delta_pos + setter_name: homing_planner_set_max_stall_delta_pos + summary: The velocity below which (and together with `stall_detect.delta_pos`) stall detection mode is triggered. + - name: t + dtype: float + unit: s + meta: {export: True} + getter_name: homing_planner_get_max_stall_t + setter_name: homing_planner_set_max_stall_t + summary: The time to remain in stall detection mode before the motor is considered stalled. + - name: home + summary: Perform the homing operation. + caller_name: homing_planner_home + dtype: void + arguments: [] + - name: watchdog + remote_attributes: + - name: enabled + dtype: bool + getter_name: Watchdog_get_enabled + setter_name: Watchdog_set_enabled + summary: Whether the watchdog is enabled or not. + - name: triggered + dtype: bool + meta: {dynamic: True} + getter_name: Watchdog_triggered + summary: Whether the watchdog has been triggered or not. + - name: timeout + dtype: float + unit: s + meta: {export: True} + getter_name: Watchdog_get_timeout_seconds + setter_name: Watchdog_set_timeout_seconds + summary: The watchdog timeout period. diff --git a/tests/test_metadata_generation.py b/tests/test_metadata_generation.py new file mode 100644 index 0000000..46f657c --- /dev/null +++ b/tests/test_metadata_generation.py @@ -0,0 +1,107 @@ +""" +Tests for endpoint metadata C code generation (avlos_endpoint_metadata.h / .c). + +The metadata table describes each endpoint's kind, value_dtype, and (for callables) +num_args and arg_dtypes, so firmware can do type-aware UART ASCII parsing without +a hand-maintained table. +""" + +import importlib.resources +import os +import unittest + +import yaml + +from avlos.deserializer import deserialize +from avlos.generators import generator_c +from avlos.generators.filters import avlos_endpoints + + +class TestMetadataGeneration(unittest.TestCase): + """Test that generator_c emits endpoint metadata when paths are configured.""" + + def setUp(self): + """Load good_device spec and set up output paths.""" + def_path = str(importlib.resources.files("tests").joinpath("definition/good_device.yaml")) + with open(def_path) as f: + self.device = deserialize(yaml.safe_load(f)) + self.out_dir = str(importlib.resources.files("tests").joinpath("outputs")) + self.meta_header = os.path.join(self.out_dir, "avlos_endpoint_metadata_test.h") + self.meta_impl = os.path.join(self.out_dir, "avlos_endpoint_metadata_test.c") + + def _config_with_metadata(self): + """Config that includes optional metadata output paths.""" + return { + "paths": { + "output_enums": os.path.join(self.out_dir, "metadata_test_enums.h"), + "output_header": os.path.join(self.out_dir, "metadata_test_header.h"), + "output_impl": os.path.join(self.out_dir, "metadata_test_impl.c"), + "output_metadata_header": self.meta_header, + "output_metadata_impl": self.meta_impl, + }, + } + + def test_metadata_files_generated_when_paths_set(self): + """When both output_metadata_header and output_metadata_impl are set, both files are created.""" + config = self._config_with_metadata() + generator_c.process(self.device, config) + self.assertTrue(os.path.exists(self.meta_header), "Metadata header should be created") + self.assertTrue(os.path.exists(self.meta_impl), "Metadata implementation should be created") + + def test_metadata_count_matches_endpoint_count(self): + """avlos_endpoint_meta_count in generated C must match the number of endpoints.""" + config = self._config_with_metadata() + generator_c.process(self.device, config) + expected_count = len(avlos_endpoints(self.device)) + with open(self.meta_impl) as f: + content = f.read() + # Count variable and sizeof expression (clang-format may wrap the line) + self.assertIn("avlos_endpoint_meta_count", content) + self.assertIn("sizeof(avlos_endpoint_meta)", content) + self.assertIn("sizeof(avlos_endpoint_meta) / sizeof(avlos_endpoint_meta[0])", content) + + def test_metadata_read_only_uint32(self): + """A read-only uint32 endpoint (e.g. sn) has READ_ONLY kind and UINT32 value_dtype.""" + config = self._config_with_metadata() + generator_c.process(self.device, config) + with open(self.meta_impl) as f: + content = f.read() + self.assertIn("AVLOS_EP_KIND_READ_ONLY", content) + self.assertIn("AVLOS_DTYPE_UINT32", content) + # good_device has 'sn' as first endpoint: read-only uint32 (full_name is "sn" at root) + self.assertIn("avlos_sn", content) + + def test_metadata_call_with_args(self): + """A callable with two float args (e.g. set_pos_vel_setpoints) has CALL_WITH_ARGS and arg_dtypes.""" + config = self._config_with_metadata() + generator_c.process(self.device, config) + with open(self.meta_impl) as f: + content = f.read() + self.assertIn("AVLOS_EP_KIND_CALL_WITH_ARGS", content) + self.assertIn("avlos_controller_set_pos_vel_setpoints", content) + # Should have two AVLOS_DTYPE_FLOAT for the two float arguments + self.assertIn("AVLOS_DTYPE_FLOAT", content) + # num_args = 2 for this endpoint + self.assertIn(".num_args = 2", content) + + def test_metadata_not_required(self): + """Generator runs successfully without metadata paths (backward compatibility).""" + # Use a path we never pass to the generator; without metadata keys it must not be created + never_passed_meta_header = os.path.join(self.out_dir, "avlos_metadata_absent_test.h") + never_passed_meta_impl = os.path.join(self.out_dir, "avlos_metadata_absent_test.c") + config = { + "paths": { + "output_enums": os.path.join(self.out_dir, "no_meta_enums.h"), + "output_header": os.path.join(self.out_dir, "no_meta_header.h"), + "output_impl": os.path.join(self.out_dir, "no_meta_impl.c"), + }, + } + generator_c.process(self.device, config) + self.assertFalse( + os.path.exists(never_passed_meta_header), + "Metadata header must not be created when paths are not in config", + ) + self.assertFalse( + os.path.exists(never_passed_meta_impl), + "Metadata impl must not be created when paths are not in config", + ) diff --git a/tests/test_tinymovr_spec.py b/tests/test_tinymovr_spec.py new file mode 100644 index 0000000..23e4ae1 --- /dev/null +++ b/tests/test_tinymovr_spec.py @@ -0,0 +1,478 @@ +""" +Tests for Tinymovr specification parsing and code generation. +""" + +import importlib.resources +import os +import unittest + +import yaml + +from avlos.datatypes import DataType +from avlos.definitions.remote_attribute import RemoteAttribute +from avlos.definitions.remote_bitmask import RemoteBitmask +from avlos.definitions.remote_enum import RemoteEnum +from avlos.definitions.remote_function import RemoteFunction +from avlos.deserializer import deserialize +from avlos.generators import generator_c, generator_cpp + + +class TestTinymovr_Parsing(unittest.TestCase): + """Test parsing of Tinymovr specification.""" + + @classmethod + def setUpClass(cls): + """Load the Tinymovr specification once for all tests.""" + def_path_str = str(importlib.resources.files("tests").joinpath("definition/tinymovr_2_3_x.yaml")) + + with open(def_path_str) as device_desc_stream: + cls.device = deserialize(yaml.safe_load(device_desc_stream)) + + def test_device_loaded(self): + """Test that device specification loads successfully.""" + self.assertIsNotNone(self.device) + self.assertEqual(self.device.name, "tm") + + def test_root_level_attributes(self): + """Test that root-level attributes are parsed correctly.""" + attr_names = [attr.name for attr in self.device.remote_attributes.values()] + + # Test for specific root attributes + self.assertIn("protocol_hash", attr_names) + self.assertIn("uid", attr_names) + self.assertIn("fw_version", attr_names) + self.assertIn("Vbus", attr_names) + self.assertIn("temp", attr_names) + self.assertIn("calibrated", attr_names) + + def test_nested_attributes(self): + """Test that nested remote_attributes are parsed correctly.""" + # Find scheduler attribute + scheduler = next((attr for attr in self.device.remote_attributes.values() if attr.name == "scheduler"), None) + self.assertIsNotNone(scheduler) + + # Check it has nested attributes + self.assertTrue(hasattr(scheduler, "remote_attributes")) + self.assertGreater(len(scheduler.remote_attributes), 0) + + # Check nested attribute names + nested_names = [attr.name for attr in scheduler.remote_attributes.values()] + self.assertIn("load", nested_names) + self.assertIn("warnings", nested_names) + + def test_deeply_nested_attributes(self): + """Test that deeply nested attributes (3+ levels) are parsed correctly.""" + # Find controller.position.setpoint (3 levels deep) + controller = next((attr for attr in self.device.remote_attributes.values() if attr.name == "controller"), None) + self.assertIsNotNone(controller) + + position = next((attr for attr in controller.remote_attributes.values() if attr.name == "position"), None) + self.assertIsNotNone(position) + + nested_names = [attr.name for attr in position.remote_attributes.values()] + self.assertIn("setpoint", nested_names) + self.assertIn("p_gain", nested_names) + + def test_data_types(self): + """Test that various data types are parsed correctly.""" + # uint32 + uid_attr = next((attr for attr in self.device.remote_attributes.values() if attr.name == "uid"), None) + self.assertEqual(uid_attr.dtype, DataType.UINT32) + + # float + vbus_attr = next((attr for attr in self.device.remote_attributes.values() if attr.name == "Vbus"), None) + self.assertEqual(vbus_attr.dtype, DataType.FLOAT) + + # bool + calibrated_attr = next((attr for attr in self.device.remote_attributes.values() if attr.name == "calibrated"), None) + self.assertEqual(calibrated_attr.dtype, DataType.BOOL) + + # string + fw_version_attr = next((attr for attr in self.device.remote_attributes.values() if attr.name == "fw_version"), None) + self.assertEqual(fw_version_attr.dtype, DataType.STR) + + def test_getter_setter_names(self): + """Test that getter and setter names are parsed correctly.""" + # Attribute with only getter + uid_attr = next((attr for attr in self.device.remote_attributes.values() if attr.name == "uid"), None) + self.assertEqual(uid_attr.getter_name, "system_get_uid") + self.assertIsNone(uid_attr.setter_name) + + # Attribute with both getter and setter (need to find one in controller) + controller = next((attr for attr in self.device.remote_attributes.values() if attr.name == "controller"), None) + state_attr = next((attr for attr in controller.remote_attributes.values() if attr.name == "state"), None) + self.assertEqual(state_attr.getter_name, "controller_get_state") + self.assertEqual(state_attr.setter_name, "controller_set_state") + + def test_enum_attributes(self): + """Test that attributes with options (enums) are parsed correctly.""" + controller = next((attr for attr in self.device.remote_attributes.values() if attr.name == "controller"), None) + + # Test controller.state enum + state_attr = next((attr for attr in controller.remote_attributes.values() if attr.name == "state"), None) + self.assertIsInstance(state_attr, RemoteEnum) + member_names = [m.name for m in state_attr.options] + self.assertIn("IDLE", member_names) + self.assertIn("CALIBRATE", member_names) + self.assertIn("CL_CONTROL", member_names) + + # Test controller.mode enum + mode_attr = next((attr for attr in controller.remote_attributes.values() if attr.name == "mode"), None) + self.assertIsInstance(mode_attr, RemoteEnum) + member_names = [m.name for m in mode_attr.options] + self.assertIn("CURRENT", member_names) + self.assertIn("VELOCITY", member_names) + self.assertIn("POSITION", member_names) + self.assertIn("TRAJECTORY", member_names) + self.assertIn("HOMING", member_names) + + def test_bitmask_attributes(self): + """Test that attributes with flags (bitmasks) are parsed correctly.""" + # Test root-level errors bitmask + errors_attr = next((attr for attr in self.device.remote_attributes.values() if attr.name == "errors"), None) + self.assertIsInstance(errors_attr, RemoteBitmask) + self.assertIn("UNDERVOLTAGE", errors_attr.bitmask.__members__) + + # Test warnings bitmask + warnings_attr = next((attr for attr in self.device.remote_attributes.values() if attr.name == "warnings"), None) + self.assertIsInstance(warnings_attr, RemoteBitmask) + self.assertIn("DRIVER_FAULT", warnings_attr.bitmask.__members__) + self.assertIn("CHARGE_PUMP_FAULT_STAT", warnings_attr.bitmask.__members__) + + def test_function_attributes(self): + """Test that function attributes are parsed correctly.""" + # Test void function without arguments + reset_func = next((attr for attr in self.device.remote_attributes.values() if attr.name == "reset"), None) + self.assertIsInstance(reset_func, RemoteFunction) + self.assertEqual(reset_func.dtype, DataType.VOID) + self.assertEqual(reset_func.caller_name, "system_reset") + self.assertEqual(len(reset_func.arguments), 0) + + def test_function_with_return_value(self): + """Test that functions with return values are parsed correctly.""" + controller = next((attr for attr in self.device.remote_attributes.values() if attr.name == "controller"), None) + + # Test set_pos_vel_setpoints function (returns float) + func = next((attr for attr in controller.remote_attributes.values() if attr.name == "set_pos_vel_setpoints"), None) + self.assertIsInstance(func, RemoteFunction) + self.assertEqual(func.dtype, DataType.FLOAT) + self.assertEqual(func.caller_name, "controller_set_pos_vel_setpoints_user_frame") + + def test_function_with_arguments(self): + """Test that functions with arguments are parsed correctly.""" + traj_planner = next((attr for attr in self.device.remote_attributes.values() if attr.name == "traj_planner"), None) + + # Test move_to function (has 1 argument) + move_to_func = next((attr for attr in traj_planner.remote_attributes.values() if attr.name == "move_to"), None) + self.assertIsInstance(move_to_func, RemoteFunction) + self.assertEqual(len(move_to_func.arguments), 1) + self.assertEqual(move_to_func.arguments[0].name, "pos_setpoint") + self.assertEqual(move_to_func.arguments[0].dtype, DataType.FLOAT) + + # Test set_pos_vel_setpoints function (has 2 arguments) + controller = next((attr for attr in self.device.remote_attributes.values() if attr.name == "controller"), None) + func = next((attr for attr in controller.remote_attributes.values() if attr.name == "set_pos_vel_setpoints"), None) + self.assertEqual(len(func.arguments), 2) + self.assertEqual(func.arguments[0].name, "pos_setpoint") + self.assertEqual(func.arguments[1].name, "vel_setpoint") + + def test_units(self): + """Test that units are parsed correctly.""" + # Test volt unit + vbus_attr = next((attr for attr in self.device.remote_attributes.values() if attr.name == "Vbus"), None) + self.assertEqual(str(vbus_attr.unit), "volt") + + # Test ampere unit + ibus_attr = next((attr for attr in self.device.remote_attributes.values() if attr.name == "Ibus"), None) + self.assertEqual(str(ibus_attr.unit), "ampere") + + # Test degC unit + temp_attr = next((attr for attr in self.device.remote_attributes.values() if attr.name == "temp"), None) + self.assertEqual(str(temp_attr.unit), "degree_Celsius") + + def test_metadata(self): + """Test that metadata is parsed correctly.""" + # Test dynamic flag + vbus_attr = next((attr for attr in self.device.remote_attributes.values() if attr.name == "Vbus"), None) + self.assertIsNotNone(vbus_attr.meta) + self.assertTrue(vbus_attr.meta.get("dynamic", False)) + + # Test export flag + controller = next((attr for attr in self.device.remote_attributes.values() if attr.name == "controller"), None) + position = next((attr for attr in controller.remote_attributes.values() if attr.name == "position"), None) + p_gain_attr = next((attr for attr in position.remote_attributes.values() if attr.name == "p_gain"), None) + self.assertTrue(p_gain_attr.meta.get("export", False)) + + # Test reload_data flag + reset_func = next((attr for attr in self.device.remote_attributes.values() if attr.name == "reset"), None) + self.assertTrue(reset_func.meta.get("reload_data", False)) + + # Test jog_step metadata + setpoint_attr = next((attr for attr in position.remote_attributes.values() if attr.name == "setpoint"), None) + self.assertEqual(setpoint_attr.meta.get("jog_step"), 100) + + def test_attribute_count(self): + """Test that we have the expected number of attributes at various levels.""" + # Root level should have a significant number of attributes + self.assertGreater(len(self.device.remote_attributes), 10) + + # Controller should have many nested attributes + controller = next((attr for attr in self.device.remote_attributes.values() if attr.name == "controller"), None) + self.assertGreater(len(controller.remote_attributes), 10) + + # Sensors should have complex nesting + sensors = next((attr for attr in self.device.remote_attributes.values() if attr.name == "sensors"), None) + self.assertIsNotNone(sensors) + self.assertGreater(len(sensors.remote_attributes), 2) + + def test_full_name_generation(self): + """Test that full names are generated correctly for nested attributes.""" + controller = next((attr for attr in self.device.remote_attributes.values() if attr.name == "controller"), None) + position = next((attr for attr in controller.remote_attributes.values() if attr.name == "position"), None) + setpoint_attr = next((attr for attr in position.remote_attributes.values() if attr.name == "setpoint"), None) + + # Full name should be dot-separated + self.assertEqual(setpoint_attr.full_name, "controller.position.setpoint") + + def test_endpoint_function_name_generation(self): + """Test that endpoint function names are generated correctly.""" + controller = next((attr for attr in self.device.remote_attributes.values() if attr.name == "controller"), None) + position = next((attr for attr in controller.remote_attributes.values() if attr.name == "position"), None) + setpoint_attr = next((attr for attr in position.remote_attributes.values() if attr.name == "setpoint"), None) + + # Endpoint function name should be avlos_ + full_name with underscores + self.assertEqual(setpoint_attr.endpoint_function_name, "avlos_controller_position_setpoint") + + +class TestTinymovr_CodeGeneration(unittest.TestCase): + """Test code generation from Tinymovr specification.""" + + @classmethod + def setUpClass(cls): + """Load the Tinymovr specification once for all tests.""" + def_path_str = str(importlib.resources.files("tests").joinpath("definition/tinymovr_2_3_x.yaml")) + + with open(def_path_str) as device_desc_stream: + cls.device = deserialize(yaml.safe_load(device_desc_stream)) + + def test_c_generation_succeeds(self): + """Test that C code generation completes without errors.""" + output_impl = str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test.c")) + + config = { + "hash_string": "0xTINYMOVR", + "paths": { + "output_enums": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_enum.h")), + "output_header": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_header.h")), + "output_impl": output_impl, + }, + } + + # Should not raise any exceptions + generator_c.process(self.device, config) + + # Verify files were created + self.assertTrue(os.path.exists(output_impl)) + self.assertTrue(os.path.exists(config["paths"]["output_enums"])) + self.assertTrue(os.path.exists(config["paths"]["output_header"])) + + def test_generated_c_contains_endpoint_functions(self): + """Test that generated C code contains endpoint functions.""" + output_impl = str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test.c")) + + config = { + "hash_string": "0xTINYMOVR", + "paths": { + "output_enums": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_enum.h")), + "output_header": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_header.h")), + "output_impl": output_impl, + }, + } + + generator_c.process(self.device, config) + + with open(output_impl) as f: + content = f.read() + + # Check for root-level endpoint functions + self.assertIn("avlos_protocol_hash", content) + self.assertIn("avlos_uid", content) + self.assertIn("avlos_fw_version", content) + + # Check for nested endpoint functions + self.assertIn("avlos_controller_state", content) + self.assertIn("avlos_controller_position_setpoint", content) + + # Check for function endpoints + self.assertIn("avlos_reset", content) + self.assertIn("avlos_controller_calibrate", content) + + def test_generated_c_contains_enums(self): + """Test that generated C code contains enum definitions.""" + output_enum = str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_enum.h")) + + config = { + "hash_string": "0xTINYMOVR", + "paths": { + "output_enums": output_enum, + "output_header": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_header.h")), + "output_impl": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test.c")), + }, + } + + generator_c.process(self.device, config) + + with open(output_enum) as f: + content = f.read() + + # Check for enum type definitions + self.assertIn("typedef enum", content) + + # Check for specific enum values + self.assertIn("CONTROLLER_STATE_IDLE", content) + self.assertIn("CONTROLLER_STATE_CALIBRATE", content) + self.assertIn("CONTROLLER_MODE_CURRENT", content) + self.assertIn("CONTROLLER_MODE_VELOCITY", content) + + def test_generated_c_contains_bitmasks(self): + """Test that generated C code contains bitmask definitions.""" + output_enum = str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_enum.h")) + + config = { + "hash_string": "0xTINYMOVR", + "paths": { + "output_enums": output_enum, + "output_header": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_header.h")), + "output_impl": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test.c")), + }, + } + + generator_c.process(self.device, config) + + with open(output_enum) as f: + content = f.read() + + # Check for bitmask definitions + self.assertIn("ERRORS_UNDERVOLTAGE", content) + self.assertIn("WARNINGS_DRIVER_FAULT", content) + + def test_generated_c_contains_string_helpers(self): + """Test that generated C code contains string helper functions.""" + output_impl = str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test.c")) + + config = { + "hash_string": "0xTINYMOVR", + "paths": { + "output_enums": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_enum.h")), + "output_header": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_header.h")), + "output_impl": output_impl, + }, + } + + generator_c.process(self.device, config) + + with open(output_impl) as f: + content = f.read() + + # Should contain string helper functions (since fw_version is string type) + self.assertIn("_avlos_getter_string", content) + + def test_generated_c_endpoint_array(self): + """Test that generated C code contains the endpoint array.""" + output_impl = str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test.c")) + + config = { + "hash_string": "0xTINYMOVR", + "paths": { + "output_enums": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_enum.h")), + "output_header": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_header.h")), + "output_impl": output_impl, + }, + } + + generator_c.process(self.device, config) + + with open(output_impl) as f: + content = f.read() + + # Should have endpoint array + self.assertIn("avlos_endpoints[", content) + + # Should have proto hash function + self.assertIn("_avlos_get_proto_hash", content) + + def test_cpp_generation_succeeds(self): + """Test that C++ code generation completes without errors.""" + config = { + "hash_string": "0xTINYMOVR", + "paths": { + "output_helpers": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_helpers.hpp")), + "output_header": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test.hpp")), + "output_impl": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test.cpp")), + }, + } + + # Should not raise any exceptions + generator_cpp.process(self.device, config) + + # Verify files were created + self.assertTrue(os.path.exists(config["paths"]["output_helpers"])) + self.assertTrue(os.path.exists(config["paths"]["output_header"])) + self.assertTrue(os.path.exists(config["paths"]["output_impl"])) + + def test_generated_cpp_contains_classes(self): + """Test that generated C++ code contains class definitions.""" + output_header = str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test.hpp")) + + config = { + "hash_string": "0xTINYMOVR", + "paths": { + "output_helpers": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test_helpers.hpp")), + "output_header": output_header, + "output_impl": str(importlib.resources.files("tests").joinpath("outputs/tinymovr_test.cpp")), + }, + } + + generator_cpp.process(self.device, config) + + with open(output_header) as f: + content = f.read() + + # Should contain class definitions + self.assertIn("class", content) + + def test_attribute_index_generation(self): + """Test that attribute indices are generated correctly.""" + + # Get all attributes as a flat list (including nested groups) + def get_all_attrs(obj, attrs_list=None): + if attrs_list is None: + attrs_list = [] + for attr in obj.remote_attributes.values(): + # Add this attribute to the list + attrs_list.append(attr) + # If it has nested attributes, recursively get those too + if hasattr(attr, "remote_attributes"): + get_all_attrs(attr, attrs_list) + return attrs_list + + all_attrs = get_all_attrs(self.device) + + # Each attribute should have an ep_id + ep_ids = [attr.ep_id for attr in all_attrs if hasattr(attr, "ep_id") and attr.ep_id >= 0] + + # Should have many endpoints (Tinymovr has over 70 endpoints) + self.assertGreater(len(ep_ids), 70) + + # All ep_ids should be unique + self.assertEqual(len(ep_ids), len(set(ep_ids))) + + # ep_ids should start from 0 + self.assertEqual(min(ep_ids), 0) + + # ep_ids should be consecutive + self.assertEqual(max(ep_ids), len(ep_ids) - 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_validation.py b/tests/test_validation.py index 8bc1f14..430933f 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -82,8 +82,11 @@ def test_long_identifier_warning(self): """Test that very long identifiers generate warnings.""" # 64+ character identifier (C99 requires at least 63 significant chars) long_name = "a" * 70 - # Should not raise, but might print warning - validate_c_identifier(long_name) + # Should not raise; warning is logged (captured here so it does not print) + with self.assertLogs("avlos", level="WARNING") as cm: + validate_c_identifier(long_name) + self.assertIn("very long", cm.output[0]) + self.assertIn("63 characters", cm.output[0]) def test_valid_device_passes_all_validation(self): """Test that good_device.yaml passes all validations."""