OpenLCB Note: On Using Producer/Consumer Events

This note discusses aspects of the Producer/Consumer model and its use of Events.

It's possible to use a Producer/Consumer (P/C) system in many ways, which is one of it's main strengths, but it's based on some simple ideas which you should master first.

A “Consumer” is a very simple thing that is notified that a particular “Event” has happened. A “Producer” notifies Consumers, via the network, that an Event has happened.

Neither a Producer or Consumer actually does anything other than that. Rather, they're connected to something else, which does it.

For example, consider using a P/C system to turn on a lamp when a button is pushed. We can draw it schematically like this:




The large boxes represent the hardware that connects to a switch and a lamp, respectively. These are parts of “Nodes”. The small box in the upper-left is a single “Producer”, the small box in the lower-right is a single “Consumer”, and the line between them represents a specific “Event”.

By convention, little boxes on the right side of big boxes are Producers, sending information to the right, and little boxes on the left side of big boxes are Consumers, accepting information from the left. The “Pushed” label means that Producer sends its programmed Event when that node tells it that the button has been pushed. The “Turn On” label means that when the Consumer sees its programmed Event, it tells the node “Event received to Turn On”, and presumably the node will turn the lamp on.

Graphically, the Event that's exchanged doesn't have to be labelled. The fact that it connects the boxes is enough to indicate that they are programmed to work with the same Event ID. In a real system, you might want to label the Event with a name or numeric Event ID to make the diagrams easier to understand.

Events have the advantage that separate Consumers in multiple devices can listen for the same one:




The button doesn't have to be configured to talk to all the lamps, the lamps are individually configured, and they don't even have to be of the same type.

It's also possible for multiple Producers to cause the same actions by producing the same Event:






The “Turn on” operation of the lamp will happen if any of the controls is used, because their Producers all emit the same Event. The Consumer associated with the lamp neither knows nor cares which one sent the Event ID. None of the Producers need to know about each other. A new one can be added without reconfiguring anything, by setting it to use an existing EventID.

Sometimes a node will do several things, e.g. handle a switch flipped either way or set an output either on or off. To do that, each operation will have a separate Producer or Consumer attached to it.






In that example, normal operation has the bulb following the on/off toggle switch. The pushbutton can also command the light off at any time.

The toggle switch is connected to two Producers: One that will send an Event when “Set On” happens, and another that will send a different Event when “Set Off” happens. The lamp has two Consumers, one that will “Turn On” when its Event is received and another that will “Turn Off”.

Status Events

In addition to consuming events for “Turn On” or “Turn Off”, it can be helpful for nodes with consumers produce events to describe their status. This can simplify setup, particular if there are local controls involved.

For example, say you have a lturnout controller that consumes “Set Straight” and “Set Diverging” events. It also has a local input that can control the turnout, without going through the Layout Control Bus. On the other side of the room you want to have to lamps that indicate the status of the turnout. They're connected to a consumer board, but what events should they consume? If they consume just the commands to the turnout, they'll miss the changes when the turnout is commanded to change by the local input. One solution is to have the turnout controller emit events that indicate “points moving”, “points set straight” and “points set diverging”. Then the lamps can both turn off when they consume “points moving”, and turn back on for “points set straight” or “points set diverging” respectively. This works even if the turnout is changed locally, ensuring that the lamps show a consistent state.

In general, emitting (or being able to emit) an event on any state change provides a lot of power to the Producer/Consumer method.

Consumer Multiplicity vs Producer Multiplicity

P/C systems can be made even more powerful by having multiple Producers and/or multiple Consumers available for each operation.

Consider an example where you want to have four buttons:

In general, this can't be done with just one Producer per switch and one Consumer per lamp, because something has to be able to respond to two Events, while others have to respond to a different two.

It can be done if you attach two Consumers (either day or night, and either on or off):




In the diagram above, the blue line is a “All Lights On” event, the black line is “All Lights Off”, the red one is “Set Lights to Day”, etc.

It can also be done if you attach two Producers to each button, so you can send two Events when they are pushed, e.g. “on for on at night” and “on for on in daytime” to turn all on:




This is a different approach to what the events mean. In the 2nd diagram, the blue line is a “Turn on Day Lamp” event, the black is “Turn off Night Lamp”, etc, The user gets to decide what the events mean when configuring the system, and that will determine what goes where.

