JMR2.4 - John's Model Railway

A.J.Hurst

Version 2.4.3

20150829:162117

Table of Contents

1 Introduction
1.1 What this suite provides
2 The Data Structures
2.1 Some definitions
2.2 Basic JMR Data
2.3 The Persistent Data
2.3.1 The Layout Input Language (not yet used in 2.4)
2.3.2 The signals table (updated 20150820:182224)
2.3.3 The points table (updated 20150821:111101)
2.3.4 The track segment table (updated 20150821:111209)
2.3.5 Signal Control Definitions
2.3.6 The conflicts table
2.3.7 The Routes Definition
2.4 The Device Driver Translate Tables
3 The LayoutComponent Module
3.1 Signals
3.1.1 The Signal class
3.1.2 The Signals class
3.2 Points
3.2.1 The Point class
3.2.2 The Points class
3.3 Tracks
3.3.1 The Track class
3.3.2 The Tracks class
3.4 Routes
3.4.1 the Route class
3.4.2 the Routes class
3.5 Blocks
3.6 The Layout class
3.6.1 the checkInterlocking method
3.6.2 the changeSignal method
3.6.3 the setClearPath method
3.6.4 the getPath method
3.6.5 the collectSignals method
3.6.6 The trackProtected method
3.7 The main routine
4 The External Server Program jmr.py
4.1 jmr: perform route action
5 The fat-controller html server trains.py
5.1 The trains.py cgi script
6 The Points Module
7 The Interlocking Module
8 The SVG Diagram Module
8.1 Imports and Globals
8.1.1 Track and Signal Colour Definitions
8.2 Stroke Colour Setting Routines
8.3 The Layout class
8.3.1 class SvgLayout initialization
8.3.2 Get Layout Signals
8.3.3 Get Layout Points
8.3.4 Save Layout
8.3.5 Set Signal
8.4 Set Points
8.5 svgLayout: setSegments
9 Help Text
10 Setup of Server
11 Support Programs
11.1 The walkNetwork Program
11.2 makeJmrData.py
11.3 makeJmrData: recursive descent routines
11.4 makeJmrData: main routine
12 The Arduino Programs
12.1 The Points Driver Arduino
13 TODOs
14 Makefile
15 Indices
15.1 Identifiers
15.2 Macros
16 Document History


1. Introduction

This is a suite of programs to drive John's Model Railway (version 2), aka JMR2. It is a rewrite of the version 1 code, to operate from the fat-controller computer system. Version 1 was itself a rewrite from earlier version 0. More details to follow.

Version 2.1 restructures much of the code into more coherent modules, so that both the server programs (trains.py and jmr.py) use common code where possible.

Version 2.2 attempts to rationalize the point and signal interconnections so that a more systematic naming and operational convention applies.

Version 2.3 removes a lot of the signal and point route setting/conflict logic from the svgLayout.py module, and places it instead into the jmr.py and interlocking.py modules.

Version 2.4 restructures most of the layout components into a module called LayoutComponents, written in a more OO style.

1.1 What this suite provides

This suite of code defines all the constants, modules, programs, and server programs used to control John's Model Railway. It is written as a literate program.

There are two server programs and two Arduino programs that comprise the system. The main server program provides a public web interface that delivers an updateable layout schematic that not only enables the reader to see the current state of the signals and points throughout the system, but also provides control of the system through mouse clickable representations of the points and signals. This server does not interface directly to the layout, but instead sends http requests to a local server (which is not publically available).

This local server interfaces directly to two Arduino systems via USB connections, which drive the points and signal circuitry respectively. This is via an I2C bus connecting to a fan-out of driver transistors, one for each point motor and signal lamp. (The points circuitry actually uses a cross-bar mechanism to reduce the number of direct transistor drivers.)

The local server is designed to be compatible with the code used in the main server. The main differences between the two servers are that the local server has no responsibility for displaying the track schematic, while the main server has no direct connection to the Arduino sub-systems. The reasons for this are partly historical and partly technical, but the use of a two-layer system also allows for an additional level of security in that public users cannot interfere directly with the track control circuitry..

2. The Data Structures

2.1 Some definitions

Aspects
The colour shown by a signal. Besides the obvious colours of R (red), Y (yellow) and G (green), there is also X (unknown) and B (blue, or bad).
Signals
Each signal is known by three components: an area code (C,G,M,P,S for Cabernet, Grenache, Merlot, Pinot and Shiraz respectively), the signal type indicator 's', and a cardinal number to distinguish within the area. This composite is called a signal name, and an example is Ms04. There is a data structure signals which is a dictionary from signal names to aspects: {signal names => aspects}. A composite of signal name and aspect is called a signal aspect, such as Ms04G (signal named Ms4 is showing a green aspect).
Directions
The direction in which a point lies. It can be either L (left) or R (right), and in the case of three-way points, C (centre). There is also the unknown state X.
Points
Each point is known by three components: an area code (C,G,M,P,S for Cabernet, Grenache, Merlot, Pinot and Shiraz respectively), the point type indicator 'p', and a cardinal number to distinguish within the area. This composite is called a point name, and an example is Gp02. There is a data structure points which is a dictionary from point names to directions: {point names => directions}. A composite of point name and direction is called a point direction, such as Gp02L (point named Gp02 is thrown to the left direction).

Each point has an orientation, indicating which way the toe is facing. This is 'C' for clockwise trains, and 'A' for anticlockwise trains. Note that for the inner tracks, the orientation has to be carefully defined, particularly where reversing loops occur. (More on this later.)

Track Segment
A track segment is a piece of track that is controlled as a unit. It has a state which may be "unknown" ('X'), "set" ('S') or "clear" ('C'). If the state of a track segment is unknown, it may mean exactly that, or it may indicate that it is specifically not set and not clear. In either form, it is not suitable for a train to be travelling over that track segment, as the safety of the train cannot be guaranteed. Track segments are named in two ways:
  1. As part of the toe of a point, when it is labelled with a point direction, such as Cp02L.
  2. As an independent piece of track, when it is labelled with an area code, component type 't', and a cardinal number. An example is 'Ct03'.
Blocks
A block is a key component in the signalling of the layout. Each signal controls entry to one or more blocks, where each block is a sequence of contiguous track segments to the next (facing) signal. The status of a block reflects the status of the track segments from which it is composed. If one or more track segments is unknown, the state of the block is unknown. If all the track segments in the block are set, the block is set. If all the track segments are clear, the block is clear. (It is normally an error if some track segments in a block are clear and some are set.) Note that blocks are defined in the signalControls data table, which is a mapping from signal names to a list of blocks: {signal names => Blocks}
Paths
svgdom elements
route
[path names]
Routes
[[path names]]
section
A section is a list of path names that may be cleared on the setting of a signal. Note that sections may have forks in them where they include facing points. It is up to the display logic to show which particular route in a section is cleared.
Sections
[[path names]]

2.2 Basic JMR Data

"jmrData.py" 2.1 =
regionPat='C|c|G|g|M|m|P|p|S|s' numberPat='\d+' directionPat='R|L'

This module defines constants used throughout the suite of programs.

regionPat
defines a regular expression to match region designations.
numberPat
defines a regular expression to match the nominal number of the signal or point designation.
directionPat
defines a regular expression to match the direction setting of a point.

2.3 The Persistent Data

A number of files are used to store persistent data associated with the layout. A brief overview of the role of these files is given here, and more information appears within each definition.

pointsData.py
Define the state of each set of points in the layout. This is a simple dictionary structure, mapping point names to directions.
signalsData.py
Define the state of each signal in the layout. This is a mapping from signal names to aspects.
signalControls.py
This table identifies the routes controlled by each signal. It takes the form of a dictionary, indexed by signal name, into a list of blocks for each signal. A block is defined as a list of track segments (q.v.).
blocksData.py
A table of the blocks controlled by each signal. Since these are directional, and each signal can control more than one block, each block is identified by a pair a contiguous facing signals, such as Cs7-Ps2.

2.3.1 The Layout Input Language (not yet used in 2.4)

An input language for describing the layout has been developed. This consists of two parts: the points definition, and the signals, or block definitions. Here is the BNF definition of the input language: (see also the Support Programs section)

          Line   = (Signal '-' ControlList) | (Point '-' AlternatePath) .
          ConflictLine = Signal '-' AlternatePath
          Signal = Area 's' Number .
          Area   = 'C' | 'G' | 'M' | 'P' | 'S' .
          Number = ('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9')+ .
          Track  = Area 't' Number .
          ControlList = Path [',' Path] .
          Path   = Signal | PointPath | Track | AlternatePath .
          PointPath = Area 'p' Number [ Dir ] .
          Dir    = 'L' | 'R' .
          AlternatePath = '(' Guard [',' ControlList] ')' ['/' AlternatePath] .
          Guard  = PointPath .
        

2.3.2 The signals table (updated 20150820:182224)

