Detector Software

Detector Software Introduction

Details on the detector software go below here ("Real Soon Now"). At this point, I'm developing the software. I have two possible approaches. Both take advantage of the fact that, if you use a little logic, you can tell what's going on with a train by what has happened recently to the detectors. When a train enters a track block, you can log the fact, including the time. When the train is completely past the entrance detector and it switches off, you can log that time, and know that the train is inside the block. When the train hits the next detector and is entering the next block, you can note that time and know that it's exiting. When the exit detector switches off, you know that the train has exited the block and that trackage is now unoccupied. ...piece of cake, right? Well, we'll see. It's time to make sure that we can reliably detect signals and their corresponding times. Then, we'll build the data structures and the logic to interpret the information. Clearly, some of these items are done at the detector level and others are done at the track block level. Keep that in mind for the software development.

Preliminaries: Code Design:

Here are a few prerequisite items, starting with hardware/software interfacing. Let me stress setting the i2C channel for input. It is set up as follows. As mentioned, the i2C activation is done bit-by-bit. For my setup, I needed the following initialization code (the 3rd argument, 0xFF, is a one-byte value which turns on all 8 detector circuits on each port, and I have a total of 16). The ADDRESS23 business is set by a hardware jumper on the expansion board:

IOPi_init(ADDRESS23); // initialize io pi channel on i2c

set_port_direction(ADDRESS23, 0, 0xFF); // set bus 0 to be all INputs

set_port_direction(ADDRESS23, 1, 0xFF); // set bus 1 to be all INputs

Speaking of hardware, it is important that your layout is sufficiently completed, enough that you can continuously run trains without frequent derailments. Until you reach that stage, please put aside the detector and computer work and get your layout operating smoothly. You'll save a whole lot of frustration with few or no train wrecks. Not only that, but spouse and kids will admire it, and the language and volume of your complaints will moderate. ...somewhat.

Next, be sure to test out the detectors individually, verifying that the detector hardware configuration is correctly represented in the software initialization. There must be provision in each track block's data array to describe (for software access, for my layout anyway) up to four entering blocks (each of which has an associated detector) and four exiting blocks. Also, the software must be able to distinguish between forward and reverse operating direction.


In my railroad control program, the detector boards are accessed frequently via the 300 ms function interrupt. That call processes the detector level signal information (raw occupancy and time signals), checks to de-bounce each signal, buffers it, then does some interpretation.

Having done that, the next level (5-second) interrupt processes the detector information at track block level, to display indications, and retains values for use in the position processing logic, a fancy name for checking where trains are located, how long they are, how fast they are moving, and if the electrical settings are suitable. This isn't needed as frequently.

The 300 ms Interrupt

Let's now dive into the weeds, er, details.

Next, as discussed below, the code must cycle through all detector inputs, storing information rapidly and frequently. Then, the code should cycle through all track blocks, checking the appropriate detector information, to determine block status. It should then issue display status, and appropriate warnings if there are mismatches between track block settings or something doesn't make sense (such as excessive time in a track block, or a misaligned turnout). Most of the above is accomplished in the fast 300 millisecond (ms) interrupt.

First in this interrupt code, the detectors are polled in a "for" loop, which in artificial intelligence jargon is a depth-first search. Each detector position is polled and the results are stored. If a detector is not active, meaning its phototransistor is not obscured, no further signal processing is necessary, the information is stored, and the loop moves on to the next detector position. However, if the detector is obscured, the code stores that fact plus some timing information.

