aboutsummaryrefslogtreecommitdiff
path: root/ui_remote.py
blob: a04f3f0faaa5c507296438ccf76adfabda743b11 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#!/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.
#

import atexit
import curses
import pygdbmi.gdbcontroller
import queue
import threading
import sys


def command_consumer(quit_event, command_queue, gdbmi):
    while not quit_event.is_set():
        # block until a command is ready to send
        # non-blocking read so we can respond to quit_event as needed
        command = None
        try:
            command = command_queue.get(block=True, timeout=0.1)
        except queue.Empty:
            continue

        # stop the remote program while run the command and run any other
        # commands in the queue since `interrupt` is expensive
        gdbmi.write('interrupt')
        gdbmi.write(command)
        try:
            while command := command_queue.get(block=False):
                gdbmi.write(command)
        except queue.Empty:
            gdbmi.write('continue &')

    gdbmi.exit()


def curses_init():
    stdscr = curses.initscr()
    curses.cbreak()
    curses.noecho()
    return stdscr


def curses_end(stdscr):
    curses.nocbreak()
    stdscr.keypad(False)
    curses.echo()
    curses.endwin()


def main():
    # 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
    stdscr = curses_init()
    atexit.register(lambda: curses_end(stdscr))

    # queue and thread so we can push UI events to the remote asynchronously
    quit_event = threading.Event()
    command_queue = queue.Queue(maxsize=1024)
    gdb_handler_thread = threading.Thread(
        target=command_consumer,
        args=(quit_event, command_queue, mi))
    gdb_handler_thread.start()

    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")
    global running
    running = True
    while running:
        stdscr.clrtoeol()
        key = stdscr.getch()
        stdscr.clrtoeol()
        stdscr.refresh()
        if key == ord('q'):
            running = False
            quit_event.set()
        elif key == ord('w'):
            command_queue.put('call up()')
        elif key == ord('s'):
            command_queue.put('call down()')
        elif key == ord('a'):
            command_queue.put('call hold()')
        elif key == ord('d'):
            command_queue.put('call press()')

    gdb_handler_thread.join()


if __name__ == "__main__":
    sys.exit(main())