Contents
- Writing Scripts
- colours_script
- boolean_script
- flash_script
- action_script
- Builtin Script Functions
- Accessing Speeduino Data Values
- When scripts are not working
- A practical example of scripts - Shift Lights
- Another example - Current Speed Limit Dynamically Appearing when the Speed Limit is Set
- The Global Script
- The Startup Script
- Page Scripts
Writing Scripts
Writing scripts is more of an advanced topic as you will require some knowledge of programming to do it. The scripting language that I use is PascalScript, which is compiled into byte-code. This is not quite as efficient as “real” machine code so you must be careful about how you use it and how much of it you use on a given page. Adding scripts to certain gauges gives you the option to make them react dynamically in certain ways.
To be able to write scripts you will need to understand the Pascal language. There are many references on the internet you can read to get started and those who are familiar with ‘C’ should be relatively comfortable with it. The syntax is different but the concepts are the same.
Scripts can either be specified directly within the xml file inside the appropriate attribute, or alternatively a filename can be referenced and then the script can be stored in the specified file in the \scripts folder on the SD card. There are two good reasons to use separate files for scripts:
- Any script that is the same across multiple gauges can be written once and then referenced via the filename on every gauge that uses it.
- XML uses the < and > characters a lot, and so any script that needs to perform comparative operations of the form
if (x < 50) then
would need to use the XML escape sequences for these characters which results in text such asif (batteryv < 120) then
, which is pretty awkward to type if you are making manual edits to the XML.
Scripts can’t be just added anywhere; they are used in relation to specific attributes. The following script types are currently supported:
colours_script
This script can be used to dynamically determine a colour selection. For example, you might want to make a Bar Chart gauge which uses the ‘clt’ (coolant temperature attribute) to change its colour based on the coolant temperature. The script can take an infinite number of forms and is not restricted to referencing the attribute that the gauge is displaying.
The colours_script takes the following format (the context here is an attribute on a gauge):
<colours_script>
function colourname : string;
begin
if (batteryv < 120) then
Result := 'cltbad'
else
if (batteryv < 125) then
Result := 'cltwarn'
else
Result := 'cltok'
end;
begin
end.
</colours_script>
As mentioned, you can specify a filename instead like this:
<colours_script>changecolour.pas</colours_script>
When using files rather than inline text, you can use < and > characters directly, instead of using the <
style notation shown above.
And then you would place your function in the changecolour.pas file. There are some example files in the download in the \scripts folder and you will find that these are referenced in the sample XML files that are supplied.
The function must be named “colourname” (and yes, that’s a UK spelling of the word “colour” so colorname will not be recognised) and the function must return a string data type. Strings in PascalScript are delimited with single quotes, not double quotes as in C.
The result of this function should be a colour string, in either of the two colour string formats. You will be able to see above that I have referenced the strings ‘cltok’, ‘cltwarn’ and ‘cltbad’. These strings are colour definitions from the <definitions> section of the XML file.
The following gauges support the colours_script attribute:
- ValueGauge
- RectangleGauge
- VerticalGauge
- HorizontalGauge
- NeedleGauge
boolean_script
The boolean_script attribute is used to determine a “true” or “false” condition. The function takes the following form:
<boolean_script>
function booleanexpression : boolean;
begin
Result := (currentspeedlimit=50);
end;
begin
end.
</boolean_script>
As you can see, the function must return a boolean datatype and must have the name “booleanexpression”. Unlike in a normal application, these functions are all independently compiled so they do not share program space and therefore there can be many functions all called “booleanexpression” attached to many gauges, all with different internal logic.
boolean_script is used by 2 different gauges:
- BooleanGauge. Here the function determines whether the state of the gauge is “on” or “off” via the True or False return value. For example, if you had a boolean gauge to represent a low battery warning light, you could write code to compare the battery voltage with a predefined limit and return true if the current voltage is below the limit. This would turn the “light” on.
- ValueGauge. For a ValueGauge the boolean_script determines the visibility of the gauge. If the script returns true then the gauge is displayed. If the script returns false then the gauge is hidden.
flash_script
The flash_script attribute is used determine the flash rate of a BooleanGauge. The flash period is used when the Gauge is determined to be in the “on” state by some other means (either a bit from an attribute or via a boolean_script) . See the shift light example further down this page for an example which uses both of those scripts together. The script should return the flash period in milliseconds. That period is applied to both the On and Off states so if you have a flash expression that returns 500 then it will take 1 second for the whole flash cycle.
Example
<flash_script>
function flashexpression : integer;
begin
if (rpm > cltshiftlightrpm+800) then
Result := 120
else
Result := 0;
end;
begin
end.
</flash_script>
For this script the function must be called ‘flashexpression’ and must return an integer value. If you set the value to zero this means no flashing, and the boolean gauge would be permanently on if the boolean expression it is based on evaluates to true. The above example is explained in more detail in the example further down this page.
Although I use a different technique myself, it is possible
action_script
The action_script is specifically for use by the TouchGauge.
Example:
<action_script>
program setmode;
begin
currentclustermode := 0;
end.
</action_script>
The action script is defined slightly differently to the others and is defined as a “program” this time. This is because the action_script is not used to control anything else. Instead it is executed when the TouchGauge is actioned (either via a touch screen or via a hardware device i.e momentary button or rotary encoder button). The action_script is executed before the touch action given in the <action> gauge attribute is executed. There are limited uses for this but typically they relate to maintaining state that will be saved between system restarts on a system that has one SDC screen controlling another over a canbus.
Builtin Script Functions
SDC extends PascalScript with a few functions in order to allow for a subset of gauge properties to be manipulated by scripts. These are as below:
- function FindGauge(name : string) : integer
var g : integer; begin g := FindGauge('RPMGAUGE'); SetX(g, 100); end;
This function returns the index of the specified gauge name. The index is required to pass to other functions that can set gauge properties.
- procedure SetColour(gaugeid : integer; colour : string)
Sets the colour of the gauge. The colour value must either be a properly formed colour string, or a definition value that contains a colour string. The definition you use must have the type of ‘string’ and provided that is the case you can use the definition as though it were a variable.g := FindGauge('RPMVALUE'); SetColour(g, '0,255,0,0,255,0'); // bright blue, day and night. SetColour(g, 'red'); // e.g. red is defined as 255,0,0,128,0,0 in a definition.
The above example assumes there is a definition named
red
which is defined as a string and contains a valid colour string e.g. “255,0,0,128,0,0”. -
procedure SetX(gaugeid : integer; x : integer)
Sets the x coordinate of the specified gauge id. You can find the id by using the FindGauge function. The Id can be hard coded but if the order of the gauges in the list (see screen editor) is changed then any hard coded references will suddenly be pointing to different gauges than they used to. -
procedure SetY(gaugeid : integer; y : integer)
Sets the Y coordinate. -
procedure SetWidth(gaugeid : integer; w : integer)
Sets the width -
procedure SetHeight(gaugeid : integer; h : integer)
Sets the height -
procedure SetVisible(gaugeid : integer; v : boolean)
Shows or hides the gauge -
procedure ShowWarning(id : word)
Shows the warning with the specified id. This will only have an effect if there is a composite on the page configured for the specified id. -
procedure CancelWarning(id : word)
Cancels the warning of the specified id. -
procedure SetProperty(gaugeid : integer; propname : string; value : string); Sets the property of gaugeid given by propname to the value given by the value parameter. To find the gauge id, use the FindGauge function. You can hard code the id if you are confident you won’t changed the order of gauges in the future.
- procedure DebugMsg(message : string); This procedure sends a string to the SDC tools application for script debugging purposes. The rate at which this procedure will generate messages is capped at 1 message per second. Any messages sent by your script sooner than that will be silently rejected to avoid scripts running at a high frame rate flooding the network with debug messages. Messages will appear in the Script Information window in SDC Tools.
Accessing Speeduino Data Values
The key feature of scripting support is that within the scripts you can reference the current values of some of the Speeduino dataset. The current set of supported attributes is:
Attribute | Type | Description |
---|---|---|
rpm | word | RPM |
map | word | Manifold Pressure |
tps | byte | TPS raw value. Note latest versions of firmware have a 0.5% scale, meaning a range of 0 to 200. |
o2 | byte | Primary AFR |
dwell | byte | Dwell time ms |
iat | byte | Inlet air temperature. This value is already adjusted by the Speeduino temperature offset so is a ‘sane’ value |
clt | byte | Coolant temperature. This value is already adjusted by the Speeduino temperature offset so is a ‘sane’ value |
batteryv | byte | Battery voltage * 10 |
ve | byte | Current VE |
afrtarget | byte | AFR Target |
advance | byte | Current Advance |
gammae | byte | Total gamma (fuel corrections) |
speedmph | byte | Current speed, copied from whatever the speed source is |
ecuvss | byte | Speeduino ECU VSS (speed) |
gear | byte | Current gear, SDC calculated |
ecugear | byte | Speeduion ECU gear calculated |
oilpressure | byte | Oil pressure |
fuellevel | word | Fuel level |
currentspeedlimit | word | Current speed limit (where set by the user) |
wifistatus | byte | WIFI connection status 1 or 0 |
In addition to this, all of the <definitions> from the end of the XML file are accessible and all entries in the [settings] section of the SDC.ini file are also accessible. The values of Definitions can be updated by the script, although they will go back to their original values after you disconnect the power. Similarly Settings can also be updated by scripts, and these are saved at application shutdown, but this will not happen unless you implement a proper Controlled Shutdown rather than just disconnecting the power.
All of these attibutes appear as ‘global variables’ to the scripts, so you must be careful not to use a name that has already been used when entering a <definition> or a setting.
When scripts are not working
Scripts are compiled at boot time. This means two things:
- If compilation of the script fails, there is no means to report it to you. In order to see any compilation errors you will need Console Access to SDC. An exception to this is that compilation errors are reported when you update the script from the Screen Editor, but under normal running circumstances you won’t be using that.
- The more scripts you have, the longer your cluster will take to boot. In practice you will need a lot of scripts before you start to notice this, but it is worth being aware of it.
It is possible to execute a script from the console to test it. See the Application Console for further details.
A practical example of scripts - Shift Lights
In this example we will use the BooleanGauge to create a set of shift lights which will light up in sequence as the rpm rises, and then when it reaches a critical level the lights will flash. We will also add in some intelligence that makes the shift lights come on earlier when the coolant temperature is low. It would be preferable to do that with oil temperature but the coolant temperature is more readily available in Speeduino.
The first step is to add two definitions to the end of the XML file:
<definitions>
<cltshiftlightrpm pascaltype="word">3000</cltshiftlightrpm>
<baseshiftlightrpm pascaltype="word">4200</baseshiftlightrpm>
</definitions>
Add these definitions with the screen editor if you can; it’s a lot easier.
This specifies that there are two variables available to scripts with names cltshiftlightrpm (value 3000) and baseshiftlightrpm (value 4200) and both have a datatype of “word” which is a 16 bit unsigned quantity. Enough to store values up to 65,535.
Next, we need a pair of bitmaps; one showing a light switched on, and another showing the same light switched off:
We will then define a BooleanGauge for the first of the shift lights. The gauge will be switched to the ‘on’ position using a BooleanScript that checks the rpm is above a certain limit. This example would be the first shift light of several in the sequence:
<gauge>
<name>shiftlight1</name>
<type>BooleanGauge</type>
<x>638</x>
<y>90</y>
<radius>20</radius>
<bitvalue>16</bitvalue>
<boolean_script>
function booleanexpression : boolean;
begin
if (clt < 50) then
cltshiftlightrpm := 3000
else
if (clt < 76) then
cltshiftlightrpm := baseshiftlightrpm - (75 - clt) * 48
else
cltshiftlightrpm := baseshiftlightrpm;
Result := (rpm > cltshiftlightrpm);
end;
begin
end.
</boolean_script>
<flash_script>
function flashexpression : integer;
begin
if (rpm > cltshiftlightrpm+800) then
Result := 120
else
Result := 0;
end;
begin
end.
</flash_script>
<attribute>rpm</attribute>
<colours>green</colours>
<visible>1</visible>
<onbitmap>greenon2.bmp</onbitmap>
<offbitmap>greenoff.bmp</offbitmap>
</gauge>
Three of the attributes defined here - radius, colours, and bitvalue - are mandatory but will not actually be used here because the gauge uses a boolean_script which takes precedence over the bitvalue to determine the boolean state of the gauge, and radius will be ignored because the gauge has <onbitmap> and <offbitmap> images.
As you can see there are two scripts in this example. The first one is the boolean function which determines the state of the gauge (on or off). The second one is the flash frequency which is calculated at either 120 milliseconds or zero, depending upon the RPM.
Explanation
The booleanexpression function tests the coolant temperature (clt). For coolant temperatures below 50, a value of 3000 is assigned to “cltshiftlightrpm” which is the variable added to <definitions> earlier and is available as a global variable to all script functions. For coolant temperatures between 50 and 76, cltshiftlightrpm is set to a sliding scale based on the coolant temperature. Otherwise cltshiftlightrpm is set to the value of baseshiftlightrpm, the other definition that was added. baseshiftlightrpm represents the maximum rpm that the shift lights could start to switch on at.
What this does is to gradually increase the point the first shift light is lit up until the coolant temperature reaches 77c, at which point the first shift light will always come on at whatever the value of “baseshiftlightrpm” is (4200rpm).
The flashexpression, which is also executed every time the gauge is drawn, simply looks at the value of the “cltshiftlightrpm” variable to see if the RPM is now at the upper end of the rev light range (base + 800). This because the implementation that this comes from has a set of shift lights that cover 800rpm in 100rpm intervals (9 lights).
The second shift light
We can now take a look at the second shift light. This one is not quite a copy of the first one, because it takes advantage of the fact that all <definitions> values are global variables, and that scripts for shiftlight1 have already been executed because shiftlight1 appears in the XML file before shiftlight2 and any other subsequent shiftlights. Thus cltshiftlightrpm does not need to be recalculated and it can simply be referenced knowing the value is up to date.
<gauge>
<name>shiftlight2</name>
<type>BooleanGauge</type>
<x>596</x>
<y>150</y>
<radius>20</radius>
<bitvalue>16</bitvalue>
<boolean_script>
function booleanexpression : boolean;
begin
Result := (rpm > cltshiftlightrpm + 100);
end;
begin
end.
</boolean_script>
<flash_script>
function flashexpression : integer;
begin
if (rpm > cltshiftlightrpm+800) then
Result := 120
else
Result := 0;
end;
begin
end.
</flash_script>
<attribute>rpm</attribute>
<colours>green</colours>
<visible>1</visible>
<onbitmap>greenon2.bmp</onbitmap>
<offbitmap>greenoff.bmp</offbitmap>
</gauge>
This time the booleanexpression function checks whether rpm is greater than cltshiftlightrpm+100. In other words, this shift light will be switched on if the rpm is 100rpm greater than the first shift light. Subsequent shift lights would test for +200, +300 etc up to +800 for the ninth shift light, at which point all lights would begin to flash at 0.12s intervals as the “flashexpression” function would suddenly start to return true instead of false.
The full page definition for these shift lights is available in the XML file supplied with the software. There are actually two different pages with shift lights on. The “sport” page has two pairs of lights that start coming on at 3000-4200rpm depending on coolant temperature. The “track” gauge has just one set of 9 lights which are larger in size (using the scaling attributes so that bitmaps can be re-used), and start to come on at anything from 3000 to 5200rpm again based on coolant temperature.
In addition, extra bitmaps have been added so that the lower range lights are green, the mid-range lights are amber, and the higher range lights are red. This is just done by changing the on and off bitmaps for each gauge depending upon its location in the sequence.
Another example - Current Speed Limit Dynamically Appearing when the Speed Limit is Set
This second example uses a BooleanGauge and a ValueGauge to show a speed limit sign with the current speed limit set on it. Since the speed limit could be set automatically via the speed camera data, it means it lights up when a speed camera is in the vicinity.
We need two Gauges. First in the XML file should be the BooleanGauge. It has an ‘on’ bitmap but the ‘off’ bitmap is omitted. This means that when the gauge is in the off state, nothing will be drawn:
<gauge>
<name>currentspeedlimitbackground</name>
<type>BooleanGauge</type>
<x>987</x>
<y>79</y>
<radius>40</radius>
<bitvalue>192</bitvalue>
<xscale>0.7</xscale>
<yscale>0.7</yscale>
<attribute>accessorystatus1</attribute>
<boolean_script>
function booleanexpression : boolean;
begin
Result := (currentspeedlimit <> 0);
end;
begin
end.
</boolean_script>
<colours>blue</colours>
<onbitmap>cancellimit.bmp</onbitmap>
<flashfrequency>0</flashfrequency>
<visible>1</visible>
</gauge>
The boleanexpression this time simply looks at the ‘currentspeedlimit’ global variable and if it is greater than zero the function returns true, otherwise it returns false. The currentspeedlimit global is set internally by SDC either when a manual speed limit is applied (over the canbus or from controlling the device via some form of input - touch or hardware button).
The cancellimit.bmp file looks like this:
i.e. it’s a UK speed limit sign with nothing written on it. Note that this file is a bit large here and it has been scaled down to 0.7 of its original size in the gauge definition. Also note bitvalue, radius, and colours are ignored in this definition.
Next we want a ValueGauge to be added which will show the current speed limit, placed over the top of the sign.
<gauge>
<name>speedlimitvalue</name>
<type>ValueGauge</type>
<x>967</x>
<y>81</y>
<w>50</w>
<h>20</h>
<min>0</min>
<max>250</max>
<initial>0</initial>
<divideby>0</divideby>
<fontsize>40</fontsize>
<attribute>currentspeedlimit</attribute>
<colours>blackblack</colours>
<visible>1</visible>
<units></units>
<justification>center</justification>
<boolean_script>
function booleanexpression : boolean;
begin
Result := (currentspeedlimit <> 0);
end;
begin
end.
</boolean_script>
</gauge>
This gauge uses the ‘currentspeedlimit’ attribute and has the same booleanexpression function that the previous BooleanGauge has. This means that when the speed limit is greater than zero, the value of the attribute the gauge references will be shown. As this value is the current speed limit, it will appear on top of the blank speed limit sign when the limiter goes to a non-zero value. Given the two functions are the same you could save the script into a file in \scripts and reference the filename in the gauge attributes instead of having the script inline.
The <colours> of “blackblack” is a definition which has both the day and night colours set to black.
You can see this example in action on multiple occasions from the playback file that starts playing when the application first boots (see the Getting Started section)
The Global Script
SDC also incorporates support for a global script, which must have the name globalscript.pas. The global script takes the following form:
program globalscript;
begin
end.
The code for your global script goes between the begin
and end
statements. Note that the end statement must have the full stop after it. That is part of the Pascal language.
The global script is executed at a rate of 2hz, so you can use it to perform basic housekeeping or to create behavour that reacts to certain changes in data values. The script is stateless i.e. you can’t declare Pascal variables inside the script and expect their values to be remembered each time the script executes. You can however create a definition in the XML file or a Setting in the ini file and these will be remembered between calls to the script. All engine data items are available too, as they are in any other script.
The global script supports some extended procedure calls;
- showclusterwarning(id : word); This raises the cluster warning with the specified ID. If there is a composite page representing the ID then it will be shown.
- cancelclusterwarning(id : word); This cancels any warnings with the specified ID.
-
log(msg : string); This puts an entry in the log file.
- You can change the current page by assigning a page number to the “currentpage” variable. There is no means to enumerate the individual pages so you must just know the number that needs to be assigned.
- You can set the current speed limit by assigning a value to the “currentspeedlimit” variable.
As with all scripts you must take care not to do too much work in the global script as it will eventually consume too much CPU to the detriment of gauge and data logging performance.
The Startup Script
Unlike the global script which is run every 500ms, the startup script is run only once at the start of execution. It is executed after the pages have been loaded but before any data has been read. So at the moment it executes, all data attributes will be zero. The startup script does not exist by default. To create one, start with the following template:
program startup;
begin
end.
Put your code between the begin
and end
statements. There must be period after the end as that is part of the pascal langauge. Save the file in the scripts folder as startup.pas
and it will be executed next time the system boots.
Page Scripts
Each page can have a page script defined. This script is executed before every frame is drawn. You can use this to prepare values that you want to be updated on the page. For example you might wish to control one or more boolean gauges with a given value or set of values that are computed from the incoming engine data. Although this can be done directly with scripts on certain gauges, a more efficient means might be to compute a value in the page script which can then be shared by all gauges that use it.
Implementing shift lights with a Page Script
The earlier example of shift lights uses the BooleanGauge’s boolean script to determine whether to turn a given light on in the sequence, based on the current engine RPM. There is a script for every light dealing with just what that light requires.
An alternative way of doing this is to define a custom attribute into which we will place a value based on which shift lights will be turned on (i.e. we create a bitmap with it). We can then assign the custom attribute to each of the BooleanGauge shift lights, and select whether the light should be on or not using the ‘bits’ property of the BooleanGauge.
First, here is the page script:
program pagescript;
begin
// calculate a base RPM derived from current coolant temperature.
if (clt < 50) then
cltshiftlightrpm := shiftlightbase - 1000
else
if (clt < 76) then
cltshiftlightrpm := shiftlightbase - (75 - clt) * 40
else
cltshiftlightrpm := shiftlightbase;
// now populate a bitmap for each of the 6 shift lights.
if (rpm > cltshiftlightrpm) then
shiftbits := shiftbits or 1
else
shiftbits := shiftbits and 254;
if (rpm > cltshiftlightrpm+250) then
shiftbits := shiftbits or 2
else
shiftbits := shiftbits and 253;
if (rpm > cltshiftlightrpm+500) then
shiftbits := shiftbits or 4
else
shiftbits := shiftbits and 251;
if (rpm > cltshiftlightrpm+750) then
shiftbits := shiftbits or 8
else
shiftbits := shiftbits and 247;
if (rpm > cltshiftlightrpm+1000) then
shiftbits := shiftbits or 16
else
shiftbits := shiftbits and 239;
if (rpm > cltshiftlightrpm+1250) then
shiftbits := shiftbits or 32
else
shiftbits := shiftbits and 223;
end.
This script does 2 things:
- It calculates the start point of the shift lights based on coolant temperature. This means the lights will come on earlier when the coolant temperature is low. The minimum point is “shiftlightbase-1000”. There is a sliding scale of rpm between 51 and 75 degrees coolant temperature, and then at 76 or above the start point becomes shiftlightbase.
Shiftlightbase is a definition whose datatype is set to ‘word’. You can configure a definition using the Screen Editor, or enter it by editing the XML.
cltshiftlightrpm is also a definition, although its current value is ignored since it is immediately assigned to.
shiftbits is also a definition, with a type of ‘word’. Again, the value of this definition is irrelevant when it is defined as it will be immediately overwritten by the script. - It calculates a bitmap based on RPM with a separation of 250rpm for each of the bits in the map, using the already calculated start point. Each of the ‘if’ statements that alters shiftbits is setting or clearing a bit in the value. This is why the numbers used are in powers of 2.
To get the shift lights to use ‘shiftbits’ you would define the following XML (example for the first shift light):
<gauge>
<name>SHIFTLIGHT0</name>
<type>BooleanGauge</type>
<attribute>shiftbits</attribute>
<x>90</x>
<y>160</y>
<w>20</w>
<h>20</h>
<min>0</min>
<max>1</max>
<initial>0</initial>
<xscale>1.00</xscale>
<yscale>1.00</yscale>
<visible>1</visible>
<colours>0,255,0,0,255,0</colours>
<alpha>255</alpha>
<radius>20</radius>
<bitvalue>1</bitvalue>
<flashfrequency>0</flashfrequency>
<onbitmap>greenon2.bmp</onbitmap>
<offbitmap>greenoff2.bmp</offbitmap>
<flash_script>5kflash.pas</flash_script>
</gauge>
As you can see, this first shiftlight uses bit 0 (bitvalue=1). For the second shiftlight (bit 1) you would use bitvalue=2, for the third shiftlight (bit 2) you would use bitvalue=4 etc.
Note that in this example you still require a separate flash script for each shift light (as per the earlier example), since the flash cannot be driven through a bitmap at this time. This will likely be added in the future.
The advantage of doing shift lights this way is that it is more efficient. There is only one script to run at the beginning of the page draw, instead of running one per shift light. Even though this single script is more or less the sum of the individual scripts in the earlier example, there is a fair amount of processing required to set up execution of a script, so you gain by not having to do this n times, where n is the number of shift lights. This can be an advantage on slower devices.
Additionally if you wanted more than one light to be switched on for the same RPM, then both shift lights could use the same bit, which is again more efficient. I have a page on my own car that does this.
As with other scripts, the page script can be written inline in the xml or alternatively it can be stored in a .pas file in the scripts folder. Using a file enables the same page script to be shared across multiple pages.