Web display for OpenOCD

 

ocd_web1

In my reporta project, I used a PyQt program to drive an FTDI adapter, producing a graphical display of the CPU’s internals: a real-time animation showing the I/O states, that doesn’t require any additional programming on the target system.

This post aims to produce a more powerful version, namely:

  • Use a Raspberry Pi as the interface to the CPU (SWD or JTAG)
  • Allow remote diagnosis, by using network communications
  • Use standard web browser graphics in place of PyQt

There are many advantages to using the Web browser as a display tool; most importantly, there is no need to install extra software on the display system; you can even use your smartphone as a display device. Wireless communication between the data acquisition & display can be really useful when working on real-world industrial systems, which are often in cramped and inaccessible locations.

First we have to create the display graphic, and I’m using Scalable Vector Graphics (SVG). Since everything is drawn on-the-fly from 2-dimensional x,y positions, it automatically resizes from large to small screens, which is important for mobile devices.

Scalable Vector Graphics

In a previous post, I created some simple graphics in SVG; now I need to draw something that looks like my demonstration target system, with a pushbutton, seven-segment display and ‘blue pill’ STM32F103 CPU module:

target_sys

My previous PyQt display looked like this:

reporta

..but we can do better than that! My first idea was to create the SVG graphics in Inkscape, then add Javascript code to animate them. The problem with this approach is the very large number of tools & settings in Inkscape; it is easy to create something that looks really good visually, but is extremely difficult (or impossible) to animate. So it is much easier to create the SVG from scratch using the Python ‘svgwrite’ library; the display elements can be structured so as to make animation easy.

Background

The background component is a solderless breadboard, with holes at 0.1 inch pitch. This can be created in SVG using a ‘pattern’:

import svgwrite
PIN_PITCH   = 10
PIN_SIZE    = 2
BB_SIZE     = PIN_PITCH*31, PIN_PITCH*11
TILE_SIZE   = PIN_PITCH, PIN_PITCH
TILE_CENTRE = PIN_PITCH/2.0,PIN_PITCH/2.0

# Create maximised SVG drawing
def create_svg(fname, size):
    return svgwrite.Drawing(fname,
            width="100%", height="100%",
            viewBox=("0 0 %u %u" % size),
            debug=False)

# Add a breadboard background pattern
def add_breadboard(dwg, pos=(XPAD,TPAD), size=BB_SIZE):
    dots = svgwrite.pattern.Pattern(width=TILE_SIZE[0], height=TILE_SIZE[1],
                                    id="dots", patternUnits="userSpaceOnUse")
    dots.add(dwg.rect((0,0), TILE_SIZE, fill="#f0f0f0"))
    dots.add(dwg.circle(TILE_CENTRE, 1, fill="white"))
    dwg.defs.add(dots)
    dwg.add(dwg.rect(pos, size, stroke="darkgray", fill="url(#dots)", filter="url(#shadow)"))

dwg = create_svg(FNAME, DWG_SIZE)
add_breadboard(dwg)

A single light grey tile of 10 x 10 units is defined, with a small white dot in the middle. This is used to fill a full-size rectangle; the SVG interpreter automatically duplicates the tile to fill the area.

You may wonder why I have chosen to make the holes 10 units apart, instead of redefining the SVG coordinate system so they are 0.1 units apart, to match the real-world value. The reason is that I’ve found the 10-unit convention to be much more convenient, as it allows positioning to be done with integer values, and the default line width of 1 unit looks fine, so doesn’t need to be modified.

CPU module

ocd_web_cpu

A blue rectangle is created, with red ‘pins’ that can be animated to show the I/O on/off status. A list is used to define the pins and their I/O functions:

# STM32F103 'blue pill' board pinout, starting top left
BOARD_PINS=("GND", "GND", "3V3", "NRST","PB11","PB10","PB1", "PB0", "PA7", "PA6",
            "PA5", "PA4", "PA3", "PA2", "PA1", "PA0", "PC15","PC14","PC13","VBAT",
            "PB12","PB13","PB14","PB15","PA8", "PA9", "PA10","PA11","PA12","PA15",
            "PB3", "PB4", "PB5", "PB6", "PB7", "PB8", "PB9", "5V",  "GND", "3V3")

