Model Train-related Notes Blog -- these are personal notes and musings on the subject of model train control, automation, electronics, or whatever I find interesting. I also have more posts in a blog dedicated to the maintenance of the Randall Museum Model Railroad.

2022-01-21 - Reboot on the Reboot on Conductor 2

Category Rtac

That Conductor 2 project has been taking too long now. I still think there are good ideas in the design and the core foundation for it. The implementation has been taking forever due to a lack of clarity combined with some other projects taking precedence.

In between I have started an implementation in Groovy which is not quite satisfactory. It was merely a prototype to explore the DSL syntax and as such the project structure has not been carefully planned. It’s already hard to extend and I realize I’m not able to express the script logic as I want with it. The core issue is that the prototype tried to do too many things -- explore how to create a Groovy DSL, figure out the language I need, and at the same time the business logic of the script. So I have a bit of everything, and nothing concrete for each category.

So let’s reboot it. Let’s tackle it differently. Ignore the syntax. Focus on what the script business logic needs to work. I can express that “on the paper” using the ANTLR script language from Conductor 1. I’ll figure the DSL and engine structure from there.

Some thoughts here:

  • Stop using timers as functions. Introduce a real “Function Name -> { instruction }” syntax.
    • Functions are invoked by using just their name in an instruction sequence.
  • Unify the instruction format to be:
            Object [ Action ] [ = Parameters ]
    • Some objects may not have an action.
    • Some actions may not have parameters.
    • Parameter types: literals int or string, object names, lists, or dictionaries.
    • When parameters are lists, unify to be a comma-separated one.
    • This changes:
      • Enum lists need commas.
      • GA-Event & JSON-Event need an = before their parameters.
  • Uniformize “instruction sequences” to be:
    • A ;-separated list of instructions, with no end ;
    • A { ;-separated list } block, which allows nesting.
    • The { } block is itself an instruction so use { … } ; to have it in a list.
  • The { } blocks create sequences of instructions.
    • There is no scoping in the sense of limited variable scopes. All definitions are global.
    • Any place that currently allows an instruction sequence can use a block: -> { …} is the syntax.
  • We introduce a syntax for “scoped variables”, or more exactly “object properties”. See below.
  • We add the definition of a Route:
    • A route has properties: throttle, toggle, an enum state, error.
      • These are accessed from within the route as “Self.<property”>
      • And outside as <RouteName>.<property>
      • The “toggle” property is not used for now. Omit it.
    • A route has a type / driver / route manager.
      • The only types are the fixed-block manager (default), or idle.
    • The default manager has these properties:
      • There’s one active block, which is where the train should be / occupied.
      • It’s an error to have 0 or more than 1 block occupied.
      • The route has a “block list”.
        • This indicates which block we’re starting from.
        • Block occupation can only move from one block to the expected next one. Activating a different block results in an error.
      • When a new block is occupied, the “enter block” instructions are executed.
      • When the route is activated, the first block is “entered” with the engine in Stopped mode.
      • To handle shuttle modes, the “enter block” is considered to be re-entered when the direction changes.
    • The “idle” manager is a special one. It defines a route that controls no trains and does nothing.
    • Routes are either active or inactive. There’s a corresponding function executed: OnActivate, OnDeactivate.
    • Routes define a number of block instructions: “[ Block | Enter ] [ Sensor ] [Forward | Reverse | Stopped] -> { instructions }”
      • The Forward/Reverse/Stopped refers to the current state of Route’s Throttle.
      • The Block/Sensor number refers to the Route’s current block being occupied.
      • A “Block” definition means the instructions are evaluated every time that this block is active with the engine in that current direction.
      • An “Enter” definition means the instructions are evaluated once when that block is activated in the block sequence.
        • However if there’s an “After” instruction, it is started and continues to be evaluated if the block is still active with the right direction.
        • Or said otherwise, any block change in the route cancels any pending timer.
    • Routes have “event” functions. For now:
      • OnActivate
      • OnDeactivate
      • OnError
  • We add the definition of an “ActiveRoute”:
                    ActiveRoute Route-Name = Route1 Route2 Route3…
    • This behaves as an enum that select one and only one of multiple routes.
    • There is no concept of “null” so we must define an empty “idle” route as the default. This has the side effect that the idle route can have OnActivate/OnDeactivate functions.
    • By default the first route is the active one.
    • An ActiveRoute object has one instruction: Activate
              Route-Name = Activate Route2
  • We define a new instruction named “After”:
    • Simple form:
              After [Int | Timer] -> { instructions }”.
    • Chained form:
              After [Int | Timer] -> { instructions 1 }”
              Then After [Int | Timer] -> { instructions 2 }”...
    • This is the same as starting a timer with the given delay.
    • The argument can be a literal number, or an integer variable, or a Timer variable.
      • For the 2 formers, an anonymous timer is created. For the latter, that timer is actually used.
    • The instructions are carried away when the timer expires.
    • If the “After” instructions are executed as part of a Route “Enter” definition, the instructions are only executed if the block is still active and the train is in the expected direction.
    • Once the instructions finish executing, they start the following “Then After” timer if present. This means they all form a chain, executed in sequence.
    • Question: Can we have an “After” instruction inside another “After” instruction? How does that behave exactly? → Upfront I’d say no we can’t. It seems pointless and illogical.

Doing this allows us to encode the same script as we have now, but with a lot of simplification in the script text.

On the scripting side, the “novelty” is adding generic support for functions and blocks of instructions. This is needed to be able to write the “After” blocks inside parent blocks.

The core structural change is the handling of the route and active route.

On one hand, we can use this as “syntactic sugar”, and think of them as macros that internally generate flat conditions lists.

However it would make more sense to fully develop the concept of the route manager. In this case the route manager tracks the block changes and schedules executing of the various instruction sequences.

What won’t be back-ported from Conductor 2.02:

One difference with Conductor 2.02 is the throttle management for a route. In v2.02, instructions inside a route implicitly used the throttle for that route. Here the throttle is externally defined and the instructions just act on a global throttle object. That lets the script be more flexible, e.g. the AM route can also act on the SP throttle to e.g. stop the sound.

One thing I’m not carrying over from Conductor 2.02 is the notion of virtual blocks. These seem pointless in this context. Blocks are purely mapped to a sensor each as in Conductor 1.


 Generated on 2024-12-21 by Rig4j 0.1-Exp-f2c0035