Search code examples
dml-lang

How do I Implement a traffic light state machine in DML?


I am originally a C-developer and I am now getting familiar with DML. I often need to write state machines and I always design them very C-stylish. I suspect that I am not utilizing the DML language to its full extent. Could someone port this traffic light C-implementation to modern DML?

enum TRAFFIC_LIGHT {
    RED,
    YELLOW,
    GREEN,
}

int state;
timer_t timer;

int redTimeMs = 15000;
int greenTimeMs = 10000;
int transitionTimeMs = 1000;

void start_transition_to_green() {
    start_timer(transitionTimeMs, tick_to_green);
}
void start_transition_to_red() {
    start_timer(transitionTimeMs, tick_to_red);
}

void tick_to_green() {
    switch (state) {
    case RED:
        state = YELLOW;
        start_timer(transitionTimeMs, tick_to_green);
        break;
    case YELLOW:
        state = GREEN;
        start_timer(transitionTimeMs, tick_to_green);
        break;
    case GREEN:
        start_timer(greenTimeMs, start_transition_to_red);
        break;
    }
}
void tick_to_red() {
    switch (state) {
    case GREEN:
        state = YELLOW;
        start_timer(transitionTimeMs, tick_to_red);
        break;
    case YELLOW:
        state = RED;
        start_timer(transitionTimeMs, tick_to_red);
        break;
    case RED:
        start_timer(redTimeMs, start_transition_to_green);
        break;
    }
}

void main(void) {
    state = RED;
    start_transition_to_green();
}    

I am expecting a DML implementation that is checkpointable.


Solution

  • The easy translation is of "start_timer" to the 'after' construct. This will give you checkpointing of both the "direction" of change (towards green/red) and the time until next change for free.

    Enums do not exist (as such) in DML, instead we can define the states as instances of a common template type. Additionally; a template type is serializable so we can store this in a 'saved' variable and obtain the checkpointing for free.

    The 'after' can only accepts a delay in cycles or seconds, so I have pre-divided the constants by 1000 and stored them as top-level parameters.

    dml 1.4;
    
    device traffic_light;
    
    param desc = "traffic light state machine";
    
    param documentation = "switches red-yellow-green-yellow-red etc...";
    
    // Defines behavior of a traffic light state
    // The instantiation if 'name' here is necessary if we want inspection
    // as it will put the object name into the template type
    template traffic_state is name {
        method enter() default {
            log info, 2: "Entered %s", name;
            current_light_state = this;
        }
        method tick_to_green();
        method tick_to_red();
    }
    
    saved traffic_state current_light_state;
    
    param red_time_s = 15;
    param green_time_s = 10;
    param transition_time_s = 1;
    

    After this, we define each state as a 'group' instantiating our template type. Implementing the correct behavior for ticking into/out-of them.

    group red is traffic_state {
        method tick_to_red() {
            enter();
            after red_time_s s: tick_to_green();
        }
        method tick_to_green() {
            after transition_time_s s: yellow.tick_to_green();
        }
    }
    group yellow is traffic_state {
        method tick_to_red() {
            enter();
            after transition_time_s s: red.tick_to_red();
        }
        method tick_to_green() {
            enter();
            after transition_time_s s: green.tick_to_green();
        }
    }
    group green is traffic_state {
        method tick_to_red() {
            after transition_time_s s: yellow.tick_to_red();
        }
        method tick_to_green() {
            enter();
            after green_time_s s: tick_to_red();
        }
    }
    

    Then, we need to start up the chain of 'after' calls exactly once upon creating the device. And additionally since the next after call will be checkpointed we must guard against setting it up again when loading a checkpoint. Using 'SIM_is_restoring_state' on our device object allows us to only execute code while not loading a checkpoint. (note: we need to do this in 'post_init' and not 'init'. This is because the 'queue' attribute that 'after' statements rely on is not yet set on the device object)

    method post_init() {
        if (!SIM_is_restoring_state(dev.obj)) {
            red.enter();
            red.tick_to_green();
        }
    }
    

    Finally, if we want to inspect the state from the simulator we need to expose it as an attribute. Suitably done with a "read-only" (setting state from simulator would be more complicated) and "pseudo" (does not contain state) atribute.

    attribute current_light is (pseudo_attr, read_only_attr) {
        param type = "s";
        method get() -> (attr_value_t) {
            return SIM_make_attr_string(current_light_state.name);
        }
    }