CSS styles are used to define the box colour and pin size. The pin text is also defined, using a ‘writing mode’ of top-to-bottom, which produces the vertical labels.

STYLES = """
    .cpu_style     {stroke:darkblue; stroke-width:1; fill:#b0c0e0}
    .pin_style     {stroke:red; stroke-width:1; fill:red}
    .pin_text      {font-size:6px; writing-mode:tb; font-family:Arial}
"""

dwg.defs.add(dwg.style(STYLES))

It is then just a question of iterating across the pins, drawing them and the optional text labels; these are optional so the same code can be used to draw the (unlabeled) seven-segment display pins.

# Add a dual-in-line part
def add_dil_part(dwg, pos, row_pitch, idents, label=False, style="part_style"):
    g = Group(transform="translate"+str(pos), class_="pin_text")
    row_pins = len(idents) / 2
    g.add(dwg.rect((0,0), (row_pins*PIN_PITCH, row_pitch), class_=style))
    for n, ident in enumerate(idents):
        pos = pin_pos(n, row_pins, PIN_PITCH/2,
                      (PIN_PITCH/2, row_pitch-PIN_PITCH/2))
        g.add(dwg.circle(pos, PIN_SIZE/2, class_="pin_style", id=ident))
        if label:
            pos = pin_pos(n, row_pins, PIN_PITCH/2,
                          (PIN_PITCH, row_pitch-PIN_PITCH*2.5))
            g.add(svgwrite.text.Text(ident, pos))
    dwg.add(g)

add_dil_part(dwg, (100,30), PIN_PITCH*7, BOARD_PINS, True, "cpu_style")

A ‘group’ is used to house the complete part, so it can be styled and positioned as a single item.

Seven-segment display

ocd_web_disp

The same dual-in-line code is used to draw the component base; the pins aren’t actually visible in the real part, but have been included as a handy on/off status indication.

The display segments are drawn using a list of points, arranged so that the line drawing is sequential; this is transformed by the ‘zip’ function into a list of start & end points for each line:

# Dimensions of 7-seg display
D7W,D7H,D7L = 20,20,2       # X and Y seg length, and X-direction lean

# Segment endpoints in the order FABCDEG (for continous drawing)
SEG_LINES   = ((D7L, D7H), (D7L*2,0),(D7L*2+D7W,0),(D7L+D7W,D7H),
               (D7W,D7H*2),(0,D7H*2),(D7L,D7H),    (D7L+D7W,D7H))

# Idents for the display pins, starting top left
DISP_PINS = ("PB11","PB10","GND", "PB1", "PB0",
             "PB12","PB13","GND", "PB14","PB15")

# Idents for the segments, in order ABCDEFGH
SEG_PINS =  ("PB1", "PB0", "PB14","PB13",
             "PB12","PB10","PB11","PB15")

STYLES = """
    .seg_stroke    {stroke:#00a000; stroke-width:5; stroke-linecap:round}
"""

# Add 7 display segments
def add_disp_segs(dwg, pos):
    g = Group(transform="translate"+str(pos), class_="seg_stroke")
    lines = zip(SEG_LINES[:-1], SEG_LINES[1:])
    for n, line in enumerate(lines):
        g.add(dwg.line(*line, id=SEG_PINS[n]))
    dwg.add(g)

Pushbutton

A simple square-plus circle gives an approximation to the real button. The square has slightly rounded corners, using the ‘rx’ parameter.

PB_SIZE = 20

# Add a pushbutton
def add_pb(dwg, pos, ident, size=PB_SIZE, fill="darkred"):
    g = Group(transform="translate"+str(pos))
    g.add(dwg.rect((0,0), (size,size), rx=2, fill=fill, opacity=0.8))
    g.add(dwg.circle((size/2,size/2), size/2, fill=fill, id=ident))
    dwg.add(g)