Because having multiple Producers and Consumers can make products more powerful, manufacturers may want to provide them in their products. How many to provide is up to them, though. A simple product might have just one, and a top-end product might have many.

The situation gets more complicated when there are a large number of devices and a large number of inputs that make arbitrary combinations. The limiting case is N states and M outputs, where each of the outputs has to be set for each of the inputs. Somewhere, the mapping between those needs to be maintained. At some point, you might have to add some element (e.g. a computer) that takes a specific Events from Producers and converts them into lots more that are aimed at specific Consumers, but P/C systems are powerful enough that this is rarely necessary.

Compound yard throat example

Each digit is a turnout which consumes Events to set “straight” and other Events to set “diverging”. There are a total of 2*8 = 16 Consumers. The letters are 9 pushbuttons that can produce one or more Events when pressed. The goal is to align the turnouts for the pressed button.

One way to do this is to have the buttons send a single Event, which we'll label with the name of the button, and have turnouts be able to receive all relevant Events.


1

2

3

4

5

6

7

8

A

S

D





S


B

S

D





D

D

C

S

D





D

S

D

S

S




D



E

S

S




S



F

D


D


D




G

D


D


S




H

D


S

D





I

D


S

S







Although this table looks complicated, the configuration can be created manually using e.g. the blue/gold algorithm: Go to each turnout along the route to a specific track, set it as required and put it in acquisition mode, then press the button for that track. Repeat for each track. This works so long as enough Events can be recognized by each Consumer.

In the worst case, turnout 1 must receive and respond to every Event by either setting straight or diverging. (Look down column 1 to see this) Five Events must be recognized to set Turnout 1 straight. Turnout 2 needs to respond to 5 total Events, 2 for straight and 3 for diverging. The others need to respond to 1 or 2 for each of straight and diverging.

In general, this approach will have a few turnouts at the start of the throat that must respond to many Events, with turnouts later on needing to look for only a few Events.

There's another approach available: Note that no Event sets more than four turnouts, and some set three. For this type of ladder, having the Producers send multiple Events can be simpler.

For example, if we define 2*8 Events as individual “Set Turnout 1 straight”, Set Turnout 1 diverging”, “Set Turnout 2 straight”, etc, then each button Producer needs to send only three or four of these to set a route. This is very similar to the usual method of using turnout commands, and has the same drawback: All the buttons must be programmed in detail, and must be able to send lots of commands.

The entire yard can be done with two-Event Producers and two-Event Consumers using a combination of these. Define three Events, called i, ii and iii, which connect to tracks A,B,C; D,E; and F,G,H,I; respectively. Each button sends one of these in addition to the usual A though I Events. The i-iii Events are aimed at the start of the throat, routing to groups of turnouts that can be handled with just two Consumers. See table below.



1

2

3

4

5

6

7

8

A

i, A

s (i,ii)

d (i)





s (A)


B

i, B

s (i,ii)

d (i)





d (B,C)

s (B)

C

i, C

s (i,ii)

d (i)





d (B,C)

s (C)

D

ii, D

s (i)

s (ii)




d (D)



E

ii, E

s (i)

s (ii)




s (E)



F

iii, F

d(iii)


d(F,G)


d (F)




G

iii, G

d (iii)


d(F,G)






H

iii, H

d (iii)


s(iii)

d (H)





I

iii, I

d (iii)


s (iii)

s(I)







A much simpler solution is available when the turnout-controller can produce an event to indicate that it's been aligned. Then, you can separate the yard into parts:

For example, say that there's one node “X” controlling turnouts 7 and 8 at the top, another node “Y” controlling turnouts 1, 2 and 6 in the middle, and a third node “Z” that's controlling 3, 4 and 5 at the bottom.

Controller X would be configured to set turnouts 7 and 8 when events A, B or C are received. In each case, it is also configured to emit a specific event that tells controller Y to set turnout 1 to straight and turnout 2 to diverging so that the entire throat is aligned properly.

Similarly, controller Z would be configured to set 3, 4 and 5 properly when events F through I are received. Controller Z would also emit a separate event that would tell Y to set turnout 1 to diverging.

Finally, events D and E would command controller Y to set the appropriate route.

In this case, each controller needs to be able to set a maximum of 3 turnouts while listening for only four events.

Linear yard throat example

Many yard throats are linear: To get to track M, you go through M-1 turnouts set straight, and then finally one set diverging.

