Mark DiVecchio's O-Gauge Train Layouts

1992 Layout Page

2006 Layout Page

2009 Layout Page

P&LE Postcards by Howard Fogg 

Plasticville Buildings

Portable Layout Page

Train Clubs

Bing Track

Remote Train Control Program

YouTube Channel

OOK Radio Support

Technical Videos

3D Prints for my Layout

More RTC Videos

ADPCM - Playing clips from .mth sound files

P&LE McKees Rocks Locomotive Shop
3D Printer Project

White Tower Restaurant
3D Printer Project

RFID Train Detection

Engine and Car Operation
Hints and Tricks

RFID Tag Programmer using PN532

RTC Control Language - Scripting

RTC WiFi Support
Getting rid of (most of) the wires

More RFID Tag Videos

RTC Control Language - Signaling


RTC Control Langauge - Scripting

This Page last updated on .

As Mike Hewett pointed out, one of the limitations of the new (in 2018) "Program Control" ability is that you have to know how to program in C++ and have the complete RTC development environment on your computer. Not at all practical......

(Always email me (markdsilogic.com) if you are going to use this program to be sure that you have the latest version)



To let the user write his own scripts to control the RTC program, I needed a scheme which would read in a script written in a (fairly) simple control language. That script would be able to send commands to the TIU. That scheme was extended to has the RFID based train detectors. This is the third implementation of my Control Program.

Searching on the Internet turned up several possibilities. I was amazed that people had written programs to handle script languages. Its a lot of work. I found several good candidates.

I picked one, called "Lua". From the lua wikipedia page:

Lua (/ˈlə/ LOO; from Portuguese: lua [ˈlu.(w)ɐ] meaning moon)[a] is a lightweight, multi-paradigm programming language designed primarily for embedded use in applications.[2] Lua is cross-platform, since the interpreter is written in ANSI C,[3] and has a relatively simple C API.[4]

Lua was originally designed in 1993 as a language for extending software applications to meet the increasing demand for customization at the time. It provided the basic facilities of most procedural programming languages, but more complicated or domain-specific features were not included; rather, it included mechanisms for extending the language, allowing programmers to implement such features. As Lua was intended to be a general embeddable extension language, the designers of Lua focused on improving its speed, portability, extensibility, and ease-of-use in development.

Lua was created in 1993 by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, and Waldemar Celes, members of the Computer Graphics Technology Group (Tecgraf) at the Pontifical Catholic University of Rio de Janeiro, in Brazil


I've used Lua version 5.3.5 for RTC. There may be newer versions released from time to time.

For information, the other scripting language I looked at was AngelScript. My selection of Lua was that Lua had been compiled using the Borland/Embarcadero C++ compilers. After some testing, I found that I could link the Lua code with the RTC code that I've written. I probably could have gotten AngelScript to link if I spent more time on it.


What is Lua?

From the Lua home page:

Lua is a powerful, efficient, lightweight, embeddable scripting language. It supports procedural programming, object-oriented programming, functional programming, data-driven programming, and data description.

Lua combines simple procedural syntax with powerful data description constructs based on associative arrays and extensible semantics. Lua is dynamically typed, runs by interpreting bytecode with a register-based virtual machine, and has automatic memory management with incremental garbage collection, making it ideal for configuration, scripting, and rapid prototyping.


Whew! A lot of multisyllabic words there. Some other links:

Programming in Lua (First Edition)
Programming in Lua (Fourth Editon on Amazon)
Lua Programming
Lua Documentation
Tutorial

Lua can do very complex things. We only need a small subset of what it can do. I'll describe some of the language below when I show coding examples.

1978 May - Model Railroader

Connecting with RTC

The RTC program is written in Borland/Embarcadero C++. It was easy to link the Lua library into RTC because of all the work done by others and released on the Internet for free use.

From chapter 27 of Programming in Lua by Roberto Ierusalimschy :

Lua is an embedded language. This means that Lua is not a stand-alone application, but a library that we can link with other applications to incorporate Lua facilities into them.

I created a new window on RTC titled "Program Control". The button to open the window is [Prog Ctrl]. On that window, you can select the script file, run it, and monitor its progress. You can run up to 10 independent scripts at the same time by using the [Next] and [Prev] buttons on the Program Control screen or by using the right click popup menu on the [Prog Ctrl] button.

Here is a block diagram of this implementation, it is the same as the Version 2 implementation.



I've used Lua version 5.3.5 for RTC. There may be newer versions released from time to time.

Videos are in this YouTube Playlist.

Background Information

The script language is similar to C or Pascal. You can use the scripting language with or without using my RFID train detectors. As a standard, you should name all of your script files using the ".lua" extension.


The Program Control window

Here are some screen captures of the Program Control window. Before we get into writing scripts, I'll describe the buttons and fields on this window (the progam window layout has changed a little since these screen captures were taken - you should be able to figure it out).

To access the Program Control window, press the [Prog Ctrl] button on the main window to access PC1. You can also right click on the button to access the popup menu. On the popup menu, you can select any of the 10 available PC windows.
The top of the window allows you to select the script file to use. Press the [Browse] button to select a file. You can also use the dropdown button to reselect a previous file. The program expects files to have the ".lua" extension.

The next set of buttons are used to select the function. Press [Run Script] to compile and run a script. Press [Compile Script] to just compile a script. Press [Interpreter] to open an Lua command line interpreter.

Next are the function buttons and the [STOP] button. You have 30 function buttons which you can program to perform operations. Not shown in this older screen capture are three tabs each of which selects 10 function buttons. The [STOP] button stops the script.

On the right are counters. There are 7 counters. The top one always shows the run time in seconds. The next three are used by the RFID readers if they are enabled on the Setup window. There are three more not shown just below these. These 3 and the 3 RFID counters can by used by your script to display information. The [Reset] button, clears the 6 counters to zero. A little further down on this page is a screen capture which shows the 7 counters.

The Messages box displays information from the script using the print() command. [Copy All] copies all of the messages to the Windows clipboard. [Save] writes the messages to a file. [Clear] clears the message box.
The Detected Tags window can be displayed if the RFID system is enabled. It shows all previously detected and newly detected tags. To view the Detected Tags window, press the [Show Tags] button on the Program Control window.

The tab "Engine Data Tags" shows tags that have been programmed with engine/car information. The tab "UID Only Tags" shows tags that have not been programmed and only contain the tag UID.
If you right click on the Script File input box, you will get this popup menu. Select [Edit this Script] to pass the script file to an editor. The program tries to find these four editors in this order - Notepad++, metapad, notepad, and wordpad.

I recommend Notepad++ for editing Lua files as it understands the syntax of Lua and does appropriate syntax highlighting. Notepad and wordpad come with all versions of Windows.

You can remove a script from the dropdown list or completely clear the list.
You can't disconnect from the TIU when a script is running. This message reminds you to press the [STOP] button before you try to disconnect.
This is what the window looks like if you select the "Counter Label Test.lua" script and press the [Compile Script] button. That test script defines 10 helper function buttons and all 6 available counters. I'll describe how to use these below. The newest version of RTC supports 30 buttons spread over 3 tabs.

The message in the Messages window lets you know that the script compiled successfully.

Notice here that the "RFID Enabled" LED is visible and green to indicate that the RFID train detection system is enabled.

[Emergency Stop] sends instructions to all 5 possible TIU's to cut off all power to the track. [Prev] and [Next] show the previous and next Program Control windows. There can be up to 10 of them. [Hide] minimizes the window but does not otherwise affect any running script. [Close] terminates the window and releases all used memory.
This shows the window after you press the [Run Script] button. Messages appear from the script and the counters begin to increment under control of the script.

When you press [Run Script], the script is compiled first so you don't have press [Compile Script].

The Run Time counter turns green to indicate that the script is running. The [STOP] button is enabled.
The user has pressed the [STOP] button and the script has stopped. Shown on the left for the first time are 2 of the 4 available LED's. These can be programmed by the script to display in red, green or yellow along with a label. I'll describe how to use these below.
This shows the window after the [Interpreter] button is pushed. A command input line opens at the bottom of the Messages window. You can type lua expressions and statements and they will be executed. Look at the Interpreter section below for details.

You can use the command input line dropdown list to select previous commands. You can also use the up arrow and down arrow keys.
If you use the InputDropDown,  InputBox or InputQuery functions, a box like this will open where the user can type a response. The window title, prompt, and default value can be programmed.
Here is what the Program Control window will look like if you are not using the RFID train detection system.



Scripts


Script Structure

The script files are plain text. To edit them, you can use notepad, wordpad, metapad or Notepad++. Notepad++ does syntax highlighting and knows the syntax of Lua. That is the one that I use.

This script file uses functions setup(), tag(), loop(), tick() and cleanup(). These functions, like all predefined functions in the RTC control language, are passed the Engine number and the TIU number from the RTC main window. If you don't need these values, you don't need to specify them on the function's definition.


setup(Engine, TIU)  --  is called once at the start, it is passed the engine number (1-99) and the TIU number (1-5) from the main RTC window. This function MUST be present in your script. Return true on success, false on failure or to end the script. If this function returns false, loop() and tag() will not be run. If this function returns falsecleanup() will be run.

Note that the TIU number passed may not the TIU associated with the given engine number. Use GetTIUNo(Engine) to get that TIU number. You should save the Engine and TIU numbers in local variables as you will probably need them in other script functions.


 loop(Engine, TIU)  --  This function is NOT called during setup() or cleanup(). Once setup() completes, this function is called over and over as long it returns true. Return false to break out of the loop (you must still press [STOP] to run cleanup() and end the script). If the function does not appear in your script, RTC does not attempt to call it. loop() is called in both non-RFID and RFID enabled scripts.

loop() will use up 100% of the CPU as it is called over and over. If this function returns true, you should always call Sleep() or Yield() at least once to give other parts of RTC time to run.


 tag(Detector, Reader, EngineNo, TagLocation, CarModel, CarNumber, TagPacket)  --  is called each time a tag is detected by the RFID tag readers. Requires the RFID Train Detection system to be enabled on the setup window. On the Setup window, the check box for [X ] Enable RFID must be checked. Passes in the detector number, the reader number, three decoded fields from the tag packet received and the complete tag packet itself from which you can decode the other fields.
  • Detector - the detector number as coded into the Arduino sketch. Starts at 1. The RFID system can support up to 127 detectors.
  • Reader - the reader number, starts at 0. Each detector can support up to 6 readers.
  • EngineNo - the engine number as coded into the tag. Zero if the tag is not on an engine.
  • TagLocation - the location of the tag as coded into the tag. A number. Look at the "defines.lua" file for values.
  • CarModel - the car model or engine model as coded into the tag. A number. Look at the "defines.lua" file for values.
  • Railroad - the railroad name as coded into the tag. A Number, Look at the "defines.lua" file for values.
  • CarNumber - the 6 character car or engine number as coded into the tag. A string which can contain number or letters. Leading and trailing spaces have been removed.
  • TagPacket - the entire packet of information returned by the detector. 4 bytes of UID followed by 16 bytes of Block 4 data coded into the tag. Look at the RTC_NFC.h file for values.
Return true to continue accepting tag detections, false to quit accepting tag detections (you must still press [STOP] to run cleanup() and end the script).

The helper script "functions.lua" contains functions to help you decode the values passed into the tag() function.


 tick(Engine, TIU)  --  is called approximately once per second. This function is NOT called during setup() or cleanup(). Return true on success, false on failure at which time the tick() function will no longer be called but the rest of the script will continue to run.

RTC does not guarantee this routine is called exactly once per second. If RTC script processing is extensive, a tick may be skipped. Always use RunTime() to get the exact time. If the function does not appear in your script, RTC does not attempt to call it. Do not call the Sleep() function from this function.


 cleanup(Engine, TIU)  --  is called once when the user presses the [STOP] button on the Program Control window. Return true on success, false on failure. If the function does not appear in your script, RTC does not attempt to call it.


event(FromPCNo, P1, P2, P3)  --  is called by any PC window using the SetEvent() function. It is used for commnications between the PC windows. Return true on success, false on failure. "FromPCNo" is the Program Control window # of the script which executed the SetEvent() function. P1, P2 & P3 are integers which are being sent along with the event. Look at the SetEvent() description below for details of how this function is used. If the function does not appear in your script, RTC does not attempt to call it.

Scripts do not stop on their own. Even if loop() or tag() returns false, you need to press [STOP] or call the functions Stop() or EmergencyStop() to terminate the script so you can compile or run another script.

The minimal script (not using RFID) which does nothing but shows the structure of a script file:

Here is a example listing of a simple script DoNothing Loop Test.lua
(this may not be as up to date as the zip file linked to below):

-- Semicolons are not required in Lua code
-- Do Nothing Loop Test
title = "Do Nothing Loop Test"
--
-- This test runs a simple "do nothing" script
--
require("defines");
require("functions");

local MyEngineNo, MyTIUNo;

-- Counter Labels
Counter01Label = "Do Nothing";

--[[---------------------------------------------------------------------------------------]]
function setup(Engine)
-- the setup() function is called once at the beginning of the run
MyEngineNo = Engine; -- Engine number selected on the RTC Main window
MyTIUNo = GetTIUNo(MyEngineNo); -- TIU associated with MyEngineNo
print("setup(): Engine Number " .. MyEngineNo .. " TIU Number " .. MyTIUNo .. " Debug " .. Debug());
-- Do nothing
return true; -- false=setup failed, true=setup succeeded
end
--[[---------------------------------------------------------------------------------------]]
function loop()
-- The loop() function is repeatedly called
-- Always call Sleep() at least once in this function if it returns true
print(string.format("loop %.2f", RunTime()));
Sleep(10);
-- Do nothing
return true; -- false=do not loop, true=continue to loop
end
--[[---------------------------------------------------------------------------------------]]
function cleanup()
-- the cleanup() function is called once when the [STOP] button is pressed
print(string.format("cleanup %.2f", RunTime()));
-- Do nothing
return true; -- false=cleanup failed, true=cleanup succeeded
end
--[[---------------------------------------------------------------------------------------]]
function tick()
-- the tick() function is called once per second
-- do not call Sleep() from this function
print(string.format("tick --> %.2f", RunTime()));
-- Do nothing;
return true; -- false=stop calling tick(), true=continue to tick
end
--[[---------------------------------------------------------------------------------------]]
function DL()
-- the DL() function is called when the Function 1 button is pressed
print("Do Nothing : PCNo = " .. PCNo() .. " Debug " .. Debug());
-- Do almost nothing
BumpCounter(COUNTER01);
return true;
end
Function01Name, Function01Label = DL, 'Print("Do Nothing")';
--[[---------------------------------------------------------------------------------------]]




This script shows the use of a configuration variable, "title. If you assign a string to this variable, it is used to add a meaningful description to the Program Control window. In this example, the Program Control window will display Do Nothing Loop Test. You can also use the function "Title(text, EngineNo)" to change the title during a run and enter an engine number into the EngineNo spinner.

Here is a 2nd example of a script that uses the RFID tag detectors and also does almost nothing:


Here is a example listing of a simple script DoNothing Tag Test.lua
(this may not be as up to date as the zip file linked to below):

-- Semicolons are not required in Lua code
-- Do Nothing Tag Test
title = "Do Nothing Tag Test"
--
-- This test runs a simple "do nothing" script with tag detection enabled
--
require("defines");
require("functions");

local MyEngineNo, MyTIUNo;

-- Counter Labels
Counter01Label = "Do Nothing";

--[[---------------------------------------------------------------------------------------]]
function setup(Engine, TIU)
-- the setup() function is called once at the beginning of the run
MyEngineNo = Engine; -- Engine number selected on the RTC Main window
MyTIUNo = GetTIUNo(MyEngineNo); -- TIU associated with MyEngineNo

print("setup(): Engine Number " .. MyEngineNo .. " TIU Number " .. MyTIUNo .. " Debug " .. Debug());
-- Do nothing
return true; -- false=setup failed, true=setup succeeded
end
--[[---------------------------------------------------------------------------------------]]
function tag(Detector, Reader, EngineNo, TagLocation, CarModel, Railroad, CarNumber, TagPacket)
-- this function is called each time a tag is detected
-- this function always waits until loop() finishes
if (EngineNo > 0) then -- its an engine
print(string.format([[tag(%.2f) : Detector %d Reader %d EngineNo %d TagLocation %d EngineModel %d]],
RunTime(), Detector, Reader, EngineNo, TagLocation, CarModel));
else -- it's a car
print(string.format([[tag(%.2f) : Detector %d Reader %d CarModel %d]], RunTime(),
Detector, Reader, CarModel));
end
-- Do nothing
return true;
end
--[[---------------------------------------------------------------------------------------]]
function cleanup()
-- the cleanup() function is called once when the [STOP] button is pressed
print(string.format("cleanup %.2f", RunTime()));
-- Do nothing
return true; -- false=cleanup failed, true=cleanup succeeded
end
--[[---------------------------------------------------------------------------------------]]
function tick()
-- the tick() function is called once per second
-- do not call Sleep() from this function
print(string.format("tick --> %.2f", RunTime()));
-- Do nothing;
return true; -- false=stop calling tick(), true=continue to tick
end
--[[---------------------------------------------------------------------------------------]]
function DL()
-- the DL() function is called when the Function 1 button is pressed
print("Do Nothing : PCNo = " .. PCNo() .. " Debug " .. Debug());
-- Do almost nothing
BumpCounter(COUNTER01);
return true;
end
Function01Name, Function01Label = DL, 'Print("Do Nothing")';
--[[---------------------------------------------------------------------------------------]]