Why times...? Well, it's possible, knowing when the entrance detector for one track block gap is first obscured and when that block's exit detector (the next block's entrance detector) is obscured, to compute the time difference, then divide the physical length of the track block by it, multiply by a scale factor (in my case, the N-scale factor of 1∶148 to 1∶160, and the result is the average speed through that block.

Next in the 300 ms interrupt code, the information from the detectors is interpreted. The code looks at how long the detector was obscured (switching from enumeration symbol DET_OFF to DET_OBSCURED), then (once the detector is no longer obscured) how long it has been since the detector has been passed (DET_PASSED). The other state, DET_OFF, is set when the train has entered the next track block and the exit detector is cleared to DET_PASSED status.

This is a depth-first search, but it's quick in that, for each track block, the entering and the exiting detectors are checked and if neither has been triggered (DET_OFF condition for both), the block is skipped. Typically, only one or two blocks will have detectors which are not DET_OFF, so this search strategy works efficiently. If both are DET_OFF, that track block is skipped. However, if one or both detectors have status other than DET_OFF, further processing is needed.

// case entering block leaving block occupied? block state action

// 0 in=DET_OFF out=DET_OFF occ=yes state=occupied (startup) no action

// 0 in=DET_OFF out=DET_OFF occ=no state=unoccupied (startup?) no action

// 1 in=DET_OBSCURED out=DET_OFF occ=yes state=occupied (entering) set entering occupied

// 1 in=DET_OBSCURED out=DET_OFF occ=no set occupied (entering) set entering occupied

// 2 in=DET_PASSED out=DET_OFF occ=yes state=occupied reset entering unoccupied

// 2 in=DET_PASSED out=DET_OFF occ=no set occupied reset entering unoccupied

// 3 in=DET_OFF out=DET_OBSCURED occ=yes state=occupied set leaving occupied

// 3 in=DET_OFF out=DET_OBSCURED occ=no set occupied set leaving occupied

// 4 in=DET_OBSCURED out=DET_OBSCURED occ=yes state=occupied (long train) set entering&leaving occupied

// 4 in=DET_OBSCURED out=DET_OBSCURED occ=no set occupied (long train) set entering&leaving occupied

// 5 in=DET_PASSED out=DET_OBSCURED occ=yes state=occupied (exiting) set leaving occupied

// 5 in=DET_PASSED out=DET_OBSCURED occ=no set occupied (exiting) set leaving occupied

// 6 in=DET_OFF out=DET_PASSED occ=yes set unoccupied set leaving occupied

// 6 in=DET_OFF out=DET_PASSED occ=no state=unoccupied set leaving occupied

// 7 in=DET_OBSCURED out=DET_PASSED occ=yes state=occupied (HALT:reset both dets)

// 7 in=DET_OBSCURED out=DET_PASSED occ=no state=occupied (HALT:reset both dets)

// 8 in=DET_PASSED out=DET_PASSED occ=yes set unoccupied (HALT:reset both dets)

// 8 in=DET_PASSED out=DET_PASSED occ=no set unoccupied (HALT:reset both dets)

// At the moment, there is a DET_SKIPPED case which I am using for debugging. It may or may not survive.

Bear in mind that, in ANY software involving a loop or repetitive steps, you must have a means of starting the loop, a task to be done withing that loop, and a way of terminating the loop. As you will see, this can be problematic. It's easy to start the repetitive task of entering the loop (first trigger), then doing something (noting a time or incrementing a counter, for example), then terminating the task. This is hard to do at the detector interrupt level, seeing as how you want the track block to be flagged as occupied until the exit detector flags that it has been active and now no longer is. To flag the exit event, you need processing done at the track block level, not the individual detector level. This can get messy. One alternate thought is to track the time since first entry or most recent trigger before the detector goes dark, but this doesn't definitively detect the train exiting the block., keep that in mind.

Arrays, Buffers:

The first array describes what detector is associated with which block. We do that in the initialization routine. Be sure to test that the information is correct by having the entrance detector signal set the "occupied" flag, which on my layout graphic draws a little locomotive, and by the exit detector TEMPORARILY clearing the "occupied" flag. You'll want to this purely for testing reasons, because there will be more extensive logic used to clear the occupied blocks.

Here's a portion defining the array that holds detector data.

struct Block_Hdwe_type // hdwe pin addresses (currently min 6, max 10)

{ int block_no; // block identifier number

char block_name[30]; // descriptive name

int nmbr_outputs; // max 3 = on/off + direction + pwrpack#; 1 for sidings

// int nmbr_inputs; // max 7 = occupied detector + # of entrances + # exits

int nmbr_entrances; // max 4

int nmbr_exits; // max 4

// the following are all OUTPUT signals (may want to add on-layout signal LEDs later.)

int OffOn_pinMode_nmbr;

int FwdRev_pinMode_nmbr;

int Pwr1_2_pinMode_nmbr;

// Count entrances, exits from fwd direction main entrance

// the following are all INPUT signals (allowance for 4 entrances/exits, 0 thru 3)

// int occupied_pinMode_nmbr; // occupation detector, actually the entrance detector: to be determined

int enter_from_block_nmbr[4]; // entering from where?

int enter_detector_nmbr[4]; // 0 thru 15; -1=none

int exit_to_block_nmbr[4]; // exiting to where?

int exit_detector_nmbr[4]; // 0 thru 15; -1=none

// "active" items are set via code

int active_enter_det_nmbr;

int active_enter_block_nmbr;

int active_exit_det_nmbr;

int active_exit_block_nmbr;


struct Block_Hdwe_type block_hdwe[MAX_BLOCKS];

Here's a piece of the initialization code that specifies detectors.

gboolean Initialize_data_arrays()

{ int i,j;



// define layout topology

for(i=0;i<MAX_BLOCKS;i++) // for starters, init all topology to zero

{ block_hdwe[i].block_no=i; // block identifier number


block_hdwe[i].nmbr_outputs=3; // max 3 = on/off + direction + pwrpack#

block_hdwe[i].nmbr_entrances=0; // max 3

block_hdwe[i].nmbr_exits=0; // max 3



// Count entrances, exits from fwd direction main entrance

// all INPUT signals (allowance for 4 entrances/exits)

for(j=0;j<4;j++) //block_hdwe[i].nmbr_entrances;j++)

{ block_hdwe[i].enter_from_block_nmbr[j]=-1; // entering from where?

block_hdwe[i].enter_detector_nmbr[j]=-1; // 0 thru 15; -1=none

block_hdwe[i].exit_to_block_nmbr[j]=-1; // exiting to where?

block_hdwe[i].exit_detector_nmbr[j]=-1; // normally,

// -1 means no entrance/exit, 99 means siding is electrically

// connected to entrance. WARNING: do not end

// a mainline block while passing a siding. End it

// past where the siding comes back in.

} // for(j...)


// Now, go back and put in the specifics for each block.

// This is set up for LEFT-RUNNING

i=0; // for this block number

block_hdwe[i].block_no=i; // block identifier number

strcpy(block_hdwe[i].block_name,"Upper Loop");

block_hdwe[i].nmbr_outputs=3; // max 3 = on/off + direction + pwrpack#

block_hdwe[i].nmbr_entrances=1; // max 4 LEFT-RUNNING

block_hdwe[i].nmbr_exits=1; // max 4 LEFT-RUNNING

// all INPUT signals (allowance for 4 entrances/exits)

block_hdwe[i].enter_from_block_nmbr[0]=1; // entering from LEFT-RUNNING

block_hdwe[i].enter_detector_nmbr[0]=1; // Gap_01_00

block_hdwe[i].exit_to_block_nmbr[0]=2; // LEFT-RUNNING

block_hdwe[i].exit_detector_nmbr[0]=0; // Gap_00_02

// gap_ptr_array[i].label_ptr=NULL; // gap_00_02




...and so on


// Here's a messy one!This has 3 entrances & 3 exits.

i=5; // for this block number

block_hdwe[i].block_no=i; // block identifier number

strcpy(block_hdwe[i].block_name,"Lower Level Horizontal");

block_hdwe[i].nmbr_outputs=3; // max 3 = on/off + direction + pwrpack#

block_hdwe[i].nmbr_entrances=3; // max 4

block_hdwe[i].nmbr_exits=2; // max 4

// all INPUT signals (allowance for 3 entrances, 2 exits)

block_hdwe[i].enter_from_block_nmbr[0]=7; // LEFT

block_hdwe[i].enter_detector_nmbr[0]=7; // Gap_07_05, LEFT-RUNNING

block_hdwe[i].enter_from_block_nmbr[1]=7; // crossover

block_hdwe[i].enter_detector_nmbr[1]=10; // Gap_05_07, crossover

block_hdwe[i].enter_from_block_nmbr[2]=9; // siding

block_hdwe[i].enter_detector_nmbr[2]=11; // Gap_05_09 siding


block_hdwe[i].exit_detector_nmbr[0]=5; // Gap_05_03

block_hdwe[i].exit_to_block_nmbr[1]=6; // exiting to crossover





.et cetera




return TRUE;

} // gboolean Initialize_data_arrays()

You'll need a couple of buffers:


enum detector_status{DET_OFF, DET_OBSCURED, DET_PASSED};

typedef enum detector_status detector_status;

struct Detector_Data_Type // fast-reads/buffers individual detector data only.

{ char value; // read into here

detector_status status_flag;

gint64 timestamp1; // 1st, microseconds

gint64 timestamp2; // rolling next-to-latest

gint64 timestamp3; // latest

gint64 delta; // elapsed time, start signal to latest or end signal

gint64 delta2; // elapsed time, next-to-latest or end to latest signal


struct Detector_Data_Type raw_detector_data[MAX_DETECTORS];

struct Block_Detector_Data_Type // Ties detector data to blocks

{// The following two, entrance/exit, are for designated by normal

// running direction. "block_direction" does not affect these.

// Watch out if running counter to normal direction!

gboolean block_occupied; // set by software logic.

int block_which_entrance_detected[3]; // which way did it come in? 0=not entered

gint64 time_in; // when, counts up while detector is darkened

int block_which_exit_detected[3]; // which way did it go out?

gint64 time_out; // when, counts upward while next block is being entered


struct Block_Detector_Data_Type block_detector_buffer[MAX_BLOCKS];

With the layout and detector hardware specified, it's time to look at how the detector information is recorded and what is done with it. We open with a couple of auxiliary subroutines, then tackle recording the detector information (in the 300 ms interrupt code).

void reset_detector(int det_nmbr) // reset detector

{ raw_detector_data[det_nmbr].value=0; // current detector, NOT block #










// block_detector_buffer[det_nmbr].block_occupied_changed=FALSE;


} // void reset_detector()

The above gets confusing, I know. It gets called from within loops which will be outlined shortly. Here's a portion of the 300 ms interrupt code.


GBoolean read_raw_data (gpointer data) // every 300 milliseconds

{ int i,j,k,entering,leaving;//,code,xx,xxx;

// Process raw_data from hdwe into buffer

// Updates pulsing progress bar,

// then reads & stores raw data into a buffer and sets xxx_changed datum,

// or leaves it alone in raw_data array and leaves xxx_changed datum

// so display_data array can be updated appropriately later (not here).

// This de-bounces detector data.

gtk_progress_bar_pulse (GTK_PROGRESS_BAR (data));

// iterate on detectors first.

// Note that this does only detector (input) signals. All other

// control (output) signals write directly to the display_data array.

// All (input) detector signals write to their own 4-deep set of

// arrays, initiated here. The rest, output signals, do not require

// debouncing and can be written directly to the display_data array.

// The input arrays are defined as a detector count of decimal 16,

// 0 thru 15, i.e. octal 00 thru 017 determined by MAX_DETECTORS.

// Loop on detectors first

for(i=0;i<MAX_DETECTORS;i++) // get current detector values

{ // block entering detector

char value=read_pin(ADDRESS23,i); // get current value

if(value==1) // DET ACTIVE

{ if(raw_detector_data[i].value==0)


printf("read_raw_data[%i] ***activated 1st pass***\n",i);

// block_detector_buffer[i].block_occupied=TRUE;


// timestamp in microseconds if track block is active









// block_detector_buffer[i].block_which_entrance_detected=

// block_detector_buffer[i].block_which_exit_detected=

// // flag as occupied

// block_detector_buffer[i].block_occupied=TRUE;

// block_detector_buffer[i].block_occupied_changed=TRUE;

// block_detector_buffer[i].time_in=block_detector_buffer[i].time_out=0;

} // Det active, 1st time triggered, newly occupied


{ // DET ACTIVE, occupied, 2nd & subsequent times

// fill in for delta-time test

printf("read_raw_data[%i] active 2nd or later pass\n",i);





// timestamp1 still equals 1st trigger time

// timestamp2 equals next-to-last trigger time

// timestamp3 equals this latest trigger time






} // Det active, occupied, 2nd & subsequent times

} // if(raw_detector_data[i].value=0)



//if(block_detector_buffer[i].block_occupied) // occupied/not-active

printf("read_raw_data[%i] not active\n",i);

if(raw_detector_data[i].timestamp2>0) // occupied/not-active

{ raw_detector_data[i].timestamp3



-raw_detector_data[i].timestamp2; // time since end-of-signal

// timestamp1 still equals 1st trigger time

// timestamp2 still equals final trigger time

// timestamp3 equals current time, and is after

// the last trigger time but block is still

// occupied. Once block is registered as

// empty, it'll quit hitting here.



else // skip empty block


} // if(raw_detector_data[i].value=0) ELSE

// cases: delta=0 && delta2=0 --> xxx, 1st pass, go again

// delta=0 && delta2=xxx --> xxx, 2nd pass, go again

// delta=xxx && delta2-delta<1000 or so,

// 3rd or more pass, obscured but timestamp1 good)

// delta=xxx && delta2-delta>1000,

// no longer obscured (move on, timestamp1 is good)

} // for(i=0;i<MAX_DETECTORS;i++)



>>>Block code UNDER DEVELOPMENT<<<

The code that handles the track blocks' status has also been lumped into the 300 millisecond routine. Using the raw detector information, this code interprets the status, i.e. what is going on, within each track block. Because this information changes rapidly, it's located here in the 300 ms interrupt.


// make sense/use of raw_detector_data[] for regular blocks

for(i=0;i<MAX_REGULAR_BLOCKS;i++) // blocks 0 thru 7 only

{// copy signals from raw_data into buffer, and evaluate.

// first, which detectors?


{ k=block_hdwe[i].enter_from_block_nmbr[j];

if (raw_detector_data[k].status_flag!=DET_OFF) entering=k;



{ k=block_hdwe[i].exit_to_block_nmbr[j];

if (raw_detector_data[k].status_flag!=DET_OFF) leaving=k;


// gboolean tmp_occ; // if need to swap direction sense

// int tmp_in_out;

// gboolean in_block_occ,this_block_occ, exit_block_occ;


{ case DET_OFF:


{ case DET_OFF:

// 0 in=DET_OFF out=DET_OFF state=unoccupied




// 1 in=DET_OBSCURED out=DET_OFF state=occupied (entering)







// 2 in=DET_PASSED out=DET_OFF state=occupied






{ case DET_OFF:

// 3 in=DET_OFF out=DET_OBSCURED state=occupied (startup)





// 4 in=DET_OBSCURED out=DET_OBSCURED state=occupied (long train)


// should probably turn previous block loco yellow too




// 5 in=DET_PASSED out=DET_OBSCURED state=occupied (exiting)








{ case DET_OFF:

// 6 in=DET_OFF out=DET_PASSED state=unoccupied (both to be cleared)






// 7 in=DET_OBSCURED out=DET_PASSED state=occupied (ERROR: HALT)

// (possible rear-end collision imminent)


// send signal to cut power to block. Turn warning red; turn OCCUPIED graphic red.




// 8 in=DET_PASSED out=DET_PASSED state=unoccupied (both to be cleared)








} // for(i=0;i<MAX_REGULAR_BLOCKS;i++)

return TRUE;// Return true so function is called again; returning false removes the timeout function.

} // gboolean read_raw_data (gpointer data)

Let me point out, at this point, that "the loop" is started by setting a detector from DET_OFF to DET_OBSCURED in the loop which operates on individual detectors. Data keeps being processed by the same detector in that section of code until it is reset by code in the block processing code below it. This is because information from both the entering and the leaving detectors is needed. Likewise, determination of whether or not the block is occupied requires information from both this block's entering detector and the next block's entering detector. Got it? (Yeah. Right.) This is actually bad coding practice, but then, what are rules for if not to violate? You can get beaten up for writing self-modifying code, but occasionally it's necessary, particularly in the Artificial Intelligence world. But let's not jump the gun just yet, okay?


Now it's time to point out that this code only works for blocks 00 through 07. The other higher-numbered blocks are sidings and such. Code for them gets really complicated, and one of those complications is that the program needs information on how the turnouts are aligned. Eeewww! Seeing as how I don't have that kind of input, I've added some GLADE code which allows the operator to request permission to use any given turnout or siding.

...cheating? Yup. "Sorry about that," at least for now. Maybe some day "Real Soon Now."

Here's a handy table listing how all the gaps in my layout are laid out. You can refer to the electrical diagram to see what is where.

// gap_ptr_array[MAX_GAPS=16] vs. block_hdwe[MAX_BLOCKS=14].block_no


// line__ gap______ gap_ptr_array[] block_hdwe[].block_no___

// OUT1 gap_00_02 00 block boundary 00

// OUT2 gap_01_00 01 " 01

// OUT3 gap_02_04 02 " 02

// OUT4 gap_03_01 03 " 03

// OUT5 gap_04_06 04 " 04

// OUT6 gap_05_03 05 " 05

// OUT7 gap_06_07 06 to end loop 06

// OUT8 gap_07_05 07 crossover 07

// OUT1 gap_02_10 08 siding 10/13 08

// OUT2 gap_02_01 09 crossover 09

// OUT3 gap_05_07 10 from end loop 10

// OUT4 gap_05_09 11 sidings 9/12 11

// OUT5 gap_06_08 12 sidings 8/11 12

// OUT6 gap_06_05 13 crossover 13

// OUT7 spare 14 -- 14

// OUT8 spare 15 -- 15

[more details progress]