Drop shadow

Adding a drop shadow to a component is a simple way of creating a 3-dimensional effect.

ocd_web2

Confusingly, there are two ways this effect can be achieved; using a CSS definition, or an SVG filter. The CSS method is simpler (since the CSS functionality is a subset of the SVG functionality) but doesn’t work on all browsers, so I’ve used the SVG method instead.

The filter definition consists of a series of steps, with an input and an output; the steps I’ve used are:

  • Get the alpha (i.e. the monochrome) values of the image, and offset by 2 units
  • Add Gaussian blur to the offset image
  • Combine the original image with the offset & blurred image
# Define a shadow filter
def define_shadow(dwg):
    f = dwg.defs.add(dwg.filter(id="shadow", x=0, y=0, width="150%", height="150%"))
    f.feOffset(in_="SourceAlpha", result="AlphaOset", dx="2", dy="2")
    f.feGaussianBlur(in_="AlphaOset", result="AlphaBlur", stdDeviation=2)
    f.feBlend(in_="SourceGraphic", in2="AlphaBlur", mode="normal")

# Add filter to a rectangle, e.g. for the breadboard:
    dwg.add(dwg.rect(pos, size, stroke="darkgray", fill="url(#dots)", filter="url(#shadow)"))

Note the appending of an underscore to ‘in’. This is necessary to avoid a Python syntax error; it is stripped off when the SVG output file is written

Control button, and text display

We need some method of controlling the data connection between the browser and Web server, also displaying the current status. This is achieved by adding an area at the bottom of the graphic.

ocd_web1

The ‘connect’ button is drawn as a group, containing a rounded-corner rectangle, and text.

CTRL_SIZE    = 60,19

STYLES = """
    .ctrl_style    {stroke:black; stroke-width:0.5;
                    font-size:9px; font-family:Arial; text-anchor:middle}
"""

# Add a pushbutton control
def add_ctrl_button(dwg, pos, ident, text, onclick, size=CTRL_SIZE, fill="palegreen"):
    g = Group(transform="translate"+str(pos), onclick=onclick, class_="ctrl_style")
    g.add(dwg.rect((0,0), size, rx=5, fill=fill))
    g.add(svgwrite.text.Text(text, (size[0]/2,12), fill="black", id=ident))
    dwg.add(g)

add_ctrl_button(dwg, (20,133), "button1", "Connect", "click_handler()")

The ‘onclick’ parameter will trigger the given JavaScript function when the button is clicked, e.g.

var connected=0;
function click_handler()
{
    if (connected)
        disconnect();
    else
        connect();
}

The status display consists of 2 lines of text; there is no need for scrolling, so the lines are tagged individually:

TEXTBOX_SIZE = 200,20

STYLES = """
    .textbox_style {stroke-width:1.0; stroke:lightgray; fill:none}
    .text_style    {font-size:8px; font-family:Courier}
"""

# Add a text area
def add_textbox(dwg, pos, size=TEXTBOX_SIZE):
    g = Group(transform="translate"+str(pos))
    g.add(dwg.rect((0,0), size, class_="textbox_style"))
    g.add(svgwrite.text.Text("Line1", (5,8), class_="text_style", id="text1"))
    g.add(svgwrite.text.Text("Line2", (5,17), class_="text_style", id="text2"))
    dwg.add(g)

Updating the text in Javascript just requires the ‘textContent’ to be set, e.g.:

// Connect to host
    function connect()
    {
        text2.textContent = "Connected";
        button1.textContent = "Disconnect";
        connected = 1;
    }

    // Disconnect from host
    function disconnect()
    {
        text2.textContent = "Disconnected";
        button1.textContent = "Connect";
        connected = 0;
    }

 

To be concluded…

The next blog will describe how Raspberry Pi OpenOCD data is used to animate the graphics. It will include a link to the full source code.