To use the tick()  function, just define it in your script. It is called approximately once per second.

Here is a example listing of a tick function

--[[---------------------------------------------------------------------------------------]]
function tick()
-- Do not call Sleep() from this function
print("tick " .. RunTime());
-- do something
return true;
end
--[[---------------------------------------------------------------------------------------]]





Functions that do the hard work

Here are the functions available to the script. These functions are included automatically in your script if you use  require("defines") .

The parameters to each function call can be:


Functions that interact with the Program Control window

print(...);   -- displays the arguments in the
    -- Program Control "Messages" Window. Displays in Black.
    -- To display a formated string, use
        print(string.format(fmt, vars, ..))
    -- Multiple arguments, which can be strings, booleans, integers or numbers, are
    -- concatenated for display:
        print("TIU ", TIUNo, " Engine ", EngineNo);
        print("TIU " .. TIUNo .. " Engine " .. EngineNo);
    -- these two print functions calls produce the same result

printc(color, ...);    -- same as print(...) but displays in color
    -- Colors are defined in "defines.lua" and are:
    -- clRed
    -- clGreen
    -- clYellow
    -- clBlue
    -- clFuchsia
    -- clBlack (default if the color is not defined)

BumpCounter(ID);   -- Increments one of the counters on the Program Control window by 1
DecCounter(ID);    -- Decrements one of the counters by 1
GetCounter(ID);    -- Returns the value in one of the counters
ClearCounter(ID);  -- Sets one of the counters to zero.
SetCounter(ID, value);  -- sets one of the counters to the value given
    -- Returns true on success
    -- On Error: returns nil

    -- ID as follows:
    -- COUNTER01 - User defined counter 1
    -- COUNTER02 - User defined counter 2
    -- COUNTER03 - User defined counter 3
    -- COUNTER04 - User defined counter 4
    -- COUNTER05 - User defined counter 5
    -- COUNTER06 - User defined counter 6
    -- ENGINENO - the value in the Engine Number spinner on the Program Control window.
    --
    -- The next counter is used by the program and probably should not be changed by your script
    -- although your script can "GetCounter(ELAPSEDTIME)" it:
    -- ELAPSEDTIME - the run time of your script in seconds. Starts at zero when the [Run Script]
    -- button is pushed.
    --
    -- The next three counters appear on the Tags Detected window:
    -- TAGCNT - the number of tags detected by the tag detection readers during this run
    -- UNIQTAG - the number of unique tags ever detected by the tag detection readers
    -- TAGSUB - the number of UID only tags detected that were replaced by tag data from previous tags
    --
    -- Use this: require("defines") to define these ID's.

