Compare commits
2 Commits
449812f1df
...
3a55cad86f
Author | SHA1 | Date |
---|---|---|
Brian S. Stephan | 3a55cad86f | |
Brian S. Stephan | f6d3ad02e0 |
|
@ -214,11 +214,16 @@ def write_new_config_to_filename(config: Message, filename: str, inject: bool =
|
|||
with open(filename, 'rb') as file:
|
||||
existing_binary = file.read()
|
||||
binary = replace_config_in_binary(bytearray(existing_binary), config_binary)
|
||||
with open(filename, 'wb') as file:
|
||||
file.write(binary)
|
||||
else:
|
||||
binary = serialize_config_with_footer(config)
|
||||
|
||||
with open(filename, 'wb') as file:
|
||||
file.write(binary)
|
||||
with open(filename, 'wb') as file:
|
||||
if filename[-4:] == '.uf2':
|
||||
file.write(convert_binary_to_uf2(pad_config_to_storage_size(binary),
|
||||
start=USER_CONFIG_BINARY_LOCATION))
|
||||
else:
|
||||
file.write(binary)
|
||||
|
||||
|
||||
def write_new_config_to_usb(config: Message, endpoint_out: object, endpoint_in: object):
|
||||
|
|
|
@ -10,7 +10,7 @@ Button {
|
|||
margin: 0 1;
|
||||
}
|
||||
|
||||
EditScreen, MessageScreen {
|
||||
EditScreen, MessageScreen, SaveAsScreen {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ EditScreen Label {
|
|||
visibility: hidden;
|
||||
}
|
||||
|
||||
#edit-dialog {
|
||||
#edit-dialog, #save-as-dialog {
|
||||
padding: 0 1;
|
||||
grid-rows: 1fr 1fr 1fr 1fr;
|
||||
width: 50%;
|
||||
|
|
|
@ -16,7 +16,7 @@ from textual.app import App, ComposeResult
|
|||
from textual.containers import Container, Grid, Horizontal
|
||||
from textual.logging import TextualHandler
|
||||
from textual.screen import ModalScreen
|
||||
from textual.validation import Number
|
||||
from textual.validation import Length, Number
|
||||
from textual.widgets import Button, Footer, Header, Input, Label, Pretty, Select, TextArea, Tree
|
||||
from textual.widgets.tree import TreeNode
|
||||
|
||||
|
@ -125,11 +125,61 @@ class MessageScreen(ModalScreen):
|
|||
self.app.pop_screen()
|
||||
|
||||
|
||||
class SaveAsScreen(ModalScreen):
|
||||
"""Present the option of saving the configuration as a new file."""
|
||||
|
||||
def __init__(self, config, *args, **kwargs):
|
||||
"""Initialize a filename argument to be populated."""
|
||||
self.config = config
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Build the pop-up window prompting for the new filename to save the configuration as."""
|
||||
self.filename_field = Input(value=None, id='field-input', validators=[Length(minimum=1)])
|
||||
yield Grid(
|
||||
Container(Label("Filename (.uf2 or .bin) to write to:", id='field-name'), id='field-name-container'),
|
||||
Container(self.filename_field, id='input-field-container'),
|
||||
Container(Pretty('', id='input-errors', classes='hidden'), id='error-container'),
|
||||
Horizontal(Container(Button("Cancel", id='cancel-button'), id='cancel-button-container'),
|
||||
Container(Button("Confirm", id='confirm-button'), id='confirm-button-container'),
|
||||
id='button-container'),
|
||||
id='save-as-dialog',
|
||||
)
|
||||
|
||||
@on(Input.Changed)
|
||||
def show_invalid_reasons(self, event: Input.Changed) -> None:
|
||||
"""Update the UI to show why validation failed."""
|
||||
if event.validation_result:
|
||||
error_field = self.query_one(Pretty)
|
||||
save_button = self.query_one('#confirm-button', Button)
|
||||
if not event.validation_result.is_valid:
|
||||
error_field.update(event.validation_result.failure_descriptions)
|
||||
error_field.classes = ''
|
||||
save_button.disabled = True
|
||||
else:
|
||||
error_field.update('')
|
||||
error_field.classes = 'hidden'
|
||||
save_button.disabled = False
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Process the button actions."""
|
||||
if event.button.id == 'confirm-button':
|
||||
logger.debug("calling _save")
|
||||
self._save()
|
||||
self.app.pop_screen()
|
||||
|
||||
def _save(self):
|
||||
"""Save the configuration to the specified file."""
|
||||
write_new_config_to_filename(self.config, self.filename_field.value, inject=False)
|
||||
self.notify(f"Saved to {self.filename_field.value}.", title="Configuration Saved")
|
||||
|
||||
|
||||
class ConfigEditor(App):
|
||||
"""Display the GP2040-CE configuration as a tree."""
|
||||
|
||||
BINDINGS = [
|
||||
('a', 'add_node', "Add Node"),
|
||||
('a', 'save_as', "Save As..."),
|
||||
('n', 'add_node', "Add Node"),
|
||||
('s', 'save', "Save Config"),
|
||||
('q', 'quit', "Quit"),
|
||||
('?', 'about', "About"),
|
||||
|
@ -238,6 +288,10 @@ class ConfigEditor(App):
|
|||
self.notify(f"Saved to {self.config_filename}.",
|
||||
title="Configuration Saved")
|
||||
|
||||
def action_save_as(self) -> None:
|
||||
"""Present a new dialog to save the configuration as a new standalone file."""
|
||||
self.push_screen(SaveAsScreen(self.config))
|
||||
|
||||
def action_quit(self) -> None:
|
||||
"""Quit the application."""
|
||||
self.exit()
|
||||
|
@ -409,8 +463,9 @@ def edit_config():
|
|||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('--usb', action='store_true', help="retrieve the config from a RP2040 board connected over USB "
|
||||
"and in BOOTSEL mode")
|
||||
group.add_argument('--filename', help=".bin file of a GP2040-CE board's config + footer or entire storage section, "
|
||||
"or of a GP2040-CE's whole board dump if --whole-board is specified")
|
||||
group.add_argument('--filename', help=".bin of a GP2040-CE's whole board dump if --whole-board is specified, or a"
|
||||
".bin file of a GP2040-CE board's config + footer or entire storage section; "
|
||||
"if creating a new config, it can also be written in .uf2 format")
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
if args.usb:
|
||||
|
|
|
@ -15,7 +15,7 @@ from gp2040ce_bintools.builder import (FirmwareLengthError, combine_firmware_and
|
|||
concatenate_firmware_and_storage_files, get_gp2040ce_from_usb,
|
||||
pad_binary_up_to_board_config, pad_binary_up_to_user_config,
|
||||
replace_config_in_binary, write_new_config_to_filename, write_new_config_to_usb)
|
||||
from gp2040ce_bintools.storage import (get_board_storage_section, get_config, get_config_footer,
|
||||
from gp2040ce_bintools.storage import (STORAGE_SIZE, get_board_storage_section, get_config, get_config_footer,
|
||||
get_user_storage_section, serialize_config_with_footer)
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
@ -272,6 +272,23 @@ def test_write_new_config_to_config_bin(firmware_binary, tmp_path):
|
|||
assert len(config_dump) == config_size + 12
|
||||
|
||||
|
||||
@with_pb2s
|
||||
def test_write_new_config_to_config_uf2(firmware_binary, tmp_path):
|
||||
"""Test that the config can be written to a file."""
|
||||
tmp_file = os.path.join(tmp_path, 'config.uf2')
|
||||
config_pb2 = get_config_pb2()
|
||||
config = config_pb2.Config()
|
||||
config.boardVersion = 'v0.7.5-COOL'
|
||||
write_new_config_to_filename(config, tmp_file)
|
||||
|
||||
# read new file
|
||||
with open(tmp_file, 'rb') as file:
|
||||
config_dump = file.read()
|
||||
# the current implementation of UF2 writing does it in 256 blocks, so each 256 byte block of
|
||||
# binary is 512 bytes in the UF2
|
||||
assert len(config_dump) == STORAGE_SIZE * 2
|
||||
|
||||
|
||||
@with_pb2s
|
||||
def test_write_new_config_to_usb(config_binary):
|
||||
"""Test that the config can be written to USB at the proper alignment."""
|
||||
|
|
|
@ -183,7 +183,7 @@ async def test_add_node_to_repeated():
|
|||
tree.root.expand_all()
|
||||
await pilot.wait_for_scheduled_animations()
|
||||
tree.select_node(altpinmappings_node)
|
||||
await pilot.press('a')
|
||||
await pilot.press('n')
|
||||
newpinmappings_node = altpinmappings_node.children[0]
|
||||
newpinmappings_node.expand()
|
||||
await pilot.wait_for_scheduled_animations()
|
||||
|
@ -216,3 +216,28 @@ async def test_save(config_binary, tmp_path):
|
|||
|
||||
config = get_config_from_file(new_filename)
|
||||
assert config.boardVersion == 'v0.7.5-bss-wuz-here'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@with_pb2s
|
||||
async def test_save_as(config_binary, tmp_path):
|
||||
"""Test that we can save to a new file."""
|
||||
filename = os.path.join(tmp_path, 'config-original.bin')
|
||||
with open(filename, 'wb') as file:
|
||||
file.write(config_binary)
|
||||
original_config = get_config(config_binary)
|
||||
|
||||
app = ConfigEditor(config_filename=filename)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('a')
|
||||
await pilot.wait_for_scheduled_animations()
|
||||
await pilot.click('Input#field-input')
|
||||
await pilot.wait_for_scheduled_animations()
|
||||
await pilot.press('/', 't', 'm', 'p', '/', 'g', 'p', 't', 'e', 's', 't')
|
||||
await pilot.wait_for_scheduled_animations()
|
||||
await pilot.click('Button#confirm-button')
|
||||
|
||||
with open('/tmp/gptest', 'rb') as new_file:
|
||||
test_config_binary = new_file.read()
|
||||
test_config = get_config(test_config_binary)
|
||||
assert original_config == test_config
|
||||
|
|
Loading…
Reference in New Issue