Copyright (c) Jeremy P Bentham 2019. Please credit this blog if you use the information or software in it.

Creating real-time Web graphics with Python

Drawing graphics in browser-friendly SVG, with Javascript animation

My previous attempt at real-time graphics involved direct-drawing the shapes using Python and PyQt. This works well for a standalone system, but is less useful in the current world of ‘everything-on-a-browser’. To make the graphics Web-friendly, there is a choice; either create an animated bitmap (e.g. GIF, PNG or JPEG) or use vector graphics (SVG).

Vector graphics have various advantages; not just that they can be resized without degradation (pixellation) but also that they can preserve some of the structural characteristics of the image that is being animated. For example, we can draw a detailed circuit diagram complete with voltage and current measurements, and if it is too large to display in a browser screen, the user can zoom out to get the big picture, then zoom in to see the data in a specific area; the components will always appear well-drawn, no matter what scale is used.

qtraction_whole2
Animated circuit diagram
qtaction_part
Zoomed in to show voltage & current values

If the components are drawn sensibly they can easily be annotated & animated to reflect the current data values. Unfortunately, although most graphics packages can export SVG, very few preserve the underlying structure of the parts. For example, the above circuit diagram was originally exported from Visio as an 800K file, filled with tiny vector & text fragments, that were very difficult to relate back to the original components. The final version, created by a Python program from a netlist, was 33KB in size, and much easier to animate.

I certainly wouldn’t give up on the idea of animating pre-existing circuit diagrams, but for the purposes of this blog, am taking the easy way out, and creating the graphics from scratch in Python. Also, it is quite a big step to create & animate a full circuit diagram in one go, so I’ll start with something much simpler, a 7-segment digital clock.

seg_clock
Clock display in Web browser

This will involve Python, SVG, style sheets and even a smattering of Javascript, so is complicated enough for starters. If you are going to do a lot of graphical manipulation, it is well worth reading the SVG specification.

svgwrite

My code is compatible with Python 2.7 or 3.x, you just need to install the ‘svgwrite’ package using pip or pip3.

As its name suggests, svgwrite is a relatively slim wrapper that simplifies the task of writing SVG files, for example:

# Create simple SVG
import svgwrite
FILENAME      = "simple.svg"
WIDTH, HEIGHT = 250, 150

dwg = svgwrite.Drawing(FILENAME, (WIDTH, HEIGHT))
dwg.add(dwg.rect((0,0),
                 (WIDTH-1,HEIGHT-1),
                 stroke="red",
                 fill="none"))
dwg.add(dwg.line((0,0),
                 (WIDTH-1,HEIGHT-1),
                 stroke="green",
                 stroke_width=8))
dwg.add(dwg.circle((WIDTH/2,HEIGHT/2),
                   HEIGHT/2,
                   stroke_width=3,
                   stroke="salmon",
                   fill="moccasin"))
dwg.add(dwg.circle((WIDTH/2,HEIGHT/2),
                   HEIGHT/3,
                   stroke_width=3,
                   stroke="salmon",
                   fill="moccasin"))
dwg.save()

If the resulting SVG file is dragged into a browser, it looks like:

simple1

There are various points to note (apart from the author’s strange colour choices):

  • The origin (x=0, y=0) is in the top left corner
  • Coordinates are specified as (x,y) tuples
  • The order of drawing is significant: earlier objects may be obscured by later
  • Lines outside the drawing area are clipped
  • To eliminate fill, you have to use ‘fill=”none”‘, rather than the more Pythonic ‘fill=None’

It is instructive to view the resulting file in a text editor; after the namespace boilerplate, there is a close match with the Python code:

<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="full" height="150" version="1.1" width="250" 
 xmlns="http://www.w3.org/2000/svg" 
 xmlns:ev="http://www.w3.org/2001/xml-events"
 xmlns:xlink="http://www.w3.org/1999/xlink">
 <defs />
 <rect fill="none" height="149" stroke="red" width="249" x="0" y="0" />
 <line stroke="green" stroke-width="8" x1="0" x2="249" y1="0" y2="149" />
 <circle cx="125" cy="75" fill="moccasin" r="75" stroke="salmon" stroke-width="3" />
 <circle cx="125" cy="75" fill="moccasin" r="50" stroke="salmon" stroke-width="3" />
</svg>

Style

The most obvious issue is the repetition of the colour definitions; using a style sheet would shorten the file, and separate the style from the drawing commands, making it easier to change the colours, for example:

import svgwrite

FILENAME      = "simple.svg"
WIDTH, HEIGHT = 250, 150

STYLES = """
  .circle_style  {stroke-width:3; stroke:salmon; fill:moccasin}
"""

dwg = svgwrite.Drawing(FILENAME, (WIDTH, HEIGHT))
dwg.defs.add(dwg.style(STYLES))
dwg.add(dwg.rect((0,0),
                 (WIDTH-1,HEIGHT-1),
                 stroke="red",
                 fill="none"))
dwg.add(dwg.line((0,0),
                 (WIDTH-1,HEIGHT-1),
                 stroke="green",
                 stroke_width=8))
dwg.add(dwg.circle((WIDTH/2,HEIGHT/2),
                   HEIGHT/2,
                   class_="circle_style"))
dwg.add(dwg.circle((WIDTH/2,HEIGHT/2),
                   HEIGHT/3,
                   class_="circle_style"))
dwg.save(pretty=True)

The browser display is exactly the same, and the SVG code now has an entry in the ‘defs’ section, with a strange addition:

<![CDATA[
  .circle_style {stroke-width:3; stroke:salmon; fill:moccasin}
]]></style>

The CDATA encapsulation is a signal to the browser that the normal search for HTML tags should be disabled; it isn’t really necessary in this case, but is essential if, for example, you are inserting a Javascript comparison such as ‘a < b’, since the browser would normally treat ‘<‘ as the start of an HTML tag.

Two important points:

  1. Note the underscore on the end of ‘class_’ in the Python code. If you forget that, Python will error out, as ‘class’ is a reserved word; svgwrite automatically strips off the trailing underscore when writing the SVG file.
  2. If you make any errors in the definition, the whole class will be ignored, and the object will be drawn in the default style.

The first issue is very common; you will see many complaints from svgwrite users that they can’t assign a class. The second issue can be annoying, since a trivial mistake can produce an ugly (or invisible!) mess, for example the following display results from putting the colour names in quotes (stroke:”salmon”; fill:”moccasin”).

style_error

The ‘developer tools’ mode in Chrome has highlighted the incorrect colour definitions.

Grouping

A useful SVG feature is that several items can be grouped together and treated as one, for example, instead of adding the 2 circles directly to the drawing, they can be added to a group, which is then added to the drawing:

g = svgwrite.container.Group(class_="circle_style")
g.add(dwg.circle((WIDTH/2,HEIGHT/2), HEIGHT/2))
g.add(dwg.circle((WIDTH/2,HEIGHT/2), HEIGHT/3))
dwg.add(g)

This is of most use when handling a complex shape, as all grouped items will be moved as one; if you want to move the 2 grouped circles to the right by 10 pixels, the first line becomes:

g = svgwrite.container.Group(class_="circle_style", transform="translate(10 0)")

You might think that the ‘add’ function would allow an offset to be applied to the items, but that isn’t true; it simply inserts the item into the file.

Using predefined items

If the the same element appears 2 or more times in your graphics, it is worthwhile defining it in the ‘defs’ section, then just adding it to the drawing with a ‘use’ command, which also allows the predefined graphic to be positioned and styled as required.

g = svgwrite.container.Group()
g.add(dwg.circle((WIDTH/2,HEIGHT/2), HEIGHT/2))
g.add(dwg.circle((WIDTH/2,HEIGHT/2), HEIGHT/3))
dwg.defs.add(g)
dwg.add(dwg.use(g, class_="circle_style"))
dwg.add(dwg.use(g, insert=(40,0), style="stroke:green; fill:palegreen"))
dwg.save()