SetLED(LED#, State, Caption);    -- There are four LED on the left edge of the Program Control
    -- window (1 thru 4), "State" is "clRed" to set the LED to red, "clGreen" to set the LED to
    -- green or "clYellow" to set the LED to yellow. Use "clClear" to make the LED disappear.
    -- Caption (in quotes) is optional and if given,
    -- sets the text under the LED to that value. There is only
    -- room for 6-8 characters under the LED.

Title(textvalue, EngNo);    -- sets the title bar text on the Program Control window and optionally,
    -- the engine number in the spinner on the Program Control window. If the engine number
    -- is not given, the value in the spinner is not changed. If the textvalue is an empty string,
    -- the title on the Program Control window is not changed.
    -- examples
        Title(title .. " - " .. GetName(EngineNo));
        Title(title .. " - " .. GetName(EngineNo), EngineNo);
    -- You can read back the Engine number from the spinner with:
        var = GetCounter(ENGINENO);

Functions that start up or shutdown engines

NOTE: Commands which startup/shutdown engines may take up to 20 seconds for the startup/shutdown sound sequence to complete. This means you may have put a Sleep() function call before you send more commands to the engine.

"when" is the time in seconds in the future to execute this command. Set it to zero if you want the
function to execute immediately.

GetTIUNo(Engine);    -- returns the TIU Number that is associatd with the given Engine number the last time the
    -- engine was seen by the [READ] button on the RTC Main Window.
    -- Engine number must be between 1 and 99
    -- usage:
        MyEngineNo = Engine;
        MyTIUNo = GetTIUNo(MyEngineNo);

    -- Use the returned TIU Number in the following commands.


First, you need to know how to stop all engines if something goes wrong:

EmergencyStop(message, beep);
    -- Displays optional message if present. Then sends an emergency stop command to all TIU
    -- Always returns false. Causes all TIU to remove power from the track.
    --
Displays optional message if given. Displays message in red.
    -- Second parameter is also optional, set to true to get a "CRITICAL_STOP" beep when stopping.

    -- Usually after this function, you must exit RTC, power down the TIU and then start everything again.
    -- Use only as the parameter for a return statement as:
        return 
EmergencyStop();
    -- example:
      
if (test) then
          -- Send Emergency Stop to all TIU
          return EmergencyStop("Emergency Stop - Engine " .. Engine .. " did not stop on command");   
          end

The next four functions startup/shutdown the engine/lashup after a delay of "when" seconds. The Operations
window is NOT displayed. Unless otherwise specified, these functions return true on success and nil on error. Note that if "when" is greater than zero, a true return value only indicates that the command was successfully added to the Dispatch List for future execution.

StartUp(when, EngineNo, 
TIUNo);
    -- simple starting up of an engine. Use 
EngineStartUp() to get an Operations Window

ShutDown(when, EngineNo, TIUNo);
    -- simple shutting down of an engine. Use EngineShutDown() to close an Operations Window

StartUp(when, LashUpNo, LashUpEngineListTIUNo);
    -- simple starting up of a Lashup.
Use LashUpStartUp() to get an Operations Window

ShutDown(when, LashUpNo, TIUNo);
    -- simple shutting down of a Lashup. Use LashUpShutDown() to close an Operations Window
    -- You might want to do a Feature Reset when you shutdown a lashup. Something like:
    for i,v in ipairs(LashUpEngineList) do
        print(string.format("Feature Reset - Engine %d", v));
        FeatureReset(math.abs(v), MyTIUNo);
        end

The next four functions startup/shutdown the engine/lashup and open/close the Operations Window for the given engine or lashup. These functions take effect immediately and cannot be scheduled for the future.

The effect of having the Operation window displayed is that if you call any function (like Bell(), Whistle(), etc) that has buttons or switches on the window, those buttons or switches will move just as if someone manually activated the button or switch. You can also manually control the engines (but that might mess up the script)

    -- EngineNo is between 1-99
    -- LashUpName is a 16 character name for the Lashup
    -- LashUpNo is between 101-120
    -- LashUpEngineList : a table containing numeric values for Lead EngineNo,
    --    Middle EngineNo's (if any), Trailing EngineNo. Add 128 to number if the
    --    engine is reverse running. usage:  LashUpEngineList = {3,32,21,4+128};
    --    where engine 3 is the lead engine, engines 32 and 21 are middle engines and
    --    engine 4 (running in reverse) is the trailing engine.
    --
    -- Return nil on error
    -- Returns true on sucess

EngineStartUp(EngineNo, 
TIUNo);
    -- Starts a single engine with an Operations window

EngineShutDown(EngineNo, TIUNo);
    -- Shutsdown a single engine and closes the Operations window

LashUpStartUp(LashUpName, LashUpNo
, LashUpEngineListTIUNo);
    -- Starts a lashup with an Operations window

LashUpShutDown(LashUpNo, 
TIUNo);
    -- Shutsdown a lashup and closes the Operations window

Engine Reset Functions

These funtions return true on success and nil on failure unless noted.

FeatureReset(EngineNo, TIUNo);
    -- sends a feature reset command to the engine.

FactoryReset(EngineNo, TIUNo);
    -- sends a factory reset command to the engine.

Functions that control engines

These funtions return true on success and nil on failure unless noted.

If "when" is greater than 0 seconds, a "true" return only means that the command was successfully
queued for future execution.

Commands that are scheduled for future execution never actually return the results of the command itself.

GetTIUNo(EngineNo);    -- returns the TIU Number that is associatd with the given Engine number the last time the
    -- engine was seen by the [READ] button on the RTC Main Window.

    -- Engine number must be between 1 and 99
    -- Use the returned TIU Number in the following commands.

Setting(iSet, when, EngineNo/LashUpNo, TIUNo);
    -- sends the "ab" command with value iSet


Schedule(Command, when, EngineNo/LashUpNo
TIUNo);
    -- sends any Command passed to it. For example "u2", "w4", "s010", etc.


Whistle(bState, when, EngineNo/LashUpNo
TIUNo);
    -- Turns the whistle ON or OFF

Bell(bState, when, EngineNo/LashUpNo
TIUNo);
    -- Turns the bell ON or OFF

Markers(bState, when, EngineNo/LashUpNo
TIUNo);
    -- Turns the marker lights ON or OFF

Beacon(bState, when, EngineNo/LashUpNo
TIUNo);
    -- Turns the beacon ON or OFF

CabChat(bState, when, EngineNo/LashUpNo
TIUNo);
    -- Turns the cab chat ON or OFF

Smoke(bState, when, EngineNo/LashUpNo
TIUNo);
    -- Turns the smoke ON or OFF

ChuffRate(iRate, when, EngineNo/LashUpNo
TIUNo);
    -- Sets the chuff rate

Rate(iAcc, iDec, when, EngineNo/LashUpNo
TIUNo); -- sets the Acceleration and Decelleration
    -- rates of the engine. Rates can be from 1 to 25 Smph/sec. Value of 0 for either means no change.

GetRate(
EngineNo/LashUpNoTIUNo);   -- returns two values, the Acceleration and Decelleration
    -- Rates of the engine.
    -- usage:  Acc, Dec = GetRate(Eng, TIU);

OpenCoupler(bCoupler, when, EngineNo/LashUpNo
TIUNo);
    -- Opens the front or rear coupler
    -- bCoupler is FRONT or REAR

SetDirection(bDIR, when, EngineNo/LashUpNo
TIUNo);
    -- Sets the direction of the engine
    -- bDIR is FORWARD or REVERSE

GetDirection(
Engine, TIUNo);    -- return the direction as an boolean,
    -- true = 
FORWARD, false = REVERSE

Throttle(iMPH, when, EngineNo/LashUpNo
TIUNo);
    -- Sends only a speed command unless the Operations Window is displayed for the engine.
    -- Returns iMPH as an integer on success

SetSpeed(iMPH, when, 
EngineNo/LashUpNoTIUNo);
    -- Changes the speed with all of the appropriate bells and whistles (like SFS and SRS) if AutoBell and/or
    -- AutoWhistle is true (see the next item in this list).
    -- Returns iMPH as an integer on success

AutoBell = true;
AutoWhistle = true;
AutoBell = false;
AutoWhistle = false;

    -- Put these statements at the beginning of your script if you want to force the SetSpeed() command to play all
    -- appropriate bells and whistles when start an engine moving or when you stop it. This setting will
    -- override other AutoBell/AutoWhistle settings. If this statement is not present, the
    -- "Auto Bell" and "Auto Whistle" values from Setup and the "Auto Bell" and "Auto Whistle" values from
    -- the Operations Window will be used.
    -- If all of these setting are false, RTC will just play the standard sounds.


GetSetSpeed(
Engine, TIUNo);    -- return the set speed as an integer in Smph
    -- engine may be accelerating or decelerating to reach this speed

GetSpeed(Engine, 
TIUNo);    -- return the instantaneous speed of
    -- the engine as an integer in Smph


PlaySound(iSoundNo, when, EngineNo/LashUpNo
TIUNo);
    -- Plays the sound given by iSoundNo. The sound number corresponds to the clip
    -- number as shown by my ADPCM program.

Sound
(bState, when, EngineNo/LashUpNo
TIUNo);
    -- Turn the sound 
ON or OFF. Sets Master Volume to 0 (OFF) or 100 (ON)

ProtoWhistle(bState, when, EngineNo/LashUpNo
TIUNo);
    -- Turns the Proto Whistle ON or OFF

SetQuill(iTone, when, 
EngineNo/LashUpNoTIUNo);
    -- sets the quilling whistle tone, iTone has values 0-3
    -- Turn on Proto Whistle first, engine must support this feature


SmokeWhistle
(bState, when, EngineNo/LashUpNoTIUNo);
    -- Turns the Smoke Whistle ON or OFF
    -- engine hardware must support this feature

SwingingBell(bState, when, EngineNo/LashUpNo
TIUNo);
    -- Turns the Swinging Bell ON or OFF
    -- engine hardwaremust support this feature

SetVolume(Chan, Level, when, EngineNo/LashUpNo
TIUNo);
    -- set the Volume Level(0-100) of the channel given by Chan.


GetVolume(Chan, EngineNo, 
TIUNo);
    -- returns the volume level (0-100) of the channel given by Chan as an integer.

    -- To mute the sounds from an engine, first call GetVolume with Chan of MASTER_VOLUME. Save
    -- the volume, then use SetVolume with Chan of Master_VOLUME and Level of zero. To unmute,
    -- use SetVolume with Chan of MASTER_VOLUME and for Level, use the saved volume.


NOTE: Any command without a "when" parameter may take 1-2 seconds to complete. If you need to "Get" the result of a "Set" for confirmation, you should Sleep() for a few seconds before the "Get" command.

Functions that control Accessories and Switches

These funtions return true on success and false on failure unless noted

Accessory(bState, iChan, AIUNo, TIUNo, when);
    -- Set actual accessory state
    -- bState is ON or OFF
    -- iChan is 1 to 10
    -- AIUNo is 1 to 5
    -- TIUNo is 1 to 5
    -- Returns true on success
    -- On Error: returns nil


GetAccStatus(iChan, AIUNo, TIUNo); 
 -- returns the state of the accessory on the
    -- iChan is 1 to 10
    -- AIUNo is 1 to 5
    -- TIUNo is 1 to 5

    -- return true if the accessory is ON
    -- returns false if the accessory is OFF

GetAccName(iChan, AIUNo, TIUNo);   -- returns the name of the accessory on the
    -- given Channel, AIU and TIU as a string.
    -- iChan is 1 to 10
    -- AIUNo is 1 to 5
    -- TIUNo is 1 to 5

    -- returns "*" if the AIU or Channel is not defined.

VirtualAccessory(bState, iChan, AIUNo, TIUNo);
    -- Set virtual accessory state - does not send any commands to the TIU.
    -- Useful to pass information between PC threads. Use GetAccStatus() to read
    -- the position set by this function. You can set Virtual Accessories even in TIU that don't exist.
    -- bState is ON or OFF
    -- iChan is 1 to 10
    -- AIUNo is 1 to 5
    -- TIUNo is 1 to 5
    -- Returns true on success
    -- On Error: returns nil


Switch(bState, iChan, AIUNo, TIUNo, when);
    -- Set acutal switch state
    -- bState as described above, ie : NORMAL or REVERSE
    -- iChan is 1 to 10
    -- AIUNo is 1 to 5
    -- TIUNo is 1 to 5
    -- Returns true on success
    -- On Error: returns nil


GetSwitchPositionNames();    -- returns two values, both strings containing the words used
    -- to indicate the position of switches.
    -- For example "Straight"/"Diverge", "Closed"/
"Open", "Normal"/"Reverse".
    -- The default values used in the program are "Normal"/"Reverse".
    -- usage:
        ClosedName, OpenName = GetSwitchPositionNames();

GetSwitchStatus(iChan, AIUNo, TIUNo);   
-- returns the state of the switch on the
    -- iChan is 1 to 10
    -- AIUNo is 1 to 5
    -- TIUNo is 1 to 5

    -- returns true if the switch is in the Normal position
    -- return false if the swtich is in the Reverse position

GetSwitchName(iChan, AIUNo, TIUNo);   
-- returns the name of the switch on the
    -- given Channel, AIU and TIU as a string.
    -- iChan is 1 to 10
    -- AIUNo is 1 to 5
    -- TIUNo is 1 to 5

    -- returns "*" if the AIU or Channel is not defined.



Siding(bState, iChan, AIUNo, TIUNo, when, EngineNo);
    -- Set siding power by activating an AIU Accessory channel and then sending a ping (track signal or "p4")
    -- command to the engine to switch it from Conventional mode to Command mode.
    -- This command is just like the Accessory() command except that it sends a ping command
    -- after the AIU channel is turned ON.
    -- EngineNo is the engine on the siding about to be powered on or off
    -- bState is ON or OFF
    -- iChan is 1 to 10
    -- AIUNo is 1 to 5
    -- TIUNo is 1 to 5
    -- Returns true on success
    -- On Error: returns nil

Functions that present layout or engine status

RunTime();     -- return value is the Run Time in seconds and tenths of a second as a number since the first
    -- Program Control window was started. This function will return the same value across
    -- all PC windows if called at the same time.
    -- As a "number", it is floating point and should never be compared for
    -- equality (except to 0.0). If you need to do that, convert the number to an integer
    -- with:
math.floor(RunTime() + 0.5). When used with string.format() remember to
    -- use a floating point specifier like "%.2f".
    -- RunTime can be selected on the Debug window to timestamp each debugging message displayed
    -- to that window. You can use RunTime() to timestamp each message to the Program Control Messages
    -- window and then be able to matchup the messages in the two windows.
    --
    -- Note that RunTime is not the same as ElapsedTime (returned by GetCounter(ELAPSEDTIME)).
    -- ElapsedTime is the time in seconds since the [Run Script] button was pressed and will
    -- be different for each Program Control window.

MicroSeconds
();    -- returns the value of the Windows Performance Counter in microseconds
    -- as an integer.
    -- This value does not have an absolute meaning. Always use the difference between
    -- two calls as an elapsed time.

TIUVersion
(TIUNo);  -- return value is the version number of the given TIU as a number

    -- typical values 5.3, 6.01, etc.
    -- returns 0.0 if the TIU is not connected

CommStatus
();  -- return 3 values giving the status of the two ports (USB Serial and WiFi)
    -- used by the RTC program. Returns true or false.
    -- usage:
        RadioPortConnected, TIUConnected, RFIDPortConnected = CommStatus();

GetTagReaders();    -- returns 2 values giving the number of Tag Detectors and the
    -- number of Tag Readers connected. The Tag Reader count is only correct when the tag
    -- detectors answer up with their initial packets. That is only when they are powered
    -- on and waiting for mDNS identification when RTC is started. The Tag Detector count
    -- should be always correct.
    -- usage:
       
NumDetectors, NumReaders = GetTagReaders();

RAM(address, Engine, TIUNo);    -- returns the byte at the RAM address given as address.
    -- The first 1024 bytes (address 0x000 to 0x3FF) can be read.
    -- On error : Returns nil
    -- You can download my documentation for this function here - Engine RAM Mapping.pdf.

RAM2(address, Engine, TIUNo);    -- returns the 2 bytes at the RAM address given as address.
    -- The first 1024 bytes (address 0x000 to 0x3FF) can be read.
    -- On error : Returns nil
    -- You can download my documentation for this function here - Engine RAM Mapping.pdf.

RAM4(address, Engine, TIUNo);    -- returns the 4 bytes at the RAM address given as address.
    -- The first 1024 bytes (address 0x000 to 0x3FF) can be read.
    -- On error : Returns nil
    -- You can download my documentation for this function here - Engine RAM Mapping.pdf.

DTO(Engine, TIUNo);    -- returns the Engine's trip odometer mileage (DTO) as a number
    -- On error : Returns nil

DOD(Engine, 
TIUNo);    -- returns the Engine's odometer mileage (DOD) as a number
    -- On error : Returns nil

DCH(Engine, 
TIUNo);    -- returns the Engine's chronometer in seconds (DCH) as a number
    -- On error : Returns nil

PCNo()
;    -- returns the number of the Program Control window running this script
    -- A number from 1 to 10 as an integer


Info(Engine
);    -- under development -- the more I look at this, the less useful it appears
    -- Returns information about the engine, returns 6 values
    -- 1. Engine Type as an Integer (valid only if you have called GetSpeed() previously)
    -- 2. Scaling Factor as a Number (valid only if you have called DTO() or DOD() previously)
    -- 3. Last Sent Speed Value as an Integer (valid only if you have called SetSpeed() or Throttle() previously)
    -- 4. Motion Sound Request as a Boolean
    -- 5. Auto Bell setting as a Boolean
    -- 6. Auto Whistle setting as a Boolean
    -- Returns nil on error

Ping(Engine, TIUNo);    -- Sends 100 packets to the Engine and returns the number that
    -- were acknowledged by the Engine. Returns an integer between 0 and 100.
    -- On error : Returns nil.

Functions that make script writing easier

Sleep(seconds);   -- causes the script to pause for the given number of seconds. 'seconds' is
    -- a float and and has a 100 nanosecond resoultion. eg: 1, 1.0, 3.545363, 0.1, 0.1111.
    -- Returns true if sleep succeeded, false if sleep was terminated early, nil on error
    -- Any commands which are queued up for execution will run as
    -- scheduled while the script is sleeping.
    -- Pressing the [STOP] button will terminate a sleep in progress but will not stop
    -- queued up commands.
    -- Sleep() is the only function that moves time forward.

Yield();    -- when the script cannot call Sleep() because it must run as fast as possible,
    -- call the Yield() function inside of loops. This will enable the [STOP] button and function
    -- buttons to have an effect. Otherwise, the loop will run so fast that nothing will
    -- be able to break into it.

Beep(sound);    -- play alert or notification sounds (you must have each sound enabled
    -- on your computer or the sounds won't play). Sounds are defined in "defines.lua" and are:
    -- Beep();  play default beep sound
    -- Beep(DEFAULT_BEEP); play default beep sound
    -- Beep(CRITICAL_STOP); play critical stop sound
    -- Beep(QUESTION); play question sound
    -- Beep(EXCLAMATION); play exclamation sound
    -- Beep(ASTERISK); play asterisk sound
    -- returns true on success, false on failure

ClearDispatchList()
;    -- Clears any pending commands in the dispatch list. Useful if
    -- you are exiting on an error condition. Returns true if the Dispatch List processor was
    -- running and was cleared. Returns false if the Dispatch List processor was not running.

CountDispatchList()
;    -- returns the number of entries in the dispatch list. Useful if you want
    -- to wait for the dispatch list to empty (count == 0) on an error condition. Returns false if the
    -- dispatch list is not running

InputBox(Caption, Prompt, Default)
;    -- Displays an input box using Caption and Prompt. Input
    -- area is set to the value in Default. The user can enter a new value and press [OK] or
    -- press [Cancel] to return the default value. Returns the result as a string.
    -- Returns nil if the parameters are invalid.
    -- Use the InputBox function when there is a default value that should be used when the user
    -- chooses the [Cancel] button (or presses [Esc]) to exit the dialog. If the script needs to
    -- know whether the user chooses [OK] or [Cancel], use the InputQuery() function instead.

InputQuery(Caption, Prompt, Default)
;    -- Displays an query box using Caption and Prompt. Input
    -- area is set to the value in Default. The user can enter a new value and press [OK] which
    -- returns the new value as a string. Pressing [Cancel] or [Esc] causes the function to
    -- return false. Returns nil if the parameters are invalid.
    -- If the default value should be used when the user cancels out of the dialog, use InputBox().

InputDropDown(Caption, Prompt, Index, DataTable); -- Displays an input box similar in
    -- operation to the InputQuery() function. Instead of a single input line, a drop down box
    -- is populated with the strings contained in the DataTable and pointing to the 'Index' entry
    -- of the table as the default selection. The user can select one of the items in the drop
    -- down list or enter a new value and press [OK] which returns the value as a string. Pressing
    -- [Cancel] or [Esc] causes the function to return false. Returns nil if the parameters are invalid.
    -- The DataTable must be an ordinary array and can contain up to 99 entires.
    -- Look at the script "InputBox Test.lua" for an example of how to use this function.

Stop
(message, beep);    -- Stop the script and run the function cleanup().
    -- Use only as the parameter for a return statement as:
        return Stop(message);
    -- example:
        if (test) then return Stop("End of Test"); end;
    -- Always returns false. Displays optional message if given. Message is displayed in Red.
    -- Second parameter is also optional, set to true to get a "EXCLAMATION" beep when stopping.
    -- This is for an immediate stop of the script. Usually you must press the [STOP] button
    -- to run cleanup() and end the script.

EmergencyStop(message, beep);
    -- see next section

Debug();   -- returns the value of the global RTC debug level as set on the debug window. If you
    -- don't know how to access the debug window, send me an email. An integer between 0 (no debug
    -- messages) to 9 (full debug messages).
    -- Remember that you can't do this "if (Debug()) then ...." because in Lua, a value of 0 is true.
    -- Instead, do this "if (Debug() > 0) then ....".

Pretty(Var);
   -- returns a string which is a reasonable representation of the contents of the
    -- variable "Var". "Var" can be any Lua type
    -- This function needs defines.lua to be included with
        require("defines");

PrettyPrint(Var);
   -- Calls Pretty(Var) and displays the resultant string in
    -- Messages window. Same as print(Pretty(Var).
    -- This function needs defines.lua to be included with
        require("defines");

PopupMessage(Message, Caption, SecondMessageLine, Delay);    -- Displays a Message in a popup window using
    -- an optional Caption, an optional SecondMessageLine and an optional Delay time in seconds.
    -- If not given, Delay is set to 2 seconds. This is how long the popup is on the display.
    -- If not given, SecondMessageLine is blank.
    -- If not given, Caption is "Information".
    -- At least one parameter, the Message itself, is required.

Functions that return information about Engines and Lashups

GetTIUNo(EngineNo);    -- returns the TIU Number that is associatd with the given Engine number
    -- Engine number must be between 1 and 99
    -- Use the returned TIU Number in any RTC Control Language command.

IsEngineRunning(EngineNo)
;   -- returns true if
    -- the engine was started with StartUpActiveEngines()or EngineStartUp(). Otherwise returns false.
    -- Engines started with StartUp() are not seen by this function
    -- On Error: returns nil

IsLashUpRunning
(LashUpNo
);   -- returns true if
    -- the lashup was started with LashUpStartUp(). Otherwise returns false.
    -- On Error: returns nil

Counts
(
);
    -- returns two values, the number of active engines and the number of inactive engines
    -- usage:  
        ActiveCount, InactiveCount = Counts();
    -- returns nil,nil on error.

Exists(EngineNo)
;   -- returns true if the engine exists, otherwise returns false  (engines 1-99)
    -- returns nil on error

IsActive(EngineNo)
;  -- returns true if the engine is active, otherwise returns false (engines 1-99)
    -- returns nil on error

GetName(EngineNo);
   -- Returns two values, the engine name as a string and the PS
    -- level (2 or 3) as an integer for EngineNo (engines 1-99)
    -- example:
        Name, PS = GetName(Engine);


GetEngineName(CarModel)
; -- In the tag() function, if the EngineNo read from the tag is greater than 0,
    -- use the CarModel value with this function to get a string containing the model name of the
    -- engine, eg "SW1500", "GP7", etc.

GetCarName(CarModel)
; -- In the tag() function, if the EngineNo read from the tag is 0, use the CarModel
    -- value with this function to get a string containing the model name of the car, eg "Boxcar",
    -- "Coach", etc.

GetRailroadName(Railroad)
; -- In the tag() function, use the Railroad value with this function
    -- to get a string containing the railroad name contained in the detected tag, eg "P&LE", etc.

GetEngineType(Engineype)
; -- In the tag() function, use the Engine Type value with this function
    -- to get a string containing the type of engine ("Steam", "Diesel", etc).

GetTagLocation(TagLocation)
; -- In the tag() function, use the Tag Location value with this function
    -- to get a string containing the tag location contained in the detected tag, eg "Front Truck", etc.

GetCouplerLocation(TagLocation);
 -- In the tag() function, use the Tag Location value with this function
    -- to get a string containing the the location of the tag on the car/engine, eg "Front and Rear Couplers", etc.

Send any Command to the TIU

Command(Command, EngineNoTIUNo); -- Send any command to the TIU or Engine. Does not work with Lashups. Command can be any legal command string such as "s0" (set speed to zero), "y2" (set DCS engine 1 as working engine), etc. EngineNo in the parameter list must be the Engine Number whereas any engine number in a command must be a DCS engine number. Commands to the TIU or AIU still require an Engine Number but it is ignored.

Returns 1 value "nil" if the command fails.

Returns 3 values if the command succeeds:
    1. true as a boolean
    2. the number of characters in the response to the command as an integer
    3. the response to the command (from the TIU) as a string

Useage:   Status, CharCount, Response = Command("s0", EngineNo, TIUNo)

Very dangerous in the wrong hands. Can be used to send new commands or commands to features that I haven't implemented yet (Trolley commands come to mind).



First Easy Example

The listing of the script is in the box below. I'm not going to describe everything that you see there but I'll try to point out the major points with comments in the code. 

require(filename) is called in the script. This call reads in the script in the named file (after appending .lua) and processes it as if it were included here. Scripts using the RFID tag readers requires two files : "defines" & "functions". They are shown below also. Note that the "functions" file is only needed if you are processing my RFID tag system. When you create these files, append the file type ".lua" to the names.

print(string) displays the string on the Program Control window's message box. Used to monitor the progress of the script and to show its status.

This example does not use the RFID tag readers. On the Setup window, the check box for [ ] Enable RFID must not be checked.

When you press the [Run Script] button on the Program Control window, the setup() function is processed followed by the loop() function. setup() is run once and loop() is run over and over as long as it return true. Return false to break the loop. When you press the [Stop] button, the loop() function is terminated and the cleanup() function is run.

The Sleep(Delay) causes the script to pause for the given number of seconds.

Here is an example script (not using RFID but uses Helper Functions - see the next section):

Here is a listing of the Startup_shutdown_engine.lua file
(this may not be as up to date as in the zip file linked to below):

-- Semicolons are not required in Lua code
title = "Startup/Shutdown Test"
require("defines");

local MyEngineNo, MyTIUNo;
--[[---------------------------------------------------------------------------------------]]
function setup(Engine)
-- this function is called once when the [Run Script] button is pushed
-- Engine : Engine number selected on the RTC Main window
-- TIU : TIU number selected on the RTC Main window
print("setup()");

MyEngineNo = Engine; -- Engine number selected on the RTC Main window
MyTIUNo = GetTIUNo(MyEngineNo); -- TIU associated with MyEngineNo
-- debug level as selected on the Debug window, values 0 (off) to 9 (maximum)
if (Debug() > 0) then
print(string.format("setup(): Debug level %d", Debug()));
print("setup(): Engine Number " .. MyEngineNo .. " TIU Number " .. MyTIUNo);
end
print("TIU : " .. MyTIUNo);
assert((MyTIUNo >= 1 and MyTIUNo <= 5), "TIU Number out of range");
-- initialize the layout as needed
AC, IAC = Counts(); -- Counts() returns two values
print("TIU" .. MyTIUNo .. ": " .. AC .. " Active Engines - " .. IAC .. " Inactive Engines");
-- switches and accessories
AIUNo = 1;
Switch(STRAIGHT, 1, AIUNo, MyTIUNo, 3, MyEngineNo); -- College East
Switch(STRAIGHT, 2, AIUNo, MyTIUNo, 5, MyEngineNo); -- College West
Sleep(10);
return true; -- false=setup failed, true=setup succeeded
end
--[[---------------------------------------------------------------------------------------]]
function loop()
-- This function is called once after setup() completes and returns false.
-- You must press [STOP] to run cleanup() and finish the script.
-- TIU : TIU number selected on the RTC Main window
-- debug level as selected on the Debug window, values 0 (off) to 9 (maximum)
print(string.format("loop(): EngineNo %d TIUNo %d", MyEngineNo, MyTIUNo));
if (Debug() > 0) then
print("Debug level " .. Debug());
end
StartUp(0, MyEngineNo, MyTIUNo);
Sleep(10);
Whistle(ON, 20, MyEngineNo, MyTIUNo);
Whistle(OFF, 22, MyEngineNo, MyTIUNo);
print(string.format("loop() Sleep(10) Started at %.2f", RunTime()));
Sleep(10);
print(string.format("loop() Sleep(10) Complete at %.2f", RunTime()));

for Count = 1, 2 do
Bell(ON, 15, MyEngineNo, MyTIUNo);
Bell(OFF, 20, MyEngineNo, MyTIUNo);
Sleep(10);
end
-- after this functions returns, the Dispatch List will continue to empty until all commands are sent.
return false; -- false=do not loop, true=continue to loop
end
--[[---------------------------------------------------------------------------------------]]
function cleanup()
-- this function is called once when the user presses the [STOP] button
--print("cleanup()");
ShutDown(0, MyEngineNo, MyTIUNo);
print(string.format("cleanup() Complete at %.2f", RunTime()));
--
return true; -- false=cleanup failed, true=cleanup succeeded
end
--[[---------------------------------------------------------------------------------------]]
function BlowWhistle()
print("BlowWhistle()");
print(string.format("BlowWhistle(): Eng %d TIU %d", MyEngineNo, MyTIUNo));
print("BlowWhistle(): Eng " .. MyEngineNo .. " TIU " .. MyTIUNo .. " Debug " .. Debug());
Whistle(ON, 0, MyEngineNo, MyTIUNo);
Whistle(OFF, 1, MyEngineNo, MyTIUNo);
return true;
end
Function01Name, Function01Label = BlowWhistle, "Blow Whistle";
--[[---------------------------------------------------------------------------------------]]
function SimplePrint()
print("SimplePrint()");
print(string.format("SimplePrint(): Eng %d TIU %d", MyEngineNo, MyTIUNo));
return true;
end
Function02Name, Function02Label = SimplePrint, "Simple Print";
-- you can create 10 functions which get hooked to the 10 button on the Program Control window
--[[---------------------------------------------------------------------------------------]]
function NOP()
print("NOP()");
print(string.format("NOP() : Eng %d TIU %d", MyEngineNo, MyTIUNo));
return true;
end
Function10Name, Function10Label = NOP, "NOP";
--[[---------------------------------------------------------------------------------------]]




Helper Functions

Up to 30 special interactive helper functions can be defined. They are named "Function01" through "Function30". All thirty functions can be accessed through the use of 30 buttons which appear on the Program Control window. You select the buttons in groups of 10 using the tabs above the buttons.  If you press one of these buttons while loop() or tag() is running, the function will be executed after loop() or tag() completes.

These functions, like all predefined functions in the RTC control language, are passed the Engine number and the TIU number from the RTC main window. If you don't need these values, you don't need to specify them on the function's definition.

To use these functions, first define the function in your Lua script:
function NOP(Engine, TIU)
print("NOP() This script does (almost) nothing");
return true;
end
Following the function, define the function name and button label:
Function10Name, Function10Label = NOP, "NOP";
This line tells the Program Control window to put the phrase "NOP" on the button and when the user presses the button, to call the function NOP(Eng, TIU).


Here is a example listing of Helper Functions

--[[---------------------------------------------------------------------------------------]]
function BlowWhistle(Eng, TIU)
print("BlowWhistle()");
print(string.format("BlowWhistle(): Eng %d TIU %d Debug %d", Eng, TIU, Debug()));
print("BlowWhistle(): Eng " .. Eng .. " TIU " .. TIU .. " Debug " .. Debug());
Whistle(ON, 0, Eng, TIU);
Whistle(OFF, 1, Eng, TIU);
return true;
end
Function01Name, Function01Label = BlowWhistle, "Blow Whistle";
--[[---------------------------------------------------------------------------------------]]
function SimplePrint(Eng, TIU)
print("SimplePrint()");
print(string.format("SimplePrint(): Eng %d TIU %d Debug %d", Eng, TIU, Debug()));
return true;
end
Function02Name, Function02Label = SimplePrint, "Simple Print";
-- you can create 30 functions which get hooked to the 30 button on the Program Control window
--[[---------------------------------------------------------------------------------------]]
function NOP(Eng, TIU)
print("NOP() This script does (almost) nothing");
print(string.format("NOP() : Eng %d TIU %d Debug %d", Eng, TIU, Debug()));
return true;
end
Function10Name, Function10Label = NOP, "NOP";
--[[---------------------------------------------------------------------------------------]]


If you use more than 10 buttons, you can setup 3 tabs to make up to 30 buttons available for functions. These statements should be placed in your script before the setup() function.

-- These labels appear on each of the tabs for the Function Buttons
Tab01Label = "Engine"
Tab02Label = "Destination"
Tab03Label = "Spare";


Counters

Six counters can be defined and will appear on the Program Control window. Your script can increment, decrement or set these counters to display information to the user. They are named "COUNTER01" through "COUNTER06".

To use the counters, at the beginning of your script, define the labels which should appear on the Program Control Window
-- Counter Labels
Counter01Label = "Engines Detected";
Counter02Label = "State Machine";

Then in your script, when you want to increment the counters, do this:
BumpCounter(ID);   -- Increments one of the counters on the Program Control window by 1
DecCounter(ID); -- Decrements one of the counters by 1
GetCounter(ID); -- Returns the value in one of the counters
ClearCounter(ID); -- Sets one of the counters to zero.
SetCounter(ID, value); -- sets one of the counters to the value given
Here is an example (fairly useless) script (requires defines.lua):

Here is a listing of the CounterLabelTest.lua script file
(this may not be as up to date as the zip file linked to below):

-- Semicolons are not required in Lua code
-- Counter and Function Label Test
title = "Counter and Function Label Test and Tick Test"
require("defines");

-- Counter Labels
Counter01Label = "Engines Detected";
Counter02Label = "State Machine";
Counter03Label = "Counter03";
Counter04Label = "Counter04";
Counter05Label = "Counter05";

ENGDET = COUNTER01;
SMSTATES = COUNTER02;
EnginesUsed = {"GP38-2", "GP7", "SW1500"};
--[[---------------------------------------------------------------------------------------]]
function setup(Engine, TIU)
for idx,Eng in ipairs(EnginesUsed) do
BumpCounter(ENGDET); -- Engines Detected counter should bump to 3
print(idx .. " Engine " .. Eng);
end
BumpCounter(COUNTER03);
TIUSerialPortConnected, TIUConnected, RFIDSerialPortConnected = CommStatus();
print("TIUSerialPortConnected : " .. tostring(TIUSerialPortConnected) .. " - TIUConnected : " .. tostring(TIUConnected) .. " - RFIDSerialPortConnected : " .. tostring(RFIDSerialPortConnected));
if (RFIDSerialPortConnected) then
print("Turn off RFID Enable and rerun");
return false;
end;

return true;
end
--[[---------------------------------------------------------------------------------------]]
function loop(Engine, TIU)
-- Always call Sleep() at least once in this function.
BumpCounter(SMSTATES); -- bump states counter
Sleep(10);
return true; -- false=do not loop, true=continue to loop
end
--[[---------------------------------------------------------------------------------------]]
function cleanup(Engine, TIU)
BumpCounter(SMSTATES); -- bump states counter
BumpCounter(COUNTER05);
print("Counter01 = " .. GetCounter(COUNTER01));
print("Counter02 = " .. GetCounter(COUNTER02));
print("Counter03 = " .. GetCounter(COUNTER03));
print("Counter04 = " .. GetCounter(COUNTER04));
print("Counter05 = " .. GetCounter(COUNTER05));
print("Counter06 = " .. GetCounter(COUNTER06));
--
return true;
end
--[[---------------------------------------------------------------------------------------]]
function tick(Engine, TIU)
print(string.format("tick %.2f", RunTime()));
BumpCounter(COUNTER04);
return true;
end
--[[---------------------------------------------------------------------------------------]]
Function01Name, Function01Label = loop, "Loop01";
Function02Name, Function02Label = loop, "Loop02";
Function03Name, Function03Label = loop, "Loop03";
Function04Name, Function04Label = loop, "Loop04";
Function05Name, Function05Label = cleanup, "Cleanup05";
Function06Name, Function06Label = setup, "Setup06";
Function07Name, Function07Label = setup, "Setup07";
Function08Name, Function08Label = setup, "Setup08";
Function09Name, Function09Label = setup, "Setup09";
Function10Name, Function10Label = tick, "tick10";
--[[---------------------------------------------------------------------------------------]]





July 1954
by Joe Salame
Model Railroader Magazine

LED's

There are four LED on the left edge of the Program Control window (1 thru 4), "State" is clRed to set the LED to red, clGreen to set the LED to green or clYellow to set the LED to yellow. Use clClear to make the LED disappear. Caption (in quotes) is optional and if given, sets the text under the LED to that value. There is only room for 6-8 characters under the LED.

Use the SetLED()  function as shown in this example script:


Here is a listing of the LED Test.lua script file
(this may not be as up to date as the zip file linked to below):

-- Semicolons are not required in Lua code
title = "LED Test"

SwitchCase = 0;

require("defines");
require("functions");

--[[---------------------------------------------------------------------------------------]]
function setup(Engine, TIU)
-- this function is called once when the [Run Script] button is pushed
print("setup()");
SetLED(1, clGreen, "Speed"); -- set status LED to green
SetLED(2, clClear, "%"); -- set status LED to clear
SetLED(3, clRed, "Error"); -- set status LED to red
SetLED(4, clYellow, "Warning"); -- set status LED to yellow
return true;
end
--[[---------------------------------------------------------------------------------------]]
function loop() -- parameters not used
-- Always call Sleep() at least once in this function since it returns true;
Sleep(1);
-- cycle the LED through different colors and states
if (SwitchCase == 0) then
SwitchCase = 1;
SetLED(1,clYellow);
SetLED(2,clGreen);
SetLED(3,clClear);
SetLED(4,clRed);
return true;
end
if (SwitchCase == 1) then
SwitchCase = 2;
SetLED(1,clRed);
SetLED(2,clYellow);
SetLED(3,clGreen);
SetLED(4,clClear);
return true;
end
if (SwitchCase == 2) then
SwitchCase = 3;
SetLED(1,clClear);
SetLED(2,clRed);
SetLED(3,clYellow);
SetLED(4,clGreen);
return true;
end
if (SwitchCase == 3) then
SwitchCase = 0;
SetLED(1,clGreen);
SetLED(2,clClear);
SetLED(3,clRed);
SetLED(4,clYellow);
return true;
end
return true;
end
--[[---------------------------------------------------------------------------------------]]
function cleanup(Engine, TIU)
--
SetLED(1, clClear);
SetLED(2, clClear);
SetLED(3, clClear);
SetLED(4, clClear);

return true;
end
--[[---------------------------------------------------------------------------------------]]







Interpreter

A Lua interpreter is available when running scripts and when you press the [Interpreter] button. This is an advanced feature that you might find useful as you become more adept at understanding Lua.

A command line input box opens under the Messages window. You can type Lua commands into that box and then press Enter. You can type in any command that will fit in the 511 character input line.

There are two types of input allowed, expressions and statements.

Expressions do not return a value. They perform an operation and discard the result.  In the Lua interpreter, expressions are always preceeded by an equal "=" sign. Examples of expressions are:

    =5
    =654/45343
    =232+445*45

Statements perform assignments and call functions. Examples of statements are:

    aa = 55
    BumpCounter(1)
    print(aa)

The interpreter is useful in helping debug your scripts. For example, if your script is not doing what you expect, you can print out its variables. If your setup() function assigned the parameter Engine to MyEngine,
you can type:

    print(MyEngine)

to have the interpreter display the Engine number in the Messages window. If the EngineNo is wrong, you can change it:

    MyEngine = 22

Real World Example

For my first real script, I took the "Loop Demo" that I showed in the last few videos on my RFID Train Detection web page and converted it to a Lua script named "Three Engine Demo". The listing of the script is in the box below. I'm not going to describe everything that you see there but I tried to point out the major points with comments in the code. This real world example requires the use of my RFID tag detectors. On the Setup window, the check box for [X ] Enable RFID must be checked. The function tag() is called each time a tag is detected.

Here is a schematic of the layout. The RFID tag readers are shown in RED. There are 3 Arduino detectors, two with a single RFID tag reader and one with two RFID tag readers.




Here is the code for the Three Engine Demo (using my RFID train detectors):

Here is a listing of the Three Engine Demo.lua script file
(this may not be as up to date as the zip file linked to below):

-- Semicolons are not required in Lua code
title = "3 Engine Demo"

-- This script is written to use 3 engines. The Engine List on the Main window
-- should have exactly three active engines and the rest should be inactive.

requrie("defines");
require("functions");

MyTIUNo = 99; -- Global TIU number
MyDebug = 0; -- Global Debug Value

DETECTED = true;
NOTDETECTED = false;

STOPDELAY = 2; -- delay from tag detection to engine speed 0 command

-- all tables in Lua are 1 based, that is, T[1] is the first item in the list
EngineDetectionOrder = {0}; -- clear the tables
EnginesUsedNum = {0};
EnginesUsedFlag = {0};
EngineSpeedSave = {0};
Got = {0};
-- Counter Labels
Counter01Label = "Engines Detected";
Counter02Label = "State Machine";
-- table to hold all of the tags detected in sequence
Tags = {};
-- state machine states
IDLE = 0;
DETECTENGINES = 1;
READYGO_3 = 2;
READYGO_3a = 3;
READYGO_3b = 4;
SM = IDLE;

--[[---------------------------------------------------------------------------------------]]
function setup(Engine, TIU)
-- this function is called once when the [Run Script] button is pushed
print("setup()");
-- Engine number selected on the Main window is not used
MyTIUNo = TIU; -- TIU number selected on the Main window
MyDebug = Debug(); -- debug level, values 0 (off) to 9 (maximum)
if (MyDebug > 0) then print(string.format("Debug level %d", MyDebug)); end
if (MyDebug > 0) then print("Debug level " .. tonumber(MyDebug)); end
-- initialize variables
TagCount = 0;
iEngineCount = 0;
iEngineDetectionCount = 0;
iReqEngines = 3; -- number of engines required for this program
UniqueTagCount = 0; -- number of unique tags found

-- make sure that RFID is connected
TIUSerialPortConnected, TIUConnected, RFIDSerialPortConnected = CommStatus();
print("TIUSerialPortConnected : " .. tostring(TIUSerialPortConnected) .. " - TIUConnected : " .. tostring(TIUConnected) .. " - RFIDSerialPortConnected : " .. tostring(RFIDSerialPortConnected));
if (not RFIDSerialPortConnected) then
print("RFID Port must be enabled for this script");
return false; -- false=setup failed
end

math.randomseed(os.time()); -- random seed
assert(true); -- debug only
-- initialize the layout

-- Start All Active Engines - 1-99 (DCS 2-100)
print("Starting All Active Engines");

for iEng = 1, 99 do -- check all possible engines on one TIU
if (Exists(iEng)) then
if (IsActive(iEng)) then
print("Engine " .. iEng .. " Starting");
EngineStartUp(iEng, MyTIUNo);
Sleep(2);
end
end

if (IsEngineRunning(iEng, MyTIUNo)) then -- IsConsistRunning() REQUIRES use of StartUpActiveEngines() or EngineStartUp()
print(string.format("Initalizing Engine %d", iEng));
-- save the numbers of all started engines
iEngineCount = iEngineCount + 1; -- Count the number of engines running
EnginesUsedNum[iEngineCount] = iEng; -- EnginesUsedXXXX[] is 1 based
EnginesUsedFlag[iEngineCount] = NOTDETECTED;-- flag as not yet detected in motion
-- Numberboards
Setting(CMD_NUMBERBOARDS_ON, 18, iEng, MyTIUNo); -- not a dedicated switch
-- Set smoke off
Smoke(OFF, 2, iEng, MyTIUNo);
-- Cab Chatter Off
CabChat(OFF, 3, iEng, MyTIUNo);
-- Set Marker lights on
Markers(ON, 15, iEng, MyTIUNo);
-- Set Acc/Dec rate to 1/1 Smph/sec
Rate(1, 1, 0, iEng, MyTIUNo);
end
end
print(string.format("Engines Started %d", iEngineCount));
-- Start each train moving forward - go slow to help insure tag detection
-- Since we don't know the position of each engine, we start each at exactly
-- the same speed at exactly the same time, once we have detected the engines
-- and located them on the layout, we can vary the movement times and running speeds.
for iDex = 1, iEngineCount do -- for each running engine, set the speed to 12 Smph
local iEng = EnginesUsedNum[iDex]; -- EnginesUsedXXXX[] is 1 based
print(string.format("SetSpeed for Engine : %d", iEng));
SetSpeed(12, 20, iEng, MyTIUNo);
end

-- set State Machine to first state
SM = DETECTENGINES;
return true; -- false=setup failed, true=setup succeeded
end
--[[---------------------------------------------------------------------------------------]]
function tag(Detector, Reader, EngineNo, TagLocation, CarModel, Railroad, CarNumber, TagPacket)
-- this function is called each time a tag is detected
-- Line below shows a different way to quote a string using brackets
if (MyDebug > 4) then Beep(); end; -- sound a tone
if (MyDebug > 1) then
if (EngineNo > 0) then -- its an engine
print(string.format([[tag() : Detector %d Reader %d EngineNo %d TagLocation %d CarModel %d]],Detector, Reader, EngineNo, TagLocation, CarModel));
else -- it's a car
print(string.format([[tag() : Detector %d Reader %d CarModel %d]],Detector, Reader, CarModel));
end
end
-- The complete packet received is in TagPacket
-- (some of the most used fields have already been extracted into the first parameters to tag()
-- First 8 digits are the 4 byte Tag UID in hexidecimal
-- Final 32 digits are the 16 bytes of Block 4 Information read from the Tag in hexidecimal
if (MyDebug > 4) then print(string.format("Packet %s", TagPacket)); end
Tags[string.sub(TagPacket,1,8)] = string.sub(TagPacket,9);
TagCount = TagCount + 1; -- count the number of tags

--[[---------------------------------------------------------------------------------------]]
-- STATE MACHINE
--[[---------------------------------------------------------------------------------------]]

--[[---------------------------------------------------------------------------------------]]
if (SM == IDLE) then -- do nothing
return true;
end
--[[---------------------------------------------------------------------------------------]]
if (SM == DETECTENGINES) then
-- As engines run around the layout, go until all of them are detected by Detector 1 reader 0
-- Save the tag in the tag list
--
-- TBD - add test to be sure that we detected tags from all tag readers
--
-- single toot on engine rear truck detection from all detectors and readers
if (isEngine(EngineNo) and isTagRear(TagLocation)) then
PlaySound(41, 0, EngineNo, MyTIUNo); -- Short Toot
end

-- Schedule a random noise on engine rear truck detection on all detectors and all readers
if (isEngine(EngineNo) and isTagRear(TagLocation)) then
RandomAction(EngineNo, MyTIUNo);
end

if (isEngine(EngineNo) and isTagFront(TagLocation) and ((isDetector(Detector, 1) and isReader(Reader, 0)))) then
for iDex = 1, iEngineCount do
-- EnginesUsedXXX[] is 1 based
if (EnginesUsedNum[iDex] == EngineNo) then -- is the engine in the Engine List?
if (EnginesUsedFlag[iDex] == DETECTED) then -- has it already been detected (then all engines looped)
-- already detected so we have detected them all
print ("All Engines Detected");
-- speed up the engines to 20 Smph for the first loop
-- make the speed up from 12 to 20 more gradual
for jDex = 1, iEngineDetectionCount do
SetSpeed(15, 3, EngineDetectionOrder[jDex], MyTIUNo);
SetSpeed(18, 6, EngineDetectionOrder[jDex], MyTIUNo);
SetSpeed(20, 9, EngineDetectionOrder[jDex], MyTIUNo);
end
-- at this point, EngineDetectionOrder[1] is just beyond Detector 1 Reader 0
Got = {false, false, false}; -- set all Got[] to false
-- Require three engines to run this script
if (iEngineCount == 3) then
SM = READYGO_3;
print("Three Engines Detected");
else
SM = IDLE;
print("Three Engines Not Detected, setting state machine to IDLE");
end
PFADelay = 0;
Detector1Passenger = false;
-- All engines have been detected and we know their order on the layout
return true;
end
-- At this point, we have detected a new engine
EnginesUsedFlag[iDex] = DETECTED; -- flag as detected in motion - EnginesUsed[] is 0 based
-- EngineDetectionOrder[] is 1 based
iEngineDetectionCount = iEngineDetectionCount + 1; -- Count number of detected engines
EngineDetectionOrder[iEngineDetectionCount] = EngineNo; -- remember the detection order
print(string.format("Engine # %d detected (Index : %d)", EngineNo, iEngineDetectionCount));
BumpCounter(COUNTER01); -- bump the Engines Detected Counter on the Program Control window
return true;
end
end
end
-- continue in this state
return true;
end
--[[---------------------------------------------------------------------------------------]]
if (SM == READYGO_3) then
-- All engines are detected, their order is known - stop them over a reader
-- This should pick off the engines starting at the last one. This would make
-- sure that if two engines are between detectors, we pick them off from the last one
--
-- The state machine will now loop over three states, READYGO_3, READYGO_3a, and READYGO_3b.
-- Each state will move the engine up to the next detector and then stop it there.
--
-- Schedule a random noise on engine rear truck detection on all detectors and all readers
if (isEngine(EngineNo) and isTagRear(TagLocation)) then
RandomAction(EngineNo, MyTIUNo);
end

-- run first [1] engine to Detector 3 Reader 0
-- Detect both front and rear tags as a double check to make sure we detected an engine
if (isEngine(EngineNo) and ((isDetector(Detector, 3) and isReader(Reader, 0)))) then
if (EngineNo == EngineDetectionOrder[1]) then
if (Got[1]) then Throttle(0, STOPDELAY, EngineDetectionOrder[1], MyTIUNo); end -- already detected set Throttle to 0
if (not Got[1]) then SetSpeed(0, STOPDELAY, EngineDetectionOrder[1], MyTIUNo); end
Got[1] = true;
end
end

-- run second [2] engine to Detector 2 Reader 0 or Reader 1
-- Detect both front and rear tags as a double check to make sure we detected an engine
if (isEngine(EngineNo) and ((isDetector(Detector, 2) and isReader(Reader, 0)) or (isDetector(Detector, 2) and isReader(Reader, 1)))) then
if (EngineNo == EngineDetectionOrder[2]) then
if (Got[2]) then Throttle(0, STOPDELAY, EngineDetectionOrder[2], MyTIUNo); end -- already detected set Throttle to 0
if (not Got[2]) then SetSpeed(0, STOPDELAY, EngineDetectionOrder[2], MyTIUNo); end
Got[2] = true;
end
end

-- run third [3] engine to Detector 1 Reader 0
-- Detect both front and read tags as a double check to make sure we detected an engine
if (isEngine(EngineNo) and ((isDetector(Detector, 1) and isReader(Reader, 0)))) then
if (EngineNo == EngineDetectionOrder[3]) then
if (Got[3]) then Throttle(0, STOPDELAY, EngineDetectionOrder[3], MyTIUNo); end -- already detected set Throttle to 0
if (not Got[3]) then SetSpeed(0, STOPDELAY, EngineDetectionOrder[3], MyTIUNo); end
Got[3] = true;
end
if (isCarModel(CarModel, GP7) and isTagFront(TagLocation)) then -- Special sounds for GP7 #1501
local iDly = GP7PFA(0, EngineNo, MyTIUNo);
if (iDly > PFADelay) then
PFADelay = iDly;
end
end
-- set flag if a passenger train being pulled by a GP7 was detected
if (isCarModel(CarModel, GP7)) then
Detector1Passenger = true;
else
Detector1Passenger = false;
end
end

if (Got[1] and Got[2] and Got[3]) then -- set speed for all engines and go to next state
if (math.random(5) == 1) then -- Special sounds for SW1200 #1208 Engine #4
local iDly = SW1200PFA(0, 4, MyTIUNo);
if (iDly > PFADelay) then
PFADelay = iDly;
end
end
-- Counter to keep track of States
BumpCounter(COUNTER02); -- bump the Counter02 on the Program Control window

SetSpeed(15 + math.random(5), PFADelay + 8 + math.random(20), EngineDetectionOrder[1], MyTIUNo); -- @Detector 3 Reader 0
SetSpeed(15 + math.random(5), PFADelay + 8 + math.random(20), EngineDetectionOrder[2], MyTIUNo); -- @Detector 2 Reader 0
SetSpeed(15 + math.random(5), PFADelay + 8 + math.random(20), EngineDetectionOrder[3], MyTIUNo); -- @Detector 1 Reader 0
--
-- if a passenger train is leaving Detector 1 Reader 0, set switches to diverge
-- otherwise, set the switches to straight
-- Both after the appropriate delay to let the trains get underway
if (Detector1Passenger) then
-- set the switches to diverge, Engine Number is not used but must be specified
print("College East & West set to THROWN/DIVERGE in " .. PFADelay + 8 + 35 + 20);
Switch(DIVERGE, 1, 1, PFADelay + 8 + 35 + 20, EngineNo, MyTIUNo); -- College East
Switch(DIVERGE, 1, 2, PFADelay + 8 + 35 + 23, EngineNo, MyTIUNo); -- College West
Detector1Passenger = false;
else
-- set the switches to straight, Engine Number is not used but must be specified
print("College East & West set to CLOSED/STRAIGHT in " .. PFADelay + 8 + 35 + 20);
Switch(STRAIGHT, 1, 1, PFADelay + 8 + 35 + 20, EngineNo, MyTIUNo); -- College East
Switch(STRAIGHT, 1, 2, PFADelay + 8 + 35 + 23, EngineNo, MyTIUNo); -- College West
end
Got = {false, false, false}; -- set all Got[] to false
SM = READYGO_3a; -- go to next state
PFADelay = 0;
end
-- continue in this state
return true;
end
--[[---------------------------------------------------------------------------------------]]
if (SM == READYGO_3a) then

-- All engines are detected, their order is known - stop them over a reader
-- Schedule a random noise on engine rear truck detection on all detectors and all readers
if (isEngine(EngineNo) and isTagRear(TagLocation)) then
RandomAction(EngineNo, MyTIUNo);
end

-- run first [1] engine to Detector 1 Reader 0
-- Detect both front and rear tags as a double check to make sure we detected an engine
if (isEngine(EngineNo) and ((isDetector(Detector, 1) and isReader(Reader, 0)))) then
if (EngineNo == EngineDetectionOrder[1]) then
if (Got[1]) then Throttle(0, STOPDELAY, EngineDetectionOrder[1], MyTIUNo); end -- already detected set Throttle to 0
if (not Got[1]) then SetSpeed(0, STOPDELAY, EngineDetectionOrder[1], MyTIUNo); end
Got[1] = true;
end
if (isCarModel(CarModel, GP7) and isTagFront(TagLocation)) then -- Special sounds for GP7 #1501
local iDly = GP7PFA(0, EngineNo, MyTIUNo);
if (iDly > PFADelay) then
PFADelay = iDly;
end
end
-- set flag if a passenger train being pulled by a GP7 was detected
if (isCarModel(CarModel, GP7)) then
Detector1Passenger = true;
else
Detector1Passenger = false;
end
end

-- run second [2] engine to Detector 3 Reader 0
-- Detect both front and read tags as a double check to make sure we detected an engine
if (isEngine(EngineNo) and ((isDetector(Detector, 3) and isReader(Reader, 0)))) then
if (EngineNo == EngineDetectionOrder[2]) then
if (Got[2]) then Throttle(0, STOPDELAY, EngineDetectionOrder[2], MyTIUNo); end -- already detected set Throttle to 0
if (not Got[2]) then SetSpeed(0, STOPDELAY, EngineDetectionOrder[2], MyTIUNo); end
Got[2] = true;
end
end
-- run third [3] engine to Detector 2 Reader 0 or Reader 1
-- Detect both front and read tags as a double check to make sure we detected an engine
if (isEngine(EngineNo) and ((isDetector(Detector, 2) and isReader(Reader, 0)) or (isDetector(Detector, 2) and isReader(Reader, 1)))) then
if (EngineNo == EngineDetectionOrder[3]) then
if (Got[3]) then Throttle(0, STOPDELAY, EngineDetectionOrder[3], MyTIUNo); end -- already detected set Throttle to 0
if (not Got[3]) then SetSpeed(0, STOPDELAY, EngineDetectionOrder[3], MyTIUNo); end
Got[3] = true;
end
end

if (Got[1] and Got[2] and Got[3]) then -- set speed for all engines and go to next state
-- Counter to keep track of States
BumpCounter(COUNTER02); -- bump the Counter02 on the Program Control window

SetSpeed(14 + math.random(5), PFADelay + 8 + math.random(20), EngineDetectionOrder[1], MyTIUNo); -- @Detector 1 Reader 0
SetSpeed(14 + math.random(5), PFADelay + 8 + math.random(20), EngineDetectionOrder[2], MyTIUNo); -- @Detector 3 Reader 0
SetSpeed(14 + math.random(5), PFADelay + 8 + math.random(20), EngineDetectionOrder[3], MyTIUNo); -- @Detector 2 Reader 0
--
-- if a passenger train is leaving Detector 1 Reader 0, set switches to diverge
-- otherwise, set the switches to straight
-- Both after the appropriate delay to let the trains get underway
if (Detector1Passenger) then
-- set the switches to diverge, Engine Number is not used but must be specified
print("College East & West set to THROWN/DIVERGE in " .. PFADelay + 8 + 35 + 20);
Switch(DIVERGE, 1, 1, PFADelay + 8 + 35 + 20, EngineNo, MyTIUNo); -- College East
Switch(DIVERGE, 1, 2, PFADelay + 8 + 35 + 23, EngineNo, MyTIUNo); -- College West
Detector1Passenger = false;
else
-- set the switches to straight, Engine Number is not used but must be specified
print("College East & West set to CLOSED/STRAIGHT in " .. PFADelay + 8 + 35 + 20);
Switch(STRAIGHT, 1, 1, PFADelay + 8 + 35 + 20, EngineNo, MyTIUNo); -- College East
Switch(STRAIGHT, 1, 2, PFADelay + 8 + 35 + 23, EngineNo, MyTIUNo); -- College West
end
Got = {false, false, false}; -- set all Got[] to false
SM = READYGO_3b; -- go to next state
PFADelay = 0;
end

-- continue in this state
return true;
end
--[[---------------------------------------------------------------------------------------]]
if (SM == READYGO_3b) then
-- All engines are detected, their order is known - stop them over a reader
-- Schedule a random noise on engine rear truck detection on all detectors and all readers
if (isEngine(EngineNo) and isTagRear(TagLocation)) then
RandomAction(EngineNo, MyTIUNo);
end

-- run first [1] engine to Detector 2 Reader 0 or Reader 1
-- Detect both front and rear tags as a double check to make sure we detected an engine
if (isEngine(EngineNo) and ((isDetector(Detector, 2) and isReader(Reader, 0)) or (isDetector(Detector, 2) and isReader(Reader, 1)))) then
if (EngineNo == EngineDetectionOrder[1]) then
if (Got[1]) then Throttle(0, STOPDELAY, EngineDetectionOrder[1], MyTIUNo); end -- already detected set Throttle to 0
if (not Got[1]) then SetSpeed(0, STOPDELAY, EngineDetectionOrder[1], MyTIUNo); end
Got[1] = true;
end
end

-- run second [2] engine to Detector 1 Reader 0
-- Detect both front and read tags as a double check to make sure we detected an engine
if (isEngine(EngineNo) and ((isDetector(Detector, 1) and isReader(Reader, 0)))) then
if (EngineNo == EngineDetectionOrder[2]) then
if (Got[2]) then Throttle(0, STOPDELAY, EngineDetectionOrder[2], MyTIUNo); end -- already detected set Throttle to 0
if (not Got[2]) then SetSpeed(0, STOPDELAY, EngineDetectionOrder[2], MyTIUNo); end
Got[2] = true;
end
if (isCarModel(CarModel, GP7) and isTagFront(TagLocation)) then -- Special sounds for GP7 #1501
local iDly = GP7PFA(0, EngineNo, MyTIUNo);
if (iDly > PFADelay) then
PFADelay = iDly;
end
end
-- set flag if a passenger train being pulled by a GP7 was detected
if (isCarModel(CarModel, GP7)) then
Detector1Passenger = true;
else
Detector1Passenger = false;
end
end

-- run third [3] engine to Detector 3 Reader 0
-- Detect both front and read tags as a double check to make sure we detected an engine
if (isEngine(EngineNo) and ((isDetector(Detector, 3) and isReader(Reader, 0)))) then
if (EngineNo == EngineDetectionOrder[3]) then
if (Got[3]) then Throttle(0, STOPDELAY, EngineDetectionOrder[3], MyTIUNo); end -- already detected set Throttle to 0
if (not Got[3]) then SetSpeed(0, STOPDELAY, EngineDetectionOrder[3], MyTIUNo); end
Got[3] = true;
end
end

if (Got[1] and Got[2] and Got[3]) then -- set speed for all engines and go to next state
-- Counter to keep track of States
BumpCounter(COUNTER02); -- bump the Counter02 on the Program Control window
SetSpeed(12 + math.random(5), PFADelay + 8 + math.random(20), EngineDetectionOrder[1], MyTIUNo); -- @Detector 2 Reader 0
SetSpeed(12 + math.random(5), PFADelay + 8 + math.random(20), EngineDetectionOrder[2], MyTIUNo); -- @Detector 1 Reader 0
SetSpeed(12 + math.random(5), PFADelay + 8 + math.random(20), EngineDetectionOrder[3], MyTIUNo); -- @Detector 3 Reader 0
--
-- if a passenger train is leaving Detector 1 Reader 0, set switches to diverge
-- otherwise, set the switches to straight
-- Both after the appropriate delay to let the trains get underway
if (Detector1Passenger) then
-- set the switches to diverge, Engine Number is not used but must be specified
print("College East & West set to THROWN/DIVERGE in " .. PFADelay + 8 + 35 + 20);
Switch(DIVERGE, 1, 1, PFADelay + 8 + 35 + 20, EngineNo, MyTIUNo); -- College East
Switch(DIVERGE, 1, 2, PFADelay + 8 + 35 + 23, EngineNo, MyTIUNo); -- College West
Detector1Passenger = false;
else
print("College East & West set to CLOSED/STRAIGHT in " .. PFADelay + 8 + 35 + 20);
-- set the switches to straight, Engine Number is not used but must be specified
Switch(STRAIGHT, 1, 1, PFADelay + 8 + 35 + 20, EngineNo, MyTIUNo); -- College East
Switch(STRAIGHT, 1, 2, PFADelay + 8 + 35 + 23, EngineNo, MyTIUNo); -- College West
end
Got = {false, false, false}; -- set all Got[] to false
SM = READYGO_3; -- go to next state
PFADelay = 0;
end
-- continue in this state
return true;
end
--[[---------------------------------------------------------------------------------------]]
return true; -- true=continue to process tags, false=stop processing tags
end
--[[---------------------------------------------------------------------------------------]]
function cleanup(Engine, TIU)
-- this function is called once when the user presses the [STOP] button
print("cleanup()");
for idx,Engine in ipairs(EnginesUsedNum) do
print(idx .. " Shutdown Engine " .. Engine);
EngineShutDown(Engine, MyTIUNo);
end
ShowTags(Engine, TIU); -- dump out the Tags table
return true; -- false=cleanup failed, true=cleanup succeeded
end
--[[---------------------------------------------------------------------------------------]]

function GP7PFA(when, EngineNo, TIUNo)
--
-- Run the GP7 PFA sequence
--
-- returns additional delay for PFA on success
--
local PFADelay = when + 38;
SetVolume(ENGINE_VOLUME, 25, when + 10, EngineNo, TIUNo); -- reduce engine volume for the PFA sequence
PlaySound(110, when + 15, EngineNo, TIUNo); -- Now Boarding
PlaySound(111, when + 18, EngineNo, TIUNo); -- Ticketed Passengers Only
PlaySound(112, when + 21, EngineNo, TIUNo); -- Let me help you
PlaySound(113, when + 24, EngineNo, TIUNo); -- Let me take your baggage
PlaySound(114, when + 28, EngineNo, TIUNo); -- Thanks
PlaySound(115, when + 34, EngineNo, TIUNo); -- All Aboard
SetVolume(ENGINE_VOLUME, 100, when + 40, EngineNo, TIUNo);
print("GP7 PFA " .. PFADelay);
return PFADelay;
end
--[[---------------------------------------------------------------------------------------]]

function SW1200PFA(when, EngineNo, TIUNo)
--
-- Run the SW1200 PFA sequence
--
-- returns additional delay for PFA on success
--
local PFADelay = when + 6 + 20 + 73; -- its a long sequence
StartUp(when + 6, EngineNo, MyTIUNo); -- Startup takes 20 seconds
-- when + 6 + 20
PlaySound(80, when + 6 + 20, EngineNo, TIUNo);
PlaySound(81, when + 6 + 20 + 5, EngineNo, TIUNo);
PlaySound(82, when + 6 + 20 + 5 + 10, EngineNo, TIUNo);
PlaySound(83, when + 6 + 20 + 5 + 10 + 3, EngineNo, TIUNo);
PlaySound(84, when + 6 + 20 + 5 + 10 + 3 + 4, EngineNo, TIUNo);
PlaySound(85, when + 6 + 20 + 5 + 10 + 3 + 4 + 4, EngineNo, TIUNo);
PlaySound(86, when + 6 + 20 + 5 + 10 + 3 + 4 + 4 + 5, EngineNo, TIUNo);
PlaySound(87, when + 6 + 20 + 5 + 10 + 3 + 4 + 4 + 5 + 3, EngineNo, TIUNo);
PlaySound(88, when + 6 + 20 + 5 + 10 + 3 + 4 + 4 + 5 + 3 + 5, EngineNo, TIUNo);
PlaySound(89, when + 6 + 20 + 5 + 10 + 3 + 4 + 4 + 5 + 3 + 5 + 5, EngineNo, TIUNo);
PlaySound(90, when + 6 + 20 + 5 + 10 + 3 + 4 + 4 + 5 + 3 + 5 + 5 + 4, EngineNo, TIUNo);
ShutDown (when + 6 + 20 + 5 + 10 + 3 + 4 + 4 + 5 + 3 + 5 + 5 + 4 + 5, EngineNo, TIUNo);
print("A&S SW1200 #1208 PFA " .. PFADelay);
return PFADelay;
end
--[[---------------------------------------------------------------------------------------]]
-----------------------------------------------------------------------------
-- Schedule a Random Noise
--
-- Params:
-- EngineNo
-- TIUNo
--
-- returns true on success
--
-----------------------------------------------------------------------------
function RandomAction(iEngineNo, iTIUNo)
-- Schedule a random noise
local retv = true;

-- pick sound/effect based on a random number
-- can be adjusted if there is too much or too little background noise
--
-- choose from:
-- 1. Turn the bell on for a random number of seconds
-- 2. Blow the horn/whistle for a random number of seconds
-- 3. Play the crossing sound n42
-- 4. Turn the smoke on for a random number of seconds
-- 5. Turn on Cab Chatter for a random number of seconds
-- 6. No sounds
--
local Starttime = 0;
local Endtime = 0;
local switchval = math.random(7); -- returns 1 through 7
if (switchval == 1) then
Starttime = 30 + math.random(10);
Endtime = Starttime + 5 + math.random(5);
Bell(ON, Starttime, iEngineNo, iTIUNo); -- send bell on
Bell(OFF, Endtime, iEngineNo, iTIUNo); -- send bell off
print("RandomAction: Bell");
end
if (switchval == 2) then
Starttime = 25 + math.random(10);
Endtime = Starttime + 3 + math.random(3);
Whistle(ON, Starttime, iEngineNo, iTIUNo); -- send Whistle/Horn on
Whistle(OFF, Endtime, iEngineNo, iTIUNo); -- send Whistle/Horn off
print("RandomAction: Horn/Whistle");
end
if (switchval == 3) then
Starttime = 25 + math.random(11);
PlaySound(42, Starttime, iEngineNo, iTIUNo); -- SXS Toot
print("RandomAction: SXS");
end
if (switchval == 4) then
Starttime = 25 + math.random(10);
Endtime = Starttime + 46;
Smoke(ON, Starttime, iEngineNo, iTIUNo); -- send Smoke on
Smoke(OFF, Endtime, iEngineNo, iTIUNo); -- send Smoke off
print("RandomAction: Smoke");
end
if (switchval == 5) then
Starttime = math.random(10);
if (math.random(2) == 1) then -- math.random returns 1 or 2
CabChat(ON, Starttime, iEngineNo, iTIUNo); -- set Cab Chatter on
print("RandomAction: Cab Chatter On");
else
CabChat(OFF, Starttime, iEngineNo, iTIUNo); -- set Cab Chatter off
print("RandomAction: Cab Chatter Off");
end
end
if (switchval == 6) then
-- No sounds some of the time
print("RandomAction: Quiet");
end
return retv;
end
--[[---------------------------------------------------------------------------------------]]

function SW1200Button(Eng, TIU)
print("SW1200Button()");
SW1200PFA(0, 4, TIU); -- only works with Engine 4, the SW1200
return true;
end
Function01Name, Function01Label = SW1200Button, "SW 1200 PFA";
--[[---------------------------------------------------------------------------------------]]
function GP7Button(Eng, TIU)
print("GP7Button()");
GP7PFA(0, 7, TIU); -- only works with Engines 7 and 13, the GP7's
return true;
end
Function02Name, Function02Label = GP7Button, "GP7 PFA";
--[[---------------------------------------------------------------------------------------]]
function PauseButton(Eng, TIU)
print("PauseButton()");
for idx,Engine in ipairs(EnginesUsedNum) do
S1 = GetSpeed(Engine, MyTIUNo); -- get Speed
print(idx .. " Pause Engine " .. Engine .. " Speed " .. S1);
EngineSpeedSave[idx] = S1;
SetSpeed(0, 0, Engine, MyTIUNo);
end
return true;
end
Function06Name, Function06Label = PauseButton, "Pause";
--[[---------------------------------------------------------------------------------------]]
function ResumeButton(Eng, TIU)
print("ResumeButton()");
for idx,Engine in ipairs(EnginesUsedNum) do
S1 = EngineSpeedSave[idx];
print(idx .. " Resume Engine " .. Engine .. " Speed " .. S1);
SetSpeed(S1, 0, Engine, MyTIUNo);
end
return true;
end
Function07Name, Function07Label = ResumeButton, "Resume";
--[[---------------------------------------------------------------------------------------]]
function ShowTags(Eng, TIU)
print("Tags detected:");
for k,v in pairs(Tags) do -- dump out the Tags table
print(k .. " " .. v);
end
return true;
end
Function10Name, Function10Label = ShowTags, "Show Tags";
--[[---------------------------------------------------------------------------------------]]



setup()    This function first initializes variables. Note that the script uses the value returned by the 'Debug()' function call to display various levels of debugging information. The value of 'Debug()' is set by the Debug window and can be between 0 (no debug messages) to 9 (full debug messages). Starts all of the active engines. We need three. Choose the three engines on the main window Engine List. Then script looks at all 99 possible engines for the ones that are running. Sets the volume, smoke and few other things. Counts the number of running engines (iEngineCount) and saves a list of the three started engines (EnginesUsedNum). Clears the detected flags (EnginesUsedFlag). Set the speed to 12 Smph for all running engines. Sets the state machine (SM) to the first state - DETECTENGINES

tag()    All of the engines are running at 12 Smph. Each time a tag is detected, this function is called. Based on the value of the state machine variable (SM), run the part of the script for each state:

DETECTENGINES    In this state, we are just trying to detect all of the running engines and ascertain their order on the layout. As each tag on a front engine truck is detected, toot the horn.  This just signals to me that the script is processing tags. We care about tags on an engine, on the front truck and detected by Detector 1. As each engine is detected, I record the order (EngineDetectedOrder). When I detect the first engine again, all of the engines have gone all away around the layout once. The script increases the speed of the three engines to 15, 18, & 20 Smph respectively. Now I should have detected 3 engines. If not, the script displays a message and goes into the IDLE state. If so, the script goes into READYGO_3 state. (Now, having written this description, I see that I should have more checking for the case where more than 3 engines are detected. I'll leave that has an exercise for you.)
READYGO_3, READYGO_3a, & READYGO_3b    These three states are basically identical so I will write one description. As each tag is detected, the tag() function is called and the state machine value (SM) will cause one of these states to execute. Tags from other non-engine cars are ignored. Knowing now the order of the three engines, the script will run the engines until each passes over one of the three detectors (1, 2 & 3). Then it stops the engine. When all three engines are stopped (at a detector), the script pauses for a few seconds and then the script starts the engines moving again at different random speeds. The three states detect the three engines over the three detectors. The state machine (SM) is set to the next of these states and waits for each of the three engines to run up to the next stopping point (at the next detector). So the state machine runs these three states in sequence over and over until the [STOP] button is pushed.
READYGO_3, READYGO_3a, & READYGO_3b    These three states are basically identical so I will write one description. As each tag is detected, the tag() function is called and the state machine value (SM) will cause one of these states to execute. Tags from other non-engine cars are ignored. Knowing now the order of the three engines, the script will run the engines until each passes over one of the three detectors (1, 2 & 3). Then it stops the engine. When all three engines are stopped (at a detector), the script pauses for a few seconds and then the script starts the engines moving again at different random speeds. The three states detect the three engines over the three detectors. The state machine (SM) is set to the next of these states and waits for each of the three engines to run up to the next stopping point (at the next detector). So the state machine runs these three states in sequence over and over until the [STOP] button is pushed.

cleanup()    This function is called when you press the [STOP] button. It stops all active engines and terminates the script.

GP7PFA()    This function runs the PFA for a GP7. Called randomly during the running of the script.

SW1200PFA()    This function runs the PFA for a SW1200. In particular, the Aliquippa & Southern SW1200 #1208. Called randomly during the running of the script.

RandomAction()    This function runs a randomly selected sound or light action. This was done to just add some additional sound and light to the running of the script.

SW1200()    This function is called when the you press the [Function01] button (labeled: "SW 1200 PFA") on the Program Control window. It, in turn, calls SW1200PFA().

GP7()    This function is called when the you press the [Function02] button (labeled: "GP7 PFA") on the Program Control window. It, in turn, calls GP7PFA().



Needed Support Scripts

I've written two simple scripts that define constants and functions that make writing scripts easier. They are defines.lua and functions.lua. I've used these in some of the example scripts shown above.


defines.lua

defines.lua contains constants used the main script. For example, in your script, instead of writing out the command to turn on the bell as "w4", you can just write "BELL_ON".

Use this statement in your script so the script can access these defines:

require("defines");

Here is a listing of the defines.lua script file
(this may not be as up to date as the zip file linked to below):

--[[
---------------------------------------------------------------------------
Remote Train Control Program for Windows

© Copyright 2021 by Mark DiVecchio

This file is part of Remote Train Control.

Remote Train Control is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Remote Train Control is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Remote Train Control. If not, see <http://www.gnu.org/licenses/>.

Home Page : http://www.silogic.com/trains/RTC_Running.html
---------------------------------------------------------------------------]]

-- Semicolons are not required in Lua code
-- Commands from RTCMegaMaster.h
BELL_ON = "w4"
BELL_OFF = "bFFFB"
HDLT_ON ="ab1"
HDLT_OFF = "ab0"
WHISTLE_ON = "w2"
WHISTLE_OFF = "bFFFD"
DIRECTION_FORWARD = "d0"
DIRECTION_REVERSE = "d1"
DIRECTION_CHANGE = "d%d"
COMMAND_MODE = "m4"
COMMAND_STARTUP = "u4"
COMMAND_SHUTDOWN = "u5"
COMMAND_STARTUP_EXTENDED = "u6"
COMMAND_SHUTDOWN_EXTENDED = "u7"
COMMAND_STOP = "s0"
COUPLER_FRONT = "c0"
COUPLER_REAR = "c1"
SMOKE_EXHAUST_MIN = "ab12"
SMOKE_EXHAUST_MED = "ab11"
SMOKE_EXHAUST_MAX = "ab10"
SMOKE_EXHAUST_ON = "abF"
SMOKE_EXHAUST_OFF = "abE"
MARKERS_ON = "ab1C"
MARKERS_OFF = "ab1D"
BEACON_ON = "abD"
BEACON_OFF = "abC"
CAB_CHAT_ON = "ab13"
CAB_CHAT_OFF = "ab14"
CHUFF_RATE_2 = "g2"
CHUFF_RATE_4 = "g4"
COUPLER_CLASH = "h1"
--
AUX1_ON = "ab3"
AUX1_OFF = "ab2"
AUX2_ON = "ab4"
AUX2_OFF = "ab5"
AUX3_ON = "ab6"
AUX3_OFF = "ab7"
--
ACC_ON = "Ae%02d"
ACC_OFF = "Af%02d"
ACC_ALL_ON = "AaFFFFFFFFFFFFFF"
ACC_ALL_OFF = "Aa00000000000000"
SW_STRAIGHT = "Ac%02d"
SW_DIVERGE = "Ad%02d"
SW_ALL_STRAIGHT = "Ab05555555555555555555555555"
SW_ALL_DIVERGE = "Ab0AAAAAAAAAAAAAAAAAAAAAAAAA"
SW_CLOSED = SW_STRAIGHT
SW_THROWN = SW_DIVERGE
SW_ALL_CLOSED = SW_ALL_STRAIGHT
SW_ALL_THROWN = SW_ALL_DIVERGE
SPEED_SET = "s%d"
PLAY_SOUND = "n%d"
SINGLE_TOOT = "n41"
CROSSING = "n42"
DOUBLE_TOOT = "n43"
TRIPLE_TOOT = "n44"
IDLE_SOUND = "i%d"
VOLUME_CHANGE = "v%1d%03d"
MASTER_VOLUME = 0
ENGINE_VOLUME = 1
ACCENT_VOLUME = 2
HORN_VOLUME = 3
BELL_VOLUME = 4
COUPLER_CMD = "c%d"
FRONT = 0
REAR = 1
SETCMD = "ab%X"
CMD_HDLT_OFF = 0x00
CMD_HDLT_ON = 0x01
CMD_INT_OFF = 0x02
CMD_INT_ON = 0x03
CMD_MARS_OFF = 0x04
CMD_MARS_ON = 0x05
CMD_DITCH_OFF = 0x06
CMD_DITCH_ON = 0x07
CMD_BEACON_ON = 0x0D
CMD_BEACON_OFF = 0x0C
CMD_SMOKE_OFF = 0x0E
CMD_SMOKE_ON = 0x0F
CMD_SMOKE_MAX = 0x10
CMD_SMOKE_MED = 0x11
CMD_SMOKE_MIN = 0x12
CMD_MARKERS_ON = 0x1C
CMD_MARKERS_OFF = 0x1D
CMD_NUMBERBOARDS_ON = 0x05
CMD_NUMBERBOARDS_OFF = 0x04
ACC_RATE = "Da%d"
DEC_RATE = "Dd%d"
EMERGENCY_STOP = "o0"
LABOR_REV_UP = "r16"
LABOR_REV_DOWN = "r17"
LABOR_REV_NORMAL = "r15"

--[[---------------------------------------------------------------------------------------]]

-- Constants
-- Engine state
STARTED = true;
SHUTDOWN = false;
-- AIU Acessory state
ON = true;
OFF = false;
-- Engine direction
FORWARD = true;
REVERSE = false;
-- AIU switch position
STRAIGHT = true;
CLOSED = true;
NORMAL = true;
OPEN = false;
DIVERGE = false;
THROWN = false;
REVERSE = false;
-- Coupler
FRONT = true;
REAR = false;
--[[---------------------------------------------------------------------------------------]]
-- Speeds
NORMAL_SPEED = 60;
LIMITED_SPEED = 45;
MEDIUM_SPEED = 30;
SLOW_SPEED = 15;
RESTRICTED_SPEED = 15;

RAMMING_SPEED = 15; -- speed in Smph that helps assure successful coupling
--[[---------------------------------------------------------------------------------------]]

-- Packet Constants from RTC_NFC.h
-- Engine Data in binary - 16 bytes
--TagID; -- 0 = tag not programmed, 0x88 = tag is programmed
TAG_PROGRAMMED = 0x88
--EngineNo; -- 0 = not an Engine or LashUp (look at CarModel), otherwise Engine Number 1-99 (look at CarModel)
-- 101-120 LashUp Number
NOT_AN_ENGINE = 0
--TagLocation; -- 0 = Tag not on a truck, 1 = Tag on Front truck, 2 = Tag on Rear truck (Engines and Cars)
NOT_ON_A_TRUCK = 0
TAG_ON_FRONT_TRUCK = 1
TAG_ON_REAR_TRUCK = 2
TAG_ON_TRUCK = 4
TAG_AT_MIDPOINT = 5
--EngineType; -- 0 = not an engine, 1 = Diesel, 2 = Steam (Engines Only)
NOT_AN_ENGINE = 0
DIESEL_ENGINE_TYPE = 1
STEAM_ENGINE_TYPE = 2
GAS_MECHANICAL_ENGINE_TYPE = 3
GAS_ELECTRIC_ENGINE_TYPE = 4
STEAM_TURBINE_ENGINE_TYPE = 5
ELECTRIC_TYPE = 6
TURBINE_TYPE = 7
DIESEL_MECHANICAL_TYPE = 8
--Coupler
NO_COUPLER = 0
REAR_REMOTE_COUPLER_ONLY = 1
FRONT_REMOTE_COUPLER_ONLY = 2
FRONT_AND_REAR_TEMOTE_COUPLERS = 3
COUPLER_UNKNOWN = 4
REAR_MANUAL_COUPLER_ONLY = 5
FRONT_MANUAL_COUPLER_ONLY = 6
FRONT_AND_REAR_REMOTE_COUPLERS = 7
REAR_DUMMY_COUPLER_ONLY = 8
FRONT_DUMMY_COUPLER_ONLY = 9
FRONT_DUMMY_AND_REAR_MANUAL_COUPLERS = 10
NOT_APPLICABLE = 0x80 -- Coupler is only used for Engines
--SXS; -- 0 = no SXS Sound, 1 = has SXS sound at clip 42 (Engines only)
NO_SXS_SOUND = 0
HAS_SXS_SOUND = 1
--CarModel; -- see below
-- if EngineNo == NOT_AN_ENGINE
UNKNOWN_CARMODEL = 0 -- 0 = unknown
BOXCAR = 1 -- 1 = Boxcar
TANK_CAR = 2 -- 2 = Tank Car
FLAT_CAR = 3 -- 3 = Flat Car
CABOOSE = 4 -- 4 = Caboose
TENDER = 5 -- 5 = Tender
GONDOLA = 6 -- 6 = Gondola
COAL_HOPPER = 7 -- 7 = Coal Car
ORE_HOPPER = 8 -- 8 = Ore Car
MOW_FLATCAR = 9 -- 9 = MOW Flatcar
MOW_CRANE_CAR = 10 -- 10 = MOW Crane Car
MOW = 11 -- 11 = MOW Misc
COACH = 12 -- 12 = Passenger Coach
OBSERVATION = 13 -- 13 = Passenger Observation
BAGGAGE = 14 -- 14 = Baggage
-- xx-255 TBD
-- if EngineNo != NOT_AN_ENGINE (Lead Engine for LashUps)
UNKNOWN_ENGINEMODEL = 0 -- 0 = unknown
GP38_2 = 1 -- 1 = GP38-2
GP7 = 2 -- 2 = GP7
GP9 = 3 -- 3 = GP9
U28B = 4 -- 4 = U28B
BERKSHIRE = 5 -- 5 = Berkshire
DOODLEBUG = 6 -- 6 = Doodlebug
SW1200 = 7 -- 7 = SW1200
SW1500 = 8 -- 8 = SW1500
SWITCHER_0_6_0 = 9 -- 9 = 0-6-0 Switcher
SWITCHER_0_8_0 = 10-- 10 = 0-8-0 Switcher
H_9 = 11 -- 11 = 2-8-0 H-9 Consolidation
SW_9 = 12 -- 12 = SW-9
RS_3 = 13 -- 13 = Alco RS-3
-- xx-255 TBD
--Railroad; -- see below
UNKNOWN_RAILROAD = 0
P_AND_LE = 1
A_AND_S = 2
NYC = 3
B_AND_O = 4
P_AND_E = 5
LE_AND_E = 6
P_RR = 7
NH = 8
RDG = 9
J_AND_L = 10
Isalys = 11
LV = 12
-- Many more defined see "Tag Programmer"
--CarLength; -- Length of car in centimeters, coupler face to coupler face (include Tender)
--TagOffset; -- Distance from center of tag to coupler face in centimeters
-- for engines/cars with two tags, the distance must be same for both tags
--CarNumber[6]; -- Up to 6 digit engine/car number in ASCII (w/leading spaces) " 2808" -- NOT NULL TERMINATED
--[[---------------------------------------------------------------------------------------]]
-- The first six counters define the fields in the Program Control window that the Lua scripts can
-- increment (BumpCounter()), decrement (DecCounter()), read (GetCounter()) and clear (ClearCounter()).
-- Be careful if you change the TAGSUB, UNIQTAG or TAGCNT counters. These counters
-- are used by the tag detection system and changing them at the wrong time may confuse
-- the tag detection. Those counters appear on the Tags Detected List window.
-- The ELAPSEDTIME counter counts seconds and its probably not a good idea to change this value.
COUNTER01 = 0 -- User defined Counter 1
COUNTER02 = 1 -- User defined Counter 2
COUNTER03 = 2 -- User defined Counter 3
COUNTER04 = 3 -- User defined Counter 4
COUNTER05 = 4 -- User defined Counter 5
COUNTER06 = 5 -- User defined Counter 6
ELAPSEDTIME = 6 -- Elapsed Time Counter 7
TAGSUB = 10 -- The number of UID only tags detected that were replaced by tag data from previous tags
UNIQTAG = 11 -- The number of unique tags ever detected by your and previous scripts
TAGCNT = 12 -- The number of tags detected by your script
ENGINENO = 13 -- The value in the Engine Number spinner
--[[---------------------------------------------------------------------------------------]]
-- Beep() parameter
DEFAULT_BEEP = 0;
CRITICAL_STOP = 1;
QUESTION = 2;
EXCLAMATION = 3;
ASTERISK = 4;

--[[---------------------------------------------------------------------------------------]]
-- LED colors for the SetLED() and other functions that require a color value
clGreen = 0;
clYellow = 1;
clRed = 2;
clClear = 3;
clBlack = 4;
clBlue = 5;
clFuchsia = 6;
--[[---------------------------------------------------------------------------------------]]
-- Packet Types
-- These numeric values are the different responses that RTC can receive
-- from the Detectors
-- Start of Packet Character
START_OF_PACKET = 0xFF
-- End of Packet Character
END_OF_PACKET = 0xF0

-- NOP - ACK - NAK - WACK
NOP = 0x00
ACK = 0x61
NAK = 0x62
WACK = 0x63

--Error:
--0x10 Didn't find PN53x detector on port X
PN53xNOTFOUND = 0x10
--0x11 Unable to read Block X
CANTREADBLOCK = 0x11
--0x12 Tag not Programmed
TAGNOTPROG = 0x12
--0x13 Unable to authenticate
AUTHERROR = 0x13
--0x14 SOP character missing
SOPMISSING = 0x14
--0x15 Write block error
WRITEERROR = 0x15
--0x16 No Tag Detected When Expected
NOTAGDETECTED = 0x16

--Tag Data (TAGDATABLOCK4 also returns the Tag UID):
--0x20 not used
--0x21 Block 4
TAGDATABLOCK4 = 0x21
--0x22 Block 5
TAGDATABLOCK5 = 0x22
--0x23 Block 6
TAGDATABLOCK6 = 0x23
--0x24 Block 7
TAGDATABLOCK7 = 0x24

--Information:
--0x01 Hello X RFID Readers Attached (RTCNFCIRQ version in Reader byte and TagCount byte)
HELLOPACKET = 0x01
--0x03 Detector Y Reader X Detected
TAGDETECTED = 0x03
--0x04 Found chip PN5xx
PN53xFOUND = 0x04
--0x05 Waiting for an ISO14443A tag detection
WAITING = 0x05
--0x06 Detector Y Sensor X Detected
SENSORDETECTED = 0x06
--[[---------------------------------------------------------------------------------------]]
-----------------------------------------------------------------------------
-- Program constants
-----------------------------------------------------------------------------
-- These constants match constants in the Program Control code in RTC
-- They must not be changed
--[[-------------------------Direction-------------------------------------]]
WB = 1;
EB = 2;
DirectionNames = {
"WB",
"EB",
};
--[[----------------------Block Occupancy----------------------------------]]
CLR = 0; -- Clear
--OCC = 1; -- Occupied is any value greater than 0
--[[------------Signals on the Program Control Cab Display-----------------]]
HOMESIGNAL = 0;
DISTANTSIGNAL = 1;
--[[-----------------------Aspect Names------------------------------------]]
AspectNames = {
"None", -- [1]
"Dark", -- [2], etc
"Stop Signal",
"Clear",
"Stop and Proceed",
"Approach",
"Medium Clear",
"Medium Approach",
"Slow Clear",
"Slow Approach",
"Approach Medium",
"Approach Slow",
"Restricting",
"Approach Limited",
"Limted Clear",
"Limited Approach",
"Medium Approach Slow", --[17]
};

-- Aspect Selection
AsNone = 1; -- Invalid Aspect or the Signal does not exist
AsDark = 2; -- All Lights Off
AsStopSignal = 3; -- Rule 292 without Number Plate or with 'A' on Number Plate
AsClear = 4; -- Rule 281
AsStopandProceed = 5; -- Rule 291 with Number Plate
AsApproach = 6; -- Rule 285
AsMediumClear = 7; -- Rule 283
AsMediumApproach = 8; -- Rule 286
AsSlowClear = 9; -- Rule 287
AsSlowApproach = 10; -- Rule 288
AsApproachMedium = 11; -- Rule 282
AsApproachSlow = 12; -- Rule 284
AsRestricting = 13; -- Rule 290
AsApproachLimited = 14; -- Rule 281 (B)
AsLimitedClear = 15; -- Rule 281 (C)
AsLimitedApproach = 16; -- Rule 281 (D)
AsMediumApproachSlow = 17; -- Rule 283 (B)
AsLast = 18; -- last valid Aspect + 1

-- Signal Head Types
Hd3Light = 3
Hd2Light = 2
Hd1Light = 1
HdNone = 0

-- Signal Types
UNDEFINED_SIGNAL_TYPE = 0;
PERMISSIVE_IN_OPEN_COUNTRY = 1;
ABSOLUTE_HEAD_BLOCK = 2;
PERMISSIVE_HEAD_BLOCK = 3;
APPROACH_TO_HEAD_BLOCK = 4;
--[[---------------------------------------------------------------------------------------]]
--[[
Copyright (c) 2016 Scott Lembcke and Howling Moon Software

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

TODO:
* Print short function arguments as part of stack location.
]]
function Pretty(obj, max_depth)
if max_depth == nil then max_depth = 10 end

-- Returns true if a table has a __tostring metamethod.
local function coerceable(tbl)
local meta = getmetatable(tbl)
return (meta and meta.__tostring)
end

local function recurse(obj, depth)
if type(obj) == "string" then
-- Dump the string so that escape sequences are printed.
return string.format("%q", obj)
elseif type(obj) == "table" and depth < max_depth and not coerceable(obj) then
local str = "{"

for k, v in pairs(obj) do
local pair = Pretty(k, 0).." = "..recurse(v, depth + 1)
str = str..(str == "{" and pair or ",\n "..pair)
end

return str.."}"
else
-- tostring() can fail if there is an error in a __tostring metamethod.
local success, value = pcall(function() return tostring(obj) end)
return (success and value or "<!!error in __tostring metamethod!!>")
end
end

return recurse(obj, 0)
end
PrettyPrint = function(value, depth) print(Pretty(value, depth)) end


functions.lua

functions.lua uses several functions called by the main script. These functions help in processing the RFID tags otherwise it is not needed.

Use this statement in your script so the script can access these functions:

require("functions");

Here is a listing of the functions.lua script file
(this may not be as up to date as the zip file linked to below):

--[[
---------------------------------------------------------------------------
Remote Train Control Program for Windows

© Copyright 2021 by Mark DiVecchio

This file is part of Remote Train Control.

Remote Train Control is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Remote Train Control is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Remote Train Control. If not, see <http://www.gnu.org/licenses/>.

Home Page : http://www.silogic.com/trains/RTC_Running.html
---------------------------------------------------------------------------]]

-- Semicolons are not required in Lua code

function isEngine(EngNo)
if (EngNo > 0) then return true; end
return false;
end

function notEngine(EngNo)
if (EngNo == 0) then return true; end
return false;
end

function isTagRear(TagLocation)
if (TagLocation == TAG_ON_REAR_TRUCK) then return true; end
return false;
end

function isTagFront(TagLocation)
if (TagLocation == TAG_ON_FRONT_TRUCK) then return true; end
return false;
end

function isDetector(Detector, x)
if (Detector == x) then return true; end
return false;
end

function isReader(Reader, x)
if (Reader == x) then return true; end
return false;
end

function isCarModel(CarModel, x)
if (CarModel == x) then return true; end
return false;
end

--[[---------------------------------------------------------------------------------------
Occupancy Flags are supported as part of the Thinking Engines project.
Up to 100 flags are available to indicate block occupancy. A Zero value
means the block is unoccupied. A non zero value means the block is occupied.
-- returns two values
-- 1: true on success, false on failure
-- 2: on failure, the block numbers that would not lock, otherwise 0
-- Wait is the number of seconds to wait, default to 1200 (20 minutes)
When occupied, the Flag should be set to the engine number of the engine
in the block.
]]
--[[---------------------------------------------------------------------------------------]]
-- Set a value into the block occupancy flag
-- Waits 'forever' for the block to show as unoccupied before it sets occupancy
function SetOccupancy(block, value, Wait)
if (block < FIRST_BLOCK or block > LAST_BLOCK) then
return Stop("Invalid Block Number in SetOccupancy()");
end;
if (value == 0) then
return Stop("Value in SetOccupancy() is zero, use ClearOccupancy() instead");
end;
local WaitTime = Wait or 1800; -- let's say that 30 minutes is 'forever'
-- set the occupancy flag if it is zero or we already have ownership - use lock
while (not TrySetFlag(block, value)) do
-- wait 'forever'
if (((WaitTime >> 1) % 10) == 0) then
print("Waiting for " .. BlockName[block] .. " to clear");
end;
Sleep(2);
WaitTime = WaitTime - 2;
if (WaitTime <= 0) then
printc(clRed,"SetOccupancy() Timed out waiting for " .. BlockName[block] .. " to clear");
return false, block;
end
end;
print("Set occupancy flag on " .. BlockName[block] .. " to " .. value);
return true;
end;
--[[---------------------------------------------------------------------------------------]]
-- Tries to set a value into the block occupancy flag
-- Only tries once. Returns true on success, false if the block is occupied.
function TrySetOccupancy(block, value)
if (block < FIRST_BLOCK or block > LAST_BLOCK) then
return Stop("Invalid Block Number in TrySetOccupancy()");
end;
if (value == 0) then
return Stop("Value in TrySetOccupancy() is zero, use ClearOccupancy() instead");
end;
-- set the occupancy flag if it is zero or we already have ownership - use lock
if (not TrySetFlag(block, value)) then
-- did not get a lock
print("TrySetOccupancy() flag on " .. BlockName[block] .. " - could not get lock");
return false;
end;
print("TrySetOccupancy() flag on " .. BlockName[block] .. " to " .. value);
return true;
end;
--[[---------------------------------------------------------------------------------------]]
-- Clear a block occupancy flag to zero if we have ownership, that is,
-- we are occupying the block.
function ClearOccupancy(block, value)
if (block < FIRST_BLOCK or block > LAST_BLOCK) then
return Stop("Invalid Block Number in ClearOccupancy()");
end;
if (value ~= nil and GetFlag(block) == value) then
print("ClearOccupancy() flag on " .. BlockName[block]);
SetFlag(block, 0);
end;
return true;
end;
--[[---------------------------------------------------------------------------------------]]
-- set a block occupancy flag without waiting for lock or checking ownership
-- Can be very dangerous if your script makes a mistake.
function OverrideOccupancy(block, value)
if (block < FIRST_BLOCK or block > LAST_BLOCK) then
return Stop("Invalid Block Number in OverrideOccupancy()");
end;
if (value == nil) then
value = 0;
end;
print("OverrideOccupancy() flag on " .. BlockName[block] .. " to " .. value);
SetFlag(block, value);
return true;
end;
--[[---------------------------------------------------------------------------------------]]
-- fetch a block occupany flag
function GetOccupancy(block)
if (block < FIRST_BLOCK or block > LAST_BLOCK) then
return Stop("Invalid Block Number in GetOccupancy()");
end;
return GetFlag(block);
end;
--[[---------------------------------------------------------------------------------------]]
-- Set a value into several block occupancy flags. All blocks must be unoccupied.
-- Waits almost forever for the blocks to show as unoccupied before it sets occupancy
-- Returns two values
-- 1: true on success, false on failure
-- 2: on failure, one of the block numbers that would not lock, otherwise 0
-- Wait is the number of seconds to wait, default to 600 (10 minutes)
-- blocks is a table of the form {2, 44, 21}
function MultiSetOccupancy(blocks, value, Wait)
if (type(blocks) ~= "table") then return Stop("MultiSetOccupancy() block not a table"), 0; end
-- loop through all blocks in table
for k, v in ipairs(blocks) do
if (v < FIRST_BLOCK or v > LAST_BLOCK) then
return Stop("Invalid Block Number in MultiSetOccupancy()"), 0;
end;
end;
if (value == 0) then
return Stop("Value in MultiSetOccupancy() is zero, use ClearOccupancy() instead"), 0;
end;
local WaitTime = Wait or 1800; -- let's say 30 minutes is 'forever'
-- set the occupancy flag if it is zero or we already have ownership - use lock
local occupiedblock;
while (true) do
local result;
result, occupiedblock = TryMultiSetFlag(blocks, value)
if (result) then
break;
end;
-- wait almost forever
if (((WaitTime >> 1) % 10) == 0) then
print("MultiSetOccupancy() Waiting for " .. BlockName[occupiedblock] .. " to clear");
end;
Sleep(2);
WaitTime = WaitTime - 2;
if (WaitTime <= 0) then
printc(clRed,"MultiSetOccupancy() Timed out waiting for " .. BlockName[occupiedblock] .. " to clear");
return false, occupiedblock;
end
end;
-- loop through all blocks in table
for k, v in ipairs(blocks) do
print("MultiSetOccupancy() flag on " .. BlockName[v] .. " to " .. value);
end;
return true, 0;
end;
--[[---------------------------------------------------------------------------------------]]
-- Set a value into several block occupancy flags. All blocks must be unoccupied.
-- Only tries once
-- returns two values
-- 1: true on success, false on failure
-- 2: on failure, the block number that would not lock, otherwise 0
function TryMultiSetOccupancy(blocks, value)
-- blocks is a table of the form {2, 44, 21}
if (type(blocks) ~= "table") then return Stop("TryMultiSetOccupancy() block not a table"); end
for k, v in ipairs(blocks) do
if (v < FIRST_BLOCK or v > LAST_BLOCK) then
return Stop("Invalid Block Number in TryMultiSetOccupancy()"), 0;
end;
end;
if (value == 0) then
return Stop("Value in TryMultiSetOccupancy() is zero, use ClearOccupancy() instead"), 0;
end;
-- set the occupancy flag if it is zero or we already have ownership - use lock
local success, occupiedblock = TryMultiSetFlag(blocks, value);
if (not success) then
-- did not get a lock
print("TryMultiSetOccupancy() flag on " .. BlockName[occupiedblock] .. " - could not get lock");
return false, occupiedblock;
end;
-- loop through all blocks in table
for k, v in ipairs(blocks) do
print("TryMultiSetOccupancy() flag on " .. BlockName[v] .. " to " .. value);
end;
return true, 0;
end;
--[[---------------------------------------------------------------------------------------]]
-- Set a value into one of several block occupancy flags
-- Waits almost forever for one of the blocks to show as unoccupied before it sets occupancy
-- returns two values
-- 1: true on success, false on failure
-- 2: on success, the block number that locked, otherwise 0
-- Wait is the number of seconds to wait, default to 600 (10 minutes)
-- blocks is a table of the form {2, 44, 21} or {2}
function SingleSetOccupancy(blocks, value, Wait)
if (type(blocks) ~= "table") then return Stop("SingleSetOccupancy() block not a table"), 0; end
-- loop through all blocks in table
for k, v in ipairs(blocks) do
if (v < FIRST_BLOCK or v > LAST_BLOCK) then
return Stop("Invalid Block Number in SingleSetOccupancy()"), 0;
end;
end;
if (value == 0) then
return Stop("Value in SingleSetOccupancy() is zero, use ClearOccupancy() instead"), 0;
end;
local WaitTime = Wait or 1800; -- let's say that 30 minutes is 'forever'
-- set the occupancy flag if it is zero or we already have ownership - use lock
local occupiedblock;
while (true) do
for k,v in ipairs(blocks) do
-- lock the first block that becomes available
local result = TrySetFlag(v, value)
if (result) then
occupiedblock = v;
print("SingleSetOccupancy() flag on " .. BlockName[occupiedblock] .. " to " .. value);
return true, occupiedblock;
end;
if (((WaitTime >> 1) % 10) == 0) then
print("SingleSetOccupancy() Waiting for " .. BlockName[v] .. " to clear");
end;
end;
-- wait almost forever
Sleep(2);
WaitTime = WaitTime - 2;
if (WaitTime <= 0) then
break;
end
end;

for k,v in ipairs(blocks) do
printc(clRed,"SingleSetOccupancy() Timed out waiting for " .. BlockName[v] .. " to clear");
end;

return false, 0;
end;
--[[---------------------------------------------------------------------------------------]]
function WaitforStopSignaltoClear()
local Aspect = (GetCabAspect(HOMESIGNAL));
-- check that home signal is not AsStopSignal, if it is, we MUST Stop until it clears
if (Aspect == AsStopSignal) then
-- we are stopped, so wait until the Stop Signal aspect is cleared.
print("WaitforStopSignaltoClear : Home Signal aspect is Stop Signal. Wait for it to clear before proceeding.");
-- wait for Stop Signal to clear
local WaitTime = 1800; -- let's say that 30 minutes is 'forever'
-- Wait for the signal to changed from Stop Signal
while (Aspect == AsStopSignal) do
-- wait 'forever'
if (((WaitTime >> 1) % 10) == 0) then
print("WaitforStopSignaltoClear : Waiting for Stop Signal to clear");
end;
Sleep(2);
WaitTime = WaitTime - 2;
if (WaitTime <= 0) then
return Stop("WaitforStopSignaltoClear : Timed out waiting for Stop Signal to clear");
end;
Aspect = (GetCabAspect(HOMESIGNAL));
end;
end;
end;
--[[---------------------------------------------------------------------------------------]]




Lashups

This scripting control fully supports lashups. Lashups use numbers from 101 to 120.


Here is a listing of the Lashup_startup_shutdown.lua  script
(this may not be as up to date as the zip file linked to below):

-- Semicolons are not required in Lua code
title = "Lashup Startup/Shutdown Test"
require("defines");

local MyEngineNo, MyLashUpNo, MyTIUNo;

--[[---------------------------------------------------------------------------------------]]
function setup(Engine, TIU)
print("setup()");
MyTIUNo = TIU;
MyLashUpNo = 101;
AIUNo = 1;
return true; -- false=setup failed, true=setup succeeded
end
--[[---------------------------------------------------------------------------------------]]
function loop(Engine, TIU)
print("loop()");

LashUpEngineList = {};
EngineIndex = 1;
LashUpEngineList[EngineIndex] = 9; -- head engine P&LE SW1500#1556
EngineIndex = EngineIndex + 1; LashUpEngineList[EngineIndex] = 13; -- middle engine P&LE GP7 #1500
EngineIndex = EngineIndex + 1; LashUpEngineList[EngineIndex] = -8; -- tail engine reverse running P&LE MP15AD#1594
print("LashUp :");
for i,v in ipairs(LashUpEngineList) do
print(string.format(" Engine %d", v));
end

print("Startup");
retv = StartUp(0, MyLashUpNo, LashUpEngineList, MyTIUNo);
if (retv == nil) then print("Startup failed"); end;
Sleep(15); -- engines take time to startup

print("Smoke Off");
retv = Smoke(OFF, 0, MyLashUpNo, MyTIUNo);
if (retv == nil) then print("Smoke Off Failed"); end;

print("Whistle On");
retv = Whistle(ON, 5, MyLashUpNo, MyTIUNo);
if (retv == nil) then print("Whistle On failed"); end;
Sleep(2);

print("Whistle Off");
retv = Whistle(OFF, 5, MyLashUpNo, MyTIUNo);
if (retv == nil) then print("Whistle Off failed"); end;
Sleep(15);

print("Bell On");
retv = Bell(ON, 5, MyLashUpNo, MyTIUNo);
if (retv == nil) then print("Bell On failed"); end;
Sleep(2);

print("Bell Off");
retv = Bell(OFF, 5, MyLashUpNo, MyTIUNo);
if (retv == nil) then print("Bell Off failed"); end;
Sleep(15);

print("SetSpeed");
retv = SetSpeed(10, 0, MyLashUpNo, MyTIUNo);
if (retv == nil) then print("SetSpeed failed"); end;
Sleep(20);

return Stop(); -- false=do not loop, true=continue to loop
end
--[[---------------------------------------------------------------------------------------]]
function cleanup(Engine, TIU)
-- this function is called once when the user presses the [STOP] button
print(os.date("cleanup() %d %b %Y %X"));

print("SetSpeed");
retv = SetSpeed(0, 0, MyLashUpNo, MyTIUNo);
if (retv == nil) then print("SetSpeed failed"); end;
Sleep(10);

print("Shutdown");
retv = Shutdown(0, MyLashUpNo, MyTIUNo);
if (retv == nil) then print("Shutdown failed"); end;

print("Feature Reset");
for i,v in ipairs(LashUpEngineList) do
print(string.format("Feature Reset & Smoke Off - Engine %d", v));
FeatureReset(math.abs(v), MyTIUNo);
Smoke(OFF, 0, math.abs(v), MyTIUNo); -- Feature reset turns Smoke on
end

print("Done!");
return true; -- false=cleanup failed, true=cleanup succeeded
end
--[[---------------------------------------------------------------------------------------]]



Running Compiled Lua Scripts

Lua is an interpreted language that is compiled into bytecode before its run. If you have a very large Lua script, it may take some time to compile. You don't have to do this each time it is run. You can compile into bytecode, save the bytecode and have RTC run that bytecode.

Use the "luac.exe" compiler. The compiler produces a bytecode file named "luac.out". Rename it to your script name, keeping the ".out" extension. Then you can select that file from the [Browse] button on the Program Control window. Here is the command line that I use to compile a script (using the v5.3 Lua compiler):
luac53 -o script.out script.lua
You can also compile files if you use the require ("defines"); or require ("functions"); scripts in your main script.

To get the compiler, look for the file "lua-5.3.5_Win64_bin.zip" (or maybe a newer version number) at LuaBinaries on SourceForge. I've included a copy of the compiler in the Scripts folder of the RTC distribution.


Events


Function that Generates Events



SetEvent(DestPCNo, Type, P1, P2, P3);    -- passes the four integer parameters to the Program Control window
    -- specified by DestPCNo. As of RTC v4.4.2, there can be 10 PC windows numbered 1 through 10. The
    -- maximum may be raised in the future. In order for the destination PC window to receive the event,
    -- the script running in that window must have a function declared as "event(FromPCNo, Type, P1, P2, P3)".
    -- The receiving script will execute the event() function, presenting the source PC window number
    -- along with the four parameter values. The parameters are lua integers which are defined as
    -- 64 bit signed values. The meaning and contents of these parameters are not fixed and can be
    -- whatever you wish. Type/P1/P2/P3 are optional and if not given, zero is passed.
    -- The meaning of Type, P1, P2 and P3 must be agreed to by both the sender and receiver
    -- of the event. An example of Event Type values can be seen in the script distances.lua.
    -- The function returns nil if the destination PC window does not exist
    -- otherwise returns true when the event is sent. The function does NOT wait for the event
    -- to complete.



Here is the function that receives events

event(FromPCNo, Type, P1, P2, P3)  --  is called by any PC window using the SetEvent() function. It is used for commnications between the PC windows. Return true on success, false on failure. "FromPCNo" is the Program Control window # of the script which executed the SetEvent() function. Type, P1, P2 & P3 are integers which are being sent along with the event. Look at the SetEvent() description above for details of how this function is used. If the function does not appear in your script, RTC does not attempt to call it. Any PC window can send an event to itself.


Here is a listing of the event.lua  script
(this may not be as up to date as the zip file linked to below):

-- Semicolons are not required in Lua code
title = "Event Test"
require([[defines]]);
require([[functions]]);

--[[---------------------------------------------------------------------------------------]]
function setup(Engine, TIU)
print("setup() Running");
print("This is Program Control Window #" .. PCNo());
print("This script requires 'event.lua' be running in PC window #1 and #2");
if (PCNo() < 1 or PCNo() > 2) then
return false; -- need two PC windows 1 and 2
end
return true; -- false=setup failed, true=setup succeeded
end
--[[---------------------------------------------------------------------------------------]]
function loop(Engine, TIU)
-- Always call Sleep(X) at least once in this function if it returns true;
print("loop() Running");
if (PCNo() == 1) then
DestPC = 2;
end;
if (PCNo() == 2) then
DestPC = 1;
end
print("Sending an event to PC Window #" .. DestPC .. " in 10 seconds");
Sleep(10);
res = SetEvent(DestPC, 123456789012345, PCNo(), -876765, 556677);
if (type(res) == "nil") then
print("nil : invalid destination Program Control number - script not running");
return Stop("DONE");
end;
if (type(res) == "boolean") then
print(res, " : success");
end;
print("Almost Done...");
Sleep(10);
return Stop("DONE");
end
--[[---------------------------------------------------------------------------------------]]
function event(FromPC, Type, P1, P2, P3)
print("event() - From PC# ", FromPC, " EventType = ", Type, " Parameters = ", P1, " ", P2, " ", P3);
return true; -- false=event failed, true=event succeeded
end

--[[---------------------------------------------------------------------------------------]]
function cleanup(Engine, TIU)
-- this function is called once when the user presses the [STOP] button
print("cleanup() Running");
return true; -- false=cleanup failed, true=cleanup succeeded
end
--[[---------------------------------------------------------------------------------------]]


When you run the event.lua script in both PC window #1 and #2, here is what PC window #1 should look like after successful completion.
Here is what PC window #2 should look like.




Global Integers and Strings

These global variables are meant to share information between the 10 possible running scripts. They can be used as interlocking flags to prevent different scripts from accessing the same resource at the same time. When you successfully set a value, you get a lock on that index. As long as the other scripts use the "Try Set" routines to see if they can lock the same index, you will own the lock on that index. The "Try Set" functions are "atomic", that is, they cannot get interrupted between the time they check if an index is locked until they lock the index (if possible).

Functions that access the Global Integers and Strings



SetGlobalInt(index, value);
GetGlobalInt(index);
TrySetGLobalInt(index, value);

TryMultiSetGlobalInt(table, value); -- There are 100 global integer locations where any Program Control window
    -- can store or retrieve a value. This locations are useful for communications between Program
    -- control windows. Index is 1-100, value is any integer. Use
    -- SetGlobalInt() to store a value and GetGlobalInt() to retrieve a value.
    -- TrySetGlobalInt() attempts to set a value into a global integer and will only succeed if the
    -- global integer is zero or equal to value. This test and set is done atomically. If it succeeds
    -- it will return true, if it can't get the lock, it will return false.
    -- TryMultiSetGlobalInt() attemps to set a value into several global integers and will only succeed
    -- if all of the global integers are zero or equal to value.  This test and set is done atomically.
    -- It returns two values. The first is true if it gets a lock on all of global integers, false
    -- if it cannot. The second value returned is the index number of the global integer which would
    -- not lock otherwise returns zero. table is a lua table, for example : {4, 22, 12}

SetGlobalString(index, string);
GetGlobalString(index);  -- There are 100 global string locations where any Program Control window
    -- can store or retrieve a string. These locations are useful for communications between Program
    -- Control windows. Index is 1-100, string is a string. Use SetGlobalString() to store a string
    -- and GetGlobalString() to retrieve a string.



Here is a test script for these global variables to give you an idea of how to use them:

Here is a listing of the Global Variables Test.lua  script
(this may not be as up to date as the zip file linked to below):

-- Semicolons are not required in Lua code
title = "Global Variables Test"
require("defines");
require("functions");

--[[---------------------------------------------------------------------------------------]]
function setup(Engine, TIU)
-- this function is called once when the [Run Script] button is pushed
-- Engine : Engine number selected on the RTC Main window
-- TIU : TIU number selected on the RTC Main window
print("setup()");
for x = 1, 15 do
SetGlobalInt(x, 0);
print("GetGlobalInt(" .. x .. ") (zero) = ", GetGlobalInt(x))
end;
SetGlobalInt(3, 23456);
SetGlobalInt(99, 33);

if (GetGlobalInt(3) ~= 23456) then
print("Global Int 3 not set correctly (23456)");
end;
if (GetGlobalInt(99) ~= 33) then
print("Global Int 99 not set correctly (33)");
end;

SetGlobalString(4, "My String");
if (GetGlobalString(4) ~= "My String") then
print('Global String 4 not set correctly ("My String")');
end;

-- RunTime() must be converted to an integer
SetGlobalString(8, string.format("The time is %d seconds", math.floor(RunTime() + 0.5)));

print(string.format("RunTime = %.2f seconds", RunTime()));
return true; -- false=setup failed, true=setup succeeded
end
--[[---------------------------------------------------------------------------------------]]
function loop(Engine, TIU)
-- must call Sleep() at least once in this function
print("loop()");
print("GetGlobalInt(3) (23456) = ", GetGlobalInt(3));
GlobalInt99 = GetGlobalInt(99);
print("GetGlobalInt(99) (++) = ", GlobalInt99);
GlobalInt99 = GlobalInt99 + 1;
SetGlobalInt(99, GlobalInt99);

if (GetGlobalInt(3) ~= 23456) then
print('Global Int 3 not set correctly ("23456")');
end;
if (GetGlobalInt(99) ~= GlobalInt99) then
print("Global Int 99 not set correctly (" .. GlobalInt99 .. ")");
end;

SetGlobalString(4, "My String");
if (GetGlobalString(4) ~= "My String") then
print('Global String 4 not set correctly ("My String")');
end;

print("GetGlobalString(4) ('MyString') = ", GetGlobalString(4));
print("GetGlobalString(8) = ", GetGlobalString(8));
--print("GetGlobalString(11) = ", GetGlobalString(11)); -- gets "index out of range" error
for x = 1, 15 do
print("GetGlobalInt(" .. x .. ") = ", GetGlobalInt(x))
end;
Sleep(5);
SetGlobalString(8, string.format("The time is %d seconds", math.floor(RunTime() + 0.5)));
print("loop exit");
return true; -- false=loop failed, true=loop succeeded
end
--[[---------------------------------------------------------------------------------------]]
function cleanup(Engine, TIU)
-- this function is called once when the user presses the [STOP] button
print(string.format("cleanup() Complete at %.2f", RunTime()));
return true; -- false=cleanup failed, true=cleanup succeeded
end
--[[---------------------------------------------------------------------------------------]]

When you run the Global Variables test, this is what the PC window should look like. This test repeats over and over until you press [STOP].


Flags, Fixed Signals Display, and Cab Heads Up Display

I've started a new page to talk about signaling on the layout using RTC. Included are new Lua functions to support signaling. Click here for that web page.


Downloads

You can download RTC from here. Includes all of the above scripts and many more.


This site prepared and maintained by Mark DiVecchio

email :  markd@silogic.com

SD&A HOME
 
 Mark's Home Page

The DiVecchio genealogy home page
The Frazzini genealogy home page

This site will be under construction for a while.