Some decoders contain repeated blocks of CVs, for example to define behaviour of several accessories, each controlled by multiple CVs. An advanced turnout decoder may for example define multiple paths, each containing several turnouts and their desired position to form the travel path on the layout.
Although the decoder file must define dozens or even hundreds of CVs and their appearance on panes in total, only a fraction of the CVs or displays are actually unique: the rest can be generated from a template. While creating template, and the transformation recipe is a lot more complex than copy-pasting CV definitions, the benefit is a lot easier maintenance once the hard part is done: each change propagates consistently to all generated parts.
To give some example of simplification possible - let's take the decoder file
Public_Domain_dccdoma_ARD_SCOM_MX.xm
. It configures a decoder, capable of
displaying signal aspects on several signal masts. The configuration contains over 500 of CVs
- yet the basic idea behind the configuration is dead simple:
A few statistics:
For JMRI itself or the speed of DecoderPro operation, these two approaches are the same: the file template is internally transformed (expanded) to the decoder definition XML and processed as if it was written entirely by hand. For maintenance, it is a way easier to maintain ~600 lines of XML than 20600.
JMRI provides an option to apply a XSLT stylesheet to a decoder file, before the file is loaded into DecoderPro and before it is interpreted as CV variables and panels. This allows to hand-write unique CV definitions and their panes, and add generated content where appropriate.
To illustrate the techniques described here, a few example files are provided; all the files are licensed under GNU GPL.
The decoder template should be placed into the xml/decoders folder of the
JMRI installation. It is based on Petr Sidlo's dccdoma.cz - ARD-SCOM-MX
decoder - generates the same decoder panels as the original one (as of 12/2019). The
stylesheet (scom.xsl) should be placed also into
xml/decoder
folder of the JMRI installation.
The template can be processed from the commandline to generate the decoder XML, so you can inspect effects of changing the stylesheet and/or data embedded in the decoder template. The commandline for Linux:
xsltproc scom.xsl decoder-template.xml > decoder-gen.xml
Remember to replace the files with their actual names or locations; for experimenting from the commandline, the best is to place the decoder file template AND its stylesheet to some directory and work in there. Later, move the stylesheet and the template to the folders as described above.
An instruction to process the file as a template must be present in the file, in order to act like a template. Otherwise, JMRI would pick it as just "ordinary" decoder definition - all the display items (see below) "misused" to hold data for template processing would appear in the UI !
The processing instruction must appear at the start of the decoder definition:
<?transform-xslt href="http://jmri.org/xml/decoders/scom.xsl"?>
So the decoder template's header would look like:
<?xml version="1.0" encoding="utf-8"?> <?transform-xslt href="http://jmri.org/xml/decoders/scom.xsl"?> <decoder-config xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://jmri.org/xml/schema/decoder.xsd" showEmptyPanes="no" > <decoder> ...
One of the critical points is how to generate CV numbers or other variable parts: XSLT language provides simple numeric computation, but more sophisticated functions are typically not accessible (by default). Some generated content is composed from a list of strings (i.e. signal aspect names are repeated for each signal masts), and we have to provide such input to the stylesheet. The decoder file is the only input provided for the stylesheet by the JMRI framework.
The decoder template file is still interpreted as a decoder definition
and must adhere strictly to the decoder.xsd
XML schema. For parts that we want
to generate from the template, the prescribed elements have to be carefully
misused to provide
There is a number of ways how to approach the problem, I will present a way I see as more or less clean (although it misuses elements to provide data different than they formally should !). The guide should be seen as a recommendation to keep the generated decoders somewhat consistent. Please do not hesitate to contribute and provide simpler approaches.
Just adding variables is simple, and requires no extra placeholder in the
decoder file. However, the <variables>
element must be
present, so the technique described below for generating variables works. The element could
look like this example:
<variables> <variable CV="1" item="Short Address" default="100" > <splitVal highCV="9" upperMask="XXXXXVVV" factor="1" offset="0" /> <label>Decoder Address:</label> <tooltip>Accessory decoder address. CV1 - LSB. CV9 - MSB.</tooltip> </variable> </variables>
Additional generated content will be appended inside that element.
While variable
element's definition is rather strict, UI
definitions seems most relaxed, so we abuse them. The following section describes some
typical kind of data, how they can be represented in decoder template file,
so the text conforms to the mandatory decoder.xsd
rules. And finally how they
can be accessed from the stylesheet.
All the data (not UI panel definitions) will be placed in a single <pane> element. All panes must be named - the name can be arbitrary, but should be unique so a system-defined pane or a custom real pane is not replaced accidentally. In our example, __Aspects name is used. I recommend to prefix the panel name with two underscores. The pane's name must be used in selectors - so if you invent your own name, replace the text in examples with whatever name you choose.
Each time, a value needs to be read by the stylesheet, it must be selected by an XPath expression. For example:
<xsl:template name="generate-masts"> <xsl:variable name="cvStart" select="string(//pane[name/text() ='__Aspects']/display[@item='mastcount']/@tooltip)"/> <xsl:variable name="outputs" select="string(//pane[name/text() ='__Aspects']/display[@item='outputs']/@label)"/> <xsl:for-each select="//pane[name/text() ='__Aspects']/display[@item='masts']/label"> ... </xsl:for-each> </xsl:template>
The selector always contains the common prefix part, which finds the "data holder" pane within the decoder template file. We can save the typing by passing that element as a parameter:
<xsl:template name="generate-masts"> <xsl:param name="root"/> <xsl:variable name="cvStart" select="string($root/display[@item='mastcount']/@tooltip)"/> <xsl:variable name="outputs" select="string($root/display[@item='outputs']/@label)"/> <xsl:for-each select="$root/display[@item='masts']/label"> ... </xsl:for-each> </xsl:template>
The invocation of such a generating template must pass the parameter:
<xsl:call-template name="generate-masts"> <xsl:with-param name="root" select="//pane[name/text() ='__Aspects']//display[position() = 1]/.."/> </xsl:call-template>
Note the strange suffix. This is because the display items can not be nested directly in the pane element, they have to be in some kind of column, row, group etc. The strange selector at the end will find first nested display element and will take its parent element as the data root.
A global variable can be defined in a similar way - place this element directly as top-level element in the stylesheet:
<xsl:variable name="root" select="//pane[name/text() ='__Aspects']//display[position() = 1]/.."/>
The templates can now reference the root of data by just $root expression.
A constant can be used, e.g. as a maximum count of items, specific CV number, ... I recommend to use display element to define a constant. That element has two free-form attributes: label and tooltip. So we can define actually two constants in a single element! This can be useful, if there are values closely tied together, for example. Constants, that define maximum number of aspects handled by the UI and starting CV can be written as:
<display item="mastcount" label="15" tooltip="128"/>
The "mastcount" is an arbitrary (but unique) name. Name it so after the value's meaning to your decoder. It will be used in selectors to access the value like this:
<xsl:variable name="cvStart" select="string($root/display[@item='mastcount']/@tooltip)"/>
Sometimes a CV (variable, display item) should be generated for e.g. each output identified by a name, or number. The list can be coded as a series of <label> sub-elements of a <display> element:
<display item="masts" tooltip="512"> <label>0</label><label>1</label><label>2</label><label>3</label><label>4</label><label>5</label><label>6</label><label>7</label> <label>8</label><label>9</label><label>10</label><label>11</label><label>12</label><label>13</label><label>14</label><label>15</label> </display>
We then may either iterate those items one by one, or access them by index/position as needed. The following examples selects the masts data item under the data root (see above for data root). For each of the items it calls another template (not shown here), and passes the item's value (encoded into the label element content) to the template as mast parameter:
<xsl:template name="generate-panes"> <xsl:param name="root"/> <xsl:for-each select="$root/display[@item='masts']/label"> <xsl:variable name="mast" select="string(./text())"/> <xsl:call-template name="mast-pane"> <xsl:with-param name="root" select="$root"/> <xsl:with-param name="mast" select="$mast"/> </xsl:call-template> </xsl:for-each> </xsl:template>
Note, that element content is used as a value here - this allows to use all awkward characters like quotes, doublequotes, ">" and other chars not permitted in attributes.
Individual items may be accessed by their index (which is passed as a parameter):
<xsl:template name="generate-one-panes"> <xsl:param name="root"/> <xsl:param name="index"/> <xsl:variable name="mast" select="string($root/display[@item='masts']/label[position() = $index]/text())"/> <xsl:call-template name="mast-pane"> <xsl:with-param name="root" select="$root"/> <xsl:with-param name="mast" select="$mast"/> </xsl:call-template> </xsl:template>
You can easily use the above label list to make a loop from 1 to 15, which directly not possible in XSLT. Instead of controlling the loop by a control index variable, we control the loop by the data that should apply in individual cycle iterations and derive the index variable from them. Here's the modified example:
<xsl:template name="generate-panes"> <xsl:param name="root"/> <-- The loop count is controlled by the number of label variables --> <xsl:for-each select="$root/display[@item='masts']/label"> <xsl:variable name="mast" select="string(./text())"/> <xsl:call-template name="mast-pane"> <xsl:with-param name="root" select="$root"/> <xsl:with-param name="mast" select="$mast"/> <-- We use the current label's element position to derive the "loop control variable" value --> <xsl:with-param name="index" select="./position()"/> </xsl:call-template> </xsl:for-each> </xsl:template>
XSLT language is a declarative one, and variables, once assigned, cannot be changed - so it does not have a loop construct as most programming languages do. Sometimes, a cycle can be more illustratively replaced by iteration over the content. Sometimes it is not possible: truly some fixed number of iterations need to be done, such as generating sequential CVs with the same structure - just the sequence number and the represented function index will differ.
This can be done by tail recursion, which replaces loops by invoking a
template from that template itself. The only caveat is that the number of iterations is
limited to about 100 (?), before the stack space is exhausted. The example
can be found in TamsValleyDepot_QuadLn_s_11.xsl
,
look for template AllLEDGroups
:
<xsl:template name="AllLEDGroups"> <-- Use select="" attribute to pick an initial value for the cycle. Applies if the template does not get parameter on (first) invocation --> <xsl:param name="CV1" select="633"/> <xsl:param name="CV2" select="513"/> <xsl:param name="CV3" select="537"/> <-- This is the loop's control variable --> <xsl:param name="index" select="1"/> <!-- next line controls count --> <xsl:if test="24 >= $index"> <xsl:call-template name="OneLEDGroup"> <xsl:with-param name="CV1" select="$CV1"/> <xsl:with-param name="CV2" select="$CV2"/> <xsl:with-param name="CV3" select="$CV3"/> <xsl:with-param name="index" select="$index"/> </xsl:call-template> <!-- iterate until done --> <-- The if a few lines above makes sure, this call-template will not be executed for i > 24 --> <xsl:call-template name="AllLEDGroups"> <xsl:with-param name="CV1" select="$CV1 +1"/> <xsl:with-param name="CV2" select="$CV2 +1"/> <xsl:with-param name="CV3" select="$CV3 +2"/> <-- Call itself, but with control variable one higher, therefore counting the number of cycles--> <xsl:with-param name="index" select="$index+1"/> </xsl:call-template> </xsl:if> </xsl:template>
The template is likely to have some boilerplate instructions. The following declarations should be at the top, defining how output will be generated:
<?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:db="http://docbook.org/ns/docbook" xmlns:xi="http://www.w3.org/2001/XInclude" exclude-result-prefixes="db"> <xsl:output method="xml" encoding="utf-8" indent="yes"/> <xsl:strip-space elements=""/> <xsl:preserve-space elements="text"/> </xsl:stylesheet>
The following will copy elements, and their attributes to the output:
<xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates select="@*|node()"/> </xsl:copy> </xsl:template>
Variable definitions are usually generated by the stylesheet. Basic and fixed variables
should be provided, as usual, in the <variables>
element. The stylesheet
can then append generated variables at the end:
<xsl:template match="variables"> <variables> <xsl:copy-of select="node()"/> <!-- call-template instructions, that generate the content; example follows --> <xsl:call-template name="generate-masts"> <xsl:with-param name="root" select="//pane[name/text() ='__Aspects']//display[position() = 1]/.."/> </xsl:call-template> <xsl:call-template name="generate-aspects"> <xsl:with-param name="root" select="//pane[name/text() ='__Aspects']//display[position() = 1]/.."/> </xsl:call-template> </variables> </xsl:template>
Note that, in this example, the pane
element with a special name
(__Aspects
) is used as a holder for input data for generation. While
//pane[name/text() == '__Aspects']
selects the data holder, the
//display[position() = 1]/..
selects an element within the
holder pane XML element. Pay attention to typos in the strings, otherwise
the select clauses select empty data, and nothing - or invalid content - will be
generated.
For UI Panels I recommend to replace the data holder with the sequence of generated panels. In my example, data is provided from panel named __Aspects, which we definitely do not want to be displayed in DecoderPro as it ... isn't any UI panel, after all. The following will replace the data holder (a top-level Pane) with panels generated by the stylesheet:
<xsl:template match="pane[name='__Aspects']" priority="100"> <!-- call-template instructions for individual groups of panels to be generated; example follows --> <xsl:call-template name="generate-panes"> <xsl:with-param name="root" select="//pane[name/text() ='__Aspects']//display[position() = 1]/.."/> </xsl:call-template> </xsl:template>
The match
clause will react on the __Aspect
data holder pane
element, but unlike the variables insertion point, no copy instruction is
present. So the old content will be thrown away (entire <pane>
element!), replaced by whatever elements the call-template
instructions
generate.
If part of the generated content does not change from place to place, it
is possible to prepare it as a XML fragment to be included: it won't be a
part of XSL stylesheet with all those strange xsl:xxx instructions, but will stored
as a separate, small and clean bit of XML. This can be useful for choice
values, or even repeated UI panels without variable content. An
example of the usage is again in TamsValleyDepot_QuadLn_s_11.xsl
.
(LedPaneHeader)
Individual variables are generates using xsl:template, but the value part, mostly a choice is included from a separate file. Note that the xi:include will be generated into the resulting XML, so it is the DecoderPro panel reader, who does the content inclusion, not the generator. The template substitutes the variable parts of the definition:
<variable item="Aspect{$index} LED1 Out" CV="{$CV2}" mask="XXXVVVVV" default="0"> <xi:include href="http://jmri.org/xml/decoders/tvd/LedOutput.xml"/> </variable>
There are two important things. When using xi: prefix, that prefix must be declared at the top of the document (maybe in any parent element, but conventionally prefixes are collected at the start). Use exactly the same URL (attribute value), otherwise the instruction won't be recognized.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:db="http://docbook.org/ns/docbook" xmlns:xi="http://www.w3.org/2001/XInclude" -- this is the prefix declaration >
The second issue is that the xi:include must use URLs that JMRI is able to resolve locally. Otherwise, the DecoderPro would attempt to download parts of the definition from the Internet, which requires an online connection - and is slow. The prefix http://jmri.org/xml is guaranteed to resolve to the xml directory of your local JMRI installation. For more mapping, please see other JMRI documentation.