simple4

You’ll note that I’ve removed the class from the group definition, and instead applied class & style definitions when the graphic is used. This isn’t compulsory; you can specify a class when the group is defined, but then it remains fixed, which limits the flexibility of your predefined objects – once a graphic is defined you can’t change its underlying structure.

It can also be really useful to name the predefined items using an ‘id’ parameter; they can then be inserted into the document by referencing that name, improving readability.

g = svgwrite.container.Group(id="circles")
g.add(dwg.circle((WIDTH/2,HEIGHT/2), HEIGHT/2))
g.add(dwg.circle((WIDTH/2,HEIGHT/2), HEIGHT/3))
dwg.defs.add(g)
dwg.add(dwg.use("#circles", class_="circle_style"))

The ‘#’ prefix is necessary to turn the ID into an Internationalised Resource Identifier (IRI).

Colour gradient

The green line looks a bit boring, so I wanted to add a colour gradient, making it look 3-dimensional. All the examples I’ve seen are of horizontal or vertical boxes, but I needed the shading to be aligned along the axis of the diagonal line, so it looks like a round bar. My intention was to create some very clever code to do this, but the result always looked terrible, so in the end I just hacked the parameters to make it look roughly right. Sorry!

lg = svgwrite.gradients.LinearGradient(
     start=(0,0), end=(40,0),
     id="blue_grad", gradientUnits="userSpaceOnUse",
     gradientTransform="rotate(120)")
lg.add_colors(['blue','white'])
dwg.defs.add(lg)
dwg.add(dwg.line(
    (0,0), (WIDTH-1,HEIGHT-1),
    stroke="url(#blue_grad)",
    stroke_width=50))
dwg.save()

simple5

Modifying objects

In the above code, we’ve been creating a graphic object and and adding it to the drawing in a single line. Alternatively, we can access and modify the object’s attributes using its dictionary, for example changing the stroke colour:

line = dwg.line((10,0), (110, 100), stroke="red")
...
# Change to green using either..
line["stroke"] = "green"
# ..or..
line.update({"stroke":"green"})
...
dwg.add(line)

Javascript

The intention is to create a working seven-segment clock display; we could possibly do this in Python by continually updating an SVG file, and persuading the browser to reload it, but a much simpler method is to use Javascript.

Each object (or group of objects) to be animated must have a unique ‘id’ tag.

dwg.add(dwg.line((20,0), (120, 100), stroke="red", id="myline"))

To change this line, we have to wait until the browser has loaded the graphics, so the code is triggered by window.onload event:

SCRIPT = """
window.onload = function()
{
    var myline = document.getElementById("myline");
    if (myline)
        myline.setAttribute("stroke", "yellow");
}
"""
dwg.defs.add(dwg.script(content=SCRIPT))

That’s how easy it is to modify a graphic at run-time using Javascript.

If you are a newcomer to that language, the developer’s console in the browser is incredibly useful; you can add diagnostic printouts using console.log(), and they will only show up when the developer tools are enabled. Here is the Chrome console when ‘console.log(myline);’ has been added to Javascript; hovering the cursor over the text display causes the associated graphic bounding box to be highlighted:

simple5_console

SVG clock

I started off by drawing the segments using simple flat-colour lines:

svg_clock

However the colour gradients look much more attractive..

seg_clock

..so I decided to use them instead. This triggered a cascade of problems, because using colour gradients on lines is much more restrictive than rectangles, in respect of the overall dimensions the browser uses to calculate gradient values (see the documentation on gradientUnits); when lines are drawn in various on-screen positions using the same gradient, only one is the correct colour.

The way round this problem was to draw only one shaded line, then use the ‘transform’ function to rotate and move a copy of that line. The parameters for the transformation are in the form of a 6-value array, which allows complex changes to be made in one step. Simplistically, the following transformations can be made:

