Skip to content
Open
Show file tree
Hide file tree
Changes from 93 commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
996e480
Integrate gs building code for MacOS systems (#552) (#580)
c4bae Oct 1, 2025
ced0983
develop layout of textual cli, with logs, cli, cmds, and misc panels
c4bae Oct 4, 2025
99168c8
shift input widget into cli panel
c4bae Oct 4, 2025
8e80f2d
defined constructor for CmdButton class for button label customization
c4bae Oct 4, 2025
2d4d3b9
display intro and intro prompt in cli panel
c4bae Oct 4, 2025
8dde3c6
display input from input widget in CLI panel
c4bae Oct 4, 2025
8601945
allow execution of commands (basic ones) with arguments
c4bae Oct 8, 2025
d545817
add data table widget
c4bae Oct 9, 2025
ac53891
fix cli crash from blank input
c4bae Oct 9, 2025
e92491e
remove direct printing to cli upon executing print_logs
c4bae Oct 9, 2025
ee278ab
run cli commands in threads to avoid blocking
c4bae Oct 9, 2025
4938edb
allow the cli panel to be scrollable when text overflows
c4bae Oct 10, 2025
86f526f
allow help commands to be entered into cli
c4bae Oct 14, 2025
e53bc09
change ui theme to orbital colors (red, blue, white)
c4bae Oct 15, 2025
6c227c0
enable cmd button functionality
c4bae Oct 16, 2025
35221f5
implement exit (KeyboardInterrupt alternative) method for print_logs …
c4bae Oct 16, 2025
930f671
add textual to requirements.txt
c4bae Oct 27, 2025
822201a
format files
c4bae Oct 27, 2025
f74176b
fix mypy errors
c4bae Oct 27, 2025
e5ca241
fix mypy errors
c4bae Oct 27, 2025
d547df0
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
7601300
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
7d81f63
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
31ce559
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
f422550
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
650074b
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
7781b4d
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
648387f
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
bff4cc9
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
c5c1ead
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
e213c4c
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
9797482
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
c436e82
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
6d11561
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
730f7a6
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
c68e610
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
f11303b
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
cdc5cc6
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
4035a68
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
6d29c71
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
f074884
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
da454c5
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
ad682cc
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
9c04b1a
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
0ef9a1e
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
39a6caa
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
1536cf7
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
acb61e9
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
ba3ecf3
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
628dea2
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
42e8398
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
a3607ab
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
5ff01a7
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
1a93453
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
196a794
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
5ea9517
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
b88160e
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
bbd5c1f
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
cbf47f0
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
4b23f40
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
fe1c58c
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
dd229d3
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
d3e331a
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
600deed
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
65dcbdc
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
534ad16
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
369d8ae
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
08029d6
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 27, 2025
cf23e0a
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
02a4db8
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
ad2fa87
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
f41b21d
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
cf0ad08
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
a85f6cf
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
e6faf21
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
53b6acc
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
565c5c4
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
032c98f
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
2f23030
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
971fbfd
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
99f0cf9
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
dbd9619
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
53407e6
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
bad6995
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
796e1d6
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
c200c3a
fix ruff formatting
c4bae Oct 28, 2025
acb56a4
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Oct 28, 2025
e81f33c
flash "logs file not found" message in red
c4bae Oct 29, 2025
854f11c
redesign layout and allow for auto scrolling in containers
c4bae Nov 1, 2025
f0bb79c
redesign layout and allow for auto scrolling in containers
c4bae Nov 1, 2025
75b97dc
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Nov 1, 2025
70aab1d
fix pre-commit test fails
c4bae Nov 1, 2025
867ecdb
update docstring for cmdbutton class
c4bae Nov 1, 2025
b11f65b
implement requested changes from pr
c4bae Nov 23, 2025
fcf88aa
implement requested changes from pr
c4bae Nov 23, 2025
24cf503
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Nov 23, 2025
b50da83
remove function argument from poll
c4bae Nov 23, 2025
02b2873
Merge branch 'main' into charles/textual-gs-cli
c4bae Nov 23, 2025
a2dfe9c
move cli flies into new directory
c4bae Jan 5, 2026
bd103d2
Merge branch 'charles/textual-gs-cli' of github.com:UWOrbital/OBC-fir…
c4bae Jan 5, 2026
11f5f52
reformat file
c4bae Jan 5, 2026
094e065
move cli.tcss file and update requirements.txt
c4bae Feb 10, 2026
066f6b3
Merge branch 'main' into charles/textual-gs-cli
c4bae Feb 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ if(${CMAKE_BUILD_TYPE} MATCHES OBC)
endif()
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/cmake/toolchain_arm_none_eabi.cmake)
elseif(${CMAKE_BUILD_TYPE} MATCHES GS)
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/cmake/toolchain_ground_station_gcc.cmake)
if(NOT APPLE)
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/cmake/toolchain_ground_station_gcc.cmake)
endif()
elseif(${CMAKE_BUILD_TYPE} MATCHES HIL)
include(${CMAKE_SOURCE_DIR}/cmake/fetch_googletest.cmake)
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/cmake/toolchain_hil_gcc.cmake)
Expand Down
7 changes: 7 additions & 0 deletions gs/backend/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ target_include_directories(gs.out PUBLIC

target_compile_options(gs.out PUBLIC -Wall -g)

if(APPLE)
target_link_libraries(gs.out PUBLIC
"-framework CoreFoundation"
"-framework IOKit"
)
endif()

if(UNIX)
target_link_libraries(gs.out PUBLIC
tiny-aes
Expand Down
321 changes: 321 additions & 0 deletions gs/backend/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
import io
import sys
import threading
from collections.abc import Callable
from sys import argv
from typing import cast

from serial import Serial
from textual import on
from textual.app import App, ComposeResult
from textual.containers import HorizontalGroup, HorizontalScroll, ScrollableContainer, VerticalScroll
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Button, DataTable, Input, Label, Static

from gs.backend.ground_station_cli import GroundStationShell

COM_PORT = argv[1]
shell = GroundStationShell(COM_PORT)


class CliPanel(ScrollableContainer):
"""
CLI panel class, handling command input and output displays
"""

cli_output = reactive("")

def __init__(self, *widgets: Widget, id: str | None = None) -> None: # noqa: A002
"""
Initialize the CLI panel and set up output redirection
"""
super().__init__(*widgets, id=id)
self.shell = shell
self.cli_output_panel: Static | None = None
# Hold the original output stream
self.sys_stdout = sys.stdout

# Buf creates a StringIO instance, storing what is printed from the cli through a redirection of stdout
self.buffer = io.StringIO()

# Redirects the output stream (sys.stdout) to buffer, and anything printed will now be written to buffer
sys.stdout = self.buffer

# Redirects the output stream of the gs shell to buffer, allowing help descriptions to be displayed
self.shell.stdout = sys.stdout
# Redirects the "error" output stream of the gs shell to buffer
sys.stderr = sys.stdout

if self.shell.intro is not None:
print(self.shell.intro)

def on_mount(self) -> None:
"""
Set up periodic CLI output refresh and initialize the output panel
"""
self.output_refresh = self.set_interval(1 / 600, self.update_cli)
self.cli_output_panel = self.query_one("#cli-output-panel", Static)

# Buffer.getvalue() returns the contents of the string buffer as a str
self.cli_output = self.buffer.getvalue()

def on_unmount(self) -> None:
"""
Restore original output streams when the CLI panel is unmounted
"""
# Upon exitting the cli, restore all original output streams
sys.stdout = self.sys_stdout
sys.stderr = self.sys_stdout
self.shell.stdout = self.sys_stdout

def watch_cli_output(self, cli_output: str) -> None:
"""
Update the CLI output panel with the latest CLI output
"""
if self.cli_output_panel:
self.cli_output_panel.update(f"CLI - {COM_PORT}\n" + self.cli_output)

def update_cli(self) -> None:
"""
Refresh the CLI output from the buffer
"""
self.cli_output = self.buffer.getvalue()

# Use threads to ensure blocking commands don't block CLI
def run_cli_command_in_thread(self, cmd_function: Callable[[str], None], args: str) -> None:
"""
Run a CLI command in a separate thread to avoid blocking the UI
"""
thread = threading.Thread(target=cmd_function, args=(args,), daemon=True)
thread.start()

@on(Input.Submitted)
def submit_command(self) -> None:
"""
Handle command submission from the input widget and dispatch to the shell
"""
cli_input = self.query_one(Input)
print(f"(UW Orbital): {cli_input.value}")

# Splits the input into two parts: command name and args
command_parts = cli_input.value.strip().split(maxsplit=1)
try:
# Obtain function from gs shell instance based on inputted command
cmd_function = getattr(self.shell, f"do_{command_parts[0]}")
args = command_parts[1] if len(command_parts) > 1 and command_parts[1] is not None else ""

# Make special exception for "exit" command
if command_parts[0] == "exit":
self.app.exit()
return

# Change print_logs cmd button status to STOP if manually typed in
if command_parts[0] == "print_logs":
print("[yellow]Use print_logs button below CMDS to exit polling.[/yellow]")
# Query_one("#btn-print_logs") returns a widget, so type cast widget to button
btn = cast(Button, self.app.query_one("#btn-print_logs"))
btn.label = "STOP"

# Disable send_conn_request cmd btn if manually typed in
if command_parts[0] == "send_conn_request":
btn = cast(Button, self.app.query_one("#btn-send_conn_request"))
btn.disabled = True

self.run_cli_command_in_thread(cmd_function, args)

except (AttributeError, IndexError):
print(f"*** Unknown syntax: {cli_input.value}")

self.scroll_to_bottom()
self.query_one(Input).value = ""

def scroll_to_bottom(self) -> None:
"""
Auto-scroll to bottom of cli panel
"""
self.scroll_end()

def compose(self) -> ComposeResult:
"""
Compose the CLI panel widgets
"""
yield Static("", id="cli-output-panel")
yield Input(placeholder="Enter command here:", id="cli-input")


class CmdButton(HorizontalGroup):
"""
A horizontal group containing command buttons for the CLI
"""

def __init__(self, cmdname: str) -> None:
"""
Initialize the command button with the given command name
"""
super().__init__()
self.cmdname = cmdname
self.shell = shell

def on_button_pressed(self, event: Button.Pressed) -> None:
"""
Handle the event when the command button is pressed
"""
cmd_function = getattr(self.shell, f"do_{self.cmdname}")
args = ""

thread = threading.Thread(target=cmd_function, args=(args,), daemon=True)

if event.button.label != "STOP":
print(f"(UW Orbital): {self.cmdname}")
thread.start()

if self.cmdname != "print_logs":
event.button.disabled = True

if self.cmdname == "print_logs":
if event.button.label == "Run":
print("[yellow]Use print_logs button below CMDS to exit polling.[/yellow]")
event.button.label = "STOP"
else:
event.button.label = "Run"
self.shell.stop_printing = True

def compose(self) -> ComposeResult:
"""
Compose the command button and its label
"""
yield Label(f"{self.cmdname}", id="button-label")
yield Button("Run", id=f"btn-{self.cmdname}")


class TimeTaggedLogs(HorizontalScroll):
"""
A horizontal scrollable widget displaying time-tagged command logs
"""

def __init__(self, *widgets: Widget, id: str | None = None) -> None: # noqa: A002
"""
Initialize the time-tagged logs table with sample data
"""
super().__init__(*widgets, id=id)
self.rows = [
("CMD", "Time", "Cate1", "Cate2"),
("ABC", "ABC", "ABC", "ABC"),
("ABC", "ABC", "ABC", "ABC"),
("ABC", "ABC", "ABC", "ABC"),
("ABC", "ABC", "ABC", "ABC"),
("ABC", "ABC", "ABC", "ABC"),
("ABC", "ABC", "ABC", "ABC"),
("ABC", "ABC", "ABC", "ABC"),
("ABC", "ABC", "ABC", "ABC"),
("ABC", "ABC", "ABC", "ABC"),
("ABC", "ABC", "ABC", "ABC"),
("ABC", "ABC", "ABC", "ABC"),
]

def compose(self) -> ComposeResult:
"""
Compose the time-tagged logs table and label
"""
yield Label("TIME TAGGED CMDS")
yield DataTable()

def on_mount(self) -> None:
"""
Set up columns and rows for the time-tagged logs table
"""
table = self.query_one(DataTable)
table.add_columns(*self.rows[0])
table.add_rows(self.rows[1:])


class LogsPanel(Static):
"""
A static widget for displaying logs from a file
"""

logs = reactive("")

def on_mount(self) -> None:
"""
Set up periodic log refresh for the logs panel
"""
self.logs_refresh = self.set_interval(1 / 60, self.update_logs)

def update_logs(self) -> None:
"""
Read and update logs from the log file
"""
try:
with open("gs/backend/logs.log") as logs:
self.logs = logs.read()
except FileNotFoundError:
print("[red]Logs file not found. Try running interface from root directory (OBC-Firmware)?[/red]")

def watch_logs(self, logs: str) -> None:
"""
Update logs panel with the latest logs
"""
self.update("LOGS\n\n" + self.logs)
try:
self.scroll_to_bottom()
except Exception as e:
print(e)

def scroll_to_bottom(self) -> None:
"""
Auto-scroll to bottom of logs panel
"""
scrollable_container = self.app.query_one("#logs")
if scrollable_container:
scrollable_container.scroll_end()


class CLIWindow(App[None]):
"""
Main Textual application window for the CLI interface
"""

CSS_PATH = "cli.tcss"

def on_mount(self) -> None:
"""
Set the theme when application mounts
"""
self.theme = "dracula"

def compose(self) -> ComposeResult:
"""
Compose the main application layout with all panels and widgets
"""
yield ScrollableContainer(LogsPanel("LOGS"), can_focus=True, id="logs")
yield CliPanel(id="cli")
yield VerticalScroll(
Static("CMDS"),
CmdButton("print_logs"),
CmdButton("send_conn_request"),
CmdButton("start_logging"),
id="cmd-panel",
)
yield TimeTaggedLogs(id="timetagged")


def main() -> None:
"""
Entry point for the CLI application, sets up serial and runs the app
"""
if len(argv) != 2:
print("One argument needed: Com Port")
return

ser = Serial(COM_PORT)
print("Comm port set to: " + str(ser.name))
ser.close()

app = CLIWindow()
app.run()


if __name__ == "__main__":
main()
Loading
Loading