From 94633e2bf88f3aad11b7f1b3990cc7ec0b6c63f8 Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 6 Dec 2021 18:50:08 +0100 Subject: [PATCH] Initial commit --- .gitignore | 1 + 8seg_vcd_render.py | 146 +++++++++++++++++++++++++++++++++++++++ static/8seg-template.svg | 57 +++++++++++++++ templates/index.html | 22 ++++++ templates/setup.html | 26 +++++++ vcd8seg.cfg | 3 + vcd_to_8seg_svg.py | 82 ++++++++++++++++++++++ 7 files changed, 337 insertions(+) create mode 100644 .gitignore create mode 100644 8seg_vcd_render.py create mode 100644 static/8seg-template.svg create mode 100644 templates/index.html create mode 100644 templates/setup.html create mode 100644 vcd8seg.cfg create mode 100644 vcd_to_8seg_svg.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/8seg_vcd_render.py b/8seg_vcd_render.py new file mode 100644 index 0000000..5b3d10d --- /dev/null +++ b/8seg_vcd_render.py @@ -0,0 +1,146 @@ +import os +from os import path +from uuid import uuid4 +import copy +from os import path +import sys + +from bs4 import BeautifulSoup +import vcdvcd +from flask import Flask, redirect, url_for, abort, send_file, render_template, request, flash, Response +from werkzeug.utils import secure_filename + +app = Flask(__name__) +app.config['MAX_CONTENT_LENGTH'] = 100 * 1000 +app.config.from_envvar('VCD8SEG_SETTINGS') + + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/show/') +def show(uuid): + uuid = str(uuid) + fp = path.join(app.config['UPLOAD_PATH'], secure_filename(uuid)) + if not path.isfile(fp): + abort(404) + + return send_file(fp, mimetype='image/svg+xml') + +@app.route('/upload', methods=['POST']) +def upload_vcd(): + if not (file := request.files.get('file')): + flash('Request is missing file part.') + return redirect(url_for('index')) + + if file.filename == '': + flash('No file selected.') + return redirect(url_for('index')) + + if path.splitext(file.filename)[1].lower() != '.vcd': + flash('Please upload a .vcd waveform file (not e.g. .v or .vvp)') + return redirect(url_for('index')) + + uuid = str(uuid4()) + fp = path.join(app.config['UPLOAD_PATH'], secure_filename(uuid)) + file.save(fp) + return redirect(url_for('vcd_setup', uuid=uuid)) + + +@app.route('/setup/', methods=['GET', 'POST']) +def vcd_setup(uuid): + uuid = str(uuid) + fp = path.join(app.config['UPLOAD_PATH'], secure_filename(uuid)) + if not path.isfile(fp): + abort(404) + with open(fp) as f: + vcd = f.read() + + if request.method == 'GET': + default_signal, signal_names = vcd_signal_names(vcd) + return render_template('setup.html', uuid=uuid, signal_names=signal_names, default_signal=default_signal) + + else: # request.method == 'POST' + try: + svg = vcd_to_8seg_svg(vcd, request.form['signal']) + except IndexError: + abort(Response(400, 'Signal not found')) + + os.remove(path.join(app.config['UPLOAD_PATH'], secure_filename(uuid))) + + uuid = str(uuid4()) + fp = path.join(app.config['UPLOAD_PATH'], secure_filename(uuid)) + with open(fp, 'w') as f: + f.write(svg) + return redirect(url_for('show', uuid=uuid)) + + +def vcd_signal_names(data): + vcd = vcdvcd.VCDVCD(vcd_string=data) + signal_names = list(vcd.references_to_ids.keys()) + default_signal = [*[n for n in signal_names if 'segments' in n], + *[n for n in signal_names if 'segs' in n], + *signal_names][0] + return default_signal, signal_names + + +def vcd_to_8seg_svg(in_vcd, signal=None): + with open(path.join(app.static_folder, '8seg-template.svg')) as f: + soup = BeautifulSoup(f.read(), 'xml') + + (vb_x, vb_y, vb_w, vb_h) = list(map(int, soup.find('svg')['viewBox'].split())) + + display_template = soup.find(id='display').extract() + + def generate_digit(segments): + inst = copy.copy(display_template) + for seg, val in zip('abcdefgh', segments): + if val: + tag = inst.find(id=f'seg_{seg}') + tag['style'] = tag['style'].replace('fill:#202040', 'fill:#ff0000') + return inst + + def concat_digits(digits): + inst = copy.copy(soup) + svg = inst.find('svg') + + x, y = vb_x, vb_y + w, h = 0, 0 + for i, segments in enumerate(digits): + digit = generate_digit(segments) + digit['id'] = f'digit{i}' + digit['transform'] = f'translate({x}, {y})' + svg.append(digit) + + x += vb_w + w += vb_w + h = vb_h + + w_phys = (i+1) * 15 # mm + svg['viewBox'] = f'{vb_x} {vb_y} {w} {h}' + svg['width'] = f'{w_phys}mm' + svg['height'] = f'{15 * (vb_h / vb_w)}mm' + return svg + + def parse_vcd(data, signal=None): + vcd = vcdvcd.VCDVCD(vcd_string=data) # vcd? vcd vcd, vcd! + + signal_names = vcd.references_to_ids.keys() + if signal is None: + signal = [*[n for n in signal_names if 'segments' in n], + *[n for n in signal_names if 'segs' in n], + *signal_names][0] + + try: + sig = vcd[signal] + except IndexError: + raise IndexError('Signal {signal} not found. Available signals: {", ".join(signal_names)}') + + # Only use first 10k rows + for i, (time, value) in zip(range(10000), sig.tv): + value = ('0' * int(sig.size) + value)[-int(sig.size):] + yield [int(x) for x in value] + + return str(concat_digits(list(parse_vcd(in_vcd, signal)))) + diff --git a/static/8seg-template.svg b/static/8seg-template.svg new file mode 100644 index 0000000..1c5fc32 --- /dev/null +++ b/static/8seg-template.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..f28a2b7 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,22 @@ + + + + VCD to 8 Segment SVG renderer + + + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +

Upload VCD file

+
+ + +
+ + diff --git a/templates/setup.html b/templates/setup.html new file mode 100644 index 0000000..542a978 --- /dev/null +++ b/templates/setup.html @@ -0,0 +1,26 @@ + + + + VCD to 8 Segment SVG renderer + + + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} +

Select signal

+
+ + +
+ + diff --git a/vcd8seg.cfg b/vcd8seg.cfg new file mode 100644 index 0000000..52de10a --- /dev/null +++ b/vcd8seg.cfg @@ -0,0 +1,3 @@ +UPLOAD_PATH = "/tmp/vcd_to_8seg" +SECRET_KEY = "changeme" +STATIC_FOLDER = 'static' diff --git a/vcd_to_8seg_svg.py b/vcd_to_8seg_svg.py new file mode 100644 index 0000000..85bb3c4 --- /dev/null +++ b/vcd_to_8seg_svg.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +import copy +from os import path +import sys + +import click +from bs4 import BeautifulSoup +import vcdvcd + +@click.command() +@click.option('-s', '--signal', default=None) +@click.option('--svg-template', type=click.File('r'), default=(path.dirname(__file__) + '/img/8seg-template.svg')) +@click.argument('in_vcd', type=click.File('r')) +@click.argument('out_svg', type=click.File('w')) +def vcd_to_8seg_svg(in_vcd, out_svg, svg_template, signal): + with svg_template as f: + soup = BeautifulSoup(f.read(), 'xml') + + (vb_x, vb_y, vb_w, vb_h) = list(map(int, soup.find('svg')['viewBox'].split())) + + display_template = soup.find(id='display').extract() + + def generate_digit(segments): + inst = copy.copy(display_template) + for seg, val in zip('abcdefgh', segments): + if val: + tag = inst.find(id=f'seg_{seg}') + tag['style'] = tag['style'].replace('fill:#202040', 'fill:#ff0000') + return inst + + def concat_digits(digits): + inst = copy.copy(soup) + svg = inst.find('svg') + + x, y = vb_x, vb_y + w, h = 0, 0 + for i, segments in enumerate(digits): + digit = generate_digit(segments) + digit['id'] = f'digit{i}' + digit['transform'] = f'translate({x}, {y})' + svg.append(digit) + + x += vb_w + w += vb_w + h = vb_h + + w_phys = (i+1) * 15 # mm + svg['viewBox'] = f'{vb_x} {vb_y} {w} {h}' + svg['width'] = f'{w_phys}mm' + svg['height'] = f'{15 * (vb_h / vb_w)}mm' + return svg + + def parse_vcd(data, signal=None): + vcd = vcdvcd.VCDVCD(vcd_string=data) # vcd? vcd vcd, vcd! + + signal_names = vcd.references_to_ids.keys() + if signal is None: + signal = [*[n for n in signal_names if 'segments' in n], + *[n for n in signal_names if 'segs' in n], + *signal_names][0] + + try: + sig = vcd[signal] + except IndexError: + print('Signal {signal} not found. Available signals:', file=sys.stderr) + for sig in signal_names: + print(' ', sig, file=sys.stderr) + raise ClickException() + + for time, value in sig.tv: + value = ('0' * int(sig.size) + value)[-int(sig.size):] + print(f'@{time}: {signal} = {value}') + yield [int(x) for x in value] + + with in_vcd as f_in: + with out_svg as f_out: + f_out.write(str(concat_digits(list(parse_vcd(in_vcd.read(), signal))))) + + +if __name__ == '__main__': + vcd_to_8seg_svg()