(1, 0, 0, 1, x, y)          Move (translate) by x,y
(sx, 0, 0, sy, 0, 0)        Scale by sx, sy
(cos, sin, -sin, cos, 0, 0) Rotate by angle
(1, 0, tan, 1, 0, 0)        x-skew by angle
(1, tan, 0, 1, 0, 0)        y-skew by angle

A transformation array is needed for each of the 7 segments, so if segment A is the original drawn line:

segments

..the 7 segment transformations are..

# Transform matrix for each segment
SEG_MATRIX = ((1, 0,  0, 1,      0,        0), # A
              (0, 1, -1, 0, SEGLEN,        0), # B
              (0, 1, -1, 0, SEGLEN,   SEGLEN), # C
              (1, 0,  0, 1,      0, SEGLEN*2), # D
              (0, 1, -1, 0,      0,   SEGLEN), # E
              (0, 1, -1, 0,      0,        0), # F
              (1, 0,  0, 1,      0,   SEGLEN)) # G

The resulting lines are in a rectangular grid; the final flourish is to use a ‘skew’ transformation on the complete digit to give it a lean to the right:

# Matrix to slightly skew the displays
SKEW_MATRIX = "matrix(1,0,-0.1,1,0,0)"
...
g = Group(transform=SKEW_MATRIX)

The remaining question is how to do the animation; the simplest (and hopefully fastest) method I could think of is to stack all the digits 0 – 9 on top of each other, setting them transparent (opacity = 0). Each one is labelled with its position on the display, and its value, so ‘digit00’ is the left-most digit, with a value of 0, ‘digit01’ is the left-most digit with a value of 1, ‘digit10’ is the second digit with a value of 0, and so on. All the Javascript code has to do is set the opacity on the required digits, so to display the number 2345, digit02, digit13, digit24, and digit35 are opaque, the rest are transparent.

Source code

Here is the entire source code, compatible with Python 2.7 or 3.x

To view the resulting SVG file, click here.

# SVG clock with 7-segment display

import svgwrite
from svgwrite.container import Group

# 7-seg display
SEGWID      = 5             # Width & length of a segment
SEGLEN      = 20
NDIGITS     = 6             # Number of digits
DIG_PITCH   = 35            # Pitch of digits
DIG_SCALE   = 1             # Scaling factor for main digits
FRAC_SCALE  = 0.7           # Scaling factor for fractional part
LMARGIN     = 20            # Left margin
VMARGIN     = 5             # Vertical margins

GRAD_COLOURS= 'lightblue','blue'# Gradient colours
FNAME       = "svg_clock.svg"   # Filename

DWG_SIZE    = DIG_PITCH*NDIGITS, (VMARGIN+SEGLEN)*2

# Transform matrix for each segment
SEG_MATRIX = ((1,0, 0,1,0,      0),        # A
              (0,1,-1,0,SEGLEN, 0),        # B
              (0,1,-1,0,SEGLEN, SEGLEN),   # C
              (1,0, 0,1,0,      SEGLEN*2), # D
              (0,1,-1,0,0,      SEGLEN),   # E
              (0,1,-1,0,0,      0),        # F
              (1,0, 0,1,0,      SEGLEN))   # G

# Matrix to slightly skew the displays
SKEW_MATRIX = "matrix(1,0,-0.1,1,0,0)"

# Matrix to scale a segment down to a decimal point
DP_MATRIX   = "matrix(%g,0,0,1,0,0)" % (float(SEGWID)/SEGLEN)

# Segments to be activated for digits 0 - 9
dig_segs = ("abcdef", "bc", "abdeg", "abcdg", "bcfg",
            "acdfg", "acdefg", "abc", "abcdefg", "abcdfg")

