Topic: tech dcc pc src prev next

tech dcc pc src > menu.c

#include "menu.h"

#include <avr/interrupt.h>
#include <util/delay.h>

#include "lcd.h"
#include "millis.h"
#include "pins.h"

#define ANALOG_DEBOUNCE_LENGTH 3

#define HERMES_LONG_BUTTON_PRESS_MS 1000

#define HERMES_ANALOG_INPUT_MS 100

typedef struct
{
    adc_type history[ANALOG_DEBOUNCE_LENGTH];
    unsigned char index;
    adc_type total;
} analog_debounce;

#define ANALOG_DEBOUNCE_DEFAULT \
{ \
    .history = { 0, 0, 0 }, \
    .index = 0, \
    .total = 0 \
};

// Structure for debouncing the current sense input.
analog_debounce g_current = ANALOG_DEBOUNCE_DEFAULT;

// The topmost view has finished.
bool g_view_finished;
// The topmost view should be redrawn.
bool g_view_redraw;
// The last switch to be pressed (0 for none, 1 to 13 for a switch).
switch_type g_last_switch = 0;
// The last time the screen was fully refreshed (reset and redrawn).
millis_type g_last_screen_refresh = 0;

void record_analog(analog_debounce *debounce, unsigned short int value)
{
    debounce->total -= debounce->history[debounce->index];
    debounce->history[debounce->index] = value;
    debounce->total += value;
    debounce->index =
        (unsigned char)(debounce->index + 1) % ANALOG_DEBOUNCE_LENGTH;
}

adc_type adc_value()
{
    const adc_type adcl_cpy = ADCL;
    return (unsigned int)(ADCH << 8) | adcl_cpy;
}

void Menu_Pcint()
{
    static unsigned char pin_cache = 0;

    const unsigned char reading = HERMES_RE_PIN;
    // Ignore all but the rotary encoder pins.
    const unsigned char changed = (unsigned char)(
            (
             reading & (
                (1 << HERMES_RE_A_PIN) |
                (1 << HERMES_RE_B_PIN)
                )
            ) ^ (pin_cache & (
                (1 << HERMES_RE_A_PIN) |
                (1 << HERMES_RE_B_PIN)
                )
            )
        );
    const bool a_rising =
        (reading & (1 << HERMES_RE_A_PIN)) &&
        (changed & (1 << HERMES_RE_A_PIN));
    const bool b_high = (reading & (1 << HERMES_RE_B_PIN));

    if(a_rising && b_high)
        g_last_switch = HERMES_RE_CW;

    if(a_rising && !b_high)
        g_last_switch = HERMES_RE_ACW;

    pin_cache = reading;
}

