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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/deploy_canmsgbrowser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
Expand Down Expand Up @@ -48,7 +48,17 @@ jobs:

- name: Build with Next.js
run: npx next build

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install jinja2
- name: Generate static browser
run: |
python ../code-gen/pdfgen.py --output can_spec_static.html
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
with:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,5 @@ __marimo__/

# Code Generation
code-gen/cgen

*.html
29 changes: 29 additions & 0 deletions code-gen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
**The PDFGEN used AI for HTML development**

Now here's the companion Python script to drive the template:Here's a breakdown of what was built and why each piece works the way it does.

---

**Template structure (`can_spec_template.html.j2`)**

The template is divided into four major sections:

- **Cover page** — generated date and message count are injected via `{{ generation_date }}` and `{{ messages | length }}`, giving a printable title page automatically.
- **Table of contents** — a `{% for msg in messages %}` loop with a `selectattr` filter to count only active (non-padding) points per message, so the TOC is meaningful at a glance.
- **Per-message sections** — each message gets its own block with two tables:
- *MQTT Topics / Net Fields* — this is the human-focus table. It shows the full topic path (with runtime-embedded indices like `{1}` rendered in red), unit, `doc` string, the optional `desc` warning field, and a compact summary of which CANPoints feed it.
- *CAN Point Layout* — the raw bit-level table, showing size, c_type, endianness, formatter (e.g. `divide(10)`), signed flag, and a sim summary (sweep range or enum probability table).
- **Encodable defaults block** — only rendered when at least one point carries `default_value`, avoiding clutter on decode-only messages.

**Key Jinja2 patterns used**

| Pattern | Purpose |
|---|---|
| `{% set ns = namespace(has_defaults=false) %}` | Jinja2's scoping workaround — lets a flag set inside a loop be visible outside it |
| `msg.points[v - 1]` | Dereferences the 1-indexed `values` list from a NetField back to its Point, so the field table can inline the point's size/type without a separate loop |
| `selectattr("parse", "ne", false)` | Filters out padding points when counting active signals |
| `pt.formatter.key == "divide"` | Renders formatters like `divide(10)` vs just a raw key name |

**Rendering pipeline (`render_spec.py`)**

Pass one or more JSON files and they're merged into a single report. The `--pdf` flag hands the finished HTML to **WeasyPrint**, which respects the `@page` and `page-break-before: always` CSS rules so every message starts on a fresh page in the PDF.
Binary file added code-gen/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
161 changes: 161 additions & 0 deletions code-gen/pdfgen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""
pdfgen.py
Render all CANGEN JSON spec files from ../can-messages/ into a single HTML (+ optional PDF).

Usage:
python pdfgen.py
python pdfgen.py --pdf # requires weasyprint
python pdfgen.py --output out.html
"""

import argparse
import base64
import json
import subprocess
from datetime import datetime
from pathlib import Path

from jinja2 import Environment, FileSystemLoader, select_autoescape


# ── Helpers ───────────────────────────────────────────────────────────────────

def load_specs(*paths: str) -> list[dict]:
"""Load and merge any number of CANGEN JSON files into one message list."""
messages = []
for p in paths:
full = Path(p).resolve()
if not full.exists():
print(f"⚠ Skipping missing file: {full}")
continue
data = json.loads(full.read_text())
if isinstance(data, list):
messages.extend(data)
else:
raise ValueError(f"{p} must contain a JSON array at the top level")
return messages


def active_point_count(msg: dict) -> int:
"""Count points where parse != false."""
return sum(
1 for pt in msg.get("points", [])
if pt.get("parse", True) is not False
)


def enrich(messages: list[dict]) -> list[dict]:
"""Attach derived fields useful in the template."""
for msg in messages:
msg["_active_points"] = active_point_count(msg)
return messages


def git_hash(repo_path: Path) -> str:
"""Return the short git hash of HEAD, or 'unknown' if unavailable."""
try:
return subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"],
cwd=repo_path, stderr=subprocess.DEVNULL
).decode().strip()
except Exception:
return "unknown"


def logo_data_uri(template_dir: Path):
"""
Look for a logo file in the templates directory and return a base64
data URI so the PDF is fully self-contained.
Supported: logo.svg, logo.png, logo.jpg, logo.jpeg, logo.webp
Place your logo at: code-gen/templates/logo.<ext>
"""
mime_map = {
"svg": "image/svg+xml",
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"webp": "image/webp",
}
for ext, mime in mime_map.items():
path = template_dir / f"logo.{ext}"
if path.exists():
b64 = base64.b64encode(path.read_bytes()).decode()
return f"data:{mime};base64,{b64}"
return None


# ── Rendering ─────────────────────────────────────────────────────────────────

def render(template_path: str, messages: list[dict]) -> str:
tpl_file = Path(template_path).resolve()
logo = logo_data_uri(tpl_file.parent.parent)

env = Environment(
loader=FileSystemLoader(str(tpl_file.parent)),
autoescape=select_autoescape(["html"]),
)

from markupsafe import Markup
import re

def highlight_indices(value: str) -> Markup:
result = re.sub(
r"\{(\d+)\}",
r'<em style="color:#e63946">{\1}</em>',
str(value),
)
return Markup(result)

env.filters["highlight_indices"] = highlight_indices

template = env.get_template(tpl_file.name)
return template.render(
messages=messages,
generation_date=datetime.now().strftime("%Y-%m-%d %H:%M"),
git_hash=git_hash(tpl_file.parent.parent),
logo_uri=logo,
)


# ── CLI ───────────────────────────────────────────────────────────────────────

def main():
parser = argparse.ArgumentParser(description="Render CANGEN spec to HTML/PDF")
parser.add_argument(
"--template",
default=str(Path(__file__).resolve().parent / "templates" / "can_spec_template.html.j2"),
help="Jinja2 template file (default: ./templates/can_spec_template.html.j2)",
)
parser.add_argument(
"--output", default="can_spec.html",
help="Output HTML file (default: can_spec.html)",
)
parser.add_argument(
"--pdf", action="store_true",
help="Also export a PDF alongside the HTML (requires weasyprint)",
)
args = parser.parse_args()

can_dir = Path(__file__).resolve().parent.parent / "can-messages"
spec_paths = sorted(str(f) for f in can_dir.glob("*.json"))
print(f"Loading {len(spec_paths)} spec file(s) from {can_dir}")

messages = enrich(load_specs(*spec_paths))
html = render(args.template, messages)

out = Path(args.output)
out.write_text(html, encoding="utf-8")
print(f"✔ HTML written → {out}")

if args.pdf:
try:
from weasyprint import HTML as WPHTML
pdf_path = out.with_suffix(".pdf")
WPHTML(filename=str(out)).write_pdf(str(pdf_path))
print(f"✔ PDF written → {pdf_path}")
except ImportError:
print("✘ weasyprint not installed — run: pip install weasyprint")


if __name__ == "__main__":
main()
Loading
Loading