Initialize template action
This commit is contained in:
commit
47a2a1419b
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
FROM python:3.11-bookworm
|
||||
|
||||
COPY requirements.txt /requirements.txt
|
||||
COPY generate_wireviz_template.py /generate_wireviz_template.py
|
||||
|
||||
RUN pip install -r /requirements.txt
|
||||
|
||||
ENTRYPOINT [ "/generate_wireviz_template.py" ]
|
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 AllSpice
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
66
README.md
Normal file
66
README.md
Normal file
@ -0,0 +1,66 @@
|
||||
# Generate WireViz Template for a Multiboard Assembly
|
||||
|
||||
Generate a WireViz template YAML file for a multiboard assembly using
|
||||
[AllSpice Actions](https://learn.allspice.io/docs/actions-cicd).
|
||||
The template file includes all the connectors and their pins, but does NOT
|
||||
include any wires or connections.
|
||||
|
||||
## Usage
|
||||
|
||||
Add the following step to your actions:
|
||||
|
||||
```yaml
|
||||
- name: Generate WireViz Template
|
||||
uses: https://hub.allspice.io/Actions/generate-wireviz-template@v0.1
|
||||
with:
|
||||
# The path to the assembly YAML file
|
||||
assembly_file_path: path/to/assembly.yaml
|
||||
# The variant of the assembly to generate the template for (optional)
|
||||
variant: ""
|
||||
# The name of the output file (optional)
|
||||
output_file: wireviz.yaml
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- This action generates a WireViz template YAML file based on a multiboard
|
||||
assembly configuration. You can then edit the template to set up the wiring
|
||||
connections between the boards, and use the wireviz tool to generate a
|
||||
visualization.
|
||||
- You do not need to check out the repository for this action to work.
|
||||
|
||||
### Customizing the Template Generation
|
||||
|
||||
1. `assembly_file_path`: Path to the assembly YAML file. If not specified,
|
||||
"assembly.yaml" at the root of the repository is used.
|
||||
2. `variant`: Specific variant of the assembly to generate the template for. If
|
||||
not specified, the default variant is used.
|
||||
3. `output_file`: Name of the output file for the generated template. If not
|
||||
specified, it will be saved as "wireviz.yaml" in the root.
|
||||
|
||||
Example with custom inputs:
|
||||
|
||||
```yaml
|
||||
- name: Generate WireViz Template
|
||||
uses: https://hub.allspice.io/Actions/generate-wireviz-template@v0.1
|
||||
with:
|
||||
assembly_file_path: configs/my_assembly.yaml
|
||||
variant: Double
|
||||
output_file: my_wireviz_template.yaml
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
If you encounter any issues or need more detailed information about the
|
||||
action's execution, you can set the `log_level` input to 'DEBUG' for more
|
||||
verbose logging:
|
||||
|
||||
```yaml
|
||||
- name: Generate WireViz Template
|
||||
uses: https://hub.allspice.io/Actions/generate-wireviz-template@v0.1
|
||||
with:
|
||||
assembly_file_path: path/to/assembly.yaml
|
||||
log_level: DEBUG
|
||||
```
|
||||
|
||||
For more information about AllSpice Actions and how to use them in your workflows, please refer to the [AllSpice Documentation](https://learn.allspice.io/docs/actions-cicd).
|
45
action.yml
Normal file
45
action.yml
Normal file
@ -0,0 +1,45 @@
|
||||
name: "Generate WireViz template for a Multiboard Assembly"
|
||||
|
||||
description: >
|
||||
Generate a WireViz template YAML file for a multiboard assembly.
|
||||
The template file includes all the connectors and their pins, but does NOT
|
||||
include any wires or connections.
|
||||
|
||||
inputs:
|
||||
assembly_file_path:
|
||||
description: The path to the assembly YAML file.
|
||||
required: false
|
||||
default: "assembly.yaml"
|
||||
variant:
|
||||
description: The variant of the assembly to generate the template for.
|
||||
required: false
|
||||
default: ""
|
||||
output_file:
|
||||
description: The name of the file to output.
|
||||
required: false
|
||||
default: "wireviz.yaml"
|
||||
log_level:
|
||||
description: The log level used by the action. Used for debugging.
|
||||
required: false
|
||||
default: "INFO"
|
||||
|
||||
runs:
|
||||
using: "docker"
|
||||
image: "Dockerfile"
|
||||
args:
|
||||
- "--assembly-file"
|
||||
- "${{ github.workspace}}/${{ inputs.assembly_file_path }}"
|
||||
- "--variant"
|
||||
- ${{ inputs.variant }}
|
||||
- "--output-file"
|
||||
- "${{ github.workspace}}/${{ inputs.output_file }}"
|
||||
- "--repository"
|
||||
- ${{ github.repository }}
|
||||
- "--ref"
|
||||
- ${{ github.ref }}
|
||||
- "--allspice-hub-url"
|
||||
- ${{ github.server_url }}
|
||||
- "--log-level"
|
||||
- ${{ inputs.log_level }}
|
||||
env:
|
||||
ALLSPICE_AUTH_TOKEN: ${{ github.token }}
|
325
generate_wireviz_template.py
Executable file
325
generate_wireviz_template.py
Executable file
@ -0,0 +1,325 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
"""
|
||||
generate_wireviz_template.py: Generate a wireviz template for the connections
|
||||
in the given multiboard assembly.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Mapping, Sequence, TextIO, Optional
|
||||
|
||||
from allspice import AllSpice, Repository
|
||||
from allspice.utils.list_components import list_components, ComponentAttributes
|
||||
|
||||
ASSEMBLY_YAML_FILE_PATH = "assembly.yaml"
|
||||
|
||||
ATTRIBUTES_TO_CHECK = [
|
||||
"_reference",
|
||||
"_logical_reference",
|
||||
"Designator",
|
||||
"Part Reference",
|
||||
"Reference",
|
||||
]
|
||||
"""
|
||||
Attributes to check for in the component attributes to determine if the
|
||||
component is a connector.
|
||||
"""
|
||||
|
||||
CONNECTOR_DESIGNATOR_REGEX = r"^(J\d+|P\d+|CN\d+|X\w\d+)$"
|
||||
"""
|
||||
If a designator starts with:
|
||||
|
||||
- J: Jack
|
||||
- P: Plug
|
||||
- CN: Connector
|
||||
- X(A-Z): Socket connector. The second character has to be present and be
|
||||
immediately followed by a digit distinguish it form X(TAL).
|
||||
|
||||
Source: https://en.wikipedia.org/wiki/Reference_designator
|
||||
|
||||
In each case, we accept designators of the pattern (letter)(numbers).
|
||||
We do not need to consider multi-part components here as we request combined
|
||||
components from py-allspice.
|
||||
"""
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MultiboardAssembly:
|
||||
@dataclass
|
||||
class Dependency:
|
||||
repo: str
|
||||
"""Repository URL"""
|
||||
|
||||
source_path: str
|
||||
reference: Optional[str] = None
|
||||
variant: Optional[str] = None
|
||||
metadata: Optional[Mapping[str, str]] = None
|
||||
quantity: int = 1
|
||||
|
||||
@property
|
||||
def owner(self) -> str:
|
||||
return self.repo.split("/")[-2]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.repo.split("/")[-1]
|
||||
|
||||
Variants = Mapping[str, Sequence[Dependency]]
|
||||
|
||||
name: str
|
||||
description: str
|
||||
dependencies: Sequence[Dependency] = field(default_factory=list)
|
||||
variants: Variants = field(default_factory=dict)
|
||||
|
||||
@staticmethod
|
||||
def load_yaml(file: TextIO) -> "MultiboardAssembly":
|
||||
data = yaml.safe_load(file)
|
||||
|
||||
dependencies = [
|
||||
MultiboardAssembly.Dependency(**dep) for dep in data.get("dependencies", [])
|
||||
]
|
||||
|
||||
variants = {}
|
||||
for variant_name, variant_deps in data.get("variants", {}).items():
|
||||
variant_dependencies = [
|
||||
MultiboardAssembly.Dependency(**dep) for dep in variant_deps
|
||||
]
|
||||
variants[variant_name] = variant_dependencies
|
||||
|
||||
return MultiboardAssembly(
|
||||
name=data["name"],
|
||||
description=data["description"],
|
||||
dependencies=dependencies,
|
||||
variants=variants,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WirevizConnector:
|
||||
designator: str
|
||||
notes: str
|
||||
pin_labels: Sequence[str]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
Returns a dict that can be used to generate a wireviz template.
|
||||
"""
|
||||
|
||||
return {
|
||||
self.designator: {
|
||||
"notes": self.notes,
|
||||
"pinlabels": list(self.pin_labels),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_dependency_components(
|
||||
client: AllSpice,
|
||||
dependency: MultiboardAssembly.Dependency,
|
||||
) -> list[ComponentAttributes]:
|
||||
"""
|
||||
Get the components list of a dependency.
|
||||
|
||||
Right now, this assumes the dependency is on the same AllSpice Hub instance
|
||||
as the client.
|
||||
"""
|
||||
|
||||
try:
|
||||
repo_name, repo_owner = dependency.repo.split("/")[-1:-3:-1]
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid repository URL: {dependency.repo}")
|
||||
|
||||
repo = Repository.request(client, repo_owner, repo_name)
|
||||
return list_components(
|
||||
client,
|
||||
repo,
|
||||
dependency.source_path,
|
||||
variant=dependency.variant,
|
||||
ref=dependency.reference or repo.default_branch,
|
||||
combine_multi_part=True,
|
||||
)
|
||||
|
||||
|
||||
def get_component_designator(component: ComponentAttributes) -> str:
|
||||
for attribute in ATTRIBUTES_TO_CHECK:
|
||||
attr_value = component.get(attribute)
|
||||
if attr_value is None or not isinstance(attr_value, str):
|
||||
continue
|
||||
else:
|
||||
return attr_value
|
||||
return ""
|
||||
|
||||
|
||||
def find_connector_components(
|
||||
components: list[ComponentAttributes],
|
||||
) -> list[ComponentAttributes]:
|
||||
connector_components = []
|
||||
|
||||
for component in components:
|
||||
for attribute in ATTRIBUTES_TO_CHECK:
|
||||
attr_value = component.get(attribute)
|
||||
if attr_value is None or not isinstance(attr_value, str):
|
||||
continue
|
||||
|
||||
if re.match(CONNECTOR_DESIGNATOR_REGEX, attr_value):
|
||||
connector_components.append(component)
|
||||
break
|
||||
|
||||
return connector_components
|
||||
|
||||
|
||||
def convert_component_to_wireviz_connector(
|
||||
component: ComponentAttributes,
|
||||
source_dependency: MultiboardAssembly.Dependency,
|
||||
) -> list[WirevizConnector]:
|
||||
component_designator = get_component_designator(component)
|
||||
combined_designator = f"{source_dependency.name}_{component_designator}"
|
||||
# For Altium components, we get a better result indexing by part_id instead
|
||||
# of name, so we'll use fallbacks.
|
||||
component_name = component.get("_part_id") or component.get("_name") or ""
|
||||
|
||||
notes = f"{component_name} from {source_dependency.repo}"
|
||||
if source_dependency.variant:
|
||||
notes += f" (variant {source_dependency.variant})"
|
||||
|
||||
# The ComponentAttributes type doesn't know that pins is a list
|
||||
pins: list[dict] = component.get("_pins", []) # type: ignore
|
||||
logger.debug("Found %d pins for component %s", len(pins), component_designator)
|
||||
pin_labels = []
|
||||
if pins:
|
||||
for pin in pins:
|
||||
pin_name = pin.get("name") or pin.get("designator")
|
||||
if pin_name:
|
||||
pin_labels.append(pin_name)
|
||||
else:
|
||||
logger.warn("Pin %s does not have a name or designator; skipping.", pin)
|
||||
|
||||
if source_dependency.quantity > 1:
|
||||
return [
|
||||
WirevizConnector(
|
||||
designator=f"{combined_designator}_{i}",
|
||||
notes=notes + f" (part {i})",
|
||||
pin_labels=pin_labels,
|
||||
)
|
||||
for i in range(1, source_dependency.quantity + 1)
|
||||
]
|
||||
else:
|
||||
return [
|
||||
WirevizConnector(
|
||||
designator=combined_designator,
|
||||
notes=notes,
|
||||
pin_labels=pin_labels,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
arg_parser = argparse.ArgumentParser(description=__doc__)
|
||||
arg_parser.add_argument(
|
||||
"--assembly-file",
|
||||
required=True,
|
||||
help="Path to the assembly file to generate the wireviz template for.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--variant",
|
||||
required=False,
|
||||
help="Variant to use for the assembly file. If blank or not given, the default variant is used.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--output-file",
|
||||
required=True,
|
||||
help="Path to the output file to write the wireviz template to.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--repository",
|
||||
required=True,
|
||||
help="Repository name in owner/repo formmat.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--ref",
|
||||
required=True,
|
||||
help="Reference to use for the repository.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--allspice-hub-url",
|
||||
required=True,
|
||||
help="URL of the AllSpice Hub instance to use.",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--log-level",
|
||||
required=False,
|
||||
default="INFO",
|
||||
help="Level of logger to use, default INFO.",
|
||||
)
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
logger.setLevel(args.log_level.upper())
|
||||
logger.info("Starting wireviz template generator, with args %s", vars(args))
|
||||
|
||||
token_text = os.getenv("ALLSPICE_AUTH_TOKEN")
|
||||
if not token_text:
|
||||
raise ValueError("ALLSPICE_AUTH_TOKEN environment variable not set.")
|
||||
|
||||
client = AllSpice(
|
||||
allspice_hub_url=args.allspice_hub_url,
|
||||
token_text=token_text,
|
||||
log_level=args.log_level.upper(),
|
||||
)
|
||||
owner_name, repo_name = args.repository.split("/")
|
||||
repository = Repository.request(client, owner_name, repo_name)
|
||||
|
||||
assembly_data = repository.get_raw_file(args.assembly_file, ref=args.ref)
|
||||
assembly = MultiboardAssembly.load_yaml(io.StringIO(assembly_data.decode("utf-8")))
|
||||
variant = args.variant
|
||||
|
||||
logger.info(
|
||||
"Loaded assembly file %s from repository %s",
|
||||
args.assembly_file,
|
||||
args.repository,
|
||||
)
|
||||
|
||||
dependencies = list(assembly.dependencies)
|
||||
if variant:
|
||||
try:
|
||||
dependencies += assembly.variants[variant]
|
||||
except KeyError:
|
||||
raise ValueError(f"Variant {variant} not found in assembly file.")
|
||||
|
||||
logger.info(
|
||||
"Found %d dependencies to generate wireviz template for",
|
||||
len(dependencies),
|
||||
)
|
||||
|
||||
wireviz_connectors = {}
|
||||
for dependency in dependencies:
|
||||
components = get_dependency_components(client, dependency)
|
||||
connector_components = find_connector_components(components)
|
||||
|
||||
for component in connector_components:
|
||||
connectors = convert_component_to_wireviz_connector(
|
||||
component,
|
||||
dependency,
|
||||
)
|
||||
for connector in connectors:
|
||||
wireviz_connectors |= connector.to_dict()
|
||||
|
||||
wireviz_output = {"connectors": wireviz_connectors}
|
||||
|
||||
with open(args.output_file, "w") as output_file:
|
||||
yaml.dump(wireviz_output, output_file)
|
||||
|
||||
logger.info("Wireviz template generated and written to %s", args.output_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
py-allspice @ git+https://github.com/AllSpiceIO/py-allspice@dd317edb066b7e3248333784e4acac7b535696c5
|
||||
pyyaml~=6.0.2
|
Loading…
Reference in New Issue
Block a user