void Menu_RescanKeypad()
{
    // The last time each button was pressed down.  0 if the button is not
    // pressed down.
    static millis_type g_last_button_press[13] = { 0 };
    // The action has been triggered for a switch.
    static unsigned short g_switch_action_triggered = 0;

    unsigned short out = 0;

    HERMES_SWSEL_DDR &= (unsigned char)~(
            (1 << HERMES_SWSEL1_PIN) | (1 << HERMES_SWSEL2_PIN) |
            (1 << HERMES_SWSEL3_PIN)
            );
    // The four read lines should be configured as inputs with pull up
    // resistors.
    HERMES_SW_DDR &= (unsigned char)~(
            (1 << HERMES_SW1_PIN) | (1 << HERMES_SW2_PIN) |
            (1 << HERMES_SW3_PIN) | (1 << HERMES_SW4_PIN)
            );
    HERMES_SW_PORT |= (unsigned char)(
            (1 << HERMES_SW1_PIN) | (1 << HERMES_SW2_PIN) |
            (1 << HERMES_SW3_PIN) | (1 << HERMES_SW4_PIN)
            );
    // The three select lines should be configured as outputs.  They are
    // pulled high when off.
    HERMES_SWSEL_PORT |= (unsigned char)(
            (1 << HERMES_SWSEL1_PIN) | (1 << HERMES_SWSEL2_PIN) |
            (1 << HERMES_SWSEL3_PIN)
            );
    //HERMES_SWSEL_DDR |= (
            //(1 << HERMES_SWSEL1_PIN) | (1 << HERMES_SWSEL2_PIN) |
            //(1 << HERMES_SWSEL3_PIN)
            //);

    // Pull each select line low in turn.
    HERMES_SWSEL_PORT &= (unsigned char)~(1 << HERMES_SWSEL1_PIN);
    HERMES_SWSEL_DDR |= (unsigned char)(1 << HERMES_SWSEL1_PIN);
    _delay_ms(1);
    out |= (unsigned short)(
            ( (HERMES_SW_PIN & (1 << HERMES_SW1_PIN)) ? 0 : (1 << 0))
            | ( (HERMES_SW_PIN & (1 << HERMES_SW2_PIN)) ? 0 : (1 << 1))
            | ( (HERMES_SW_PIN & (1 << HERMES_SW3_PIN)) ? 0 : (1 << 2))
            | ( (HERMES_SW_PIN & (1 << HERMES_SW4_PIN)) ? 0 : (1 << 3))
            );
    HERMES_SWSEL_DDR &= (unsigned char)~(1 << HERMES_SWSEL1_PIN);
    HERMES_SWSEL_PORT |= (unsigned char)(1 << HERMES_SWSEL1_PIN);

    HERMES_SWSEL_PORT &= (unsigned char)~(1 << HERMES_SWSEL2_PIN);
    HERMES_SWSEL_DDR |= (unsigned char)(1 << HERMES_SWSEL2_PIN);
    _delay_ms(1);
    out |= (unsigned short)(
            ((HERMES_SW_PIN & (1 << HERMES_SW1_PIN)) ? 0 : (1 << 4))
            | ((HERMES_SW_PIN & (1 << HERMES_SW2_PIN)) ? 0 : (1 << 5))
            | ((HERMES_SW_PIN & (1 << HERMES_SW3_PIN)) ? 0 : (1 << 6))
            | ((HERMES_SW_PIN & (1 << HERMES_SW4_PIN)) ? 0 : (1 << 7))
            );
    HERMES_SWSEL_DDR &= (unsigned char)~(1 << HERMES_SWSEL2_PIN);
    HERMES_SWSEL_PORT |= (unsigned char)(1 << HERMES_SWSEL2_PIN);

    HERMES_SWSEL_PORT &= (unsigned char)~(1 << HERMES_SWSEL3_PIN);
    HERMES_SWSEL_DDR |= (unsigned char)(1 << HERMES_SWSEL3_PIN);
    _delay_ms(1);
    out |= (unsigned short)(
            ( (HERMES_SW_PIN & (1 << HERMES_SW1_PIN)) ? 0 : (1 << 8))
            | ((HERMES_SW_PIN & (1 << HERMES_SW2_PIN)) ? 0 : (1 << 9))
            | ((HERMES_SW_PIN & (1 << HERMES_SW3_PIN)) ? 0 : (1 << 10))
            | ((HERMES_SW_PIN & (1 << HERMES_SW4_PIN)) ? 0 : (1 << 11))
            );
    HERMES_SWSEL_DDR &= (unsigned char)~(1 << HERMES_SWSEL3_PIN);
    HERMES_SWSEL_PORT |= (unsigned char)(1 << HERMES_SWSEL3_PIN);

    if(!(HERMES_RE_PIN & (1 << HERMES_RE_C_PIN)))
        out |=  (unsigned short)(1 << 12);

    const millis_type current_millis = millis();

    for(unsigned char i = 0; i < 13; ++i)
    {
        // Inputs have a trigger- reset mechanism.  g_switch_action_triggered
        // is written when the switch action is sent to the menu event loop and
        // cleared (reset) when the button is no longer pressed down.
        if(
                (out & (unsigned short)(1 << i)) &&
                !(g_switch_action_triggered & (unsigned short)(1 << i)) &&
                g_last_button_press[i] == 0
                )
        {
            // The button has been pressed down initially.
            g_last_button_press[i] = current_millis;
        }
        else if(
            (out & (unsigned short)(1 << i)) &&
            !(g_switch_action_triggered & (unsigned short)(1 << i)) &&
            current_millis - g_last_button_press[i] > HERMES_LONG_BUTTON_PRESS_MS
            )
        {
            // The button has been pressed for a 'long press' time.
            g_last_button_press[i] = 0;
            g_last_switch = HERMES_SWITCH_LONG(i + 1);
            g_switch_action_triggered |= (unsigned short)(1 << i);
        }
        else if(
                !(out & (unsigned short)(1 << i)) &&
                (g_switch_action_triggered & (unsigned short)(1 << i))
                )
        {
            // The button has already been triggered and has now been released.
            g_switch_action_triggered &= (unsigned short)~(1 << i);
        }
        else if(!(out & (unsigned short)(1 << i)) && g_last_button_press[i] > 0)
        {
            // A previously pressed button has been released.  The switch
            // action has not been triggered.
            g_last_button_press[i] = 0;
            g_last_switch = HERMES_SWITCH_SHORT(i + 1);
        }
    }
}