"signalsData.py" 2.2 =
{ # key:[aspect,controls,orientation] 'Cs01':['R',['Cp03L'],'A'], 'Cs02':['R',['Cp02L'],'A'], 'Cs03':['R',['Ct07'],'A'], 'Cs04':['R',['Ct04'],'C'], 'Cs05':['R',['Cp01L'],'C'], 'Cs06':['R',['Cp05L'],'C'], 'Cs07':['R',['Ct02'],'A'], 'Cs08':['R',['Cp04R'],'C'], 'Gs01':['R',['Gt01'],'A'], 'Gs02':['R',['Gp05R'],'A'], 'Gs03':['R',['Gp01R'],'C'], 'Gs04':['R',['Gp02R'],'C'], 'Gs05':['R',['Gt04'],'C'], 'Gs06':['R',['Gp03R'],'A'], 'Ms01':['R',['Mt05'],'C'], 'Ms02':['R',['Mt10'],'A'], 'Ms04':['R',['Mt02'],'C'], 'Ps01':['R',['Pt01'],'C'], 'Ps02':['R',['Mt08'],'A'], 'Ps03':['R',['Mt06'],'A'], 'Ps04':['R',['Pp56C'],'C'], 'Ps05':['R',['Pt06'],'A'], 'Ps06':['R',['Pp09R'],'A'], 'Ps07':['R',['Pt08'],'C'], 'Ps08':['R',['Pt05'],'C'], 'Ps09':['R',['Pp10R'],'A'], 'Ps10':['R',['Pp03L'],'C'], 'Ss01':['R',['St04'],'C'], 'Ss02':['R',['Sp02L'],'C'], 'Ss03':['R',['St03'],'A'], 'Ss04':['R',['St05'],'A'], 'Ss05':['R',['St08'],'C'], 'Ss06':['R',['Sp06L'],'C'], 'Ss09':['R',['St09'],'A'], 'Ss10':['R',['Sp13L'],'A'], }

Each signal is given a name, consisting of its region prefix (C/G/M/P/S), followed by the letter 's', followed by a nominal number unique within the region. The table is a dictionary indexed by the signal name, with value a list of attributes. The first attribute is the aspect of the signal. The second attribute is list of track segments controlled by the signal (usually only one segment), and the third is the orientation of the points, with 'C' indicating that the toe faces clockwise trains, and 'A' indicating that the toe faces anticlockwise trains.

Note that the aspect of the signal is drawn from the character set R (for Red), G (for Green), or B (for Blue). The latter is not an aspect shown by the signal, but is used to indicate where a signal is in conflict with the current route setting. A Blue signal must be reset to Red before it can be cleared.

2.3.3 The points table (updated 20150821:111101)

"pointsData.py" 2.3 =
{ # key:[throw,orientation,[facing segments],[left segments],[right segments] 'Cp01':['L','A',['Cp02L', 'Cp02R'],['Ct01'],['Cp03R']], 'Cp02':['L','C',['Cp01L', 'Cp01R'],['Ct03'],['Cp05R']], 'Cp03':['L','A',['Ct04'],['Gt02'],['Cp01R']], 'Cp04':['R','A',['Ct02'],['Ct08'],['Ct08']], 'Cp05':['R','A',['Ct07'],['Mt09'],['Cp02R']], 'Gp01':['L','A',['St02'],['Gp05L'],['Gt02']], 'Gp02':['R','A',['Gp05L', 'Gp05R'],['Gp03L'],['Gt03']], 'Gp03':['R','C',['Gp04L'],['Gp02L'],['Mt01']], 'Gp04':['R','C',['Gt04'],['Gp03L', 'Gp03R'],['Pt05']], 'Gp05':['R','C',['Gp02L', 'Gp02R'],['Gp01L'],['St01']], 'Mp01':['L','C',['Mt05'],['Mp02R'],['Mp03L']], 'Mp02':['R','A',['Mt10'],['Mp03R'],['Mp01L']], 'Mp03':['L','C',['Mt03'],['Mp01R'],['Mp02L']], 'Mp04':['R','A',['Mt03'],['Mp05R'],['Mp06L']], 'Mp05':['L','A',['Mt04'],['Mp06R'],['Mp04L']], 'Mp06':['L','C',['Mt02'],['Mp04R'],['Mp05L']], 'Pp01':['R','C',['Pt01'],['Mt07'],['Pt02']], 'Pp02':['R','C',['Pt02'],['Pp04L'],['Pp07L']], 'Pp03':['L','A',['Pp04L', 'Pp04R'],['Pt03'],['Pp56L', 'Pp56C', 'Pp56R']], 'Pp04':['R','C',['Pp03L', 'Pp03R'],['Pp02L'],['Mt08']], 'Pp07':['L','A',['Pt06'],['Pp56R'],['Pp02R']], 'Pp08':['R','A',['Pp04L', 'Pp04R'],[],['Pp11L', 'Pp11R']], 'Pp09':['L','C',['Pt05'],['Pp10L'],['Pt04']], 'Pp10':['R','C',['Pt08'],['Pp09L'],['Pt09']], 'Pp11':['L','A',['Pp08R'],[],[]], 'Pp56':['C','A',['Pp03R'],['Pp08L', 'Pp08R'],['Pp07R']], 'Sp01':['L','C',['St04'],['Sp03L'],['Sp02R']], 'Sp02':['L','A',['St03'],['St01'],['Sp01R']], 'Sp03':['L','C',['St05'],['Sp01L'],[]], 'Sp04':['R','A',['Sp05R'],[],[]], 'Sp05':['L','A',['Sp06R'],[],['Sp04L', 'Sp04R']], 'Sp06':['L','A',['Sp07L', 'Sp07R'],['St07'],['Sp05L', 'Sp05R']], 'Sp07':['R','C',['Sp06L', 'Sp06R'],['Sp10L', 'Sp10R'],['Sp12R']], 'Sp08':['R','C',['St08'],['Sp09L'],['Sp14L', 'Sp14R']], 'Sp09':['L','C',['Sp12L'],['Sp08L'],[]], 'Sp10':['L','C',['Sp07L'],['Sp11L', 'Sp11R'],[]], 'Sp11':['L','C',['Sp10L'],[],[]], 'Sp12':['R','A',['Sp13L', 'Sp13R'],['Sp09L', 'Sp09R'],['Sp07R']], 'Sp13':['L','C',['Sp12L', 'Sp12R'],['St11'],['Sp15R']], 'Sp14':['L','A',['Sp08R'],['Sp15L'],[]], 'Sp15':['L','A',['St09'],['Sp14L'],['Sp13R']], }

Each point is given a name, consisting of its region prefix (C/G/M/P/S), followed by the letter 'p', followed by a nominal number unique within the region. The table consists of a dictionary indexed by the point name, with value a list of attributes.

There are normally 5 attributes, but in the case of 3-way points, there is a 6th attribute. These are:

  1. The throw of the point: 'L' (left); 'R' (right); or 'C' (centre).
  2. The orientation of the point: 'C' (clockwise); or 'A' (anticlockwise).
  3. A list of track segments on the left throw of the points.
  4. A list of track segments on the right throw of the points.
  5. A list of track segments facing the points.
  6. A list of track segments on the centre throw of the points.
(currently if the 6th attribute is present, it appears as the 4th element, but this is to be restructured.)

The throw is determined by looking at the point from the facing heel (switch blade) end, towards the frog or toe (as opposed to looking at the heel from the toe, or frog end). The heel end is also called the facing end, and the toe end the trailing end. A good way of remembering this is to note that the human foot has a narrow or heel end, and a wide or toe end just like points.

Double slips are regarded as two back-to-back points, even though the actual heels are reversed, such that the facing heel of the direction of travel is encountered first (whereas for back-to-back points, the facing heel in the direction of travel is encountered last).

Three-way points are labelled as a pair of points, although only 3 of the four possible settings are permitted. This is indicated by the possible directions being L, R and C (centre). There is only one three-way point in this layout: Pp56.

The track segment lists may be empty (a buffer stop segment, although unusual for points), a singleton (the usual case), or a pair of segments (this throw leads to another set of points). A triple is also possible in the case of 3-way points.

The route selected by the current setting of the point is shown on the schematic with a green track. The non-selected branch is shown black. If setting a point is in conflict with current route settings, the selected route is shown on the schematic as blue.

2.3.4 The track segment table (updated 20150821:111209)

"trackData.py" 2.4 =
{ # key:[status,[clockwise],[anticlockwise]] 'Cp01L':['C',['Cp02L', 'Cp02R'],['Ct01']], 'Cp01R':['X',['Cp02L', 'Cp02R'],['Cp03R']], 'Cp02L':['C',['Ct03'],['Cp01L', 'Cp01R']], 'Cp02R':['X',['Cp05R'],['Cp01L', 'Cp01R']], 'Cp03L':['S',['Gt02'],['Ct04']], 'Cp03R':['X',['Cp01R'],['Ct04']], 'Cp04L':['C',['Ct02'],['Ct08']], 'Cp04R':['X',['Ct02'],['Ct06']], 'Cp05L':['S',['Ct07'],['Mt09']], 'Cp05R':['X',['Ct07'],['Cp02R']], 'Ct01':['C',['Cp01L'],['Ct02']], 'Ct02':['C',['Ct01'],['Cp04L', 'Cp04R']], 'Ct03':['C',['Gt03'],['Cp02L']], 'Ct04':['S',['Cp03L', 'Cp03R'],['Ct05']], 'Ct05':['S',['Ct04'],['Mt10']], 'Ct06':['S',['Cp04R'],['Mt08']], 'Ct07':['S',['Gt04'],['Cp05L', 'Cp05R']], 'Ct08':['C',['Cp04L'],['Mt06']], 'Gp01L':['X',['Gt01'],['Gp05L']], 'Gp01R':['S',['Gt01'],['Gt02']], 'Gp02L':['X',['Gp05L', 'Gp05R'],['Gp03L']], 'Gp02R':['C',['Gp05L', 'Gp05R'],['Gt03']], 'Gp03L':['S',['Gp02L'],['Gp04L']], 'Gp03R':['S',['Mt01'],['Gp04L']], 'Gp04L':['S',['Gp03L', 'Gp03R'],['Gt04']], 'Gp04R':['X',['Pt05'],['Gt04']], 'Gp05L':['X',['Gp01L'],['Gp02L', 'Gp02R']], 'Gp05R':['C',['St01'],['Gp02L', 'Gp02R']], 'Gt01':['S',['St02'],['Gp01L', 'Gp01R']], 'Gt02':['S',['Gp01R'],['Cp03L']], 'Gt03':['C',['Gp02R'],['Ct03']], 'Gt04':['S',['Gp04L', 'Gp04R'],['Ct07']], 'Mp01L':['X',['Mp02R'],['Mt05']], 'Mp01R':['S',['Mp03L'],['Mt05']], 'Mp02L':['X',['Mt10'],['Mp03R']], 'Mp02R':['S',['Mt10'],['Mp01L']], 'Mp03L':['S',['Mt03'],['Mp01R']], 'Mp03R':['X',['Mp02L'],['Mt03']], 'Mp04L':['X',['Mp05R'],['Mt03']], 'Mp04R':['S',['Mt03'],['Mp06L']], 'Mp05L':['S',['Mt04'],['Mp06R']], 'Mp05R':['X',['Mt04'],['Mp04L']], 'Mp06L':['S',['Mp04R'],['Mt02']], 'Mp06R':['X',['Mp05L'],['Mt02']], 'Mt01':['S',['Mt02'],['Gp03R']], 'Mt02':['S',['Mp06L', 'Mp06R'],['Mt01']], 'Mt03':['S',['Mp03L', 'Mp03R'],['Mp04L', 'Mp04R']], 'Mt04':['S',['Mt09'],['Mp05L', 'Mp05R']], 'Mt05':['S',['Mp01L', 'Mp01R'],['St11']], 'Mt06':['C',['Ct08'],['Mt07']], 'Mt07':['C',['Mt06'],['Pp01L']], 'Mt08':['S',['Ct06'],['Pp04R']], 'Mt09':['S',['Cp05L'],['Mt04']], 'Mt10':['S',['Ct05'],['Mp02L', 'Mp02R']], 'Pp01L':['C',['Mt07'],['Pt01']], 'Pp01R':['X',['Pt02'],['Pt01']], 'Pp02L':['X',['Pp04L'],['Pt02']], 'Pp02R':['S',['Pp07L'],['Pt02']], 'Pp03L':['X',['Pp04L', 'Pp04R'],['Pt03']], 'Pp03R':['S',['Pp04L', 'Pp04R'],['Pp56L', 'Pp56C', 'Pp56R']], 'Pp04L':['X',['Pp02L'],['Pp03L', 'Pp03R']], 'Pp04R':['S',['Mt08'],['Pp03L', 'Pp03R']], 'Pp07L':['X',['Pt06'],['Pp02R']], 'Pp07R':['S',['Pt06'],['Pp56R']], 'Pp08L':['X',['Pp56L'],[]], 'Pp08R':['S',['Pp56L'],['Pp11L', 'Pp11R']], 'Pp09L':['S',['Pp10L'],['Pt05']], 'Pp09R':['X',['Pt04'],['Pt05']], 'Pp10L':['X',['Pp09L'],['Pt08']], 'Pp10R':['S',['Pt09'],['Pt08']], 'Pp11L':['S',['Pp08R'],[]], 'Pp11R':['S',['Pp08R'],[]], 'Pp56C':['X',['Pp03R'],['Pt04']], 'Pp56L':['X',['Pp03R'],['Pp08L', 'Pp08R']], 'Pp56R':['S',['Pp03R'],['Pp07R']], 'Pt01':['C',['Pp01L', 'Pp01R'],['St10']], 'Pt02':['S',['Pp02L', 'Pp02R'],['Pp01R']], 'Pt03':['S',['Pp03L'],['Pt09']], 'Pt04':['S',['Pp56C'],['Pp09R']], 'Pt05':['S',['Pp09L', 'Pp09R'],['Gp04R']], 'Pt06':['S',['Pt07'],['Pp07L', 'Pp07R']], 'Pt07':['S',['Pt08'],['Pt06']], 'Pt08':['S',['Pp10L', 'Pp10R'],['Pt07']], 'Pt09':['S',['Pt03'],['Pp10R']], 'Sp01L':['S',['Sp03L'],['St04']], 'Sp01R':['X',['Sp02R'],['St04']], 'Sp02L':['C',['St03'],['St01']], 'Sp02R':['S',['St03'],['Sp01R']], 'Sp03L':['S',['St05'],['Sp01L']], 'Sp03R':['X',['St05'],[]], 'Sp04L':['S',['Sp05R'],[]], 'Sp04R':['S',['Sp05R'],[]], 'Sp05L':['S',['Sp06R'],[]], 'Sp05R':['S',['Sp06R'],['Sp04L', 'Sp04R']], 'Sp06L':['S',['Sp07L', 'Sp07R'],['St07']], 'Sp06R':['X',['Sp07L', 'Sp07R'],['Sp05L', 'Sp05R']], 'Sp07L':['X',['Sp10L', 'Sp10R'],['Sp06L', 'Sp06R']], 'Sp07R':['S',['Sp12R'],['Sp06L', 'Sp06R']], 'Sp08L':['X',['Sp09L'],['St08']], 'Sp08R':['C',['Sp14L', 'Sp14R'],['St08']], 'Sp09L':['S',['Sp12L'],['Sp08L']], 'Sp09R':['X',[],[]], 'Sp10L':['S',['Sp11L', 'Sp11R'],[]], 'Sp10R':['X',[],['Sp07L']], 'Sp11L':['S',[],['Sp10L']], 'Sp11R':['X',[],['Sp10L']], 'Sp12L':['X',['Sp13L', 'Sp13R'],['Sp09L', 'Sp09R']], 'Sp12R':['S',['Sp13L', 'Sp13R'],['Sp07R']], 'Sp13L':['S',['St11'],['Sp12L', 'Sp12R']], 'Sp13R':['X',['Sp15R'],['Sp12L', 'Sp12R']], 'Sp14L':['C',['Sp15L'],['Sp08R']], 'Sp14R':['X',[],['Sp08R']], 'Sp15L':['C',['St09'],['Sp14L']], 'Sp15R':['X',['St09'],['Sp13R']], 'St01':['C',['Sp02L'],['Gp05R']], 'St02':['S',['St04'],['Gt01']], 'St03':['C',['St06'],['Sp02L', 'Sp02R']], 'St04':['S',['Sp01R', 'Sp01L'],['St02']], 'St05':['S',['St07'],['Sp03R', 'Sp03L']], 'St06':['C',['St08'],['St03']], 'St07':['S',['Sp06L'],['St05']], 'St08':['C',['Sp08L', 'Sp08R'],['St06']], 'St09':['C',['St10'],['Sp15L', 'Sp15R']], 'St10':['C',['Pt01'],['St09']], 'St11':['S',['Mt05'],['Sp13L']], }

Each track segment is given a name, consisting of its region prefix (C/G/M/P/S), followed by the letter 't', followed by a nominal number unique within the region. The table consists of a dictionary indexed by the track name, with the value being a list of attributes.

There are 3 attributes:

  1. The status of the segment: 'X' (unknown); 'S' (set); 'C' (clear); and 'O' (occupied). A 'set' track means that the points are aligned for this track segment, but the controlling signals are not clear. The 'clear' status indicates that controlling signals are green (or yellow).
  2. A list of track segments following in the clockwise direction.
  3. A list of track segments following in the anticlockwise direction.

2.3.5 Signal Control Definitions

This file is generated by the makeJmrData.py program (q.v.)

The file defines the pathways, known as blocks, controlled by each signal. Each block is a list of track segments from one facing signal to the next facing signal. A given signal may control more than one block. A track segment is a contiguous piece of track, which may be "set" or "cleared".

Setting a track segment means that the track segment is added to a block being built and made ready for clearing. Once a complete block is set, it may be cleared by setting the controlling signal to green. (Note that a signal may not be cleared if it does not have a set block that it controls.) A clear track segment is part of a cleared block.

Track segments are of two types: point controlled segments, and standalone segments. A point controlled segment is set by the process of toggling the points to enable a route through the given segment, that is, the points are thrown in the direction of the segment. Standalone segments are set whenever the segments at either end are set. A standalone segment at the heel of the set of points (the common path through the points) is set whenever the points are thrown in either direction.

It follows that the only time that a heel segment is not set or cleared is when the the entire layout is reset, that is, no information about the state of points is known, and all signals are red.

2.3.6 The conflicts table

The conflicts table defines those aspects of signals and directions of points (subsequently called just "aspects") that would preclude the clearing of a signal (setting to green), or the changing of a point.

Each signal or point is defined by its name, and the name is used as index into a dictionary of lists (called the "conflict list" for that particular signal or point). Each element of the the conflict list is itself a list (called the "conflict pathway"), defining one set of aspect settings that identifies a conflict. If any conflict pathway indicates a conflict, then the entire conflict list identifies a conflict - that is, the overall conflict is the OR of each conflict pathway.

A conflict pathway identifies one or more points or signals. Only when all the listed points/signals have the given aspect is a conflict defined - that is, the overall conflict of a conflict pathway is the AND of all the indicated points and signals.

The aspects of the given pathway points/signals are indicated by a list (the conflict pathway list) of pairs of firstly point/signal names, followed by the aspect. Note that a red signal can never cause a conflict, so where signals are indicated as the first of a pair, the second component must always be 'G'.

This file is now generated by the makeJmrData.py program.

2.3.7 The Routes Definition

The routes data table defines the signals and points settings require to set certain predefined routes. The routes are defined with single names, while routes and directions settings are defined with pairs of names. The first group set just points, while the second group set both points and signals.

"routesData.py" 2.5 =
{ 'inner' : [('Gp02','R'),('Gp05','R'), ('Cp01','L'),('Cp02','L'),('Cp04','L'), ('Pp01','L'), ('Sp15','L'),('Sp14','L'),('Sp08','R'),('Sp02','L')], 'inner2pinot' : [('Gp02','R'),('Gp05','R'), ('Cp01','L'),('Cp02','L'),('Cp04','R'), ('Pp01','R'),('Pp02','R'),('Pp03','L'),('Pp04','R'), ('Pp10','R'),('Pp07','L'),('Sp15','L'),('Sp14','L'),('Sp08','R'),('Sp02','L')], 'outer' : [('Gp01','R'),('Cp03','L'),('Mp02','R'),('Mp01','L'), ('Sp13','L'),('Sp12','R'),('Sp07','R'),('Sp06','L'),('Sp03','L'),('Sp01','L')], 'upper' : [('Cp05','L'),('Mp05','L'),('Mp06','R'),('Gp03','R'),('Gp04','L')], 'inner,clock': ['Gs02','Cs02','Cs07','Ps03','Ss09','Ss03','Cs09', 'Cs05','Gs04','Ss02','Ss05','Ps01'], 'inner,anti' : ['Cs09','Cs05','Gs04','Ss02','Ss05','Ps01','Gs02', 'Cs02','Cs07','Ps03','Ss09','Ss03'], 'outer,clock': ['Gs01','Cs01','Ms02','Cs04','Gs03','Ms01','Ss06','Ss01'], 'outer,anti' : ['Cs04','Gs03','Ms01','Gs01','Cs01','Ms02','Ss10','Ss04'], 'upper,clock': ['Gs06','Cs03','Ms03','Gs05','Ms04','Cs06'], 'upper,anti' : ['Gs05','Ms04','Cs06','Gs06','Cs03','Ms03'], }

2.4 The Device Driver Translate Tables

The driver translate tables define how point and signal numbers translate into I2C bus addresses.

To drive the 42 points with two active settings (L/R direction) requires 84 active leads. This complexity is reduced by the use of crossbar switching, which reduces the number of transistors from 84 to 20. These are arranged in two banks of 2x8 and 1x4 groups, with the first group driving one side of the active pairs, and the second group driving the other side. Selection within a group is done by the I2C bus address.

"pointsDriverData.py" 2.6 =
{ 'Cp1':[(2,0b00000001),(0,0b00000001),(2,0b00000001),(0,0b00000010)], 'Cp2':[(2,0b00000001),(0,0b00000100),(2,0b00000001),(0,0b00001000)], 'Cp3':[(2,0b00000001),(0,0b00010000),(2,0b00000001),(0,0b00100000)], 'Cp4':[(2,0b00000001),(0,0b01000000),(2,0b00000001),(0,0b10000000)], 'Cp5':[(2,0b00000010),(0,0b00000001),(2,0b00000010),(0,0b00000010)], 'Gp1':[(2,0b00000010),(0,0b00000100),(2,0b00000010),(0,0b00001000)], 'Gp2':[(2,0b00000010),(0,0b00010000),(2,0b00000010),(0,0b00100000)], 'Gp3':[(2,0b00000010),(0,0b01000000),(2,0b00000010),(0,0b10000000)], 'Gp4':[(2,0b00000100),(0,0b00000001),(2,0b00000100),(0,0b00000010)], 'Gp5':[(2,0b00000100),(0,0b00000100),(2,0b00000100),(0,0b00001000)], 'Mp1':[(2,0b00000100),(0,0b00010000),(2,0b00000100),(0,0b00100000)], 'Mp2':[(2,0b00000100),(0,0b01000000),(2,0b00000100),(0,0b10000000)], 'Mp3':[(2,0b00001000),(0,0b00000001),(2,0b00001000),(0,0b00000010)], 'Mp4':[(2,0b00001000),(0,0b00000100),(2,0b00001000),(0,0b00001000)], 'Mp5':[(2,0b00001000),(0,0b00010000),(2,0b00001000),(0,0b00100000)], 'Mp6':[(2,0b00001000),(0,0b01000000),(2,0b00001000),(0,0b10000000)], 'Pp1':[(2,0b00000001),(1,0b00000001),(2,0b00000001),(1,0b00000010)], 'Pp2':[(2,0b00000001),(1,0b00000100),(2,0b00000001),(1,0b00001000)], 'Pp3':[(2,0b00000001),(1,0b00010000),(2,0b00000001),(1,0b00100000)], 'Pp4':[(2,0b00000001),(1,0b01000000),(2,0b00000001),(1,0b10000000)], 'Pp5':[(2,0b00000010),(1,0b00000001),(2,0b00000010),(1,0b00000010)], 'Pp6':[(2,0b00000010),(1,0b00000100),(2,0b00000010),(1,0b00001000)], 'Pp7':[(2,0b00000010),(1,0b00010000),(2,0b00000010),(1,0b00100000)], 'Pp8':[(2,0b00000010),(1,0b01000000),(2,0b00000010),(1,0b10000000)], 'Pp9':[(2,0b00000100),(1,0b00000001),(2,0b00000100),(1,0b00000010)], 'Pp10':[(2,0b00000100),(1,0b00000100),(2,0b00000100),(1,0b00001000)], 'Pp11':[(2,0b00000100),(1,0b00010000),(2,0b00000100),(1,0b00100000)], #free:... 'Sp1':[(3,0b00000001),(0,0b00000001),(3,0b00000001),(0,0b00000010)], 'Sp2':[(3,0b00000001),(0,0b00000100),(3,0b00000001),(0,0b00001000)], 'Sp3':[(3,0b00000001),(0,0b00010000),(3,0b00000001),(0,0b00100000)], 'Sp4':[(3,0b00000001),(0,0b01000000),(3,0b00000001),(0,0b10000000)], 'Sp5':[(3,0b00000010),(0,0b00000001),(3,0b00000010),(0,0b00000010)], 'Sp6':[(3,0b00000010),(0,0b00000100),(3,0b00000010),(0,0b00001000)], 'Sp7':[(3,0b00000010),(0,0b00010000),(3,0b00000010),(0,0b00100000)], 'Sp8':[(3,0b00000010),(0,0b01000000),(3,0b00000010),(0,0b10000000)], 'Sp9':[(3,0b00000100),(0,0b00000001),(3,0b00000100),(0,0b00000010)], 'Sp10':[(3,0b00000100),(0,0b00000100),(3,0b00000100),(0,0b00001000)], 'Sp11':[(3,0b00000100),(0,0b00010000),(3,0b00000100),(0,0b00100000)], 'Sp12':[(3,0b00000100),(0,0b01000000),(3,0b00000100),(0,0b10000000)], 'Sp13':[(3,0b00001000),(0,0b00000001),(3,0b00001000),(0,0b00000010)], 'Sp14':[(3,0b00001000),(0,0b00000100),(3,0b00001000),(0,0b00001000)], 'Sp15':[(3,0b00001000),(0,0b00010000),(3,0b00001000),(0,0b01000000)], #free:... }
"signalsDriverData.py" 2.7 =
{ 'Cs1':[(0,0b00000001),(0,0b00000010)], 'Cs2':[(0,0b00000100),(0,0b00001000)], 'Cs3':[(0,0b00010000),(0,0b00100000)], 'Cs4':[(0,0b01000000),(0,0b10000000)], 'Cs5':[(1,0b00000001),(1,0b00000010)], 'Cs6':[(1,0b00000100),(1,0b00001000)], 'Cs7':[(1,0b00010000),(1,0b00100000)], 'Cs8':[(1,0b01000000),(1,0b10000000)], 'Cs9':[(2,0b00000001),(2,0b00000010)], 'Gs1':[(2,0b00000100),(2,0b00001000)], 'Gs2':[(2,0b00010000),(2,0b00100000)], 'Gs3':[(2,0b01000000),(2,0b10000000)], 'Gs4':[(3,0b00000001),(3,0b00000010)], 'Gs5':[(3,0b00000100),(3,0b00001000)], 'Gs6':[(3,0b00010000),(3,0b00100000)], 'Ms1':[(3,0b01000000),(3,0b10000000)], 'Ms2':[(4,0b00000001),(4,0b00000010)], 'Ms3':[(4,0b00000100),(4,0b00001000)], 'Ms4':[(4,0b00010000),(4,0b00100000)], 'Ps1':[(4,0b01000000),(4,0b10000000)], 'Ps2':[(5,0b00000001),(5,0b00000010)], 'Ps3':[(5,0b00000100),(5,0b00001000)], 'Ps4':[(5,0b00010000),(5,0b00100000)], 'Ps5':[(5,0b01000000),(5,0b10000000)], 'Ps6':[(6,0b00000001),(6,0b00000010)], 'Ps7':[(6,0b00000100),(6,0b00001000)], 'Ps8':[(6,0b00010000),(6,0b00100000)], 'Ps9':[(6,0b01000000),(6,0b10000000)], #free:[(7,0b00000001),(7,0b00000010)], #free:[(7,0b00000100),(7,0b00001000)], #free:[(7,0b00010000),(7,0b00100000)], #free:[(7,0b01000000),(7,0b10000000)], # 'Ss1':[(8,0b00000001),(8,0b00000010)], 'Ss2':[(8,0b00000100),(8,0b00001000)], 'Ss3':[(8,0b00010000),(8,0b00100000)], 'Ss4':[(8,0b01000000),(8,0b10000000)], 'Ss5':[(9,0b00000001),(9,0b00000010)], 'Ss6':[(9,0b00000100),(9,0b00001000)], 'Ss7':[(9,0b00010000),(9,0b00100000)], 'Ss8':[(9,0b01000000),(9,0b10000000)], 'Ss9':[(0b1010,0b00000001),(0b1010,0b00000010)], }

3. The LayoutComponent Module

This code module supplies classes to define all the components of a layout: signals, points, tracks, together with methods to alter signals and points, and change the track status accordingly, subject to various interlocking rules that afford safe operation to trains using the layout.

"LayoutComponents.py" 3.1 =
import getopt import re import sys DEBUG=[] SIGNALS='signalsData.py' POINTS='pointsData.py' TRACKS='trackData.py' ROUTES='routesData.py' BLOCKS='blocksData.py' def localFlag(msg): pass flagError=localFlag class Component(): '''The base class for all layout components. Components are named according to their region, with an initial letter drawn from: C - Cabernet G - Grenache M - Merlot P - Pinot S - Shiraz ''' def __init__(self,name): self.name=name def __str__(self): '''return a string representation of this component''' return self.name <LayoutComponents: Signal class 3.2> <LayoutComponents: Point class 3.4> <LayoutComponents: Track class 3.6> <LayoutComponents: Route class 3.8> <LayoutComponents: the Signals class 3.3> <LayoutComponents: the Points class 3.5> <LayoutComponents: the Tracks class 3.7> <LayoutComponents: the Routes class 3.9> <LayoutComponents: the Layout class 3.10> <LayoutComponents: the main routine 3.18> if __name__ == '__main__': (vals,path)=getopt.getopt(sys.argv[1:],'dcpst:f:', ['debug','debcon','path','points','signals','controls=','conflicts=']) for (opt,val) in vals: if opt=='-d' or opt=='--debug': DEBUG.append('debug') elif opt=='-c' or opt=='--debcon': DEBUG.append('debcon') elif opt=='--path': DEBUG.append('path') elif opt=='-p' or opt=='--points': DEBUG.append('points') elif opt=='-s' or opt=='--signals': DEBUG.append('signals') elif opt=='-f' or opt=='--conflicts': conflictfile=val elif opt=='-t' or opt=='--controls': controlfile=val if len(path)>0: srcfile=path[0] else: srcfile='jmrData.txt' # all set, now run the main loop main()

This module is designed to be imported into other programs, and defines a number of classes for use within those programs. The key class is the Layout class, which can be instantiated to represent an entire layout.

When called as a stand-alone program however, the main routine runs a number of test cases to verify the correct operation of the layout. After these few test cases are run, the user is prompted to enter a variety of specific test cases, such as:

        Ps03G
        Pp04L
      
These examples would (attempt to) set signal Ps03 to green, and then throw point Pp04 to the left.
        signals
        tracks
        points
      
These three examples display the signals/tracks/points data tables respectively.

The following three subsections describe two classes for each of the layout component types: signals, points and tracks. The first class in each case provides a definition for a single component, while the second class defines the collection of all the components of that type that make up the layout.

3.1 Signals

3.1.1 The Signal class

<LayoutComponents: Signal class 3.2> =
class Signal(Component): '''Class to instantiate all instances of signals. Attributes are: name (inherited): signal name of the form Xsnn X is region name, 's' is signal, nn is signal number (2 digits) aspect: either 'R'ed or 'G'reen permit: track segment name controlled by this signal direction: signal faces trains in 'C'lockwise or 'A'nticlockwise direction ''' def __init__(self,name,permit=[],direction='C',aspect='R'): Component.__init__(self,name) self.aspect=aspect # 'R'ed or 'G'reen, default to red self.permit=permit # set of track segments self.direction=direction # 'C'lockwise or 'A'nticlockwise, default to clockwise def __str__(self): '''return a string representation of this component''' str="%s" % Component.__str__(self) str+=":aspect=%s,permit=%s,direction=%s" % (self.aspect,self.permit,self.direction) return str def stringSignal(self): str="'%s'" % Component.__str__(self) str+=":['%s',%s,'%s']" % (self.aspect,self.permit,self.direction) return str def changeSignal(self,aspect): '''set this signal to aspect 'R' or 'G' Raw change only: no interlocking performed. ''' self.aspect=aspect
Chunk referenced in 3.1

The Signal class instantiates objects representing the individual signals within the layout. These are identified by a name, given by the rules in paragraph <signalsData.py 2.2>. There are 4 attributes to a signal instance:

name
as identified in paragraph <signalsData.py 2.2>
permit
A list of track segment names of those track segments controlled by this signal (usually a singleton, but defaults to empty).
direction
(aka orientation) an indication of whether the signal is oriented against trains travelling in the clockwise or anticlockwise direction (defaults to clockwise).
aspect
the indication shown by the signal, either red, green, or yellow (defaults to red).

The stringSignal method returns a string containing indications of the values of these 4 attributes.

The changeSignal method changes the aspect of the signal to the given value. No check is made if this value is legitimate.

3.1.2 The Signals class

<LayoutComponents: the Signals class 3.3> =
class Signals(): def __init__(self): self.signalsfname='' self.signals={} return def load(self,fname): ''' load the signals table from the file with name fname. The table must be specified as a python-format dictionary definition, where the signal name is the key to a list value containing permit,direction,aspect ''' self.signalsfname=fname signalsfile=open(fname) signalstr=signalsfile.read() signalsfile.close() signaldefs=eval(signalstr) keys=signaldefs.keys() keys.sort() for key in keys: val=signaldefs[key] self.signals[key]=Signal(key,permit=val[1],direction=val[2],aspect=val[0]) return def __str__(self): '''return a string representation of this component''' keys=self.signals.keys() keys.sort() str='' for key in keys: str+=' %s' % (self.signals[key]) return str def stringSignals(self): sigs=self.signals keys=sigs.keys() keys.sort() str='{\n' str+=' # key:[aspect,controls,orientation]\n' for s in keys: signal=sigs[s] str+=' %s,\n' % (signal.stringSignal()) str+='}\n' return str
Chunk referenced in 3.1

The Signals class manages all the signals associated with a given layout. This is specified by the persistent data file <signalsData.py 2.2>, which is a Python dictionary definition where the entries are labelled by signal names, and the values for each key define the 3 subordinate signal attributes, namely, permit, direction, and aspect.

The definition is read and evaluated to define the signals attribute of class instances. This is the only externaly useful attribute of the class.

The stringSignals method returns a string in the same format as used in the <signalsData.py 2.2> file (augmented with a comment showing the names of the fields), and is suitable for saving back to the file, or for debugging purposes.

3.2 Points

3.2.1 The Point class

<LayoutComponents: Point class 3.4> =
class Point(Component): '''define a point object, a subclass of a layout Component. A point has the following variables: name - inherited from Component setting - the way the points are set, Left or Right, as seen from the heel or facing end. direction - the direction from which the facing end is encountered first. left - the track segment when set to the left right - the track segment when set to the right centre - the track segment when set to the centre (optional, only used for 3-way points) ''' def __init__(self,name,setting='L',direction='C', facing=[],left=[],right=[],centre=[]): self.name=name self.setting=setting # 'L'eft or 'R'ight, default to left self.direction=direction # 'C'lockwise or 'A'nticlockwise, # default to clockwise self.left=left # set of track segments on left setting self.centre=centre self.right=right # set of track segments on right setting self.facing=facing # set of track segments on facing setting def __str__(self): '''return a string representation of this component''' str="'%s'" % Component.__str__(self) str+=":['%s','%s',%s,%s,%s]" % \ (self.setting,self.direction,self.facing,self.left,self.right) return str def changePoint(self,setting): '''set point to either 'L' or 'R' L - points set left (from facing position) R - points set right (from facing position) C - points set centre (from facing position) Raw change only: no interlocking performed. ''' self.setting=setting def changeRight(self): '''deprecated in favour of changePoint('R')''' self.setting='R' def changeLeft(self): '''deprecated in favour of changePoint('L')''' self.setting='L'
Chunk referenced in 3.1

3.2.2 The Points class

<LayoutComponents: the Points class 3.5> =
class Points(): def __init__(self): self.points={} return def load(self,fname): ''' load the points table from the file with name fname. The table must be specified as a python-format dictionary definition, where the point name is the key to a list value containing setting,direction,facing,left,right,centre(if present) ''' pointsfname=fname pointsfile=open(fname) pointstr=pointsfile.read() pointsfile.close() pointdefs=eval(pointstr) keys=pointdefs.keys() keys.sort() for key in keys: val=pointdefs[key] # check here for 3-way points if len(val)==5: self.points[key]=Point(key,setting=val[0],direction=val[1],\ facing=val[2],left=val[3],right=val[4]) elif len(val)==6: # points are 3-way self.points[key]=Point(key,setting=val[0],direction=val[1],\ facing=val[2],left=val[3],right=val[4],\ centre=val[5]) else: flagError("Error in points definition for points %s" % (key)) return def __str__(self): '''return a string representation of this component''' keys=points.keys() keys.sort() str='' first=True for key in keys: if first: str+=' %s' % (points[key]) first=False else: str+=', %s' % (points[key]) return str def stringPoints(self): keys=self.points.keys() keys.sort() str='{\n' str+=' # key:[throw,orientation,[facing segments],[left segments],[right segments]' first=True for key in keys: str+='\n %s,' % (self.points[key]) str+='\n}\n' return str
Chunk referenced in 3.1

3.3 Tracks

3.3.1 The Track class

<LayoutComponents: Track class 3.6> =
class Track(Component): def __init__(self,name,status='X',clock=[],anti=[]): self.name=name self.status=status # 'X'cluded, 'C'lear, 'S'et, 'O'ccupied, '-'non-existent, default to excluded self.clock=clock # set of track segments in clockwise direction self.anti=anti # set of track segments in anticlockwise direction self.signal=None # this segment controlled by this signal def __str__(self): str="'%s'" % Component.__str__(self) str+=":['%s',%s,%s]" % (self.status,self.clock,self.anti) return str def setStatus(self,status): '''set track segment to have new status 'X','S','C','O' - - track is non-existent X - track is out of use, or not set for travel S - track is set, ready for use C - track is cleared for traffic O - track is occupied (not currently used) Raw change only: no interlocking performed. ''' self.status=status
Chunk referenced in 3.1

3.3.2 The Tracks class

<LayoutComponents: the Tracks class 3.7> =
class Tracks(): def __init__(self): self.tracks={} return def load(self,fname): ''' load the tracks table from the file with name fname. The table must be specified as a python-format dictionary definition, where the track name is the key to a list value containing status,clock,anti ''' tracksfname=fname tracksfile=open(fname) trackstr=tracksfile.read() tracksfile.close() trackdefs=eval(trackstr) keys=trackdefs.keys() keys.sort() for key in keys: val=trackdefs[key] self.tracks[key]=Track(key,status=val[0],clock=val[1],anti=val[2]) return def __str__(self): keys=tracks.keys() keys.sort() str='' first=True for key in keys: if first: str+=' %s' % (tracks[key]) first=False else: str+=', %s' % (tracks[key]) return str def stringTracks(self): trks=self.tracks keys=trks.keys() keys.sort() str='{\n' str+=' # key:[status,[clockwise],[anticlockwise]]\n' for t in keys: str+=' %s,\n' % (trks[t]) str+='}\n' return str def setTrack(self,track): '''a track that is set means it is ready to be cleared once the controlling signal is also cleared. ''' if not track: return if not self.tracks.has_key(track): flagError("No definition for track %s" % (track)) return track=self.tracks[track] track.setStatus('S') def excludeTrack(self,track): if 'tracks' in DEBUG: print "excluding track %s" % (track) if not track: return if not self.tracks.has_key(track): flagError("No definition for track %s" % (track)) return track=self.tracks[track] track.setStatus('X')
Chunk referenced in 3.1

3.4 Routes

3.4.1 the Route class

<LayoutComponents: Route class 3.8> =
class Route(Component): def __init__(self,name,value): self.name=name self.route=value pass def _str__(self): str="'%s'" % Component.__str__(self) str="%s" % (self.route) return str
Chunk referenced in 3.1

3.4.2 the Routes class

<LayoutComponents: the Routes class 3.9> =
class Routes(): def __init__(self): self.routes={} pass def load(self,fname): routesfname=fname routesfile=open(fname) routesstr=routesfile.read() routesfile.close() routesdefs=eval(routesstr) keys=routesdefs.keys() keys.sort() for key in keys: val=routesdefs[key] self.routes[key]=Route(key,val) return def __str__(self): keys=routes.keys() keys.sort() str='' first=True for key in keys: if first: str+=' %s' % (routes[key]) first=False else: str+=', %s' % (routes[key]) return str
Chunk referenced in 3.1

3.5 Blocks

3.6 The Layout class

<LayoutComponents: the Layout class 3.10> =
class Layout(): global DEBUG def __init__(self): self.signalsData=Signals() self.signals=self.signalsData.signals self.pointsData=Points() self.points=self.pointsData.points self.tracksData=Tracks() self.tracks=self.tracksData.tracks self.routesData=Routes() self.routes=self.routesData.routes self.clearedBlocks=[] self.occupiedBlocks=[] return def load(self): self.signalsData.load(SIGNALS) self.signals=self.signalsData.signals self.pointsData.load(POINTS) self.points=self.pointsData.points self.tracksData.load(TRACKS) self.tracks=self.tracksData.tracks self.routesData.load(ROUTES) self.routes=self.routesData.routes f=open(BLOCKS,'r') fst=f.readline().strip() if fst: cleared=occupied=[] exec(fst) self.clearedBlocks=cleared self.occupiedBlocks=occupied # basic data initialized, now compute segment-signal relationships permissions={} keys=self.signals.keys() for s in keys: permit=self.signals[s].permit[0] permissions[permit]=s self.permissions=permissions def __str__(self): str='Signals:\n ' sigs=self.signals keys=sigs.keys() keys.sort() for s in keys: str+='%s,' % (sigs[s]) str+='\nPoints:\n ' pnts=self.points keys=pnts.keys() keys.sort() for p in keys: str+='%s,' % (pnts[p]) str+='\nTracks:' trks=self.tracks keys=trks.keys() keys.sort() first='' for t in keys: track=trks[t] if track.name[0:2]!=first: str+='\n ' first=track.name[0:2] str+='%s,' % (track) str+='\nSigSegs:\n' perms=self.permissions keys=perms.keys() keys.sort() for t in keys: str+=' %s:%s,\n' % (t,perms[t]) return str def stringBlocks(self): clrs=self.clearedBlocks occs=self.occupiedBlocks str='cleared=[' for s in clrs: str+="'%s'," % (s) str+='];' str+='occupied=[' for s in occs: str+="'%s'," % (s) str+=']\n' return str def changePoint(self,point,newsetting,interlock=True): if 'points' in DEBUG: print "changePoint(%s,%s,%s)" % (point,newsetting,interlock) # check interlocking if not self.points.has_key(point): flagError("No definition for point %s" % (point)) return thispoint=self.points[point] if interlock: cursetting=thispoint.setting thistrack=point+cursetting # check protection for thispoint protected=self.trackProtected(thistrack) if not protected: flagError("points %s are not protected against conflicting movements" % (point)) return # now set points, and clear route thispoint.changePoint(newsetting) if 'points' in DEBUG: print "changePoint(%s,%s) changed" % (point,newsetting) left="%sL" % (point) right="%sR" % (point) centre="%sC" % (point) # check if centre exists if not self.tracks.has_key(centre): centre='' setlist=[] if newsetting=='L': if 'points' in DEBUG: print "changePoint(%s,%s): left=%s" % (point,newsetting,left) self.tracksData.setTrack(left) setlist=thispoint.left if 'points' in DEBUG: print "changePoint(%s,%s): right=%s" % (point,newsetting,right) self.tracksData.excludeTrack(right) self.tracksData.excludeTrack(centre) elif newsetting=='R': if 'points' in DEBUG: print "changePoint(%s,%s): left=%s" % (point,newsetting,left) self.tracksData.setTrack(right) setlist=thispoint.right if 'points' in DEBUG: print "changePoint(%s,%s): right=%s" % (point,newsetting,right) self.tracksData.excludeTrack(left) self.tracksData.excludeTrack(centre) elif newsetting=='C': if 'points' in DEBUG: print "changePoint(%s,%s): centre=%s" % (point,newsetting,centre) self.tracksData.setTrack(centre) setlist=thispoint.centre if 'points' in DEBUG: print "changePoint(%s,%s): left=%s" % (point,newsetting,left) print "changePoint(%s,%s): right=%s" % (point,newsetting,right) self.tracksData.excludeTrack(left) self.tracksData.excludeTrack(right) else: flagError("bad point setting for %s:%s" % (point,newsetting)) return # now set tracks controlled by this point for t in setlist: if 'points' in DEBUG: print "changePoint: clears track %s" % (t) self.tracksData.setTrack(t) return <LayoutComponents: Layout class: setClearPath method 3.14> <LayoutComponents: Layout class: checkInterlocking method 3.11> <LayoutComponents: Layout class: changeSignal method 3.13> <LayoutComponents: Layout class: getPath method 3.15> <LayoutComponents: Layout class: collectSignals method 3.16> <LayoutComponents: Layout class: trackProtected method 3.17> def close(self): str=self.signalsData.stringSignals() f=open(SIGNALS,'w') f.write(str) f.close() str=self.pointsData.stringPoints() f=open(POINTS,'w') f.write(str) f.close() str=self.tracksData.stringTracks() f=open(TRACKS,'w') f.write(str) f.close() str=self.stringBlocks() f=open(BLOCKS,'w') f.write(str) f.close() return def stringRepr(self): str=self.signalsData.stringSignals() str+=self.pointsData.stringPoints() str+=self.tracksData.stringTracks() return str
Chunk referenced in 3.1

3.6.1 the checkInterlocking method

<LayoutComponents: Layout class: checkInterlocking method 3.11> =
def checkInterlocking(self,signal): # first get the path from this signal in direction dirn. thissignal=self.signals[signal] # first check if conflicting tracks are not cleared controlled=thissignal.permit if len(controlled)>1: # yes, check conflicting tracks are not cleared for t in controlled[1:]: trackseg=self.tracks[t] if trackseg.status in ['C','O']: flagError("conflicting track at %s" % (t)) return (False,None) # others OK, now check path controltrack=controlled[0] dirn=thissignal.direction if 'interlock' in DEBUG: print "<P>checkInterlocking(signal=%s,thissignal=%s,controltrack=%s,dirn=%s)" % \ (signal,thissignal,controltrack,dirn) (path,failtrack,reason)=self.getPath(controltrack,dirn,[]) if 'interlock' in DEBUG: print "<P>checkInterlocking has path %s" % (path) # scan this path checking for signal constraints firstFaceRed=False <checkInterlocking: check all segments in path 3.12> # run out of pathway, why? Need to look at last element of path # does it exist? Is its status 'X'? if 'interlock' in DEBUG: print "<BR>checkInterlocking: at end of path %s" % (p) print "<BR>checkInterlocking: with permissions=%s" % (self.permissions) lasttrack=self.tracks[p] if dirn=='C': nexttrack=lasttrack.clock else: nexttrack=lasttrack.anti # check if there is a nexttrack. We might be at a buffer stop if 'interlock' in DEBUG: print "<BR>checkInterlocking: at buffer? lasttrack=%s,nexttrack=%s" % \ (lasttrack,nexttrack) if not nexttrack: # buffer stop, all OK to here return (True,path) # end of path, check if we are at a red signal? if self.permissions.has_key(nexttrack[0]): # there is a signal, is it facing? if 'interlock' in DEBUG: print "<BR>checkInterlocking: found last facing at %s" % (nexttrack[0]) lastsignalname=self.permissions[nexttrack[0]] lastsignal=self.signals[lastsignalname] if 'interlock' in DEBUG: print "<BR>checkInterlocking: signal is %s" % (lastsignal) if lastsignal.direction==d: # yes, is it red? if lastsignal.aspect=='R': # yes, OK return (True,path) else: # no, but this should not happen? flagError("green signal at %s controls unset track %s" % (lastsignalname,nexttrack)) # trailing direction, ignore if 'interlock' in DEBUG: print "<BR>checkInterlocking: got nexttrack %s" % (nexttrack) if nexttrack: # route continues, but not set flagError("route not set at %s" % (nexttrack[0])) return (False,None) return (True,path)
Chunk referenced in 3.10

From a given signal, check if route is set in given direction dirn. The constraints for a route to be cleared are:

  1. If signal is currently green, the interlocking is skipped. (This is done by changeSignal, before control gets to here.)
  2. Track must be set (not clear) to the next facing signal, whether it be red or green.
  3. If any trailing signal passed is green, FAIL.
  4. If next facing is green, OK and stop checking interlocking at this point.
  5. If the next facing is red, must continue checking to the facing signal after that, and ensure that it is red also, when OK,
  6. If second trailing signal is green, FAIL.
  7. If there is no facing signal before the end of the route (current one excepted), the route ends at buffer stops, and the route may be cleared OK.
NO! This needs further thought. The next segment may be 'X' for good reason. Need an explicit end of track test!!

Where a signal has more than one controlled track, it is assumed that the first track segment is the segment directly controlled by the signal, while others listed must be not cleared for this signal to be cleared. This situation arises where there is direct track conflict, such as at track crossings.

<checkInterlocking: check all segments in path 3.12> =
thisblock=[]; thisdir='' for (p,d) in path: if 'interlock' in DEBUG: s=self.tracks[p].status print "<BR>checkInterlocking: explore path at %s with status %s" % (p,s) if not thisdir: thisdir=d # is track p set? if self.tracks[p].status not in ['S']: # no, not enough set pathway or conflicting cleared pathway to check signals # but first, check that we are not looking beyond a facing green if self.permissions.has_key(p): sig=self.permissions[p] thissignal=self.signals[sig] # is it facing or trailing? facing=thissignal.direction==d if facing and thissignal.aspect=='G': # can safely ignore thisblock.append(p) return (True,thisblock) if 'interlock' in DEBUG: print "<BR>checkInterlocking: path not clearable at %s" % (p) flagError("route not set at segment %s" % (p)) return (False,None) # track p is set, so check for signal if self.permissions.has_key(p): # there is one, so get it sig=self.permissions[p] if 'interlock' in DEBUG: print "<BR>checkInterlocking: signal %s at path %s" % (sig,p) thissignal=self.signals[sig] # is it facing or trailing? facing=thissignal.direction==d if not facing: if 'interlock' in DEBUG: print "<BR>checkInterlocking: trailing signal at %s" % (thissignal) # must be red if thissignal.aspect!='R': # signal constraint fail flagError("opposing movement signal %s is green" % (thissignal.name)) return (False,None) # is red, count first trailing passed else: # is a facing signal if 'interlock' in DEBUG: print "<BR>checkInterlocking: facing signal at %s" % (thissignal) if thissignal.aspect=='G': # is green, so can stop checking interlock return (True,path) # facing is red, check if first elif firstFaceRed: # passed the first facing, so we can exit with success! return (True,path) # this is first facing red, must continue checking until next firstFaceRed=True if 'interlock' in DEBUG: print "<BR>checkInterlocking: passing first face red at %s" % \ (thissignal) # and press on pass # end if not facing pass # if self.permissions pass # for p
Chunk referenced in 3.11

For every track segment p in the path, we check that its status is set.

3.6.2 the changeSignal method

<LayoutComponents: Layout class: changeSignal method 3.13> =
def changeSignal(self,signal,aspect): '''change a signal aspect with interlocking. Returned is success or failure, and: - in the case of failure, the track segment or signal that fails the interlocking constraints; - in the case of success, the block that has been cleared (which is added to the class variable clearedBlocks). ''' #print "<P>layout changeSignal, DEBUG=%s" % (DEBUG) thissignal=self.signals[signal] if 'signals' in DEBUG: print "changeSignal: current %s.aspect=%s" % (signal,thissignal.aspect) if aspect=='R': # can always change signal to red thissignal.changeSignal('R') return (True,None) # but for green, must check interlocking # however, if already green, ignore and don't check interlocking if thissignal.aspect=='G': return (True,None) # currently red, must check interlocking (clear,block)=self.checkInterlocking(signal) if clear: thissignal.changeSignal('G') if thissignal.name not in self.clearedBlocks: self.clearedBlocks.append(thissignal.name) return (True,block) else: return (False,None)
Chunk referenced in 3.10

3.6.3 the setClearPath method

<LayoutComponents: Layout class: setClearPath method 3.14> =
def setClearPath(self,signal,lay,status): '''set, clear or occupy path to next (but not included) facing signal''' thissignal=self.signals[signal] controltrack=thissignal.permit[0] dirn=thissignal.direction (path,failtrack,reason)=self.getPath(controltrack,dirn,[]) if 'segments' in DEBUG: print "setClearPath: looking at path %s" % (path) for (p,d) in path: if self.permissions.has_key(p): # found a signal, is it facing? s=self.permissions[p] sig=self.signals[s] if sig.direction==d: # yes, but is it the starting signal? if s!=signal: # not the starter, so exit done return # no, keep going # clear the track self.tracks[p].setStatus(status) lay.setSegment(p,status) pass # for p return
Chunk referenced in 3.10

3.6.4 the getPath method

<LayoutComponents: Layout class: getPath method 3.15> =
def getPath(self,track,direction,sofar): '''sofar is a list of tuples of (tracksegment,direction), because some tracks traversed may be reversing tracks. The returned value is a tuple: (list of tracks in the path that are set , next track not in path, status of next track not in path) ''' if 'path' in DEBUG: print "<P/>getPath(%s,%s,%s)" % (track,direction,sofar) if (track,direction) in sofar: # closed loop, return return (sofar,None,None) tracks=self.tracks retval=(sofar,None,None) if not tracks or not tracks.has_key(track): return (sofar,track,'X') st=tracks[track].status if 'path' in DEBUG: print "track %s has status %s" % (track,st) if len(sofar)>=1: if 'path' in DEBUG: print "looking at %s, sofar=%s" % (track,sofar) (lasttrack,lastdirn)=sofar[-1] else: lasttrack=None if st in ['S','C','O']: sofar.append((track,direction)) if tracks[track].status not in ['S','C','O']: return (sofar,track,tracks[track].status) if direction=='C': tracks=tracks[track].clock elif direction=='A': tracks=tracks[track].anti else: print "Bad direction flag:%s" % (direction) # check what choices if 'path' in DEBUG: print "getPath: what to choose from:%s (sofar=%s)" % (tracks,sofar) nalts=len(tracks) if nalts==0: # hit the buffers, add current track and return if 'path' in DEBUG: print "getPath: 0 alternatives, sofar=%s" % (sofar) return (sofar,track,'-') elif nalts==1: # only one choice, recurse to look further if 'path' in DEBUG: print "getPath: 1 alternative down track %s with lasttrack=%s,sofar=%s" % (tracks,lasttrack,sofar) if tracks[0]==lasttrack: # reversing track, change direction, and recurse for rest if direction=='C': return self.getPath(track,'A',sofar[0:-1]) else: return self.getPath(track,'C',sofar[0:-1]) tt=tracks[0] if 'path' in DEBUG: print "getPath: 1 alternative, new track heading %s" % (tt) if 'path' in DEBUG: print "getPath: 1 alternative, sofar=%s" % (sofar) return self.getPath(tt,direction,sofar) else: # check first if reversing track if lasttrack in tracks: # reversing track, change direction, and recurse for rest if direction=='C': return self.getPath(track,'A',sofar[0:-1]) else: return self.getPath(track,'C',sofar[0:-1]) # multiple choices, try them all tt=tracks; status='X' while tt: if 'path' in DEBUG: print "getPath: while tt=%s" % (tt) # careful about reference semantics here! possibletrack=tt[0] if self.tracks.has_key(possibletrack): status=self.tracks[possibletrack].status if self.tracks[possibletrack].status in ['S','C','O']: return self.getPath(possibletrack,direction,sofar) else: if 'path' in DEBUG: print "getPath: no track at %s" % (possibletrack) tt=tt[1:] # take remainder # all alternatives exhausted, no path return (sofar,possibletrack,status)
Chunk referenced in 3.10

getPath is one of the key methods in determining the interlocking requirements. It is a recursive method, finding the path of set or clear track segments from the given track segment track in the direction direction, until a track segment is found that is not set or clear (that is, either unknown or occupied). A list of the track segments traversed is returned. The parameter sofar is supplied on recursive calls to indicate the path so far identified.

getPath returns a tuple of: the tracks that make up the path found; the next track in sequence that does not have a set or clear status; and the actual status of the next track.

There are a couple of subtleties.

3.6.5 the collectSignals method

<LayoutComponents: Layout class: collectSignals method 3.16> =
def collectSignals(self,track,dir): '''given a track segment and direction, compute what signals lie upon the current path define by these parameters. Returns a list of tuples in track direction order of [(sig,dir),...] where dir is either 'F'acing or 'T'railing ''' retval=[] (path,segment,reason)=self.getPath(track,dir,[]) if 'signals' in DEBUG: print "collectSignal: collecting along path: %s,%s,%s" % (path,segment,reason) for (seg,dir) in path: if 'signals' in DEBUG: print "collectSignal: next path element: %s" % (seg) if 'signals' in DEBUG: print "collectSignal: permissions=%s" % (self.permissions) if self.permissions.has_key(seg): # signal on this segment sig=self.permissions[seg] if 'signals' in DEBUG: print "collectSignals: got a signal segment %s with signal %s" % (seg,sig) # determine direction of signal if self.signals[sig].direction==dir: # facing signal, add to list retval.append((sig,'F')) else: # trailing signal, add to list retval.append((sig,'T')) if 'signals' in DEBUG: print "collectSignals()=>%s" % (retval) return retval
Chunk referenced in 3.10

Given a track segment and direction, compute what signals lie upon the current path define by these parameters. Returns a list of tuples in track direction order of [(sig,dir),...] where dir is either 'F'acing or 'T'railing

3.6.6 The trackProtected method

<LayoutComponents: Layout class: trackProtected method 3.17> =
def trackProtected(self,track): '''for a given track segment, check that all protecting signals are red. ''' # look in both clockwise and anticlockwise directions if 'points' in DEBUG: print "trackProtected: checking %s" % (track) directions=0 for d in ['C','A']: directions+=1 sigs=self.collectSignals(track,d) if 'points' in DEBUG: print "trackProtected: signal list is %s" % (sigs) # if there are no signals, must be (?) dead end! if not sigs: if 'points' in DEBUG: print "trackProtected: protection suspect, no signals left on route (%s)" % (sigs) return True # rule is that the first trailing signal must be red for (s,d) in sigs: if d=='T': # must be red if self.signals[s].aspect=='R': # good if 'points' in DEBUG: print "trackProtected: protection OK in %s direction, signal %s is red" % (d,s) break else: # bad if 'points' in DEBUG: print "trackProtected: protection fails,signal %s is not red" % (s) return False return directions==2 # only if both directions OK
Chunk referenced in 3.10

Returns, for the given track segment, a True/False indication that all signals protecting this track segment are set to red.

3.7 The main routine

<LayoutComponents: the main routine 3.18> =
def main(): # make a layout layout=Layout() layout.load() keys=layout.points.keys() for p in keys: s=layout.points[p].setting print "changing point %s to setting %s" % (p,s) layout.changePoint(p,s) print print layout.getPath('St06','C',[]) print layout.collectSignals('St06','C') layout.changePoint('Sp08',"R") print layout.getPath('St06','C',[]) print layout.collectSignals('St06','C') # define routines for testing routines={'checkInterlocking':layout.checkInterlocking, 'getPath':layout.getPath, } # perform some testing l='OK' while l: l=raw_input().strip() res=re.match('(.)(.)(\d+),?(.)',l) if res: a=res.group(1).upper() t=res.group(2) n=res.group(3) d=res.group(4).upper() component=a+t+n if t=='s': msg1=layout.changeSignal(component,d) msg2=layout.signals[component] print "%s: %s" % (msg1,msg2) elif t=='p': msg1=layout.changePoint(component,d) msg2=layout.points[component] print "%s: %s" % (msg1,msg2) elif l=='signals': print "Signals:\n%s" % (layout.signalsData.stringSignals()) elif l=='points': print "Points:\n%s" % (layout.pointsData.stringPoints()) elif l=='tracks': print "Tracks:\n%s" % (layout.tracksData.stringTracks()) else: res=re.match('([^(]+)(\((.+)\))?',l) if res: print res.group(1),res.group(2),res.group(3) routine=res.group(1) print eval("layout.%s" % (l)) print "end of %s" % (routine) pass # if res pass # else print "Signals:\n%s" % (layout.signalsData.stringSignals()) print "Points:\n%s" % (layout.pointsData.stringPoints()) print "Tracks:\n%s" % (layout.tracksData.stringTracks()) layout.close()
Chunk referenced in 3.1

4. The External Server Program jmr.py

"jmr.py" 4.1 =
#!/usr/bin/python ## P R O G R A M N A M E ## ## ********************************************************** ## * DO NOT EDIT THIS FILE! * ## * Use $HOME/Computers/Sources/jmr24/jmr.xlp instead * ## ********************************************************** ## <jmr: import modules 4.2> <jmr: initialize globals 4.3> <jmr: initialize fat controller interfacing 4.4> <jmr: define subroutines 4.5> <jmr: initialize web page 4.7> <jmr: collect web page parameters 4.6> <jmr: handle debug mode 4.8> <jmr: load all data variables 4.9> saveDEBUG=DEBUG #DEBUG=[] # turn debugging off for initialization svgLayout.DEBUG=DEBUG LayoutComponents.DEBUG=DEBUG # initial check of points table if 'points' in DEBUG: print "<P>", for k in layout.points.keys(): print "%s:%s" % (k,layout.points[k]), print "</P>" # and force consistency with tracks: #for k in layout.points.keys(): # point=layout.points[k] # # and ignore interlocking constraints # layout.changePoint(k,point.setting,False) #print layout.pointsData.stringPoints() # initial check of track segments if 'tracks' in DEBUG: print "<P>", for k in layout.tracks.keys(): print "%s" % (layout.tracks[k]), print "</P>" # initial check of routes table if 'routes' in DEBUG: print "<H3>Routes Table</H3>" print "<P>", for k in layout.routes.keys(): print "%s:%s<BR>" % (k,layout.routes[k].route), print "</P>" # set points according to points table # ... hmmm, think again here. Don't want to reset any cleared routes #for k in pKeys: # lay.setPoint(k,points[k]) # set signals according to signals table for k in layout.signals.keys(): layout.changeSignal(k,layout.signals[k].aspect) # set segments according to segment table for k in layout.tracks.keys(): lay.setSegment(k,layout.tracks[k].status) DEBUG=saveDEBUG svgLayout.DEBUG=DEBUG LayoutComponents.DEBUG=DEBUG <jmr: extract the browser parameters and process them 4.10> <jmr: close all files and windup 4.16> <jmr: render the SVG layout diagram 4.15> ## ## The End ##

The basic operation of this server program is to scan the call URL for parameters, and perform the actions specified therein. Once processed, the updated layout is re-displayed.

<jmr: import modules 4.2> =
import sys sys.path.append('/home/ajh/www/cgi-bin/jmr24') import calendar import cgi import os import os.path import re from subprocess import * import time import urllib2 import urlparse import xml.dom from xml.dom.minidom import parse #import interlocking #import pointsModule import svgLayout import LayoutComponents
Chunk referenced in 4.1
<jmr: initialize globals 4.3> =
# possible values are ['init','load','point','route','signal','segment','interlock'] DEBUG=[] regionPat=svgLayout.regionPat numberPat=svgLayout.numberPat localOnly=False logfilefn="jmr.log" logfile=open(logfilefn,'a') errMessages=[] server="localhost"
Chunk referenced in 4.1

regionPat defines a constant pattern for matching a region designation.

<jmr: initialize fat controller interfacing 4.4> =
# Need to check whether the fat-controller is awake # look at fatCheckFile for date of last awakening # if this is more than a day old, recheck if fat-controller is awake, # otherwise use the status recorded there ... fatCheckFile='/home/ajh/www/tmp/fatCheckTime' try: lastFatCheck=os.path.getmtime(fatCheckFile) except: lastFatCheck=0 nowTime=calendar.timegm(time.localtime()) dayTime=24*60*60 # seconds in day # is last information less than a day old? if nowTime-lastFatCheck < dayTime: # yes, read and use that try: fc=open(fatCheckFile) fcline=fc.readline().strip() #sys.stderr.write("fat Controller says %s" % fcline) if fcline!='OK': localOnly=True fc.close() except IOError: sys.stderr.write("fat Controller has IOError") localOnly=True # these following two lines seem to be in error. Why? #fc=open(fatCheckFile,'w') #fc.write('OFF\n') else: # no, recheck if fat-controller now awake status=os.system('ping -c1 fat-controller >/dev/null') fc=open(fatCheckFile,'w') if status==0: fc.write('OK\n') else: fc.write('OFF\n') localOnly=True fc.close() sleep='awake' if localOnly: sleep='asleep' sys.stderr.write("fat Controller is %s\n" % sleep)
Chunk referenced in 4.1
<jmr: define subroutines 4.5> =
def flagError(msg): # save message to print later if not msg: return if DEBUG: print "<BR/>flagError: %s" % (msg) errMessages.append(msg) LayoutComponents.flagError=flagError def sendCommand(device,attribute): global localOnly if DEBUG: print "sendCommand(%s,%s)" % (device,attribute) fcurl="http://fat-controller/~trains/cgi-bin/trains.py?" if localOnly: return '(fat-controller is not awake)' else: try: fc=urllib2.urlopen("%s%s=%s" % (fcurl,device,attribute),None,2) action='' for l in fc.readlines(): action+=l return action except urllib2.URLError: localOnly=False return 'Cannot contact the fat controller'
Chunk referenced in 4.1
<jmr: collect web page parameters 4.6> =
env=os.environ if DEBUG: keys=env.keys() keys.sort() for key in keys: print "%20s: %s<BR/>\n" % (key,env[key]) server='localhost' if env.has_key('SERVER_NAME'): server=env['SERVER_NAME'] urlbits=urlparse.urlparse(env['REQUEST_URI']) queries=urlparse.parse_qsl(urlbits.query,True)
Chunk referenced in 4.1
<jmr: initialize web page 4.7> =
print "Content-type: text/html\n" print "<title>John's Model Railway</title>" print "<H2>WELCOME TO JOHN&apos;S MODEL RAILWAY!</H2>",
Chunk referenced in 4.1
<jmr: handle debug mode 4.8> =
#XLP jmr: handle debug mode for (q,v) in queries: if q=='debug': DEBUG=['debug'] vals=v.split(',') if 'all' in vals: # omit 'init','load' DEBUG=['interlock','path','points','routes','segments','signals','tracks'] else: for vv in vals: DEBUG.append(vv) # interlocking.DEBUG=DEBUG svgLayout.DEBUG=DEBUG LayoutComponents.DEBUG=DEBUG elif q=='local': localOnly=True if DEBUG: print "<P>localOnly='%s'<BR/>" % (localOnly) print "query='%s'<BR/>" % (urlbits.query) print "queries='%s'<BR/>" % (queries) print "DEBUG=%s</P>" % (DEBUG)
Chunk referenced in 4.1
<jmr: load all data variables 4.9> =
# get the Layout proper layout=LayoutComponents.Layout() # load the signals, points and track segments layout.load() # get the layout diagram lay=svgLayout.SvgLayout('layout.svg',server) # and set all track and signal elements lay.getLayoutSignals() #lay.getLayoutPoints()
Chunk referenced in 4.1
<jmr: extract the browser parameters and process them 4.10> =
#XLP jmr: extract the browser parameters and process them conflict=False; reason=''; actions='' for (query,value) in queries: if DEBUG: print "<P>Performing (%s,%s)</P>" % (query,value) # check for signal/point/track action res=re.match('(%s)(s|p|t)(%s)' % (regionPat,numberPat),query) if res: <jmr: change signal or point 4.11> elif query=='redraw': # ultimately this should revisit all points and signals and re-set them pass elif query=='help': <jmr: perform help action 4.12> elif query=='reset': <jmr: perform reset action 4.13> elif query=='points': print "<P>Points table: ", print interlock.stringPoints() print "</P>" elif query=='signals': print "<P>Signals table: ", print interlock.stringSignals() print "</P>" elif query=='route': <jmr: perform route action 4.14> elif query in ['debug','load','track','signal','segment']: pass # do nothing at this stage else: print "<P>request '%s' unknown</P>" % (query)
Chunk referenced in 4.1

This section of code is responsible for performing all the work required by this call on the jmr cgi routine. For each query,value pair in the browser parameters, we decode the query and call the appropriate routine with the supplied value. The 'debug', etc., queries are passed over, since they have already been handled earlier in the <jmr: handle debug mode 4.8> code.

<jmr: change signal or point 4.11> =
#XLP jmr: change signal or point if res.group(2)=='s': # change a signal #print "<P>jmr change signal, DEBUG=%s" % (DEBUG) # force signal key to upper case leading region signal="%ss%s" % (res.group(1).upper(),res.group(3)) aspect=value.upper() if 'interlock' in DEBUG: print "Check Interlock on signal: %s,%s" % (signal,aspect) curaspect=layout.signals[signal].aspect if aspect=='T': #pass if curaspect=='R': aspect='G' else: aspect='R' elif aspect=='O': # can only do this if signal is currently green if curaspect!='G': flagError('Signal %s is NOT cleared' % (signal)) else: # OK to occupy aspect='R' elif aspect=='S': # can only do this if signal is currently red if curaspect!='R': flagError('Signal %s should be RED!' % (signal)) else: # OK to occupy aspect='R' #print "<P>jmr calling changeSignal, DEBUG=%s" % (DEBUG) (clear,reason)=layout.changeSignal(signal,aspect) # call fails if interlocking fails if clear: lay.setSignal(signal,aspect) status='S' if aspect=='G': status='C' # cannot occupy if signal currently not G if value.upper()=='O': if curaspect=='G': # Occupy a block, and add to list of occupieds. status='O' layout.occupiedBlocks.append(signal) else: status='S' # Remember to remove this block from the cleared blocks try: layout.clearedBlocks.remove(signal) except ValueError: print "block %s not in cleared blocks??" % (signal) elif value.upper()=='S': # (re)set a block. status='S' try: layout.occupiedBlocks.remove(signal) except ValueError: print "block %s not occupied??" % (signal) layout.setClearPath(signal,lay,status) else: flagError(reason) elif res.group(2)=='p': # change a point # force point key to upper case leading region point="%sp%s" % (res.group(1).upper(),res.group(3)) direction=value.upper() if 'interlock' in DEBUG: print "Check Interlock on point: %s,%s" % (point,direction) layout.changePoint(point,direction) segs=[point+'L',point+'R'] for seg in segs: if layout.tracks.has_key(seg): st=layout.tracks[seg].status lay.setSegment(seg,st) else: flagError("There is no track segment defined for %s" % (seg)) elif res.group(2)=='t': # change a track (this must be to occupy the track segment) track="%st%s" % (res.group(1).upper(),res.group(3)) lay.setSegment(track,'O')
Chunk referenced in 4.10
<jmr: perform help action 4.12> =
# list all the possibilities fn='intro.html' introf=open(fn,'r') intro=introf.readlines() for line in intro: print line.strip()
Chunk referenced in 4.10
<jmr: perform reset action 4.13> =
# reset all signals to red for k in layout.signals.keys(): lay.setSignal(k,'R') layout.changeSignal(k,'R') # reset all cleared track sections for k in layout.tracks.keys(): st=layout.tracks[k].status if 'tracks' in DEBUG: print "reset track status for track %s (status=%s)" % (k,st) if layout.tracks[k].status=='C': lay.setSegment(k,'S') layout.tracks[k].setStatus('S') layout.clearedBlocks=[]
Chunk referenced in 4.10

4.1 jmr: perform route action

<jmr: perform route action 4.14> =
# clear a route # the value of the command is a route and optional direction # e.g., inner,clock or outer,anti to show direction of clearing # e.g., inner or outer if no direction specified if DEBUG: print "path=%s<BR>" % (value) if layout.routes.has_key(value): path=layout.routes[value].route if DEBUG: print "path=%s<BR>" % (path) for (point,throw) in path: if 'routes' in DEBUG: print "setting points %s to %s on route %s<BR/>" % (point,throw,path) layout.changePoint(point,throw) segs=[point+'L',point+'R'] for seg in segs: if layout.tracks.has_key(seg): st=layout.tracks[seg].status lay.setSegment(seg,st) else: flagError("There is no track segment defined for %s" % (seg)) pass # for (point,throw) else: print "Unknown route value=%s" % (value)
Chunk referenced in 4.10
<jmr: render the SVG layout diagram 4.15> =
# render the layout - this currently requires an svg-capable browser print "<table>" print "<tr>" print "<td>" browserSVGcapable=True if browserSVGcapable: print lay.string else: cmd=['/Applications/Inkscape.app/Contents/Resources/script'] cmd.append('/Users/ajh/www/cgi-bin/layout.svg') cmd.append('--export-pdf=/Users/ajh/www/cgi-bin/layout.pdf') print cmd diagram=Popen(cmd, stdout=PIPE).communicate()[0] print diagram print '<IMG SRC="layout.pdf" width="100%"/>' print "</td>" jmrServer="http://%(server)s/~ajh/cgi-bin/jmr24/jmr.py" % (locals()) clearedBlocks=layout.clearedBlocks cbString='(Click any of these links to occupy the cleared block)' if not clearedBlocks: clearedBlocks="There are no cleared blocks to occupy" else: clearedBlocks.sort() lastb='xxxxxx' for block in clearedBlocks: #print "check block %s, last block=%s;" % (block,lastb) link='<A HREF="%s?%s=o">%s</A>' % (jmrServer,block,block) if lastb[0]!=block[0]: cbString+='<BR>\n' if block[0]=='C': cbString+='Cabernet: %s' % (link) elif block[0]=='G': cbString+='Grenache: %s' % (link) elif block[0]=='M': cbString+='Merlot: %s' % (link) elif block[0]=='P': cbString+='Pinot: %s' % (link) elif block[0]=='S': cbString+='Shiraz: %s' % (link) else: cbString+='Somewhere: %s' % (link) lastb=block else: cbString+=',%s' % (link) clearedBlocks=cbString occupiedBlocks=layout.occupiedBlocks obString='(Click any of these links to clear the occupied block)' if not occupiedBlocks: occupiedBlocks="There are no occupied blocks to clear" else: occupiedBlocks.sort() lastb='xxxxxx' for block in occupiedBlocks: #print "check block %s, last block=%s;" % (block,lastb) link='<A HREF="%s?%s=s">%s</A>' % (jmrServer,block,block) if lastb[0]!=block[0]: obString+='<BR>\n' if block[0]=='C': obString+='Cabernet: %s' % (link) elif block[0]=='G': obString+='Grenache: %s' % (link) elif block[0]=='M': obString+='Merlot: %s' % (link) elif block[0]=='P': obString+='Pinot: %s' % (link) elif block[0]=='S': obString+='Shiraz: %s' % (link) else: obString+='Somewhere: %s' % (link) lastb=block else: obString+=',%s' % (link) occupiedBlocks=obString print "<td>" print "<A HREF=\"%(jmrServer)s?help\">Help Page</A>," % (locals()) print " " print "<A HREF=\"/~ajh/computing/jmr.html\">Documentation</A>" % (locals()) if conflict: print "<H2>An interlocking conflict occurred: " print " the offending point/signal is shown in blue</H2>" print "<P>Message was: %s -- " % (reason) print "(Change this and try again)</P>" else: if not actions: actions='(None)' print ''' <P> Actions performed: %s</P> <P> If you find any interlocking fault, please report to the author. </P> ''' % (actions) print ''' <h2>Route Settings</h2> <p> <a href="%(jmrServer)s?route=outer">Outer</a> ... <a href="%(jmrServer)s?route=inner">Inner</a> ...<BR> <a href="%(jmrServer)s?route=inner2pinot">Inner and Pinot</a> ...<BR> <a href="%(jmrServer)s?route=upper">Upper</a> ... <a href="%(jmrServer)s?reset">Reset</a> </p> <h2>Signal Settings</h2> <p> <a href="%(jmrServer)s?route=outer,clock">Outer Clockwise</a> <BR/> <a href="%(jmrServer)s?route=outer,anti">Outer Anticlockwise</a> <BR/> <a href="%(jmrServer)s?route=inner,clock">Inner Clockwise</a> <BR/> <a href="%(jmrServer)s?route=inner,anti">Inner Anticlockwise</a> <BR/> <a href="%(jmrServer)s?route=upper,clock">Upper Clockwise</a> <BR/> <a href="%(jmrServer)s?route=upper,anti">Upper Anticlockwise</a> </p> <h2>Track Occupancy</h2> %(clearedBlocks)s <h2>Track Clearing</h2> %(occupiedBlocks)s ''' % (locals()) print "</td>" print "</tr>" print "</table>" if errMessages: print "<H4>Error Messages</H4>\n<OL>" for msg in errMessages: print "<LI>%s</LI>" % msg print "</OL>"
Chunk referenced in 4.1

Unfortunately, the layout is described as an SVG graph, and for this to be displayed correctly, we require an SVG-capable browser. Known to work are Safari (v5.1.7), Google Chrome (v14.0.835.202), and Firefox (v7.0.1). Earlier versions may also work, but have not been tested.

Browser svg-capability is indicated by a flag browserSVGcapable, which strictly should be set by inspecting the browser. Currently, I cheat, and just assume it is true. (The reason for this is partly that the Inkscape code does not work correctly.)

<jmr: close all files and windup 4.16> =
# close the signals table #interlock.closeSignals() # close the points table #interlock.closePoints() # close the segments table #interlock.closeSegments() # close the logfile logfile.close() # final check of points table if 'load' in DEBUG: print "<P>Points table: ", print layout.pointsData.stringPoints() print "</P>" # save the new layout lay.saveLayout('layout.svg') layout.close()
Chunk referenced in 4.1

Make sure all data is preserved across different invocations

5. The fat-controller html server trains.py

5.1 The trains.py cgi script

"trains.py" 5.1 =
#!/usr/bin/python import os import re import sys import urlparse import jmrData import pointsModule DEBUG=False regionPat=jmrData.regionPat numberPat=jmrData.numberPat print "Content-type: text/html\n" env=os.environ if DEBUG: keys=env.keys() keys.sort() for key in keys: print "%20s: %s<BR/>\n" % (key,env[key]) server='localhost' if env.has_key('SERVER_NAME'): server=env['SERVER_NAME'] urlbits=urlparse.urlparse(env['REQUEST_URI']) queries=urlparse.parse_qsl(urlbits.query,True) bstr_pos = lambda n: n>0 and bstr_pos(n>>1)+str(n&1) or '' if 'debug' in [q for (q,v) in queries]: DEBUG=True pointsModule.DEBUG=True print "<P>query='%s'</P>" % (urlbits.query) print "<P>queries='%s'</P>" % (queries) # get the points table pointsInstance=pointsModule.Points() (points,pKeys)=pointsInstance.loadPoints('pointsData.py') pointsInstance.loadDriverData('pointsDriverData.py') # get the signals #(signals,sKeys)=signalsModule.loadSignals('signals.py') # signals driver yet to be coded # do a translation dump if DEBUG: keys=pointsInstance.pTrans.keys() keys.sort() print "<H2>Points Driver Translation Table</H2>" for k in keys: print " %s : %s<BR/>" % (k,pointsInstance.pTrans[k]) # extract the parameters and process conflict=False; reason='' if not queries: queries=[('help','')] for (query,value) in queries: if DEBUG: print "<P>Performing (%s,%s)</P>" % (query,value) # check for signal/point action res=re.match('(%s)(s|p)(%s)' % (regionPat,numberPat),query) if res: #if res.group(2)=='s': # change a signal # # force signal key to upper case leading region # signal="%ss%s" % (res.group(1).upper(),res.group(3)) # aspect=value.upper() # if aspect=='T': # curaspect=pointsInstance.signals[signal] # if curaspect=='R': # aspect='G' # else: # aspect='R' # (c,r,f)=pointsInstance.setSignal(signal,aspect) if res.group(2)=='p': # change a point # force point key to upper case leading region point="%sp%s" % (res.group(1).upper(),res.group(3)) direction=value.upper() print pointsInstance.setPoint(point,direction) pass pass elif query=='redraw': # ultimately this should revisit all points and signals and re-set them pass elif query=='help': # list all the possibilities fn='intro.html' introf=open(fn,'r') intro=introf.readlines() for line in intro: print line.strip() elif query=='reset': # reset all signals to red #for k in sKeys: # lay.setSignal(k,'R') # pointsInstance.setSignal(k,'R') pass # not implemented elif query=='points': print "<P>Points table: ", print pointsInstance.stringPoints() print "</P>" elif query=='signals': print "<P>Signals table: ", #print pointsInstance.stringSignals() print "</P>" elif query=='debug': pass # do nothing at this stage else: print "<P>action '%s' unknown</P>" % (query) # set the signals table #pointsInstance.closeSignals() # set the points table pointsInstance.closePoints() # final check of points table if DEBUG: #print "<P>Signals table: ", #print pointsInstance.stringSignals() #print "</P>" print "<P>Points table: ", print pointsInstance.stringPoints() print "</P>" ## ## The End ##

6. The Points Module

The points module deals with all the logic for driving the points motor. It maintains tables indicating the current state (in particular, the Python data structure points stored in the file pointsData.py). When used in the context of the fat-controller server, it also issues the appropriate USB data to driver the I2C bus for the points.

"pointsModule.py" 6.1 =
# P O I N T S M o d u l e # import re import jmrData regionPat=jmrData.regionPat numberPat=jmrData.numberPat directionPat=jmrData.directionPat DEBUG=False bstr_pos = lambda n: n>0 and bstr_pos(n>>1)+str(n&1) or '' def binary8(d): r='' for i in range(8): digit=(d % 2) r="%1d%s" % (digit,r) d=d/2 return r class Points(): def __init__(self): ''' create the key attributes: * the points table * filenames for all the above ''' self.points={} self.pointsfn=''
Chunk defined in 6.1,6.2,6.3,6.4,6.5,6.6,6.7
"pointsModule.py" 6.2 =
def loadPoints(self,fn): ''' load the points table from the file with name fn''' self.pointsfn=fn pointsfile=open(fn) pointstr=pointsfile.read() pointsfile.close() points=eval(pointstr) keys=points.keys() keys.sort() self.points=points self.pKeys=keys return (points,keys)
Chunk defined in 6.1,6.2,6.3,6.4,6.5,6.6,6.7

Load the points table from the file with name fn. This is part of the persistent data structure to maintain a consistent setting of the points from one invocation to another. See also closePoints.

"pointsModule.py" 6.3 =
def loadDriverData(self,fn): ''' load the points translation table from the file with name fn''' self.pTransfn=fn transfile=open(fn) transstr=transfile.read() transfile.close() trans=eval(transstr) self.pTrans=trans return
Chunk defined in 6.1,6.2,6.3,6.4,6.5,6.6,6.7
"pointsModule.py" 6.4 =
def loadTrackData(self,trackfn): trackf=open(trackfn,'r') lines=trackf.readlines() str='' for l in lines: str+=l self.trackPaths=eval(str) self.trackKeys=self.trackPaths.keys() return (self.trackPaths,self.trackKeys)
Chunk defined in 6.1,6.2,6.3,6.4,6.5,6.6,6.7
"pointsModule.py" 6.5 =
def stringPoints(self): ''' generate and return a python format definition of the current points table ''' str='{\n ' last=self.pKeys[0][0] for p in self.pKeys: if p[0]!=last: str+="\n " last=p[0] str+="'%s':'%s', " % (p,self.points[p]) str+='\n}' return str
Chunk defined in 6.1,6.2,6.3,6.4,6.5,6.6,6.7
"pointsModule.py" 6.6 =
def setPoint(self,point,direction): bstr_pos = lambda n: n>0 and bstr_pos(n>>1)+str(n&1) or '' reason='' if DEBUG: print "pointsModule setting points %s %s" % (point,direction) if not direction in ['r','l','R','L']: reason="invalid direction '%s'" % direction return reason if self.pTrans.has_key(point): self.points[point]=direction t=self.pTrans[point] print "writing ..." if direction=='L': (ua,la)=t[0]; (ub,lb)=t[1] elif direction=='R': (ua,la)=t[2]; (ub,lb)=t[3] # write to usb port usa=binary8(ua) lsa=binary8(la) usb=binary8(ub) lsb=binary8(lb) reason= "(%d,%d),(%d,%d), or, " % (ua,la,ub,lb) reason+= "(%8s %8s)," % (usa,lsa) reason+= "(%8s %8s)" % (usb,lsb) else: reason="point %s is not defined" % (point) print reason return reason
Chunk defined in 6.1,6.2,6.3,6.4,6.5,6.6,6.7
"pointsModule.py" 6.7 =
def closePoints(self): ''' write the current points table to the previously specified file name. ''' fn=self.pointsfn f=open(fn,'w') f.write(self.stringPoints()) f.close()
Chunk defined in 6.1,6.2,6.3,6.4,6.5,6.6,6.7

Save the points table to the file with name fn. This is part of the persistent data structure to maintain a consistent setting of the points from one invocation to another. See also loadPoints.

7. The Interlocking Module

The interlocking module is responsible for maintaining the signals and points tables, and for resolving the conflicts in changing any of the entries therein (i.e., any signal or point).

The points and signals are maintained in a straightforward way. Two persistent files of point/signal settings respectively are maintained as a python dictionary data structure definition. These definitions are read and evaluated as python strings, so they need to maintained as syntactically correct definitions. Each point/signal is known by a name, where the first (capital) letter defines the region of the layout, the second (lower case) letter is an 's' or a 'p' to denote signal or point respectively, and the third and subsequent digits define the signal or point number in that region. The corresponding data value stored for this key is the signal setting ('R' for Red, 'G' for Green) or point setting ('L' for Left, 'R' for Right, and occasionally 'C' for Centre (three way points only)).

It should be obvious that points and signals cannot be changed arbitrarily. The whole point of a green signal is to show that the route ahead is clear of conflicting movements, both of points, signals, and the trains controlled by them. The possible conflicts are many, and the scheme adopted provides generality, while allowing a degree of simplicity.

An interlocking conflict is defined when a change of a point or signal would adverse affect the current workings. Assuming that trains are always required to stop at a red signal, any point movement directly ahead of the signal can be made if the signal is red. It is therefore always possible to set signals to red, regardless of point or other signal settings.

Conflicts are therefore divided into two groups: those for points, controlled by the current signal settings, and those for signals, controlled by the current point settings.

For a signal, it can only be cleared if there is a route through to the next signal. However, signals may always be reset to a red aspect. For a point, it may only be toggled if the signals controlling any potential route it is on are all red. We define two sets of conflict/control pathways: those for a given signal (is there a route to the next signal?), and those for a given point (is it on any route for which the corresponding signal is green?).

These two sets of conditions are derived from a user-friendly input language, which is in two parts:

  1. A list of points, where each point defines two (or three) alternative pathways, given as a list of track segments.
  2. A list of signals, where the section(s) controlled by the signal are given by a list of track segments, concluding with a point. The point gives (through the table above) the alternative sections controlled by this signal, up to the next facing signal. (A "block" in railway parlance.)
A separate program (see makeJmrData.py) takes these two lists and generates from them the two conflict/control tables:
  1. sectionDefinitions.py
  2. conflictDefinitions.py

A section definition gives a pathway from one facing signal to the next facing signal. For each signal there are usually several sections, and the definitions are in the form of a dictionary, where the signal names are keys, and the data values are lists of sections. Each section is itself a list of track segments to the next signal.

A conflict definition gives for each point or signal, the conditions under which it cannot be changed. It defines for each signal or point what settings of other signals or points would lead to unsafe operation. Since there may be a variety of these, a conflict is defined to exist whenever any of a variety of situations is true. Each situation requires a number of conditions to hold, and hence conflicts are defined by a sums of products expression - each conflict is the AND of a number of conditions, and the overal conflict is the OR of a number of distinct conflicts.

"interlocking.py" 7.1 =
# I N T E R L O C K I N G M o d u l e # import getopt import re import pointsModule import sys import jmrData regionPat=jmrData.regionPat numberPat=jmrData.numberPat directionPat=jmrData.directionPat DEBUG=[] class interlock(pointsModule.Points): def __init__(self): ''' create the key attributes: * the points table * the signals table * the points conflicts table * the signal controls table * filenames for all the above ''' pointsModule.Points.__init__(self) self.signals={} self.conflicts={} self.paths={} self.signalsfn=self.conflictsfn=''
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.2 =
def loadSignals(self,fn): ''' load the signals table from the file with name fn. The table must be specified as a python-format dictionary definition, where the signal name is the key to a value drawn from the set {'x','r','R','g','G'} ''' self.signalsfn=fn signalsfile=open(fn) signalstr=signalsfile.read() signalsfile.close() signals=eval(signalstr) keys=signals.keys() keys.sort() self.signals=signals self.sKeys=keys return (signals,keys)
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.3 =
def loadSignalInterlocking(self,fn): ''' load the controls table from the file with name fn ''' self.controlsfn=fn interlockingfile=open(fn) interlockingstr=interlockingfile.read() interlockingfile.close() interlocking=eval(interlockingstr) keys=interlocking.keys() self.controls=interlocking self.siKeys=keys return (interlocking,keys)
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.4 =
def loadPointInterlocking(self,fn): ''' load the conflicts table from the file with name fn ''' self.conflictsfn=fn interlockingfile=open(fn) interlockingstr=interlockingfile.read() interlockingfile.close() interlocking=eval(interlockingstr) keys=interlocking.keys() self.conflicts=interlocking self.piKeys=keys return (interlocking,keys)
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.5 =
def loadSegments(self,fn): ''' load the segments table from the file with name fn ''' self.segmentsfn=fn interlockingfile=open(fn) interlockingstr=interlockingfile.read() interlockingfile.close() interlocking=eval(interlockingstr) keys=interlocking.keys() self.segments=interlocking self.segKeys=keys if 'load' in DEBUG: print "loaded track segments = %s" % (interlocking) return (interlocking,keys)
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.6 =
def loadRoutes(self,fn): ''' load the routes table from the file with name fn ''' self.routesfn=fn routefile=open(fn) routestr=routefile.read() routefile.close() routes=eval(routestr) keys=routes.keys() self.routes=routes self.rKeys=keys return (routes,keys)
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.7 =
def stringSignals(self): ''' generate and return a python format definition of the current signals table ''' str='{\n ' last=self.sKeys[0][0] for s in self.sKeys: if s[0]!=last: str+="\n " last=s[0] str+="'%s':'%s', " % (s,self.signals[s]) str+='\n}' return str
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.8 =
def stringSegments(self): ''' generate and return a python format definition of the current segments table ''' str='{\n ' keys=self.segKeys keys.sort() last=keys[-1] area=keys[0][0] # get area of first key for s in keys: if s[0]!=area: str+="\n " area=s[0] str+="'%s':'%s', " % (s,self.segments[s]) str+='\n}' return str
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.9 =
def setSignal(self,signal,aspect): clearSeg=False; fail='' reason='' if self.signals.has_key(signal): (clearSeg,reason,fail)=self.signalInterlock(signal,aspect) if clearSeg: self.signals[signal]=aspect else: reason="signal %s is not defined" % (signal) fail=signal return (clearSeg,reason,fail)
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.10 =
def setSegment(self,segname,state): if 'segment' in DEBUG: print "setSegment(%s,%s)" % (segname,state) self.segments[segname]=state
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.11 =
def setPoint(self,point,direction): # override pointsModule.setPoint(self,point,direction) clearPoint=False reason='' if self.points.has_key(point): (clearPoint,reason,fail)=self.pointInterlock(point,direction) if clearPoint: if 'interlock' in DEBUG: print "interlock setting points %s %s" % (point,direction) self.points[point]=direction self.setSegment(point+direction,'S') else: reason="point %s is not defined" % (point) return (clearPoint,reason,fail)
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.12 =
def closeSignals(self): ''' write the current signals table to the previously specified file name. ''' fn=self.signalsfn f=open(fn,'w') f.write(self.stringSignals()) f.close()
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.13 =
def closeSegments(self): ''' write the current segments table to the previously specified file name. ''' fn=self.segmentsfn f=open(fn,'w') f.write(self.stringSegments()) f.close()
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.14 =
def decodePointSignal(self,id): if not id: return (None,None,None) res=re.match('(%s)(s|p)(%s)' % (regionPat,numberPat),id) if res: region=res.group(1) component=res.group(2) number=res.group(3) return (region,component,number) else: return (None,None,None)
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.15 =
def signalInterlock(self,signal,aspect): ''' Check the interlocking for the signal specified, setting it to value aspect. There must be a track segment cleared (all components true) to the next facing signal. If there is such a segment, return the tuple (True,segment path,None) - otherwise, if no such segment exists, return (False,reason,fail) where reason specifies the track segments not set correctly, and fail provides auxiliary information about the failure. The class entity controls must be defined. The opposing signal at the end of the cleared track must also be checked that it is reset. returns True if the path is cleared, False otherwise. ''' reason=''; fail=None if not self.controls.has_key(signal): reason="no controls table for %s" % (signal) return (False,reason,signal) controls=self.controls[signal] if 'interlock' in DEBUG: print "controls[%s]=%s" % (signal,controls) clear=False # clear is False until we find a path bad=''; actual='' for prod in controls: for c in prod: if 'interlock' in DEBUG: print "checking condition %s" % c res=re.match('(.)(.)(\d+)(.)?',c) if res: a=res.group(1) t=res.group(2) n=res.group(3) d=res.group(4) if not d: d='' if t=='s': sig=a+t+n bad=sig if 'interlock' in DEBUG: print "(checking signal %s for aspect %s = " % (sig,d), actual=self.signals[sig] elif t=='p': pnt=a+t+n bad=pnt if 'interlock' in DEBUG: print "(checking point %s for direction %s = " % (pnt,d), actual=self.points[pnt] elif t=='t': pnt=a+t+n bad=pnt if aspect=='R': d='C' else: d='S' if 'interlock' in DEBUG: print "(checking track %s for state %s = " % (pnt,d), actual=self.segments[pnt] # allow an already clear track to be re-cleared if actual=='C' and d=='S': d='C' cond=actual==d if 'interlock' in DEBUG: print "%s gives %s)" % (actual,cond) if not cond: if 'interlock' in DEBUG: print "path %s fails at %s<BR>\n" % (prod,c) break else: if 'interlock' in DEBUG: print "path %s is clear!<BR>\n" % (prod) pass # if t pass # if res # if we get here, and cond is true, this path is OK if cond: clear=True reason=prod if 'interlock' in DEBUG: print "path %s is OK! (and clear=%s)<BR>\n" % (prod,clear) break else: clear=False reason='path not clear at %s with component %s=%s' % (prod,bad,actual) fail=bad+actual # is signal and path being reset? if signal[1]=='s' and aspect=='R': # never a problem to reset signals red ... # however, we do need to return the pathway that is being reset, if it exists. clear=True if 'interlock' in DEBUG: print "signalInterlock(%s,%s)=>(%s,%s)" % (signal,aspect,clear,reason) return (clear,reason,fail)
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.16 =
def pointInterlock(self,point,dirn): ''' Check the interlocking for the points specified as point, setting it to direction dirn. If there is no conflict, return the tuple (True,'no conflict found') - otherwise, if a conflict exists, and it is not clear to toggle the points, return (False,reason) where reason specifies where the conflict exists. ''' reason='no conflict found'; fail=None if not self.conflicts.has_key(point): reason="no conflicts table for %s" % (point) return (False,reason,fail) conflicts=self.conflicts[point] if 'interlock' in DEBUG: print "conflicts[%s]=%s" % (point,conflicts) clear=True # clear is set False if there is a conflict for prod in conflicts: for c in prod: if 'interlock' in DEBUG: print "checking condition %s" % c res=re.match('(.)(.)(\d+)(.)?',c) if res: a=res.group(1) t=res.group(2) n=res.group(3) d=res.group(4) if not d: d='' if t=='s': sig=a+t+n if 'interlock' in DEBUG: print "checking signal %s for aspect %s = " % (sig,d), actual=self.signals[sig] cond=actual==d if 'interlock' in DEBUG: print "%s, %s" % (actual,cond) if not cond: if 'interlock' in DEBUG: print "path %s fails at %s<BR>\n" % (prod,c) break else: if 'interlock' in DEBUG: print "path %s is clear!<BR>" % (prod) elif t=='p': pnt=a+t+n if 'interlock' in DEBUG: print "checking point %s for direction %s = " % (pnt,d), actual=self.points[pnt] cond=actual==d if pnt==point and d!=dirn: if 'interlock' in DEBUG: print "%s, %s" % (actual,cond) reason="toggling point %s in cleared path %s not allowed" % (point,prod) fail=point+actual return (False,reason,fail) if 'interlock' in DEBUG: print "%s, %s" % (actual,cond) if not cond: break pass # test for signal or point pass # test for match of signal or point pass # for all components of path # at this point, cond indicates the state of the last # conditional component. If it is false, the path being tested # is not relevant, and we can skip to the next path. clear # should remain true. If it is true, the path is clear, and # we should test the next path. pass # for all paths if 'interlock' in DEBUG: print "pointInterlock(%s,%s)=>(%s,%s)" % (point,dirn,clear,reason) return (clear,reason,fail)
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17
"interlocking.py" 7.17 =
def main(): # make an interlocking object int=interlock() int.loadSignals('signalsData.py') int.loadPoints('pointsData.py') int.loadSignalInterlocking('signalControls.py') int.loadPointInterlocking('pointConflicts.py') int.loadSegments('segmentData.py') # perform some testing l='OK' while l: l=raw_input() res=re.match('(.)(.)(\d+),?(.)',l) if res: a=res.group(1).upper() t=res.group(2) n=res.group(3) d=res.group(4).upper() component=a+t+n if t=='s': print int.setSignal(component,d) elif t=='p': print int.setPoint(component,d) print "Signals:\n%s" % (int.stringSignals()) print "Points:\n%s" % (int.stringPoints()) print int.stringSegments() if __name__ == '__main__': (vals,path)=getopt.getopt(sys.argv[1:],'dcpst:f:', ['debug','debcon','points','signals','controls=','conflicts=']) for (opt,val) in vals: if opt=='-d' or opt=='--debug': DEBUG.append('debug') elif opt=='-c' or opt=='--debcon': DEBUG.append('debcon') elif opt=='-p' or opt=='--points': DEBUG.append('points') elif opt=='-s' or opt=='--signals': DEBUG.append('signals') elif opt=='-f' or opt=='--conflicts': conflictfile=val elif opt=='-t' or opt=='--controls': controlfile=val if len(path)>0: srcfile=path[0] else: srcfile='jmrData.txt' main()
Chunk defined in 7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,7.10,7.11,7.12,7.13,7.14,7.15,7.16,7.17

8. The SVG Diagram Module

"svgLayout.py" 8.1 =
#XLP svgLayout.py <svgLayout: import and globals 8.2> <svgLayout: define stroke colour setting routines 8.4> <svgLayout: class SvgLayout 8.5>

The svgLayout module is responsible for rendering a pictorial representation of the layout. It requires an SVG-aware browser (such as Firefox or Safari) for the display to be correctly rendered.

At the moment, there is significant route and signal information handled by this module. This will (should?) eventually be moved to the jmr module or similar, to ensure separation of concerns.

8.1 Imports and Globals

<svgLayout: import and globals 8.2> =
#XLP svgLayout: import and globals import re import sys import types import xml.dom from xml.dom.minidom import parse import jmrData trackfn="trackData.py" sigcontrolfn="signalControls.py" regionPat=jmrData.regionPat numberPat=jmrData.numberPat directionPat=jmrData.directionPat directionPat='R|L|C' <svgLayout: colour definitions 8.3> DEBUG=[] # set jmr for possible values strokePat=re.compile('(.*?)stroke:#([0-9a-f]+)(.*)$')
Chunk referenced in 8.1

8.1.1 Track and Signal Colour Definitions

<svgLayout: colour definitions 8.3> =
#XLP svgLayout: colour definitions trackColourDefault='000000' trackColourSet='449900' trackColourClear='00ff00' trackColourOccupied='ff7f00' trackColourConflict='0000ff' trackColour=[trackColourDefault,trackColourSet, trackColourClear,trackColourOccupied,trackColourConflict] tcDefault = 0 tcSet = 1 tcClear = 2 tcOccupied = 3 tcConflict = 4 signalColourSet='ff0000' signalColourClear='00ff00' signalColourConflict='0000ff' signalColour=[signalColourSet,signalColourClear,signalColourConflict] scSet=0 scClear=1 scConflict=2
Chunk referenced in 8.2

These define the colours used on the svg diagram for track segments and signal aspects. The signal aspects are straightforward: only 2-aspect lights are used, red and green ('set' and 'clear' respectively). A third aspect is used only on the displayed svg diagram to indicate a signal in conflict (blue). The signal aspect itself remains unchanged (green).

The track segment colours are more subtle. Where the state of a track routing is unknown, black is used. When the points are set, and the route is known, the colour of the track segment is changed to the 'trackSet' colout, a dark green. When the signal controlling that track segment is cleared, the track segment colour is set to a bright green 'trackClear'.

As with the signals, when a track segment routing is in conflict with other settings, it is set to a conflict colour 'trackConflict' (also blue).

8.2 Stroke Colour Setting Routines

<svgLayout: define stroke colour setting routines 8.4> =
#XLP svgLayout: define stroke colour setting routines def getStrokeColour(object): style=object.getAttribute('style') res=strokePat.match(style) if res: prefix=res.group(1) colour=res.group(2) postfix=res.group(3) return colour def setStrokeColour(object,colour): style=object.getAttribute('style') res=strokePat.match(style) if res: prefix=res.group(1) curcolour=res.group(2) postfix=res.group(3) new="%sstroke:#%s%s" % (prefix,colour,postfix) object.setAttribute('style',new) if 'tracks' in DEBUG: print "setStroke sets new colour=%s" % (new) pass pass # end setStroke
Chunk referenced in 8.1

8.3 The Layout class

<svgLayout: class SvgLayout 8.5> =
#XLP svgLayout: class SvgLayout class SvgLayout(): ''' layoutFileName: sygdom: allpaths: list of svg elements that are layout paths pathDict: dictionary of same, indexed by path name pathKeys: list of keys used in pathDict ''' <svgLayout: class SvgLayout initialization 8.6> <svgLayout: getLayoutSignals 8.7> <svgLayout: getLayoutPoints 8.8> <svgLayout: saveLayout 8.9> <svgLayout: setSignal 8.10> <svgLayout: setPoints 8.11> <svgLayout: setSegments 8.12,8.13>
Chunk referenced in 8.1

8.3.1 class SvgLayout initialization

<svgLayout: class SvgLayout initialization 8.6> =
#XLP svgLayout: class SvgLayout initialization def __init__(self,layfn,server='localhost'): # get the layout self.layoutFileName=layfn try: self.layoutFile=open(layfn,'r') self.svgdom=parse(self.layoutFile) # get the xml:base attribute svg=self.svgdom.getElementsByTagName('svg') # collect the first (and only) nodelist item svg=svg.item(0) base=svg.getAttributeNS('xml','base') # and reset it to the incoming server name uri="http://%s/~ajh/cgi-bin/jmr24/" % (server) svg.setAttributeNS('xml','xml:base',uri) self.layoutFile.close() except IOError: print "Cannot open the svg layout file %s" % (layfn) sys.exit(1) # map all svg paths into a dictionary of their ids pathDict={} self.allpaths=self.svgdom.getElementsByTagName('path') for path in self.allpaths: id=path.getAttribute('id') pathDict[id]=path self.pathDict=pathDict self.pathKeys=pathDict.keys() if 'load' in DEBUG: print "<BR>Paths loaded:<UL>" for k in self.pathKeys: print "<LI>path key %s loaded</LI>" % (k) print "</UL>/n" # get all the track section data trackf=open(trackfn,'r') lines=trackf.readlines() str='' for l in lines: str+=l self.trackPaths=eval(str) self.trackKeys=self.trackPaths.keys() # get all the signal control data sigconf=open(sigcontrolfn,'r') lines=sigconf.readlines() str='' for l in lines: str+=l self.sigControls=eval(str) self.sigconKeys=self.sigControls.keys() pass
Chunk referenced in 8.5

8.3.2 Get Layout Signals

<svgLayout: getLayoutSignals 8.7> =
def getLayoutSignals(self): ''' set self.sigAspectsSvg to elements that contain the signal aspects. These elements are used to indicate the signals colour aspects. The value is constructed as a dictionary, indexed by signal name (without the aspect character). ''' allpaths=self.allpaths # filter out the signal aspect paths, indicated by ids of the form # <region>s<number>a (s = signal, a=aspect) self.sigAspectsSvg={} for path in allpaths: thisid=path.getAttribute('id') res=re.match('(%s)s(%s)a' % (regionPat,numberPat),thisid) if res: if 'load' in DEBUG: print "loading signal %s" % (thisid) sig="%ss%s" % (res.group(1),res.group(2)) # remove the aspect letter self.sigAspectsSvg[sig]=path pass
Chunk referenced in 8.5

8.3.3 Get Layout Points

<svgLayout: getLayoutPoints 8.8> =
def getLayoutPoints(self): ''' set self.pntDirectionsSvg to elements that contain the point directions. These elements are used to indicate the points directions. The value is constructed as a dictionary, indexed by point name (without the direction character). ''' allpaths=self.allpaths # filter out the point direction paths, indicated by ids of the form # <region>p<number><dir> (p = point) self.pntDirectionsSvg={} self.trackPathsvg={} if 'tracks' in DEBUG: print "<P>trackKeys=%s" % (self.trackKeys) for path in allpaths: thisid=path.getAttribute('id') res=re.match('(%s)p(%s)(%s)' % (regionPat,numberPat,directionPat),thisid) if res: if 'load' in DEBUG: print "loading point %s" % (thisid) # don't remove the direction letter pnt="%sp%s%s" % (res.group(1),res.group(2),res.group(3)) self.pntDirectionsSvg[pnt]=path pass if 'tracks' in DEBUG: print "this path = %s" % (thisid) if thisid in self.trackKeys: paths=self.trackPaths[thisid] if 'tracks' in DEBUG: print "<BR>checking paths %s" % (paths) for pnt in paths: if 'tracks' in DEBUG: print "loading trackpath of %s at %s, " % (thisid,pnt) self.trackPathsvg[pnt]=path pass if 'tracks' in DEBUG: print "trackPathsvgKeys=%s" % (self.trackPathsvg.keys()) print "<P>"
Chunk referenced in 8.5

8.3.4 Save Layout

<svgLayout: saveLayout 8.9> =
def saveLayout(self,fileName): outstring=self.svgdom.toxml() svgOut=open(fileName,'w') svgOut.write(outstring) svgOut.close() self.string=outstring
Chunk referenced in 8.5

8.3.5 Set Signal

<svgLayout: setSignal 8.10> =
def setSignal(self,sigId,aspect): aspect=aspect.upper() if 'signals' in DEBUG: print "<BR>setSignal(%s,%s)" % (sigId,aspect) try: path=self.sigAspectsSvg[sigId] except KeyError: print "KeyError: available keys are %s" % (self.sigAspectsSvg.keys()) style=path.getAttribute('style') res=re.match('fill:#([0-9a-f]+)(.*)$',style) if res: colour=res.group(1) rest=res.group(2) if aspect=='R': colour=signalColour[scSet] elif aspect=='G': colour=signalColour[scClear] elif aspect=='B': colour=signalColour[scConflict] new="fill:#%s%s" % (colour,rest) path.setAttribute('style',new) if 'signals' in DEBUG: print "setSignal sets new style=%s" % (new) else: print "Cannot find style in path %s" % (path) return
Chunk referenced in 8.5

This routine is now only responsible for setting the signal aspect. No other track logic is required, that all having been moved to jmr.py in version 2.3.1.

8.4 Set Points

<svgLayout: setPoints 8.11> =
def setPoint(self,pntId,dir): pass # there is nothing for this routine to do in version 2.3.1
Chunk referenced in 8.5

All the logic for determining the colour of the track segments controlled by a point has now been moved to jmr.py. This is now a stub for compatibility reasons.

8.5 svgLayout: setSegments

<svgLayout: setSegments 8.12> =
def setPathColour(self,path,colour): style=path.getAttribute('style') res=re.match('(.*)stroke:#([0-9a-f]+)(.*)$',style) if res: prefix=res.group(1) curcol=res.group(2) postfix=res.group(3) new="%sstroke:#%s%s" % (prefix,colour,postfix) path.setAttribute('style',new) return res
Chunk referenced in 8.5
Chunk defined in 8.12,8.13
<svgLayout: setSegments 8.13> =
def setSegment(self,segname,state): if 'segments' in DEBUG: print "setSegment(%s,%s)" % (segname,state) if not self.pathDict.has_key(segname): if segname[1]!='s': # ignore segment if signal if 'segments' in DEBUG: print "setSegment cannot find segment for %s" % segname return path=self.pathDict[segname] colour=trackColour[tcDefault] if state=='S': # segment is "set" colour=trackColour[tcSet] elif state=='C': # segment is "clear" colour=trackColour[tcClear] elif state=='O': # segment is "occupied" colour=trackColour[tcOccupied] elif state=='B': # segment is "bad" colour=trackColour[tcConflict] res=self.setPathColour(path,colour) if not res: print "Unable to set track segment %s" % (segname)
Chunk referenced in 8.5
Chunk defined in 8.12,8.13

setSegment now (in version 2.3.1) assumes only the responsibility for setting the displayed track colour. It is done on a per-segment basis, and no surrounding track segments are affected by this routine.

9. Help Text

"intro.html" 9.1 =
<P>You can set the points and signals by specifying parameters to this program. These are in the form of standard cgi queries, viz <PRE>http://dimboola.ajh.co/~ajh/cgi-bin/jmr2.py&action&action&...</PRE> where each <TT>action</TT> is as follows: <UL> <LI>help (this listing)</LI> <LI>&lt;region&gt;s&lt;number&gt;={r|R|g|G} Set signal number in region to aspect given</LI> <LI>points: list current point settings</LI> <LI>signals: list current signal settings</LI> <LI>redraw: redraw the current layout (clears any messages)</LI> <LI>reset: return all signals to red</LI> <LI>route=&lt;route-name&gt;[,&lt;path-name&gt;]: set a route and (optional) signalling path </UL> <DL>Regions are: <DT style="font-weight:bold;margin-left:2em">c|C</DT><DD>Cabernet</DD> <DT style="font-weight:bold;margin-left:2em">g|G</DT><DD>Grenache</DD> <DT style="font-weight:bold;margin-left:2em">m|M</DT><DD>Merlot</DD> <DT style="font-weight:bold;margin-left:2em">p|P</DT><DD>Pinot</DD> <DT style="font-weight:bold;margin-left:2em">s|S</DT><DD>Shiraz</DD> </DL> <DL>Routes are: <DT style="font-weight:bold;margin-left:2em">inner</DT> <DD>The inner continous loop</DD> <DT style="font-weight:bold;margin-left:2em">outer</DT> <DD>The outer continuous loop</DD> </DL> </DL> <DL>Paths are: <DT style="font-weight:bold;margin-left:2em">clock</DT> <DD>set signals for clockwise travel</DD> <DT style="font-weight:bold;margin-left:2em">anti</DT> <DD>Set signals for anti-clockwise</DD> </DL> </P> <P>So, for example <PRE>http://ajh.co/~ajh/cgi-bin/jmr2.py&points&gp1=r&gs1=g&signals</PRE> will first show the current points settings, then set points gp1 to right, and signals gs1 to green, and finally display the current signals settings.</P> <P>Note that conflicting movements are not allowed: you cannot alter any point unless the controlling signals are red, not can you set signals to green if that would conflict with any current points or signals settings. </P> <P><B>Currently only the regions <I>Cabernet</I> and <I>Grenache</I> are completed</B>. </P>

10. Setup of Server

To setup the apache server to handle the above scripts, you need to do the following:

  1. Install the apache2 server. On my system, this required
    apt-get install apache2
    Answer 'Y' to the question Do you want to continue [Y/n]?
  2. Edit the file /etc/apache2/apache2.conf to include the following lines:

    #
    # UserDir: The name of the directory that is appended onto a user's home
    # directory if a ~user request is received.  Note that you must also set
    # the default access control for these directories, as in the example below.
    #
    UserDir www
    
    # enable cgi directories
    <Directory /home/*/www/cgi-bin/>
      Options ExecCGI
      SetHandler cgi-script
    </Directory>
    
    then restart the apache server:
    apache2ctl restart
    This will enable the user sub-directories www to contain each user's public HTML, and enable cgi scripts to run from the directory www/cgi-bin beneath them.

11. Support Programs

This section describes a number of programs developed to facilitate the maintenance of the data structures used by the JMR system. Because those data structures contain finicky Python syntax, it was found to be tedious to write them all by hand, and this first program was developed to take the tedium out of maintaining the data tables for signalControls.py.

11.1 The walkNetwork Program

This program is designed to traverse every path in the network, in order to establish the integrity of the network data. It does this by traversing the paths, both clockwise and anticlockwise, accessible from every track segment in the network, and generating a list of such paths.

The algorithm is recursive. Given a track segment and a direction to traverse, it follows each path in the network (regardless of how the points are currently thrown) until it either finds a closed loop (return to the original segment), or it finds a change of direction in the orientation of the track segment (indicated by the path returning to the just-visited segment). In words, this is:

  1. If the segment is in the path already generated, a loop is identified and the walk terminates.
  2. If the segment being visited has as its next segment the segment just visited, a change of direction is indicated and the walk terminates.
  3. If the next segment has multiple choices, it is a point, and two (or three) subbranches of the walk are generated.
"walkNetwork.py" 11.1 =
#!/usr/bin/python import LayoutComponents def explore(l,tr,sofar,dir): print "explore(l,%s,%s,%s)" % (tr,sofar,dir) retval=[] if not tr: return sofar if tr in sofar: sofar.append(tr) return sofar sofar.append(tr) if dir=='C': track=l.tracks[tr].clock else: track=l.tracks[tr].anti if not track: return sofar next=track[0] return explore(l,next,sofar,dir) def main(): layout=LayoutComponents.Layout() layout.load() keys=layout.tracks.keys() keys.sort() for track in keys: for dirn in ['C','A']: print "exploring track %s in %s direction" % (track,dirn) res=explore(layout,track,[],dirn) print res print if __name__ == '__main__': main()

11.2 makeJmrData.py

The makeJmrData.py program builds the two files signalControls.py and pointsConflicts.py required by the interlocking.py module. The first file contains definitions for all the trck sections between two following signals, and the second defines those signals (and the path) that must be reset before a given set of points may be thrown.

The program takes a definition of the layout in a user-friendly form, and converts it to a synatctically correct python definition of the two tables. Here is an EBNF definition of the input language:

        Line   = (Signal '-' ControlList) | (Point '-' AlternatePath) .
        ConflictLine = Signal '-' AlternatePath
        Signal = Area 's' Number .
        Area   = 'C' | 'G' | 'M' | 'P' | 'S' .
        Number = ('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9')+ .
        Track  = Area 't' Number .
        ControlList = Path [',' Path] .
        Path   = Signal | PointPath | Track | AlternatePath .
        PointPath = Area 'p' Number [ Dir ] .
        Dir    = 'L' | 'R' .
        AlternatePath = '(' Guard [',' ControlList] ')' ['/' AlternatePath] .
        Guard  = PointPath .
      

It uses a recursive descent approach to parsing the input source.

"makeJmrData.py" 11.2 =
import copy import getopt import re import sys import types DEBUG=[] SHOWPNTS=False areacode='C G M P S'.split() aspects='R G'.split() digits='0 1 2 3 4 5 6 7 8 9'.split() direction='L R C'.split() blocksfile='' controlfile='' conflictfile='' segmentfile='' blocktable={}; sigtable={};pnttable={};conflicts={};segments={} def extendAlternates(x,y): # make x a prefix of all elements of y r=[[] for i in range(len(y))] i=0 for p in r: for q in x: p.append(q) p.extend(y[i]) i+=1 return r <makeJmrData: recursive descent routines 11.3> def DefinePointsTable(l): if 'signals' in DEBUG: print "\n\nprocessing %s" % (l) res=re.match('(.)(.)(\d+)-(.*)$',l) if res: area=res.group(1).upper() sig=res.group(2) num=res.group(3) c=res.group(4) if sig=='s': print "Error: signal found in points table (%s)" % (l) elif sig=='p': point="%s%s%s" % (area,sig,num) if 'signals' in DEBUG: print "matches point %s" % (point) (pathway,c)=AlternatePath(c,0) if 'signals' in DEBUG: print "returns alternate paths %s" % (pathway) pnttable[point]=pathway else: print "Error: cannot match line %s" % (l) def DefineSignalsTable(l): if 'signals' in DEBUG: print "\n\nprocessing %s" % (l) res=re.match('(.)(.)(\d+)-(.*)$',l) if res: area=res.group(1).upper() sig=res.group(2) num=res.group(3) c=res.group(4) if sig=='s': signal="%s%s%s" % (area,sig,num) if 'signals' in DEBUG: print "matches signal %s" % (signal) (pathway,c)=ControlList(c,0) if type(pathway[0]) is not types.ListType: pathway=[pathway] if 'signals' in DEBUG: print "returns control list %s" % (pathway) sigtable[signal]=pathway elif sig=='p': print "Error: point found in signals table" else: print "Error: cannot match line %s" % (l) def DefineBlocksTable(l): if 'blocks' in DEBUG: print "\n\nprocessing %s" % (l) res=re.match('(.)(.)(\d+)-(.)(.)(\d+):(.*)$',l) if res: area1=res.group(1).upper() sig1=res.group(2) num1=res.group(3) area2=res.group(4).upper() sig2=res.group(5) num2=res.group(6) c=res.group(7) if sig1=='s' and sig2=='s': signal1="%s%s%s" % (area1,sig1,num1) signal2="%s%s%s" % (area2,sig2,num2) blockname="%s-%s" % (signal1,signal2) if 'blocks' in DEBUG: print "matches block %s" % (blockname) (pathway,c)=ControlList(c,0) if 'blocks' in DEBUG: print "returns control list %s" % (pathway) blocktable[blockname]=pathway else: bad=sig1 if bad=='s': bad=sig2 print "Error: bad component %s is not a signal, found at %s" % (bad,l) else: print "Error: cannot match line %s" % (l) pass <makeJmrData: main routine 11.4,11.5,11.6,11.7,11.8,11.9,11.10,11.11> if __name__ == '__main__': (vals,path)=getopt.getopt(sys.argv[1:],'bdcpstfg', ['blocks=','debug','debcon','points','signals','controls=','conflicts=','segments=']) for (opt,val) in vals: if opt=='-d' or opt=='--debug': DEBUG.append('debug') elif opt=='-c' or opt=='--debcon': DEBUG.append('debcon') elif opt=='-p' or opt=='--points': DEBUG.append('points') elif opt=='-s' or opt=='--signals': DEBUG.append('signals') elif opt=='-b': blocksfile='blocksData.py' elif opt=='--blocks': blocksfile=val elif opt=='-f': conflictfile='pointConflicts.py' elif opt=='--conflicts': conflictfile=val elif opt=='-t': controlfile='signalControls.py' elif opt=='--controls': controlfile=val elif opt=='-g': segmentfile='segmentData.py' elif opt=='--segments': segmentfile=val if len(path)>0: srcfile=path[0] else: srcfile='jmrData.txt' DEBUG.append('blocks') main(srcfile)

11.3 makeJmrData: recursive descent routines

<makeJmrData: recursive descent routines 11.3> =
def Area(c): if c[0].upper() in areacode: return (c[0].upper(),c[1:]) else: print "Error: \"%s\" does not start with area code" % (c) return ('',c) def Number(c): if c[0] in digits: i=0 while i<len(c) and c[i] in digits: i+=1 return (c[0:i],c[i:]) else: print "Error: \"%s\" does not start with number" % (c) return ('',c) def Aspect(c): if c[0].upper() in aspects: return (c[0].upper(),c[1:]) else: print "Error: \"%s\" does not start with aspect" % (c) return ('',c) def Signal(c): if DEBUG: print "Signal(%s)" % c (sig,c)=Area(c) if c[0]=='s': c=c[1:] else: print "Error: \"%s\" does not start with signal flag" % (c) (num,c)=Number(c) if c and c[0].upper() in aspects: (asp,c)=Aspect(c) sig=sig+'s'+num+asp if DEBUG: "Signal(...) => (%s,%s)" % (sig,c) return (sig,c) else: sig=sig+'s'+num if DEBUG: "Signal(...) => (%s,%s)" % (sig,c) return (sig,c) def Track(c): (trk,c)=Area(c) if c[0]=='t': c=c[1:] else: print "Error: \"%s\" does not start with track flag" % (c) (num,c)=Number(c) return (trk+'t'+num,c) def Dir(c): if c[0].upper() in direction: return (c[0].upper(),c[1:]) else: print "Error: \"%s\" does not start with direction" % (c) return ('',c) def PointPath(c): (pnt,c)=Area(c) if c[0]=='p': c=c[1:] else: print "Error: \"%s\" does not start with points flag" % (c) (num,c)=Number(c) if c and c[0].upper() in direction: (dir,c)=Dir(c) return (pnt+'p'+num+dir,c) else: pntkey=pnt+'p'+num if pnttable.has_key(pntkey): pth=pnttable[pntkey] return (pth,c) else: # forward reference, return just the point id if 'signals' in DEBUG: print "PointPath returns forward reference %s" % (pntkey) return (pntkey,c) def Guard(c): if DEBUG: print "Guard(%s)" % c if c[1]=='p': (grd,c)=PointPath(c) if DEBUG: "Guard(...) => (%s,%s)" % (grd,c) return (grd,c) elif c[1]=='s': (sig,c)=Signal(c) if DEBUG: "Guard(...) => (%s,%s)" % (sig,c) return (sig,c) else: print "Error: guard %g must be a point or signal" % (grd) return (None,c) def AlternatePath(c,lev): ind='. '*lev if DEBUG: print "%sAlternatePath(%s)" % (ind,c) pathlist=[] dict={} if c[0]=='(': (grd,c)=Guard(c[1:]) path=[grd] if c[0]==',': (pth,c)=ControlList(c[1:],lev+1) if type(pth[0]) is types.ListType: if DEBUG: print "%sAlternatePath prefixs path %s with %s" % (ind,pth,path) path=extendAlternates(path,pth) if DEBUG: print "%sAlternatePath computes an extended path %s" % (ind,path) pathlist.extend(path) else: if DEBUG: print "%sAlternatePath has path that is not list of lists %s" % (ind,pth) path.extend(pth) pathlist.append(path) if DEBUG: print "%sAlternatePath computes pathlist %s" % (ind,pathlist) else: pathlist.append(path) if c and c[0]==')': c=c[1:] if c!='' and c[0]=='/': if DEBUG: print "%sgoing round for another alternate with >%s<" % (ind,c) (alt,c)=AlternatePath(c[1:],lev+1) path=[] if DEBUG: print "%sgot another alternate path %s leaving >%s<" % (ind,alt,c) if alt: for e in alt: #pathlist=extendAlternates(e,pathlist) pathlist.append(e) if DEBUG: print "%sappended %s to pathlist=%s" % (ind,e,pathlist) if DEBUG: print "%sfull path=%s" % (ind,pathlist) else: print "%sError: bad alternate path syntax in %s" % (ind,c) #if c and c[0]==')': # c=c[1:] if DEBUG: print "%sAlternatePath returns=%s" % (ind,pathlist) return (pathlist,c) def Path(c,lev): ind='. '*lev if 'signals' in DEBUG: print "%sPath(%s)" % (ind,c) pth='' if c[0]=='(': (alt,c)=AlternatePath(c,lev+1) pth=alt elif c[1]=='s': (sig,c)=Signal(c) pth=sig elif c[1]=='p': (pnt,c)=PointPath(c) pth=pnt elif c[1]=='t': (trk,c)=Track(c) pth=trk else: print "%sError: no path at \"%s\"" % (ind,c) if 'signals' in DEBUG: print "%sPath returns %s (leaving %s)" % (ind,pth,c) return (pth,c) def ControlList(c,lev): ind='. '*lev if 'signals' in DEBUG: print "%sControlList(%s)" % (ind,c) paths=[] (pth,c)=Path(c,lev+1) if type(pth[0]) is types.ListType: paths=pth else: paths.append(pth) while c!='' and c[0]==',': c=c[1:] (pth,c)=Path(c,lev+1) if type(pth[0]) is types.ListType: paths=extendAlternates(paths,pth) if DEBUG: print "%sControlList extends paths to %s" % (ind,paths) else: paths.append(pth) if 'signals' in DEBUG: print "%sControlList returns %s (leaving %s)" % (ind,paths,c) return (paths,c)
Chunk referenced in 11.2

11.4 makeJmrData: main routine

<makeJmrData: main routine 11.4> =
def main(srcname): definer=DefinePointsTable # in lieu of anything else f=open(srcname,'r') lines=f.readlines() for l in lines: l=l.strip() if l=='': continue if l[0]=='#': # skip blanks i=1 while l[i]==' ': i+=1 # check first two non-blank chars for processing type if l[i:i+2]=='si': if DEBUG: print "switching to signals" definer=DefineSignalsTable elif l[i:i+2]=='po': if DEBUG: print "switching to points" definer=DefinePointsTable elif l[i:i+2]=='bl': if DEBUG: print "switching to blocks" definer=DefineBlocksTable else: # other comment pass continue # process the line if DEBUG: print "\n\n**** processing %s" % (l) definer(l)
Chunk referenced in 11.2
Chunk defined in 11.4,11.5,11.6,11.7,11.8,11.9,11.10,11.11

The main routine is responsible for reading lines of layout definition, and switching between the points definition and signals definition. Note that this is not strictly necessary, but is a hangover from an earlier design. (It should be removed at some stage.) We handle this switching by using a different definer routine.

<makeJmrData: main routine 11.5> =
if 'points' in DEBUG: keys=pnttable.keys() keys.sort() print "{" for k in keys: print " '%s' : %s," % (k,pnttable[k]) print "}"
Chunk referenced in 11.2
Chunk defined in 11.4,11.5,11.6,11.7,11.8,11.9,11.10,11.11

If we want to see what the points table definition is, we can display it by adding the --points flag to the CLI call. Note that this points table is not used directly by the jmr.py program, and is normally used only to analyse the control paths and conflict paths. Hence it is not normally required to be output.

<makeJmrData: main routine 11.6> =
if controlfile: cf=open(controlfile,'w') print "Writing controls to file %s" % controlfile else: cf=sys.stdout keys=sigtable.keys() keys.sort() cf.write("{\n") for k in keys: cf.write(" '%s' : %s,\n" % (k,sigtable[k])) cf.write("}\n") if cf!=sys.stdout: cf.close()
Chunk referenced in 11.2
Chunk defined in 11.4,11.5,11.6,11.7,11.8,11.9,11.10,11.11

Output the control table. This table defines the track sections that are cleared when a given signal is cleared. If no file is given for this table, it is written to standard output.

<makeJmrData: main routine 11.7> =
pntkeys=pnttable.keys() pntkeys.sort() sigkeys=sigtable.keys() sigkeys.sort() conflicts={} for k in sigkeys: if 'debcon' in DEBUG: print "\nchecking signal %s - segment list=%s" % (k,sigtable[k]) pathlist=sigtable[k] for path in pathlist: pathtohere=[k+'G'] for p in path: if 'debcon' in DEBUG: print "\nchecking signal %s - path=%s" % (k,p) pathtohere.append(p) res=re.match('(.)(.)(\d+)(.)?',p) if res: a=res.group(1) t=res.group(2) n=res.group(3) d=res.group(4) if not d: d='' if 'debcon' in DEBUG: print "got a track seg = %s%s%s%s" % (a,t,n,d) if t=='p': pnt=a+t+n if pnt in pntkeys: if 'debcon' in DEBUG: print "possible conflict between signal %s and point %s - conflict path = %s" % (k,pnt,pathtohere) if conflicts.has_key(pnt): conflicts[pnt].append(pathtohere) else: conflicts[pnt]=[pathtohere]
Chunk referenced in 11.2
Chunk defined in 11.4,11.5,11.6,11.7,11.8,11.9,11.10,11.11

Analyse the conflicts in the layout.

<makeJmrData: main routine 11.8> =
if conflictfile: cf=open(conflictfile,'w') print "Writing conflicts to file %s" % conflictfile else: cf=sys.stdout cf.write("{\n") for pnt in pntkeys: if pnt in conflicts.keys(): cf.write(" '%s' : [%s,\n" % (pnt,conflicts[pnt][0])) for i in range(1,len(conflicts[pnt])): cf.write(" %s,\n" % (conflicts[pnt][i])) cf.write(" ],\n") else: cf.write("No key %s in conflicts table" % pnt) cf.write("}\n") if cf!=sys.stdout: cf.close()
Chunk referenced in 11.2
Chunk defined in 11.4,11.5,11.6,11.7,11.8,11.9,11.10,11.11

Output the conflicts table. This table defines whether a given point may be altered. It can only be altered when all of the controlling signals of the various sections that it is in are red. If no file is given for this table, it is written to standard output.

<makeJmrData: main routine 11.9> =
sigkeys=sigtable.keys() sigkeys.sort() for k in sigkeys: if DEBUG: print "\nanalysing signal %s - segment list=%s" % (k,sigtable[k]) pathlist=sigtable[k] for path in pathlist: for p in path: if DEBUG: print "\nchecking signal %s - path=%s" % (k,p) res=re.match('(.)(.)(\d+)(.)?',p) if res: a=res.group(1) t=res.group(2) n=res.group(3) d=res.group(4) if not d: d='' if DEBUG: print "got a track seg = %s%s%s%s" % (a,t,n,d) if t=='p': pnt=a+t+n+d segments[pnt]='X' if t=='t': seg=a+t+n segments[seg]='X'
Chunk referenced in 11.2
Chunk defined in 11.4,11.5,11.6,11.7,11.8,11.9,11.10,11.11

Analyse the track segments in the layout.

<makeJmrData: main routine 11.10> =
if segmentfile: cf=open(segmentfile,'w') print "Writing segments to file %s" % segmentfile else: cf=sys.stdout cf.write("{\n") keys=segments.keys() keys.sort() area='C'; type='p' for seg in keys: if area!=seg[0] or type!=seg[1]: cf.write("\n") area=seg[0]; type=seg[1] cf.write(" '%s':'%s'," % (seg,segments[seg])) cf.write("\n}\n") if cf!=sys.stdout: cf.close()
Chunk referenced in 11.2
Chunk defined in 11.4,11.5,11.6,11.7,11.8,11.9,11.10,11.11

Output the segments table. This table defines all the track segments and their current state:

X
current state undefined or unknown. Show as black segment.
S
current state is "set", meaning that it is part of a route that has been or is being set, ready for clearing. Show as dark green.
C
current state is "clear", meaning that it is part of a route that has been set and cleared, through clearing a controlling signal for the route. Show as bright green.
If no file is given for this table, it is written to standard output.

<makeJmrData: main routine 11.11> =
if blocksfile: cf=open(blocksfile,'w') print "Writing blocks to file %s" % blocksfile blockkeys=blocktable.keys() blockkeys.sort() cf.write("{\n") for key in blockkeys: cf.write(" %s : %s,\n" % (key,blocktable[key])) cf.write("}\n") if cf!=sys.stdout: cf.close()
Chunk referenced in 11.2
Chunk defined in 11.4,11.5,11.6,11.7,11.8,11.9,11.10,11.11

Analyse and output the block table for the layout. If no file is specified for this data, it is written to standard output. Two flags are available to specify that the data is written to a file:

-b
write the block data to a file called blockData.py.
--blocks=blockfilename
write the data to a file given as blockfilename.

12. The Arduino Programs

12.1 The Points Driver Arduino

"pointmotorserial.pde" 12.1 =
/* Test program for PCF8574 I2C I/O expander Used to drive point motor solenoids Each alternate port is set high and low, and then reversed after 2 seconds Note that the read operations currently do not work, due to missing pull-ups. */ #include <Wire.h> uint8_t expander = B0100001; // Address with xxx is b0100xxx // Note that the R/W bit is not part of this address. void setup() { Wire.begin(); expanderWrite(B11111111); Serial.begin(115200); } void pointMotor(byte pm) { expanderWrite(~(1 << pm)); delay(100); expanderWrite(0xff); } void loop() { // check if data has been sent from the computer: if (Serial.available()) { // read the most recent byte (which will be from 0 to 255): int pin = Serial.read(); if(pin >= '0' && pin <= '7') { Serial.print("Read: "); Serial.println(pin); pin -= '0'; pointMotor(pin); Serial.println(expanderRead(), BIN); } } Serial.print("Writing B00000000 to "); Serial.println(expander, BIN); expanderWrite(0x0a); Serial.print("Read 1: "); Serial.println(expanderRead(), BIN); delay(50); expanderWrite(0xff); delay(900); Serial.print("Writing B11111111 to "); Serial.println(expander, BIN); expanderWrite(0x05); Serial.print("Read 2: "); Serial.println(expanderRead(), BIN); delay(50); expanderWrite(0xff); delay(900); Serial.print("Writing B00000000 to "); Serial.println(expander, BIN); expanderWrite(0xfa); Serial.print("Read 1: "); Serial.println(expanderRead(), BIN); delay(50); expanderWrite(0xff); delay(900); Serial.print("Writing B11111111 to "); Serial.println(expander, BIN); expanderWrite(0xf5); Serial.print("Read 2: "); Serial.println(expanderRead(), BIN); delay(50); expanderWrite(0xff); delay(900); } void expanderWrite(byte _data ) { Wire.beginTransmission(expander); Wire.send(_data); Wire.endTransmission(); } byte expanderRead() { byte _data=0x53; Wire.requestFrom(expander, (byte)1); if(Wire.available()) { _data = Wire.receive(); } return _data; }

13. TODOs

  1. (20150830:152626) When Ps02 is cleared, and Cp04 thrown right, Cs07 cannot be cleared. If you do it in the order of clearing Cs07 first, then Ps02, it is OK.
  2. (20151011:182233) If a block is cleared, and then the controlling signal reset, the block must be uncleared (set), and removed from the list of cleared blocks.

14. Makefile

"Makefile" 14.1 =
# define all the (generated) files to be removed on make clean GenFiles=$(EMPTY) ARDUINO-POINTS=/home/ajh/Personal/Pastimes/ModelRailways/Arduino/pointmotorserial include ${HOME}/etc/MakeXLP HOME = /home/ajh CGI = $(HOME)/www/cgi-bin/jmr24/ WORLD = ajh.co:www/cgi-bin/jmr24/ XSLLIB=/home/ajh/lib/xsl XSLFILES=$(XSLLIB)/lit2html.xsl $(XSLLIB)/tables2html.xsl jmr.tangle: jmr.xlp interlocking.xlp makeJmrData.xlp svgLayout.xlp tangle: jmr.tangle jmr.py: jmr.tangle trains.py: tangle interlocking.py: interlocking.tangle makeJmrData.py: makeJmrData.tangle install: install-local install-fatcon install-local: jmr.tangle chmod 755 jmr.py cp -p jmr.py $(CGI)/ cp -p LayoutComponents.py $(CGI)/ cp -p svgLayout.py $(CGI)/ # cp -p pointsModule.py $(CGI)/ cp -p blocksData.py $(CGI)/ cp -p routesData.py $(CGI)/ install-world: jmr.tangle chmod 755 jmr.py rsync -auv jmr.py $(WORLD)/ rsync -auv LayoutComponents.py $(WORLD)/ rsync -auv svgLayout.py $(WORLD)/ # rsync -auv pointsModule.py $(WORLD)/ rsync -auv blocksData.py $(WORLD)/ rsync -auv routesData.py $(WORLD)/ install-fatcon: install-fatcon-trains install-fatcon-modules install-fatcon-trains: jmr.tangle rsync -aui trains.py trains@fat-controller:$(CGI)/trains.py install-fatcon-modules: jmr.tangle rsync -aui jmrData.py trains@fat-controller:$(CGI)/ rsync -aui pointsModule.py trains@fat-controller:$(CGI)/ chmod 666 pointsData.py rsync -aui pointsData.py trains@fat-controller:$(CGI)/ chmod 666 signalsData.py rsync -aui signalsData.py trains@fat-controller:$(CGI)/ rsync -aui pointsDriverData.py trains@fat-controller:$(CGI)/ rsync -aui intro.html trains@fat-controller:$(CGI)/ install-test: jmr.tangle chmod 755 trains.py cp -p trains.py $(CGI)/ cp -p jmrData.py $(CGI)/ cp -p pointsModule.py $(CGI)/ chmod 666 pointsData.py cp -p pointsData.py $(CGI)/ cp -p pointsDriverData.py $(CGI)/ chmod 666 signalsData.py cp -p signalsData.py $(CGI)/ cp -p conflictsData.py $(CGI)/ cp -p routesData.py $(CGI)/ install-arduino: jmr.tangle cp -p pointmotorserial $(ARDUINO-POINTS)/pointmotorserial.pde clean: litclean -rm $(GenFiles) # legacy only beyond this point #default=program #GenFiles = .program install-program install-layout: jmr.tangle rsync -auv libjmr.py dimboola:/Users/ajh/$(CGI)/libjmr.py touch install-layout local-layout: $(CGI)/libjmr.py $(CGI)/libjmr.py: jmr.tangle cp libjmr.py /Users/ajh/$(CGI)/libjmr.py #install: install-trains local: local-layout install-trains: trains.py rsync -aui trains.py trains@fat-controller:$(CGI)/trains.py install-points: points.py rsync -aui points.py trains@fat-controller:$(CGI)/points.py install-signals: signals.py rsync -aui signals.py trains@fat-controller:$(CGI)/signals.py uninstall: rsync -aui trains@fat-controller:$(CGI)/trains.py ./trains.py all: install local

15. Indices

15.1 Identifiers

Identifier Defined in Used in
closePoints 6.7
directionPat 2.1 6.1, 7.1, 8.2
directionPat 6.1 6.1, 7.1, 8.2
directionPat 7.1 6.1, 7.1, 8.2
directionPat 8.2 6.1, 7.1, 8.2
getPath 3.15 3.11, 3.14, 3.15, 3.15, 3.15, 3.15, 3.15, 3.15, 3.16, 3.18, 3.18
jmrData 5.1 5.1, 5.1, 6.1, 6.1, 6.1, 6.1, 7.1, 7.1, 7.1, 7.1, 8.2, 8.2, 8.2, 8.2
loadPoints 6.2
numberPat 2.1 4.10, 5.1, 5.1, 6.1, 7.1, 7.14, 8.2, 8.7, 8.8
numberPat 5.1 4.10, 5.1, 5.1, 6.1, 7.1, 7.14, 8.2, 8.7, 8.8
numberPat 6.1 4.10, 5.1, 5.1, 6.1, 7.1, 7.14, 8.2, 8.7, 8.8
numberPat 7.1 4.10, 5.1, 5.1, 6.1, 7.1, 7.14, 8.2, 8.7, 8.8
numberPat 8.2 4.10, 5.1, 5.1, 6.1, 7.1, 7.14, 8.2, 8.7, 8.8
pointsModule 5.1
regionPat 2.1 4.3, 4.10, 5.1, 5.1, 6.1, 7.1, 7.14, 8.2, 8.7, 8.8
regionPat 5.1 4.3, 4.10, 5.1, 5.1, 6.1, 7.1, 7.14, 8.2, 8.7, 8.8
regionPat 6.1 4.3, 4.10, 5.1, 5.1, 6.1, 7.1, 7.14, 8.2, 8.7, 8.8
regionPat 7.1 4.3, 4.10, 5.1, 5.1, 6.1, 7.1, 7.14, 8.2, 8.7, 8.8
regionPat 8.2 4.3, 4.10, 5.1, 5.1, 6.1, 7.1, 7.14, 8.2, 8.7, 8.8

15.2 Macros

Chunk Name Defined in Used in
LayoutComponents: Layout class: changeSignal method 3.13 3.10
LayoutComponents: Layout class: checkInterlocking method 3.11 3.10
LayoutComponents: Layout class: collectSignals method 3.16 3.10
LayoutComponents: Layout class: getPath method 3.15 3.10
LayoutComponents: Layout class: setClearPath method 3.14 3.10
LayoutComponents: Layout class: trackProtected method 3.17 3.10
LayoutComponents: Point class 3.4 3.1
LayoutComponents: Route class 3.8 3.1
LayoutComponents: Signal class 3.2 3.1
LayoutComponents: Track class 3.6 3.1
LayoutComponents: the Layout class 3.10 3.1
LayoutComponents: the Points class 3.5 3.1
LayoutComponents: the Routes class 3.9 3.1
LayoutComponents: the Signals class 3.3 3.1
LayoutComponents: the Tracks class 3.7 3.1
LayoutComponents: the main routine 3.18 3.1
checkInterlocking: check all segments in path 3.12 3.11
current date 16.1.2
current version 16.1.1
jmr: change signal or point 4.11 4.10
jmr: close all files and windup 4.16 4.1
jmr: collect web page parameters 4.6 4.1
jmr: define subroutines 4.5 4.1
jmr: extract the browser parameters and process them 4.10 4.1
jmr: handle debug mode 4.8 4.1
jmr: import modules 4.2 4.1
jmr: initialize fat controller interfacing 4.4 4.1
jmr: initialize globals 4.3 4.1
jmr: initialize web page 4.7 4.1
jmr: load all data variables 4.9 4.1
jmr: perform help action 4.12 4.10
jmr: perform reset action 4.13 4.10
jmr: perform route action 4.14 4.10
jmr: render the SVG layout diagram 4.15 4.1
makeJmrData: main routine 11.4, 11.5, 11.6, 11.7, 11.8, 11.9, 11.10, 11.11 11.2
makeJmrData: recursive descent routines 11.3 11.2
svgLayout: class SvgLayout 8.5 8.1
svgLayout: class SvgLayout initialization 8.6 8.5
svgLayout: colour definitions 8.3 8.2
svgLayout: define stroke colour setting routines 8.4 8.1
svgLayout: getLayoutPoints 8.8 8.5
svgLayout: getLayoutSignals 8.7 8.5
svgLayout: import and globals 8.2 8.1
svgLayout: saveLayout 8.9 8.5
svgLayout: setPoints 8.11 8.5
svgLayout: setSegments 8.12, 8.13 8.5
svgLayout: setSignal 8.10 8.5

16. Document History

20120523:114009 ajh 1.0.0 initial version. This is a rewrite of the jmr suite
20120918:130814 ajh 2.0.0 rewrite interlocking mechanism
20120919:134144 ajh 2.0.1 rationalize www server and localhost (fat-controller) server code
20120922:131048 ajh 2.1.0 restructure a number of modules and clean up code
20130623:170755 ajh 2.2.0 New points/signals r + www/cgi-bin/jmr23 route setting regime
20130704:162958 ajh 2.2.1 further changes to svgLayout and signal controls
20130712:172855 ajh 2.3.0 rewriting great slabs of interlocking, conflicts and control table generation
20130716:105534 ajh 2.3.1 add track segment data maintenance, and remove significant program logic from svgLayout
20150218:170910 ajh 2.3.2 Clarify heel and toe terminology, and correct errors
20150820:180516 ajh 2.4.0 rewrite in more OO style
20150822:120706 ajh 2.4.1 break LayoutComponents into literate program chunks
20150827:142808 ajh 2.4.2 first working version with reversing tracks
20150829:162117 ajh 2.4.3 cleaned up a few more names, and added missing signals, points, tracks
<current version 16.1.1> = 2.4.3
<current date 16.1.2> = 20150829:162117