Skip to content

Commit

Permalink
Low level schematic view (#340)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: napowderly <napowderly@gmail.com>
Co-authored-by: Matthew Wildoer <mawildoer@gmail.com>
  • Loading branch information
3 people committed May 10, 2024
1 parent aaeb747 commit f2ffd07
Show file tree
Hide file tree
Showing 29 changed files with 2,474 additions and 449 deletions.
Binary file added docs/assets/images/block_diagram_example.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/schematic_example.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 27 additions & 12 deletions docs/view.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
# atopile viewer

To use the viewer, start by building your project using:
To use the viewer, invoke the `ato view` cli command:

``` sh
ato build
ato view
```

This will create a `<project-config>.view.json` file that the viewer can consume. Invoke the viewer using:
If you have multiple build configuration, specify the one you would like to view with:

``` sh
ato view
ato view -b <your-build-config-name>
```

The viewer will spool up a server on your local machine at [http://127.0.0.1:8080](http://127.0.0.1:8080). The viewer gets access to the `<project-config>.view.json` through http://127.0.0.1:8080/data.
The viewer will spool up a server on your local machine at [http://127.0.0.1:8080](http://127.0.0.1:8080).

## Viewer interfaces

### Block diagram

![Block Diagram](assets/images/block_diagram_example.png)

The block diagram is meant to provide a view that resembles your code structure. In the block diagram view, you will see the same signals and interfaces that are present in your code as well as how they interact with each other. This view will help you navigate through your project and it's structure.

### Schematic

![Schematic](assets/images/schematic_example.png)

## Viewer interface
The schematic view follows a more standard view of your design. This view can be used for documentation or inspecting a more concrete view of your final circuit. The schematic view can be enabled by navigating with the block diagram to the block you want to inspect and pressing the schematic button. You can switch back to block diagram by pressing the same button.

### Navigate within your design
The schematic diagram will represent all the components that are at the level or below the current module.

The left pane shows you the name of the instance you are currently viewing, the parent instance and provides two buttons: "return" gets you back to the parent module. "layout" recreates the default layout after you have moved blocks around.
## Navigate within your design

To navigate within a module or component, simply click on it.
*return*: This button brings you back to the parent module
*re-layout*: This button re-lays out the modules for you
*schematic/block diagram*: Switch between the two viewing modes
*reload*: Loads the latest changes for your code. This feature hasn't been enabled from the block diagram yet.

### Inspect links

Clicking on a link will show the source and target address that the link is connection. Those could either be two signals or two compatible instances of an interface.
Clicking on a link in the block diagram will show the source and target address that the link is connection. Those could either be two signals or two compatible instances of an interface.

## Features currently not supported (but planned)

- Saving the position of blocks
- Saving the position of blocks and components
- Inspecting a links pin to pin connections
- Expanding and contracting modules (instead of navigating in and out of modules)
- A decent way to see components and their pins
- Ability to inspect multiple build configurations
- A decent way to see components and their pins
12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ classifiers = [
dependencies = [
"antlr4-python3-runtime==4.13.0",
"attrs>=23.2.0",
"cattrs>=23.2.3",
"case-converter>=1.1.0",
"cattrs>=23.2.3",
"click>=8.1.7",
"DeepDiff>=6.7.1",
"easyeda2ato>=0.2.4",
Expand All @@ -50,22 +50,22 @@ dependencies = [
"igraph>=0.11.3",
"jinja2>=3.1.3",
"natsort>=8.4.0",
"networkx>=3.2.1",
"packaging>=23.2",
"pandas>=2.1.4",
"pint>=0.23",
"pygls>=1.3.0",
"quart-schema[pydantic]>=0.19.1",
"quart>=0.19.5",
"quart-cors>=0.7.0",
"rich>=13.7.0",
"ruamel.yaml>=0.18.5",
"scipy>=1.12.0",
"schema>=0.7.5",
"scipy>=1.12.0",
"semver>=3.0.2",
"toolz>=0.12.0",
"uvicorn[standard]>=0.25.0",
"watchfiles>=0.21.0",
"networkx>=3.2.1",
"Flask>=3.0.2",
"Flask-Cors>=4.0.0",
"waitress>=3.0.0",
]
dynamic = ["version"]

Expand Down
11 changes: 7 additions & 4 deletions src/atopile/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"""
from typing import Optional, Iterable
from functools import wraps
from os import PathLike
from pathlib import Path


class AddrStr(str):
Expand Down Expand Up @@ -63,15 +65,16 @@ def get_file(address: AddrStr) -> str:
return address.split(":")[0]


def get_relative_addr_str(address: AddrStr) -> AddrStr:
def get_relative_addr_str(address: AddrStr, base_path: PathLike) -> AddrStr:
"""
Extract the relative address starting with the .ato file
/abs/path/to/file.ato:Entry.Path::instance.path -> file.ato:Entry.Path::instance.path
FIXME: relative is a little misleading, as it's not relative to anything in particular,
it's merely the address without the absolute path.
FIXME: this is the first and currently only place we're
using these relative addresses. We should codify them
"""
return address.split("/")[-1]
rel_file = Path(get_file(address)).relative_to(base_path)
return from_parts(str(rel_file), get_entry_section(address), get_instance_section(address))


def get_entry(address: AddrStr) -> AddrStr:
Expand Down
22 changes: 1 addition & 21 deletions src/atopile/cli/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@
import atopile.netlist
import atopile.variable_report
from atopile.cli.common import project_options
from atopile.components import configure_cache, download_footprint
from atopile.components import download_footprint
from atopile.config import BuildContext
from atopile.errors import handle_ato_errors, iter_through_errors
from atopile.instance_methods import all_descendants, match_components
from atopile.netlist import get_netlist_as_str
from atopile.viewer_utils import get_vis_dict

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -59,12 +58,6 @@ def build(build_ctxs: list[BuildContext]):

def _do_build(build_ctx: BuildContext) -> None:
"""Execute a specific build."""

# Configure the cache for component data
# TODO: flip this around so that the cache pulls what it needs from the
# project context
configure_cache(atopile.config.get_project_context().project_path)

# Solve the unknown variables
atopile.assertions.simplify_expressions(build_ctx.entry)
atopile.assertions.solve_assertions(build_ctx)
Expand Down Expand Up @@ -219,16 +212,3 @@ def generate_module_map(build_args: BuildContext) -> None:
def generate_assertion_report(build_ctx: BuildContext) -> None:
"""Generate a report based on assertions made in the source code."""
atopile.assertions.generate_assertion_report(build_ctx)


@muster.register("view-dict")
def generate_view_dict(build_ctx: BuildContext) -> None:
"""Generate a dictionary for the viewer."""
with open(build_ctx.output_base.with_suffix(".view.json"), "w", encoding="utf-8") as f:
f.write(get_vis_dict(build_ctx.entry))


@muster.register("variable-report")
def generate_variable_report(build_ctx: BuildContext) -> None:
"""Generate a report of all the variable values in the design."""
atopile.variable_report.generate(build_ctx)
160 changes: 125 additions & 35 deletions src/atopile/cli/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,149 @@
"""
`ato view`
"""

import logging
import textwrap
from enum import Enum
from pathlib import Path

import click

from flask import Flask, jsonify, send_from_directory
from flask_cors import CORS
import json
import os

from waitress import serve

import yaml
from pydantic import BaseModel
from quart import Quart, jsonify, send_from_directory
from quart_cors import cors
from quart_schema import QuartSchema, validate_request, validate_response
from watchfiles import awatch

import atopile.address
import atopile.config
import atopile.front_end
import atopile.instance_methods
import atopile.schematic_utils
import atopile.viewer_utils
from atopile import errors
from atopile.cli.common import project_options
from atopile.config import BuildContext
from atopile.config import BuildContext, set_project_context

log = logging.getLogger(__name__)
log.setLevel(logging.INFO)


viewer_app = Flask(__name__, static_folder='../viewer/dist', static_url_path='')
# Enable CORS for all domains on all routes
CORS(viewer_app)
app = Quart(__name__, static_folder="../viewer/dist", static_url_path="")
app = cors(app, allow_origin="*")
QuartSchema(app)


@app.route("/block-diagram")
async def send_viewer_data():
build_ctx: BuildContext = app.config["build_ctx"]
return jsonify(atopile.viewer_utils.get_vis_dict(build_ctx))


class Pose(BaseModel):
"""The position, orientation, flipping etc... of an element."""
x: float
y: float
angle: int # degrees, but should only be 0, 90, 180, 270


class DiagramType(str, Enum):
"""The type of diagram."""
block = "block"
schematic = "schematic"


@app.route("/<diagram_type>/<path:addr>/pose", methods=["POST"])
@validate_request(Pose)
@validate_response(Pose, 201)
async def save_pose(
diagram_type: str | DiagramType,
addr: str,
data: Pose
) -> tuple[Pose, int]:
"""Save the pose of an element."""
diagram_type = DiagramType(diagram_type)
build_ctx: BuildContext = app.config["build_ctx"]
addr = "/" + atopile.address.AddrStr(addr)

# FIXME: rip this logic outta here
# We save the pose information to one file per-project
# FIXME: figure out how we should actually
# interact with these config files
lock_path = build_ctx.project_context.project_path / "ato-lock.yaml"
if lock_path.exists():
with lock_path.open("r") as lock_file:
lock_data = yaml.safe_load(lock_file) or {}
else:
lock_data = {}

# Find the relative address of the element to the project
rel_addr = atopile.address.get_relative_addr_str(addr, build_ctx.project_context.project_path)
lock_data.setdefault("poses", {}).setdefault(diagram_type.name, {})[rel_addr] = data.model_dump()

@viewer_app.route('/data')
def send_json():
build_dir_path = viewer_app.config.get('build_dir_path', 'Default value if not set')
#TODO: handle other builds than defaults
view_data_path = os.path.join(build_dir_path, 'default.view.json')
with open(view_data_path, 'r') as file:
data = json.load(file)
return jsonify(data)
with lock_path.open("w") as lock_file:
yaml.safe_dump(lock_data, lock_file)

return data, 200

@viewer_app.route('/')
def home():
return send_from_directory(viewer_app.static_folder, 'index.html')

@app.route("/schematic")
async def send_schematic_data():
build_ctx: BuildContext = app.config["build_ctx"]
return jsonify(atopile.schematic_utils.get_schematic_dict(build_ctx))


@app.route("/")
async def home():
"""Serve the home page."""
return await send_from_directory(app.static_folder, "index.html")


def _ato_file_filter(_, path: str):
"""Filter for files that are not ato files."""
return path.endswith(".ato")


async def monitor_changes(src_dir: Path):
"""Background task to monitor the project for changes."""
log.info(f"Monitoring {src_dir} for changes")

async for changes in awatch(src_dir, watch_filter=_ato_file_filter, recursive=True):
for change, file in changes:
log.log(logging.NOTSET, "Change detected in %s: %s", file, change.name)
atopile.front_end.reset_caches(file)


@app.before_serving
async def startup():
"""Set up the viewer."""
log.info("Setting up the viewer")
build_ctx: BuildContext = app.config["build_ctx"]

# Monitor the project for changes to reset caches
app.add_background_task(
monitor_changes,
build_ctx.project_context.project_path
)

# Pre-build the entry point
atopile.instance_methods.get_instance(build_ctx.entry)


@click.command()
@project_options
def view(build_ctxs: list[BuildContext]):
log.info("Spinning up the viewer")

if len(build_ctxs) == 0:
errors.AtoNotImplementedError("No build contexts found.")
elif len(build_ctxs) == 1:
build_ctx = build_ctxs[0]
else:
build_ctx = build_ctxs[0]
errors.AtoNotImplementedError(
f"Using top build config {build_ctx.name} for now. Multiple build configs not yet supported."
).log(log, logging.WARNING)
if len(build_ctxs) != 1:
log.info(textwrap.dedent("""
You need to select what you want to view.
- If you use the `--build` option, you will view the entry point of the build.
- If you add an argument for the address, you'll view that.
"""))
raise errors.AtoNotImplementedError("Multiple build configs not yet supported.")

build_ctx = build_ctxs[0]
app.config["build_ctx"] = build_ctx
set_project_context(build_ctx.project_context)

viewer_app.config['build_dir_path'] = build_ctx.build_path
serve(viewer_app, host="127.0.0.1", port=8080)
app.run(host="127.0.0.1", port=8080)

0 comments on commit f2ffd07

Please sign in to comment.