If using “Consumer multiplicity”, where the track buttons just create a single specific Event for each of N tracks, all of which turnouts listen to when required, the first turnout has to listen for N-1 Events that will set “straight”, and one that will set “diverging”. For N large, the turnout might not be able to listen for that many Events in one state. A similar approach can be used, where turnouts are assigned to groups, and early turnouts just listen for a few group Events.

Clearly, tools to do this configuration will be helpful.

Another approach is to provide a Producer on each turnout controller for “Positioned Straight” and “ Positioned Diverging”. The turnout controller will emit those once the turnout has been set. Then, program each turnout controller's “ Positioned Straight” and “Positioned Diverging” Producers to send the “Set Straight” for the upstream turnout. Then, program each button to just emit “Set Diverging” for the last turnout. It will move, and command the upstream one straight, which will command the upstream one straight, etc, all up the line.

Application to Signals

Signaling usually requires more logic than can be handled via raw Events, e.g. occupancy, look ahead, etc. But a signal controller could listen to state via Events. Note that it's still useful for a signal system to emit Events for each aspect change so that e.g. a control panel can mirror the appearance of the on-layout signals.

For example, a Digitrax SE8c has the “cable” (4 heads) as unit of multiplicity for commands, as compared to a single head in a signal decoder, or individual LEDs.

Signals via single-lamp drivers

You can connect the lamps of a signal head to individual Consumers:




This is a powerful but complicated approach.

Signals via single-head drivers

You can also control signals with Events for specific colors of a single head.






Signals via mast drivers

You can control an entire signal mast via one Event for each high-level aspect of the signaling system.




Signals via control-point drivers

You can build the entire signal logic into a single node, accepting input via Events that represent high-level information.






Integrated Signals

In each of the examples above, the signal controller consumes Events that directly control the appearance of the signals. It's also possible to build a signal controller that watches status Events from the railroad and makes independent decisions about signal states and appearances.




Internal vs External configuration

How the Events are stored and mapped to actions internally is not something to be specified by OpenLCB. The configuration process just defines which Event(s) are assigned to which Producer(s) and Consumer(s).

But it's important that implementation be possible, so in this section we describe a few approaches.

Probably the hardest case is a single board that is controlling N Consumers or Producers, e.g. N/2 devices, each of which has 2 Consumers or Producers for separate states, and each of which might have to deal with up to M Events. The worst case is then that the board might have to store N*M Events IDs and test whether each incoming Event report contains any of them. This kind of quadratic growth can get out of control quickly.

In the yard example, we had 2*8 = 16 separate Consumers. One Consumer needed to listen for 6 Events, so if we round that up to 8, if we wanted to create a general purpose 8-turnout controller without limitations, one might imagine needing to store and compare (2*8)*8 = 128 Event IDs.

Note that there were only 9 unique Events, however. The total number of possible settings of the yard throats isn't relevant, because once an early turnout has ensured that a train won't pass over some of the turnouts, their position doesn't matter. For N turnouts, instead of 2^N possible unique Events to set all possible states of the throat, there are really only N+1.

This is an example of a set of Events which set the state of a number of outputs in an interconnected way. If there's sufficient interconnection, and hence a reasonable number of Events, the best way to store it is a list of Event IDs, each of which has associated set of bits that determines the output states. Our 8 output board might have 16 Event slots, each of which has a byte to determine which turnouts will be position and a byte to say whether it's straight or diverging. Total storage, even with the growth from the 9 Events needed to the larger 16 Events, is still 16*(8+2) = 160 bytes. Searching 16 Events can be done linearly, but if not a simple binary search (for unique Event IDs) or tree search (for masked Event IDs) would speed it up.

At the other end of the spectrum, imagine N outputs Consumers (N/2 devices with 2 Consumers per device, or similar), each of which responds to 1 to M Events. Naively, you might assign space to store N*M Events, and have to ensure that all of those can be searched when each Event arrives. For example, 8 turnouts is 16 Consumers, and if you want to assign up 9 Events to each (throat example above), you're up to 64 Events. Not all of these might be needed, however. A controller might be limited to, say 32 total Events. The compound ladder example above uses 30. Some outputs use 1, some 2, up to 6; that's not constrained, just the total. This could be implemented by a sorted list, each entry of which points to the associated Consumer. Storage is small, searching can be done via a binary sort or tree, and the list needs only to be reoptimized when the device is reconfigured.


Site hosted by

This is SVN $Revision: 2639 $