void Menu_Finish()
{
    g_view_finished = true;
}

void Menu_Redraw()
{
    g_view_redraw = true;
}

void Menu_FakeButton(switch_type sw)
{
    g_last_switch = sw;
}

struct view;

struct view
{
    void(*update)(void*);
    struct view *covering;
    void *context;
};

typedef struct view view;

view *g_top_view = 0;

void Menu_EventLoop(
        void(*switch_action)(void*, switch_type),
        void(*draw)(void*),
        void(*update)(void*),
        void *context
        )
{
    static millis_type g_last_current_sense_time = 0;

    view v = { .update = update, .covering = g_top_view, .context = context };
    g_top_view = &v;
    Menu_Redraw();

    for(;;)
    {
        const millis_type cmillis = millis();

        // Read from an analog input every HERMES_ANALOG_INPUT_MS.
        if(cmillis >= g_last_current_sense_time + HERMES_ANALOG_INPUT_MS)
        {
            g_last_current_sense_time = cmillis;

            const unsigned short int tmp = adc_value();
            record_analog(&g_current, tmp);

            // Trigger the next analog read.
            ADCSRA |= (1 << ADSC);
        }

        Menu_RescanKeypad();

        if(g_last_switch != 0)
        {
            const switch_type last_switch = g_last_switch;
            g_last_switch = 0;

            // The rotation of the rotary encoder gets special treatment;
            // trigger more events if it is rotated quickly.
            if(
                    HERMES_SWITCH_NO(last_switch) == HERMES_RE_ACW ||
                    HERMES_SWITCH_NO(last_switch) == HERMES_RE_CW
                    )
            {
                // The last time an action was triggered by the rotary encoder.
                static millis_type last_re_trigger_ms = 0;
                // The number of times an action has been triggered by the rotary encoder
                // recently.
                static unsigned char recent_re_trigger_count = 0;

                // Multiple triggers within this time are considered 'recent' enough to be
                // grouped as part of the same input.
                static const millis_type RECENT_MS = 250;

                unsigned char trigger_repeat = 1;

                if(cmillis < last_re_trigger_ms + RECENT_MS)
                {
                    ++recent_re_trigger_count;

                    // Trigger the appropriate action more than once to speed
                    // up data entry.
                    if(recent_re_trigger_count > 5)
                        trigger_repeat = 4;
                    else if(recent_re_trigger_count > 3)
                        trigger_repeat = 2;
                }
                else
                    recent_re_trigger_count = 0;

                last_re_trigger_ms = cmillis;

                for(unsigned char i = 0; i < trigger_repeat; ++i)
                {
                    if(switch_action)
                        (*switch_action)(context, last_switch);
                }
            }
            else if(switch_action)
                (*switch_action)(context, last_switch);
        }

        // Custom function for the topmost view.
        if(update)
            (*(update))(context);

        if(g_view_redraw && draw)
            (*(draw))(context);

        view *vu = &v;
        while((vu = vu->covering))
        {
            if(vu->update)
                vu->update(vu->context);
        }

        g_view_redraw = false;

        if(g_view_finished)
        {
            g_view_finished = false;
            break;
        }
    }
    // Reset the last switch, in case a switch was pressed but not yet
    // processed by the view.  For example, the locomotive programming view
    // hangs for a time before finishing, during which time button presses are
    // still recorded via interrupts.
    // This assignment stops the button press being picked up by the view below.
    g_last_switch = 0;
    // Reinstate the view v was covering as the top view.
    g_top_view = v.covering;
    // Redraw the view that was covered by v.
    Menu_Redraw();
}

int Menu_GetInt(const char *question, int def, int max)
{
    typedef struct {
        // Question to display.
        const char *question;
        // Highest number permitted.
        const int high;
        // Number entered.
        int number;
        // Ask for a yes/no confirmation.
        bool confirm_stage;
        // Number to return (will be the default number if left unconfirmed).
        int result;
    } number_select;

    number_select context = {
        .question = question,
        .high = max,
        .number = 0,
        .confirm_stage = false,
        .result = def
    };

    void button(void *v, const switch_type sw)
    {
        number_select *context_ = (number_select*)v;

        if(HERMES_SWITCH_NO(sw) == HERMES_SWITCH_BACK)
            Menu_Finish();

        if(context_->confirm_stage)
        {
            if(HERMES_SWITCH_NO(sw) == HERMES_RE_SEL)
            {
                // Accept the number.
                context_->result = context_->number;
                Menu_Finish();
            }

            if(HERMES_SWITCH_NO(sw) == HERMES_SWITCH_BACK)
            {
                // Reject the number.
                Menu_Finish();
            }

            if(HERMES_SWITCH_NO(sw) == 12)
            {
                // Amend the number.
                context_->confirm_stage = false;
                Menu_Redraw();
            }
        }
        else
        {
            if(HERMES_SWITCH_NO(sw) == HERMES_RE_ACW)
            {
                if(context_->number == 0)
                    context_->number = context_->high;
                else if(context_->number > 0)
                    --context_->number;
                Menu_Redraw();
            }

            if(HERMES_SWITCH_NO(sw) == HERMES_RE_CW)
            {
                if(context_->number == context_->high)
                    context_->number = 0;
                else if(context_->number < context_->high)
                    ++context_->number;
                Menu_Redraw();
            }

            if(HERMES_SWITCH_NO(sw) == HERMES_RE_SEL)
            {
                context_->confirm_stage = true;
                Menu_Redraw();
            }
        }
    }

    void draw(void *v)
    {
        number_select *context_ = (number_select*)v;
        LCD_DefineHermesChars(HERMES_LCD_CUSTOM_MODE_MENU);
        LCD_Clear();
        if(context_->confirm_stage)
        {
            LCD_PutS(context_->question);
            LCD_Move((unsigned char)(15 - LCD_DigitCount(context_->number)), 0);
            LCD_PutI(context_->number);

            LCD_Move(0, 1);
            LCD_PutC(HERMES_LCD_CHAR_UP);
            LCD_PutS("     ");
            LCD_PutC(HERMES_LCD_CHAR_OK);
            LCD_PutS("      ");
            LCD_PutC(HERMES_LCD_CHAR_ACW);
        }
        else
        {
            LCD_PutS(context_->question);
            LCD_Move((unsigned char)(15 - LCD_DigitCount(context_->number)), 0);
            LCD_PutI(context_->number);

            LCD_Move(0, 1);
            LCD_PutC(HERMES_LCD_CHAR_UP);
            LCD_PutS("     ");
            LCD_PutC(HERMES_LCD_CHAR_ACW);
            LCD_PutS("  ");
            LCD_PutC(HERMES_LCD_CHAR_CW);
            LCD_PutS("     ");
            LCD_PutC(HERMES_LCD_CHAR_OK);
        }
    }

    Menu_EventLoop(button, draw, 0, &context);
    return context.result;
}

bool Menu_Question(const char *question)
{
    typedef struct
    {
        bool result;
        const char *question;
    } question_state;

    question_state context = {
        .result = false,
        .question = question
    };

    void button(void *v, const switch_type sw)
    {
        question_state *context_ = (question_state*)v;

        if(
                HERMES_SWITCH_NO(sw) == HERMES_SWITCH_BACK ||
                HERMES_SWITCH_NO(sw) == 10
                )
            // Decline the question.
            Menu_Finish();

        if(
                HERMES_SWITCH_NO(sw) == HERMES_RE_SEL ||
                HERMES_SWITCH_NO(sw) == 11 ||
                HERMES_SWITCH_NO(sw) == 12
                )
        {
            context_->result = true;
            Menu_Finish();
        }
    }

    void draw(void *v)
    {
        question_state *context_ = (question_state*)v;
        LCD_DefineHermesChars(HERMES_LCD_CUSTOM_MODE_MENU);
        LCD_Clear();
        LCD_PutS(context_->question);
        LCD_Move(1, 1);
        LCD_PutC('X');
        LCD_Move(14, 1);
        LCD_PutC(HERMES_LCD_CHAR_OK);
    }

    Menu_EventLoop(button, draw, 0, &context);
    return context.result;
}

int Menu_GetOption(
        const char *question,
        const char **options,
        unsigned char def
        )
{
    typedef struct
    {
        const char *question;
        const char **options;
        unsigned char count;
        unsigned char index;
        int result;
    } state;

    unsigned char count = 0;
    while(options[count])
        ++count;

    state context = {
        .question = question,
        .options = options,
        .count = count,
        .index = def,
        .result = -1
    };

    void button(void *v, const switch_type sw)
    {
        state *context_ = (state*)v;

        if(HERMES_SWITCH_NO(sw) == HERMES_SWITCH_BACK)
            Menu_Finish();

        if(HERMES_SWITCH_NO(sw) == HERMES_RE_ACW)
        {
            context_->index =
                (unsigned char)((context_->count + context_->index - 1) % context_->count);
            Menu_Redraw();
        }

        if(HERMES_SWITCH_NO(sw) == HERMES_RE_CW)
        {
            context_->index =
                (unsigned char)((context_->index + 1) % context_->count);
            Menu_Redraw();
        }

        if(HERMES_SWITCH_NO(sw) == HERMES_RE_SEL)
        {
            context_->result = context_->index;
            Menu_Finish();
        }
    }

    void draw(void *v)
    {
        state *context_ = (state*)v;
        Menu_DrawList(
                context_->question,
                context_->options[context_->index],
                context_->index,
                context_->count
                );
    }

    Menu_EventLoop(button, draw, 0, &context);
    return context.result;
}

bool Menu_Current(adc_type *current)
{
    const adc_type c = (g_current.total / ANALOG_DEBOUNCE_LENGTH) *
        HERMES_CURRENT_MULTIPLIER;
    if(*current != c)
    {
        *current = c;
        return true;
    }
    return false;
}

void Menu_DrawList(
        const char *title,
        const char *text,
        unsigned char index,
        unsigned char count
        )
{
    LCD_DefineHermesChars(HERMES_LCD_CUSTOM_MODE_MENU);
    LCD_Clear();
    LCD_PutS(title);
    LCD_PrintListIndex(index + 1, count);
    LCD_Move(0, 1);
    LCD_PutS(text);
    LCD_Move(13, 1);
    LCD_PutC(HERMES_LCD_CHAR_L);
    LCD_PutC(' ');
    LCD_PutC(HERMES_LCD_CHAR_R);
}

switch_type Menu_Notice(const char *text)
{
    return Menu_TimedNotice(text, 0);
}

switch_type Menu_TimedNotice(const char *text, millis_type timeout)
{
    typedef struct
    {
        const char *text;
        millis_type dismiss;
        switch_type result;
    } notice_state;

    notice_state context = {
        .text = text,
        .dismiss = (timeout == 0) ? 0 : millis() + timeout,
        .result = HERMES_SWITCH_NULL
    };

    void draw(void *v)
    {
        notice_state *context_ = (notice_state*)v;
        LCD_DefineHermesChars(HERMES_LCD_CUSTOM_MODE_MENU);
        LCD_Clear();
        LCD_PutS(context_->text);
        if(context_->dismiss == 0)
        {
            LCD_Move(15, 1);
            LCD_PutC(HERMES_LCD_CHAR_OK);
        }
    }

    void button(void *v, const switch_type sw)
    {
        (void)sw;
        notice_state *context_ = (notice_state*)v;
        context_->result = sw;
        Menu_Finish();
    }

    void update(void *v)
    {
        notice_state *context_ = (notice_state*)v;
        if(context_->dismiss != 0 && millis() > context_->dismiss)
            Menu_Finish();
    }

    Menu_EventLoop(button, draw, update, &context);

    return context.result;
}