SCRIPT = """
    var ndigits=6, last;

    // Initialise
    window.onload = function()
    {
        last = 0;
        setInterval(tick_handler, 100)
    }

    // Timer tick handler
    function tick_handler()
    {
        var now = new Date();
        var t = now.getHours()*10000 + now.getMinutes()*100 +
                now.getSeconds();
        if (t != last)
            set_value(t, last);
        last = t;
    }

    // Set a value on the display
    function set_value(val, oldval)
    {
        var i;
        for (i=0; i<ndigits; i++)
        {
            set_digit(ndigits-1-i, oldval, 0);
            set_digit(ndigits-1-i, val, 1);
            val = Math.floor(val / 10);
            oldval = Math.floor(oldval / 10);
        }
    }

    // Set opacity of a single display digit
    function set_digit(dig, val, op)
    {
        var digit = document.getElementById("digit"+dig%10+val%10);
        if (digit)
            digit.setAttribute("opacity", op);
    }
"""

# Create maximised SVG drawing
def create_svg(fname, size):
    return svgwrite.Drawing(fname,
            width="100%", height="100%",
            viewBox=("0 0 %u %u" % size),
            debug=False)

# Add Javascript to drawing
def add_script(dwg, script):
    dwg.defs.add(dwg.script(content=script))

# Create linear gradient, add to drawing defs
def create_lin_grad(dwg, id, start, end, colours):
    grad = svgwrite.gradients.LinearGradient(
           start=start, end=end,
           id=id, gradientUnits="userSpaceOnUse")
    grad.add_colors(colours)
    dwg.defs.add(grad)

# Create a single segment, add to drawing defs
def create_seg(dwg, id, length, width, stroke):
    seg = dwg.line((0,0), (length,0), id=id,
                   stroke="url(#%s)" % stroke,
                   stroke_width=width, stroke_linecap="round")
    dwg.defs.add(seg)

# Create digits 0 - 9 by transforming a segment, add to drawing defs
def create_digits(dwg, digid, segid):
    for dig, segs in enumerate(dig_segs):
        g = Group(id="%s%u" % (digid,dig), transform=SKEW_MATRIX)
        for seg in segs:
            num = ord(seg) - ord('a')
            mat = SEG_MATRIX[num]
            g.add(dwg.use('#'+segid, transform="matrix%s" % str(mat)))
        dwg.defs.add(g)

# Create decimal point by scaling a segment, add to drawing defs
def create_dp(dwg, dpid, segid):
    g = Group(id=dpid, transform=SKEW_MATRIX)
    g.add(dwg.use('#'+segid, transform=DP_MATRIX))
    dwg.defs.add(g)

# Add seven-segment display
# Each digit is a group of coincident transparent chars 0 - 9
def add_display(dwg, pos, pitch):
    dpos = list(pos)
    for n in range(0, NDIGITS):
        scale = DIG_SCALE if n<4 else FRAC_SCALE
        g = Group(transform = "translate(%g %g) scale(%g)" %
            (dpos[0], dpos[1], scale))
        for i in range(0, 10):
            g.add(dwg.use("#dig%u"%i, id="digit%u%u"%(n,i), opacity=0))
        dwg.add(g)
        if n == 2:
            dwg.add(dwg.use("#dp", insert=(dpos[0]-SEGLEN*0.7,
                                           dpos[1]+SEGLEN*2)))
        dpos[0] += pitch * scale

if __name__ == '__main__':
    dwg = create_svg(FNAME, DWG_SIZE)
    add_script(dwg, SCRIPT)
    create_lin_grad(dwg, "blue_grad", (0,0), (0,SEGWID*3/4),
                    GRAD_COLOURS)
    create_seg(dwg, "seg", SEGLEN, SEGWID, "blue_grad")
    create_digits(dwg, "dig", "seg")
    create_dp(dwg, "dp", "seg")
    add_display(dwg, (LMARGIN, VMARGIN), DIG_PITCH)
    dwg.add(dwg.text("iosoft.blog",
            (LMARGIN-2+DIG_PITCH*4, DWG_SIZE[1]-5),
            style="font-size:8px; font-family:Arial", fill="gray"))
    dwg.save(pretty=True)

# EOF

Copyright (c) Jeremy P Bentham 2019. Please credit this blog if you use the information or software in it.