Initial commit
This commit is contained in:
commit
94633e2bf8
7 changed files with 337 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
__pycache__
|
||||||
146
8seg_vcd_render.py
Normal file
146
8seg_vcd_render.py
Normal file
|
|
@ -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/<uuid:uuid>')
|
||||||
|
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/<uuid:uuid>', 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))))
|
||||||
|
|
||||||
57
static/8seg-template.svg
Normal file
57
static/8seg-template.svg
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="210mm"
|
||||||
|
height="297mm"
|
||||||
|
viewBox="0 0 210 297"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
sodipodi:docname="8seg-template.svg"
|
||||||
|
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
style="background-color: black;">
|
||||||
|
<g id="display">
|
||||||
|
<path
|
||||||
|
style="fill:#202040;fill-opacity:1;stroke:#000000;stroke-width:0.323819px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="M 37.088054,29.110165 47.664515,21.178664 H 169.29381 l 10.57646,7.931501 -10.57646,7.931502 H 47.664515 Z"
|
||||||
|
id="seg_a" />
|
||||||
|
<path
|
||||||
|
style="fill:#202040;fill-opacity:1;stroke:#000000;stroke-width:0.317979px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 37.115086,266.93894 10.577457,-7.64727 H 169.33333 l 10.57746,7.64727 -10.57746,7.64727 H 47.692543 Z"
|
||||||
|
id="seg_h" />
|
||||||
|
<path
|
||||||
|
style="fill:#202040;fill-opacity:1;stroke:#000000;stroke-width:0.40603px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 34.105603,31.755883 8.22773,10.57745 V 254 L 34.105603,264.58333 26.458333,254 V 42.333333 Z"
|
||||||
|
id="seg_b"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#202040;fill-opacity:1;stroke:#000000;stroke-width:0.40603px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="M 182.27227,31.75588 190.5,42.33333 V 254 L 182.27227,264.58333 174.625,254 V 42.33333 Z"
|
||||||
|
id="seg_e"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#202040;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 55.5625,42.333333 -7.937501,10.583333 10e-7,82.020834 10.583333,10.58333 h 39.6875 l 10.583337,-7.9375 -10.583337,-7.9375 H 71.4375 l -7.9375,-7.9375 0,-68.791663 z"
|
||||||
|
id="seg_c"
|
||||||
|
sodipodi:nodetypes="ccccccccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#202040;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 55.5625,254 -7.9375,-10.58334 0,-82.02083 10.583333,-10.58333 h 39.6875 l 10.583337,7.9375 -10.583337,7.9375 H 71.4375 l -7.9375,7.9375 0,68.79166 z"
|
||||||
|
id="seg_f"
|
||||||
|
sodipodi:nodetypes="ccccccccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#202040;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 161.39583,254 7.9375,-10.58334 0,-82.02083 L 158.75,150.8125 h -39.6875 l -10.58333,7.9375 10.58333,7.9375 h 26.45833 l 7.9375,7.9375 0,68.79166 z"
|
||||||
|
id="seg_g"
|
||||||
|
sodipodi:nodetypes="ccccccccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#202040;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 161.39583,42.333337 7.9375,10.58333 0,82.020833 -10.58333,10.58333 h -39.6875 l -10.58333,-7.9375 10.58333,-7.9375 h 26.45833 l 7.9375,-7.9375 0,-68.791663 z"
|
||||||
|
id="seg_d"
|
||||||
|
sodipodi:nodetypes="ccccccccccc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
22
templates/index.html
Normal file
22
templates/index.html
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>VCD to 8 Segment SVG renderer</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<ul class="flashes">
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
<h4>Upload VCD file</h4>
|
||||||
|
<form method="POST" enctype="multipart/form-data" action="/upload">
|
||||||
|
<label for="file">VCD File:</label><input name="file" type="file" required/>
|
||||||
|
<input type="submit" value="Upload"/>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
templates/setup.html
Normal file
26
templates/setup.html
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>VCD to 8 Segment SVG renderer</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<ul class="flashes">
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
<h4>Select signal</h4>
|
||||||
|
<form method="POST" enctype="multipart/form-data" action="/setup/{{ uuid }}">
|
||||||
|
<select name="signal">
|
||||||
|
{% for signal in signal_names %}
|
||||||
|
<option value="{{signal}}" {{"selected" if signal == default_signal else ""}}>{{ signal }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<input type="submit" value="Submit"/>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
vcd8seg.cfg
Normal file
3
vcd8seg.cfg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
UPLOAD_PATH = "/tmp/vcd_to_8seg"
|
||||||
|
SECRET_KEY = "changeme"
|
||||||
|
STATIC_FOLDER = 'static'
|
||||||
82
vcd_to_8seg_svg.py
Normal file
82
vcd_to_8seg_svg.py
Normal file
|
|
@ -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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue