From d9527ea3c5ab6f1e0776b86ecc43459eb1a706b2 Mon Sep 17 00:00:00 2001 From: David Phillips Date: Sat, 13 Mar 2021 20:03:55 +1300 Subject: First cut of UI state machine This patch adds a first skeleton/draft of the UI state machine, with the altimeter setting logic implemented at a basic level. It also adds a Python script which uses GDB/MI to simulate/inject UI events on a GDB remote (i.e. qemu-avr running the sim copy of the firmware). --- altimeter.c | 7 +++ ui.c | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++++++---- ui.h | 16 +++++++ ui_remote.py | 79 ++++++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 9 deletions(-) create mode 100755 ui_remote.py diff --git a/altimeter.c b/altimeter.c index df238da..b0148d9 100644 --- a/altimeter.c +++ b/altimeter.c @@ -68,3 +68,10 @@ int main(void) sleep_mode(); } } + + +/* FIXME remove these - handy for testing in gdb */ +void up(void){ui_input_event(&ui_ctx, &dctx, UI_INPUT_EVENT_UP);ui_update(&ui_ctx, &dctx);} +void down(void){ui_input_event(&ui_ctx, &dctx, UI_INPUT_EVENT_DOWN);ui_update(&ui_ctx, &dctx);} +void press(void){ui_input_event(&ui_ctx, &dctx, UI_INPUT_EVENT_PRESS);ui_update(&ui_ctx, &dctx);} +void hold(void){ui_input_event(&ui_ctx, &dctx, UI_INPUT_EVENT_HOLD);ui_update(&ui_ctx, &dctx);} diff --git a/ui.c b/ui.c index 6b63a86..d6d75d0 100644 --- a/ui.c +++ b/ui.c @@ -6,9 +6,27 @@ #include "util.h" #include "ui.h" +static void adjust_setting(struct ui_ctx *ui, struct data_ctx *dctx, int adj) +{ + /* FIXME enforce bounds on setting - is this responsibility of data man? */ + dctx->setting += adj; +} + +static void adjust_selected_category(struct ui_ctx *ui, int adj) +{ + /* FIXME */ +} + +static void adjust_selected_unit(struct ui_ctx *ui, int adj) +{ + /* FIXME */ +} + + void ui_init(struct ui_ctx *ui, struct display *display) { ui->display = display; + ui->state = UI_STATE_MAIN; ui->display->init(); ui->display->clear(); } @@ -16,14 +34,6 @@ void ui_init(struct ui_ctx *ui, struct display *display) void ui_update(struct ui_ctx *ui, struct data_ctx *dctx) { char line[LCD_WIDTH+1]; - //if (rate_m_s < 0) { - // rate_sym = C_DESC; - //} else if (rate_m_s == 0) { - // rate_sym = C_IDLE; - //} else { - // rate_sym = C_ASC; - //} - //DEBUG_MARK_5_CLEAR; /* FIXME these are missing from the data context, dummy values to test */ float rate_m_s = 1.2; @@ -36,6 +46,7 @@ void ui_update(struct ui_ctx *ui, struct data_ctx *dctx) rate_sym = C_ASC; } + /* FIXME this is only valid display for UI_STATE_MAIN. Implement the other pages */ /* line 1 */ snprintf(line, sizeof(line), "%.1f m %c%d m/s", dctx->altitude, rate_sym, abs(rate_m_s)); blank_to_eol(line, sizeof(line)); @@ -50,7 +61,16 @@ void ui_update(struct ui_ctx *ui, struct data_ctx *dctx) ui->display->write(line); /* line 3 */ - snprintf(line, sizeof(line), "SET %.2f hPa", dctx->setting); + /* FIXME make nicer */ + char extra_left, extra_right; + if (ui->state == UI_STATE_MAIN_WITH_SETTING) { + extra_left = '>'; + extra_right = '<'; + } else { + extra_left = ' '; + extra_right = ' '; + } + snprintf(line, sizeof(line), "Set%c%.2f%chPa", extra_left, dctx->setting, extra_right); blank_to_eol(line, sizeof(line)); ui->display->set_cursor(0, 2); ui->display->write(line); @@ -61,3 +81,109 @@ void ui_update(struct ui_ctx *ui, struct data_ctx *dctx) ui->display->set_cursor(0, 3); ui->display->write(line); } + +void ui_input_event(struct ui_ctx *ui, struct data_ctx *dctx, enum ui_input_event event) +{ + /* FIXME state machine */ + /* XXX menus + 1 main display screen (alt+rate, pres, set, temp) + - press moves to 2 + - hold moves to 3 + 2 main display screen with setting change (alt+rate, pres, set w/icon, temp + - up increases setting + - down decreases setting + - timeout ? + - press moves to 1 + - hold? might be useful to switch unit while changing setting? + 3 unit adjustment screen. something like: + alt: m (ft) + rate: m/s (ft/min) + pressure: hPa (kPa, inHg) + temperature: °C (°F, K) + - hold moves to 1 + - if not in change mode: + - press enters change mode for the selected category + - up selects unit category above + - down selects unit category below + else: + - press exits change mode for the selected unit, saving selection + - up selects previous/left unit for category + - down selects next/right unit for category + - timeouts? + */ + /* state: + * - selected_unit_category + * - selected_unit + */ + + switch (ui->state) { + case UI_STATE_MAIN: + switch (event) { + case UI_INPUT_EVENT_PRESS: + ui->state = UI_STATE_MAIN_WITH_SETTING; + break; + case UI_INPUT_EVENT_HOLD: + ui->state = UI_STATE_CHOOSE_UNIT_CATEGORY; + break; + + /* nops for this state: */ + case UI_INPUT_EVENT_UP: + case UI_INPUT_EVENT_DOWN: + /* nop */ + break; + } + break; + case UI_STATE_MAIN_WITH_SETTING: + switch (event) { + case UI_INPUT_EVENT_PRESS: + ui->state = UI_STATE_MAIN; + break; + case UI_INPUT_EVENT_UP: + adjust_setting(ui, dctx, +1); + break; + case UI_INPUT_EVENT_DOWN: + adjust_setting(ui, dctx, -1); + break; + /* nops for this state: */ + case UI_INPUT_EVENT_HOLD: + /* nop */ + break; + /* XXX timeout? */ + } + break; + case UI_STATE_CHOOSE_UNIT_CATEGORY: + switch (event) { + case UI_INPUT_EVENT_HOLD: + ui->state = UI_STATE_MAIN; + break; + case UI_INPUT_EVENT_PRESS: + ui->state = UI_STATE_CHANGE_UNIT_MODE; + break; + case UI_INPUT_EVENT_UP: + adjust_selected_category(ui, -1); + break; + case UI_INPUT_EVENT_DOWN: + adjust_selected_category(ui, +1); + break; + } + break; + case UI_STATE_CHANGE_UNIT_MODE: + switch (event) { + case UI_INPUT_EVENT_PRESS: + ui->state = UI_STATE_CHOOSE_UNIT_CATEGORY; + break; + case UI_INPUT_EVENT_UP: + adjust_selected_unit(ui, -1); + break; + case UI_INPUT_EVENT_DOWN: + adjust_selected_unit(ui, +1); + break; + /* nops for this state: */ + case UI_INPUT_EVENT_HOLD: + /* nop */ + break; + /* XXX timeout? */ + } + break; + } +} diff --git a/ui.h b/ui.h index c51c261..3bb14c7 100644 --- a/ui.h +++ b/ui.h @@ -2,9 +2,25 @@ #include "data_manager.h" +enum ui_state { + UI_STATE_MAIN, + UI_STATE_MAIN_WITH_SETTING, + UI_STATE_CHOOSE_UNIT_CATEGORY, + UI_STATE_CHANGE_UNIT_MODE, +}; + +enum ui_input_event { + UI_INPUT_EVENT_UP, + UI_INPUT_EVENT_DOWN, + UI_INPUT_EVENT_PRESS, + UI_INPUT_EVENT_HOLD, +}; + struct ui_ctx { struct display *display; + enum ui_state state; }; void ui_init(struct ui_ctx*, struct display *); void ui_update(struct ui_ctx*, struct data_ctx*); +void ui_input_event(struct ui_ctx*, struct data_ctx *dctx, enum ui_input_event); diff --git a/ui_remote.py b/ui_remote.py new file mode 100755 index 0000000..e2cb584 --- /dev/null +++ b/ui_remote.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python + +# ui_remote.py - naive script to poke and prod the altimeter firmware over +# GDB/MI. Useful for rapid UI development/testing, all while probably not being +# strictly threadsafe. We're not triggering an ISR on the remote, so PC could +# already be in the middle of an ISR when we fire a UI event, which should +# never be allowed to happen in normal use. +# Useful nonetheless. +# +# FIXME allow smarter queueing of UI events in this script so they can be +# batched into the same (slow) interrupt-and-continue window. +# + +import atexit +import curses +import pygdbmi.gdbcontroller +import sys + + +def interrupt_run_continue(mi, command): + mi.write('interrupt') + mi.write(command) + mi.write('continue &') + + +def main(): + def curses_init(): + stdscr = curses.initscr() + curses.cbreak() + curses.noecho() + return stdscr + + def curses_end(): + curses.nocbreak() + stdscr.keypad(False) + curses.echo() + curses.endwin() + + # Give pygdbmi hardcoded GDB, ELF paths, remote. Good enough for now + mi = pygdbmi.gdbcontroller.GdbController( + command=["/usr/bin/avr-gdb", '--interpreter=mi3']) + mi.write('file build/altimeter_sim.elf', timeout_sec=5) + mi.write('target remote :1234') + mi.write('continue &') + + # atexit covers crash cases which is nice + atexit.register(curses_end) + stdscr = curses_init() + + stdscr.addstr(0, 0, "UI remote control via GDB. Press q to quit") + stdscr.addstr(1, 0, "w: up s: down a: hold d: short press") + running = True + while running: + stdscr.addstr(2, 0, "ready") + stdscr.clrtoeol() + key = stdscr.getch() + stdscr.addstr(2, 0, "busy") + stdscr.clrtoeol() + stdscr.refresh() + if key == ord('q'): + running = False + elif key == ord('w'): + interrupt_run_continue(mi, 'call up()') + pass + elif key == ord('s'): + interrupt_run_continue(mi, 'call down()') + pass + elif key == ord('a'): + interrupt_run_continue(mi, 'call hold()') + pass + elif key == ord('d'): + interrupt_run_continue(mi, 'call press()') + pass + else: + stdscr.addstr(1, 0, str(key)) + + +if __name__ == "__main__": + sys.exit(main()) -- cgit v1.1