JMRI® is...
DecoderPro®
Advanced
Applications
By the community of JMRI.org:
Tools
JMRI tools for working with your layout:
Layout Automation
Use JMRI to automate parts of your layout and operations:
Supported hardware
JMRI supports a wide range of devices, DCC systems, command stations, networks, and protocols.
JMRI Setup and Installation
JMRI environments...

JMRI Help:

Contents Index
Glossary FAQ

Donate to JMRI Donate to JMRI.org

 

 

 

JMRI: DecoderPro User Guide

Use XSLT Transformation for complex decoders

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 decode 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.

Example files

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.

Apply stylesheet to the decoder file.

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>
...
      

Provide metadata to the stylesheet

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.

Adding Variables

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.

Data holder pane

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.

Passing root of the data

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.

Constants, max/min values, single values

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)"/>          
      

Enumerations, sequences, lists

Sometimes a CV (variable, display item) should be generated for e.g. each output idenfied 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>    
      

Cycles and loops

XSLT language is a declarative one, and variables, once assigned, cannot be changed - so it does not have a loop construct as most programmig 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>
      

Creating the stylesheet

A boilerplate

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>
      

Generating content to the insertion points

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.

Using XML fragments

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 subsitutes 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 (may be 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.