cds-visu-svg — SVG → CODESYS visu transpiler¶
The user has cts visu from-svg which compiles an SVG file directly into a
CODESYS visualization .xml file. Your job is to generate valid SVG that
this tool can consume, ask the user to review/approve it, then compile it to a
CODESYS visualization object using cts visu from-svg only after approval.
Short path¶
A screen in the project, start to finish, from the project folder:
cts visu new --name Pump # a laid-out starter sketch, .visu/Pump.svg
# edit .visu/Pump.svg: real labels and bindings to real variables, delete the
# placeholder cards, lamps and buttons the screen does not need:
# <rect data-cds-type="button" data-text="Start" data-cds-tap="GVL_Pump.xStart" .../>
# <rect data-cds-type="lamp" data-color="green" data-var="GVL_Pump.xRunning" .../>
# <text data-cds-type="textfield" data-text-var="GVL_Pump.rFlow" ...>%3.1f m3/h</text>
cts visu lint --svg .visu/Pump.svg --fix # then fix what it still reports
cts visu preview --svg .visu/Pump.svg # look at the PNG
# --folder: the screen's folder as cts project-tree prints it
cts visu from-svg --svg .visu/Pump.svg --create-screen --screen-name Pump --folder "<Device>/Plc Logic/Application"
cts import # the screen, its GVL and texts into the IDE
cts build
The sections below (cts guide visu-svg --section "<title>" prints one), by
what you need: the steps in detail "Workflow"; buttons,
lamps and fields "Element vocabulary" and "Data attributes for controls";
colours "Colour classes"; placement "Layout rules"; variables the screen needs
"GVL variable declarations"; a whole example "Example — Pump Station sketch".
Workflow¶
Run the commands from the project folder (the one holding project-view/).
From anywhere else, every command below takes --sync-folder <path>;
without it the tool cannot find the project, so it cannot resolve your theme,
your existing GVLs, or where to write the screen. It works the same on every
subcommand — lint, lint --fix, preview, from-svg, check — and the
examples below omit it:
cts visu lint --svg <screen>.svg --sync-folder C:\path\to\project
- Scaffold a starter SVG with
cts visu new. This composes a complete, already-laid-out screen for the canvas size you ask for — header band, content panel, KPI cards, a bound field, a status row, an action row — plus a commented list of the colour classes. Always start from it instead of writing an SVG from a blank canvas:The scaffold is a layout, not a design. Its labels ("KPI 1") and its bindings (cts visu new --name "<Screen>" --w 800 --h 480 --out <screen>.svgVisuVars.xRunning,VisuVars.rLineSpeed) are placeholders that name nothing in the project — every one of them has to be replaced in step 2 with a variable that actually exists, or you ship a screen wired to invented names. - Edit the SVG: keep or adjust the example elements, add more as needed.
Colour comes from
class="..."(see "Colour classes" below) — you do not write colours yourself. Follow "Layout rules" below; the skeleton already does. For elements beyond the skeleton, copy from the two shipped examples — read them withcts guide visu-svg --file examples/status-panel.svgand... --file examples/pid-schematic.svg. Both lint clean, so they are safe to imitate. The screen background is set for you; never add a full-screen background rect. You can set binding attributes (data-text-var,data-cds-tap,data-cds-action, ...) inline here, or defer them to a separate pass withcts visu bind --svg <screen>.svg --elem N ...once the layout itself is drawn and previewed — useful for touching one element's wiring without risking its geometry. - Check the design with
cts visu lint. It reports the things that make a technically valid screen look unfinished — off-grid coordinates, text wider than its box, a font size outside the scale, a button too small to press, a field with nothing bound to it:Runcts visu lint --svg <screen>.svg --fix # snap the mechanical ones first cts visu lint --svg <screen>.svg # then read what is left--fixfirst and then read the report: it clears the arithmetic (grid snapping, font sizes) so what remains is only the judgement calls, and those are yours to make. Fix everything it reports before moving on.--fixrewrites only the attributes it flagged, leaving your comments and formatting intact, and it iterates until the sketch stops changing — one run is enough. - Look at what you made. The sketch itself carries no colours, so opening
it in a viewer shows black shapes on white and tells you nothing. Render it
with the real resolved palette and actually open the PNG:
This writes
cts visu preview --svg <screen>.svg<screen>.preview.svgand<screen>.preview.pngnext to the sketch. Read the PNG. Check the things lint cannot: is the hierarchy readable, is anything visually lost, does it look like a screen someone designed. Iterate here — this loop is cheap, the CODESYS round-trip is not. - Generate the GVL automatically (see "GVL variable declarations"
section below). When compiling, use the
--gvlflag to auto-generate declarations for all runtime variables detected in the SVG:For a custom output path use:cts visu from-svg --svg <screen>.svg --screen <ScreenName> --gvl VisuVarsThis is recommended for every SVG that usescts visu from-svg --svg <screen>.svg --screen <ScreenName> --gvl-file path/to/GVL_HMI.stdata-text-var,data-cds-tap, ordata-cds-action. Variables already declared in existing GVL files are silently skipped. - Pause for visual approval. Show the user the preview PNG from step 4 and
tell them the SVG path. Do not run
cts visu from-svgyet unless the user already explicitly requested an unattended compile in the same request. - After the user approves, compile it. If
ctsis available in the environment, run:cts visu from-svg --svg <screen>.svg --create-screen --screen-name <ScreenName> \ --folder "<Device>/Plc Logic/Application" --gvl VisuVars - First compile: use
--create-screen --screen-name <ScreenName> --folder "<CODESYS folder>"to create the screen. - Recompiling an existing screen: use
--screen <ScreenName> --folder "<CODESYS folder>"instead (do NOT pass--create-screenagain — it errors "Screen already exists"). --folderis the place in the CODESYS project tree, ascts project-treeprints it: the application the screen belongs to (<Device>/Plc Logic/Application), or a folder inside it.
Use the appropriate --theme (default: flat-style). Include --gvl to
auto-generate variable declarations when the SVG uses runtime bindings.
from-svg runs lint and preview itself; --strict makes lint findings fatal,
--no-preview skips the PNG.
8. Verify the screen the tool wrote. from-svg reports what it compiled;
check re-reads the .xml in the project and grades it as CODESYS will:
cts visu check --screen <ScreenName> --folder "<Device>/Plc Logic/Application"
check works on a screen that already exists, so it takes --screen, not
--name (--name belongs to new and create-screen).
from-svg writes files only: the screen's .xml, the GVL and the new rows of
GlobalTextList in project-view/. cts import brings them into the IDE,
then cts build compiles the screen with the project.
9. If cts is not available, report this clearly and provide the exact
command the user can run manually.
10. Always report the compile result (success / error / waiting for approval /
not run).
Approval rule¶
Treat SVG generation and CODESYS XML import as two separate steps. A normal
visualization request should stop after the .svg is written and reviewed.
Proceed to from-svg only after the user says to continue, approves the SVG, or
explicitly asks for immediate compilation. A request for the screen in the
project ("make a screen for motor 1, the project must build") is that ask:
compile, import and build, and show the preview in your report.
Canvas¶
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="480">
...
</svg>
width/height→ screen size.- Coordinate system: top-left origin, y-down, 1 user-unit = 1 px.
- The screen background is set automatically (from the CODESYS style /
--theme). Do NOT add a full-screen<rect>to paint the background.--background style|auto|#RRGGBBonfrom-svgoverrides it if you must.
Layout rules¶
A screen that follows these looks designed; one that ignores them looks
generated. cts visu lint grades against them, so following them is also the
fastest way to a clean run.
- 4px grid. Every
x/y/width/heightis a multiple of 4. Lay blocks out on 8 or 16 — the grid is the floor, not the rhythm.<text>is the exception: see the baseline rule below, which is what the grid is graded against there. - 24px page margin. Nothing but the background touches the outer 24px.
- 16px between blocks, 16px panel padding. Content starts 16px inside a
.panel, and panels sit 16px apart. This rhythm assumes a canvas of 480×320 or more;cts visu newwarns if you ask for less, and below that you are laying the screen out by hand. - Bands, not scatter. Header (title + a divider at y≈72), body, then an action row above the bottom margin with a divider over it. Buttons live in the action row, not floating in the body.
- One
.h1per screen..h2per section,.labelfor body text,.captionfor units and notes,.valuefor the one number that matters. Never write a barefont-size— pick a class. - Touch targets. A button is at least 48×32; 160×48 is the comfortable default. A lamp is at least 16×16.
- Text is anchored on its BASELINE. This is the single most common mistake.
For a
<text>— including adata-cds-type="textfield"— theyyou write is the baseline, and the compiled box top isy - font-size. So a 12px field whose box should start at 160 is writteny="172".
text-anchor="middle" and "end" change this rule: the box is then
centred on y, so the top is y - height/2 — height, not font-size. With no
data-height that height is the one below, so the
scaffold's <text class="caption" y="40"> has its box top at 40 - 16/2 = 32,
on-grid. Do not apply the y - font-size rule to anchored text: it gives 29
here and tells you the scaffold is broken when it is not. Give anchored text an
explicit data-height when you want the arithmetic to be obvious.
This is what the 4px grid means for text: put the box top on the grid, so
write y = (multiple of 4) + font-size. Since .h1 is 22 and .caption is
11, their baselines are deliberately not multiples of 4 — y="46" for an h1
is correct (46 − 22 = 24) and y="48" is not (48 − 22 = 26, off-grid). Every
<text> in the example at the bottom of this file is written this way.
The box is ~1.4× the font size tall, so it hangs below the baseline —
descender space, exactly like a line of type. A <text> with no data-height
compiles to max(16, font-size × 1.4) rounded up to the 4px grid, which makes
it 16 for a .label and a .caption, 24 for an .h2, 32 for an .h1 and 40
for a .value. The bottom edge is therefore 4px under the baseline for a
.label, 5 for a .caption, 8 for an .h2, 10 for an .h1, 12 for a
.value. This is what makes a value overflow the card you put it in — a
.value with y="160" needs its card to reach 172, not 168. Size a card from
the bottom of the text it holds, not from the baseline.
- x on a <text> is always the box LEFT edge, even with
text-anchor="middle" or "end". This is the one place the sketch does not
mean what an SVG viewer would show you: CODESYS puts text in a box, so the
anchor chooses where the glyphs sit inside data-width rather than moving
the box. To centre a label over a 336px panel starting at x=424, write
x="424" data-width="336" text-anchor="middle" — not x="592". Anchored text
without a data-width is a lint finding, because then the box width is guessed
from the text and the alignment lands wherever the guess falls.
"end" works the same way and catches people out harder: to right-align a
value against the inner edge of a card that ends at x=760, you still write the
box's left edge — x="640" data-width="120" text-anchor="end", not
x="736". Writing the right edge pushes the box to 736+120=856 and the bounds
rule reports it as off-canvas.
- 24px from a label's baseline to its field's baseline. That leaves 16px
between the field and the next label.
- Pipes are thin rects, not lines. A CODESYS line has no width member, so
<line> always draws one pixel wide — fine for a .divider, invisible for a
process line. Draw a pipe as an 8px-tall <rect class="pipe-water">.
- -- cannot appear inside an XML comment. Ruling a section off with
<!-- ---- Supply air ---- --> makes the whole file unparseable. Use ====.
- Only five named entities exist here. A sketch is XML, not HTML, so
< > & " ' are the whole list. HTML spellings that
look obvious in a caption — → for an arrow, ≤ for a threshold,
— — fail the parse with undefined entity and take the whole file
with them. Write the character itself (→, ≤), or a numeric reference
(→), or plain ASCII (->, <=).
Colour classes¶
You never write a colour as a hex value. Put a class on an element and the active CODESYS style / theme decides the actual colour. This means an element always gets a real colour that matches the project style — no "white elements", and the same sketch re-themes automatically.
The one place you name a colour is a lamp's data-color="red|green|yellow|blue|
gray". That is not an exception to the rule: you are naming the role the lamp
signals, and the theme still picks the pixels — exactly like a class does.
| class | use on | meaning |
|---|---|---|
panel |
<rect> |
grouping / container backing |
card |
<rect> |
inner surface, one step in from a panel |
divider |
<line> |
separator line |
h1 |
<text> |
screen title (22px) — one per screen |
h2 |
<text> |
section / panel heading (16px) |
value |
<text> |
large read-out (28px) |
label |
<text> |
body text, field name (12px) |
caption |
<text> |
units, secondary note (11px) |
muted |
<text> |
de-emphasised, keeps its size |
inverse |
<text> |
text sitting on a status / accent fill |
ok |
<rect> <circle> |
status: healthy / running |
warn |
<rect> <circle> |
status: caution |
alarm |
<rect> <circle> |
status: fault (fill + frame) |
pipe-water |
<rect> <line> <ellipse> |
P and ID fluid line / vessel |
metal |
<rect> <circle> <ellipse> |
P and ID structural / equipment |
title still works as a legacy alias of h1; prefer h1 in new sketches.
-
cardis an inner surface, not a top-level box. It is one step in from the.panelthat holds it, and in the light scheme that step lands it within ~3% of the screen background — so a.carddropped straight onto the screen has almost no visible edge (#EDEFF2 on #F4F5F7) and reads as a shape that failed to render. A box that sits directly on the background is a.panel;.cardis for the KPI tiles and read-outs inside one. -
Text over a P&ID fill.
inverseis for text on a status colour (ok/warn/alarm). There is no class for text sitting onmetalorpipe-water, andlabelon a dark vessel can be unreadable — so put tag numbers and captions beside or under the equipment rather than on it. The shippedpid-schematic.svgdoes exactly that: one row of.captiontags on a shared baseline beneath the flow path. -
Buttons, textfields and lamps take NO class. They inherit the native CODESYS control style automatically — leave their colour alone.
- Classes come with cts (its built-in stylesheet) and can be extended, or
overridden per project with a
visu.cssin the project-view directory. To add a new colour meaning, add a class there rather than hard-coding a colour. - Escape hatch (rare): an explicit
fill="#RRGGBB"/stroke="#RRGGBB"still works and overrides the class, but prefer a class so the screen stays themed. An inline<defs><style>:root { --water: #… }</style></defs>block also works and overrides the theme for that one sketch — reach for it only when a screen genuinely needs a palette the project style does not have.
Dark screens¶
A night shift reads the same screen in a dark control room. Ask for it once, at scaffold time:
cts visu new --name "<Screen>" --w 800 --h 480 --scheme dark --out <screen>.svg
That records data-cds-scheme="dark" on the root <svg>, and nothing else in
your workflow changes. You still never write a colour: the same classes resolve
to the dark palette, so lint, preview and from-svg all follow the sketch
without being told. --scheme dark|light is also accepted by lint, preview
and from-svg if you want to see one sketch both ways — it overrides the
attribute for that single run, without editing the file.
Two things to know:
- Lamps keep their indicator colours. Red stays red on a dark screen. An indicator that changed meaning with the colour scheme would be a safety problem, so lamps are deliberately outside the palette.
- In dark the palette is authoritative. Every CODESYS visual style that
ships is a light style, so on a dark screen the palette owns the surfaces,
the text on them, and the button/textfield colours —
--themeno longer changes those. Light is unchanged: it still defers to the project style.
Element vocabulary¶
| SVG | CODESYS element | Attributes consumed |
|---|---|---|
<rect> |
VisuFbElemSimple (rectangle) |
x y width height class, optional shape bindings (below) |
<rect rx="…"> |
VisuFbElemSimple (rounded) |
same as rect + rx |
<circle> |
VisuFbElemSimple (ellipse) |
cx cy r class |
<ellipse> |
VisuFbElemSimple (ellipse) |
cx cy rx ry class |
<line> |
VisuFbElemLine |
x1 y1 x2 y2 class (always 1px — see Layout rules) |
<text> |
VisuFbLabel |
x(=left) y(=baseline) class font-size font-family + textContent |
<rect data-cds-type="button"> |
VisuFbElemButton |
geometry + data-text (caption — a <rect> cannot carry text content) |
<text data-cds-type="textfield"> |
VisuFbElemTextfield |
x y(=baseline) data-width data-height data-text-var + textContent |
<rect data-cds-type="lamp"> |
VisuFbElemLamp |
geometry + data-color (red|green|yellow|blue|gray) data-var |
A control is recognised by the pair — tag plus data-cds-type. Every
control above is a <rect> except the textfield, which is a <text>. Put
data-cds-type on any other tag and it is not an error and not a control: it
falls through to the plain shape parser, data-var and data-text are dropped,
and you get a decoration that draws correctly and does nothing. The one that
catches people is the lamp — a circle is the obvious shape for an indicator
light, but <circle data-cds-type="lamp"> is just an ellipse. Draw the lamp as a
square <rect>; the native lamp bitmap is round anyway. cts visu lint reports
this as control-tag.
cts visu preview draws that lamp as a circle, not the square you wrote.
That is the preview being honest about the round bitmap CODESYS will actually
render — your sketch is not wrong and nothing rewrote it. Expect the same of
textfields: the preview shows the literal format string (%3.1f, %d), because
there is no PLC attached to supply a value.
Advanced elements (lamp, image-switcher, combobox, alarm-banner)¶
These specialized native controls are documented separately — read that
reference with cts guide visu-svg --file advanced-elements.md. Do not read
it unless the user specifically asks for one of these controls — the core
elements above cover almost every screen. When you do need one, read only its
section for the control in question.
Data attributes for controls¶
Button¶
<rect x="100" y="200" width="120" height="40"
data-cds-type="button" data-text="Start" />
data-text — button caption. The transpiler allocates a Text-ID in the
GlobalTextList automatically.
- data-cds-tap="TAP HMI.StartPump" — bind the native CODESYS button
tap/toggle variable. The verb is optional: TAP, TOGGLE, tap: and
toggle: are all stripped, and a bare data-cds-tap="HMI.StartPump" binds the
same way. cts visu new emits the bare form; both are correct.
- data-cds-action — optional native action binding. Supported forms:
TAP HMI.StartPump, TOGGLE HMI.StartPump,
OnMouseClick: ST HMI.StartPump := TRUE;,
OnMouseDown: toggle HMI.StartPump, and
OnMouseUp: screen CoolingTower. Separate multiple actions with ||.
Plain shape (a tile that shows, colours and answers a click)¶
<rect x="40" y="40" width="120" height="64" class="card"
data-text="%s" data-text-var="HMI.sCell" data-text-color="#FFFFFF"
data-color-var="HMI.xOccupied" data-on-fill="#E0B020" data-on-stroke="#FFE070"
data-cds-action="OnMouseClick: ST HMI.iPick := 1;" />
<rect>, <circle> or <ellipse> without data-cds-type takes these, all
optional — one shape per rack cell or valve instead of a shape, a field and a
button stacked on each other:
- data-text — text centred in the shape; with data-text-var it is the
format (%s, %d, %3.1f C), and a bare data-text-var shows %s.
- data-text-color, font-size, font-family — the text's colour and font.
- data-color-var — a BOOL; while TRUE the shape is painted with
data-on-fill / data-on-stroke instead of its class colours.
- data-fill-var / data-stroke-var — DWORD colours (16#FFRRGGBB) for any
number of states; the PLC picks the colour.
- data-cds-action — the click, in the button's forms.
cts visu bind --text-var / --color-var / --action set them on a sketch.
Textfield (runtime variable display)¶
<text x="424" y="172" font-size="12"
data-cds-type="textfield" data-width="336" data-height="32"
data-text-var="HMI.Temperature">%3.2f C</text>
data-width / data-height — bounding box (required; <text> has no
native width/height). Give it at least 32px of height, the same floor a button
has; the example's 32 with a 12px font is the comfortable default.
- y is the baseline, not the box top: this field's box starts at
172 - 12 = 160. Size it and place it on the grid through the box top.
- data-text-var — optional runtime variable binding (e.g. HMI.MyVar).
- Text content — the display format string. Put the unit in it (%3.1f C,
%d rpm) rather than adding a separate .caption next to the field: the
field is one box, and a unit parked beside it has to be re-aligned by hand
every time the value's width changes.
- A textfield's size comes from its font-size attribute, and class does
nothing at all on a field. This is the one element where the colour-class
system does not apply: a field takes the native CODESYS textfield style, so
class="value" changes neither its size nor its colour — it is silently
ignored. For a large read-out write font-size="28" (the box top is then
y - 28). If you need a themed colour on a field, you cannot have one; use a
<text> label beside it, or accept the native style.
GVL variable declarations (auto-generated)¶
Every variable referenced by a control element must have a corresponding
declaration in a GVL (Global Variable List) .st file. The transpiler can
auto-generate these when you pass the --gvl flag:
cts visu from-svg --svg <screen>.svg --screen <ScreenName> --gvl VisuVars
Auto-detected sources:
- data-text-var="HMI.MyVar" → MyVar declared in the GVL, typed by the format (%s STRING, %d INT, %f REAL)
- data-cds-tap="TAP HMI.StartPump" → StartPump declared in the GVL
- data-cds-action="..." → variables extracted from action bindings
- data-color-var → BOOL, data-fill-var / data-stroke-var → DWORD
Variables that already exist in the project's GVL files are skipped. If no runtime variables are detected, the GVL file is not created.
If you prefer to create the GVL manually, see the convention below.
Convention¶
- Use
GVL_HMI.stas the default GVL name for HMI-scope variables. Place it under the application folder:project-view/<Device>/Plc Logic/Application/GVL_HMI.st - If a GVL with a specific name already exists in the project (e.g.
GVL_Routing,GVL_Sensors), reuse it and add your HMI variables there rather than creating a duplicate.
Format¶
{attribute 'qualified_only'}
VAR_GLOBAL
// HMI visualization variables
MyVariable : BOOL := FALSE;
MyCounter : INT := 0;
MyValue : REAL := 0.0;
END_VAR
- Begin with
{attribute 'qualified_only'}to prevent name collisions. - Use
VAR_GLOBAL/END_VARblock. - Declare each HMI variable with its correct type and an initial value.
- Add a brief comment describing the variable's purpose.
What must be declared¶
| Source | Attribute | Variable path example | Required GVL declaration |
|---|---|---|---|
Textfield data-text-var |
data-text-var="HMI.Temperature" |
HMI.Temperature |
Temperature : REAL; inside GVL named HMI or directly in GVL_HMI |
Button data-cds-tap action |
data-cds-tap="TAP HMI.StartPump" |
HMI.StartPump |
StartPump : BOOL; inside GVL named HMI or directly in GVL_HMI |
Textfield data-text-var (structured) |
data-text-var="GVL_Sensors.Scale.Q" |
GVL_Sensors.Scale.Q |
Already declared in GVL_Sensors — no new declaration needed (reference existing) |
Rules¶
- Collect all unique variable paths from every control in the SVG.
- A path names its owner in the FIRST segment.
GVL_Sensors.Scale.Qmeans memberQof instanceScale, and whatGVL_Sensorsdeclares isScale.--gvllooks the owner up among the project's GVLs — including variables mapped to hardware asEmitter AT %QX0.3 : BOOL;— and if the owner declares it, the reference is used as written and nothing new is generated. --gvlonly ever declares into the GVL you named. The screen binds the full path you wrote, so a declaration elsewhere cannot satisfy it: puttingQ : BOOL;inVisuVarsgives youVisuVars.Q, whichGVL_Sensors.Scale.Qnever reads. When a reference cannot be served this way the tool prints[WARN] Not declared: <path> — <why>and declines rather than inventing a declaration that compiles to nothing.- Two paths can never share one declaration. Nine lamps on
GVL_Sensors.<instance>.Qare nine references to one existing GVL, not nine newQ : BOOL;lines — that would not compile. - Read the output.
Updated GVL: <path>means declarations were written;All runtime variables already declared; GVL not modifiedmeans every path resolved against an existing GVL and nothing was needed — the normal result when you bind to variables the PLC program already owns.
Signals whose TRUE is not "good"¶
An E-stop wired normally-closed reads TRUE when healthy and FALSE when pressed.
Nothing in the sketch can invert that for you, and a green lamp bound straight to
it is correct but reads backwards to anyone who has not been told. Say it in the
label rather than leaving the operator to infer it — E-Stop (OK when lit) — or
bind the lamp to an inverted flag the PLC already computes. Do not rely on colour
alone to carry the polarity.
Never state a live value in static text¶
A <text> without data-text-var compiles to a Text ID: fixed at import,
identical forever. So E-Stop = Stopped reads "Stopped" while the machine
runs, and 9 sensors — diagnostics active claims a diagnostic that was never
wired. Both pass every geometric check, both are the most confident-looking
thing on the screen, and an operator reads them as instrumentation.
Anything to the right of =, : or — that names a state must be live:
<!-- WRONG: says "Stopped" forever -->
<text class="label" x="616" y="432">E-Stop = Stopped</text>
<!-- RIGHT: the label names the signal, the field or lamp reports it -->
<text class="label" x="616" y="432">E-Stop</text>
<rect data-cds-type="lamp" x="700" y="420" width="16" height="16"
data-color="red" data-var="FIO_SIGNALS.xEStop"/>
A bare state word as a legend is fine and expected — Running beside a lamp
labels it. What is wrong is joining a name to a state, because that is a
reading. cts visu lint reports these as static-state.
Example — GVL_HMI.st for the Pump Station sketch¶
{attribute 'qualified_only'}
VAR_GLOBAL
// HMI visualization variables for Pump Station screen
StartPump : BOOL := FALSE;
StopPump : BOOL := FALSE;
Temperature : REAL := 0.0;
MotorRunning : BOOL := FALSE;
MotorSpeed : INT := 0;
END_VAR
Text & Text-IDs¶
The transpiler automatically allocates Text-IDs for any element with
non-empty text and writes the entry to the project's GlobalTextList. You
do not need to specify a Text-ID yourself.
Actions¶
You may add data-cds-tap for simple native button tap bindings:
<rect data-cds-type="button" data-text="Stop" data-cds-tap="TAP HMI.StopPump" .../>
Use data-cds-action for broader button actions:
<rect data-cds-type="button" data-text="Toggle"
data-cds-action="TOGGLE HMI.StartPump" .../>
<rect data-cds-type="button" data-text="Start"
data-cds-action="OnMouseClick: ST HMI.StartPump := TRUE;" .../>
<rect data-cds-type="button" data-text="Next"
data-cds-action="OnMouseClick: screen CoolingTower" .../>
Compiled forms:
- TAP <variable> -> CODESYS Visu_TapInput: TRUE while the button is held,
FALSE on release (a push button; the logic sees one pulse per press)
- TOGGLE <variable> -> CODESYS Visu_ToggleInput: each press flips it (a switch)
- OnMouseClick/OnMouseDown/OnMouseUp: ST <snippet> -> STSnippet
- OnMouseClick/OnMouseDown/OnMouseUp: toggle <variable> -> ToggleVariable
- OnMouseClick/OnMouseDown/OnMouseUp: screen <screen> -> change screen assignment
Hotkeys are not generated yet; the observed CODESYS XML stores them at screen
level under Hotkeys, not inside the button element.
Unsupported (will raise a clear error)¶
<polygon>,<polyline>,<image>transform, nested<svg>,viewBoxscaling, filters, masks, animation- Gradients (linear/radial, on
<rect>/<circle>/<ellipse>) are supported. stroke-width— a CODESYS line has no width member. Draw a thick run as a<rect>instead.- Table, TabControl, GroupBox, Checkbox, RadioButton, Scrollbar, SpinControl, ProgressBar, InvisibleInput
- Operator data entry. A textfield displays
data-text-var; it cannot be typed into. Only<rect data-cds-type="button">and plain shapes carry input actions, so a screen authored here is read-and-press: the operator can start, stop, toggle and navigate, but cannot key in a setpoint. Write a setpoint from a button instead (data-cds-action="OnMouseClick: ST HMI.Setpoint := 50;"). Adding the writable field in the CODESYS editor works, but do not round-trip that screen:to-svgreads a textfield's geometry, variable and font and not its input action, so the nextfrom-svgcompiles the field back as display-only.
Decompile-only (read by cts visu to-svg, but cannot yet be authored with
cts visu from-svg): slider. Decompiling a real screen emits
<rect data-cds-type="slider" ...>; treat it as read-only when round-tripping.
Frame (embedded sub-visualization / faceplate) and Dialog openers
(open-dialog input-action with optional parameters) are fully supported for
both compile and decompile — see cts guide visu-svg --file advanced-elements.md
for the capture-frame workflow and the data-open-dialog /
data-dialog-param-* authoring syntax.
Example — Pump Station sketch¶
Lints clean. Note the baselines: the field at y="172" has its box top at
172 - 12 = 160, 24px under the y="148" label above it.
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="480">
<!-- header -->
<text class="h1" x="24" y="46">Pump Station 1</text>
<text class="caption" x="616" y="40" data-width="160" text-anchor="middle">Circuit A</text>
<line class="divider" x1="24" y1="72" x2="776" y2="72"/>
<!-- left: machine state -->
<rect class="panel" x="24" y="88" width="368" height="296"/>
<text class="h2" x="40" y="120">Motor</text>
<rect data-cds-type="lamp" x="40" y="144" width="20" height="20"
data-color="green" data-var="HMI.MotorRunning"/>
<text class="label" x="72" y="160">Running</text>
<rect data-cds-type="lamp" x="40" y="176" width="20" height="20"
data-color="red" data-var="HMI.MotorFault"/>
<text class="label" x="72" y="192">Fault</text>
<rect class="card" x="40" y="224" width="336" height="96" rx="4"/>
<rect class="ok" x="40" y="224" width="8" height="96"/>
<text class="label" x="64" y="256">Speed</text>
<text class="value" x="64" y="296">1 480</text>
<text class="caption" x="176" y="295">rpm</text>
<!-- right: live values -->
<rect class="panel" x="408" y="88" width="368" height="296"/>
<text class="h2" x="424" y="120">Temperatures</text>
<text class="label" x="424" y="148">Bearing</text>
<text data-cds-type="textfield" x="424" y="172" data-width="336" data-height="32"
data-text-var="HMI.BearingTemp" font-size="12">%3.1f C</text>
<text class="label" x="424" y="220">Oil</text>
<text data-cds-type="textfield" x="424" y="244" data-width="336" data-height="32"
data-text-var="HMI.OilTemp" font-size="12">%3.1f C</text>
<rect class="alarm" x="424" y="304" width="336" height="48" rx="4"/>
<text class="inverse" x="424" y="328" data-width="336" data-height="16"
text-anchor="middle">Bearing over temperature</text>
<!-- action row -->
<line class="divider" x1="24" y1="392" x2="776" y2="392"/>
<rect data-cds-type="button" x="24" y="408" width="160" height="48"
data-text="Start Pump" data-cds-tap="TAP HMI.StartPump"/>
<rect data-cds-type="button" x="200" y="408" width="160" height="48"
data-text="Stop Pump" data-cds-tap="TAP HMI.StopPump"/>
</svg>