From fb8570d6748ddece588d122b5bddba983f226a1a Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Thu, 8 Feb 2024 16:47:12 +0000 Subject: [PATCH 01/11] Candidate change to make the removable media system more flexible (e.g. for use on the console) --- config.rc.in | 27 +++++-------- config.rc.travis | 14 ++++--- tests/test_utility.py | 8 ++-- ui/book/book_file.py | 4 +- ui/book/state.py | 2 +- ui/cleaning_and_testing.py | 2 +- ui/config_loader.py | 22 +++++----- ui/initial_state.py | 82 ++++++++++++++++++++------------------ ui/library/explorer.py | 21 +++++----- ui/library/state.py | 5 +-- ui/main.py | 40 ++++++++----------- ui/manual.py | 2 +- 12 files changed, 113 insertions(+), 116 deletions(-) diff --git a/config.rc.in b/config.rc.in index 87dc2dc8..2fcfd9b6 100644 --- a/config.rc.in +++ b/config.rc.in @@ -1,22 +1,17 @@ [files] # relative path to where log is kept -log_file = canute.log -# book directory -# where usb sticks and sd-cards will be automounted -media_dir = /media -# the exact sub mount-point directory name of the sd-card -sd_card_dir = sd-card +log_file = 'canute.log' -# Additional books made available on the USB ports will (for now at -# least) be visible in the library. The current state will be written -# to all mount points that are active, however the state file on the -# SD card will take precedence, followed by lib_1 and finally lib_2. -# These behaviours may change at any time so shouldn't be relied upon. -# -# These paths are relative to media_dir. -# These config values are optional. -additional_lib_1 = front-usb -additional_lib_2 = back-usb +# Book Directories +# Additional books made available on the USB ports will be made visible +# in the library. The current state will be written to all mount points +# that are active. The first state file found (based on the order in the +# list) will take precedence if they differ. +library = [ + { name = 'SD', path = '/media/sd-card', mountpoint = true }, + { name = 'USB1', path = '/media/front-usb', mountpoint = true }, + { name = 'USB2', path = '/media/back-usb', mountpoint = true } +] [comms] # serial timeout in seconds diff --git a/config.rc.travis b/config.rc.travis index 01e1419c..b20cc57f 100644 --- a/config.rc.travis +++ b/config.rc.travis @@ -1,11 +1,15 @@ [files] # relative path to where log is kept log_file = canute.log -# book directory -# where usb sticks and sd-cards will be automounted -media_dir = ~/canute-media -# the exact sub mount-point directory name of the sd-card -sd_card_dir = sd-card + +# Book Directories +# Additional books made available on the USB ports will be made visible +# in the library. The current state will be written to all mount points +# that are active. The first state file found (based on the order in the +# list) will take precedence if they differ. +library = [ + { name = 'SD', path = '~/canute-media/sd-card' } +] [comms] # serial timeout in seconds diff --git a/tests/test_utility.py b/tests/test_utility.py index 004f9a50..cc182af8 100644 --- a/tests/test_utility.py +++ b/tests/test_utility.py @@ -4,14 +4,14 @@ from ui.library.explorer import Library dir_path = os.path.dirname(os.path.realpath(__file__)) -test_books_dir = [('test-books', 'test dir')] +test_books_dir = [{ 'path': os.path.join(dir_path, 'test-books'), 'name': 'test dir' }] class TestUtility(unittest.TestCase): def test_find_files(self): - library = Library(dir_path, test_books_dir, ('brf',)) + library = Library(test_books_dir, ('brf',)) self.assertEqual(len(library.book_files()), 2) - library = Library(dir_path, test_books_dir, ('pef',)) + library = Library(test_books_dir, ('pef',)) self.assertEqual(len(library.book_files()), 1) - library = Library(dir_path, test_books_dir, ('brf', 'pef')) + library = Library(test_books_dir, ('brf', 'pef')) self.assertEqual(len(library.book_files()), 3) diff --git a/ui/book/book_file.py b/ui/book/book_file.py index a7ef0d1e..42b66cd7 100644 --- a/ui/book/book_file.py +++ b/ui/book/book_file.py @@ -70,5 +70,5 @@ def to_file(self): ['bookmarks', list(bms)] ]) - def relpath(self, media_dir): - return os.path.relpath(self.filename, start=media_dir) + def relpath(self): + return self.filename diff --git a/ui/book/state.py b/ui/book/state.py index 8db18184..4a7ff6db 100644 --- a/ui/book/state.py +++ b/ui/book/state.py @@ -75,7 +75,7 @@ def insert_bookmark(self): self.books[self.current_book] = book self.root.save_state(book) - def to_file(self, media_dir): + def to_file(self): current_book = self.current_book return { 'current_book': current_book, diff --git a/ui/cleaning_and_testing.py b/ui/cleaning_and_testing.py index 6d3b3f39..67485e0e 100644 --- a/ui/cleaning_and_testing.py +++ b/ui/cleaning_and_testing.py @@ -28,5 +28,5 @@ def create(): return CleaningAndTesting(cleaning_filename, NUM_COLS, NUM_ROWS, pages=pages, load_state=LoadState.DONE) - def relpath(self, media_dir): + def relpath(self): return self.filename diff --git a/ui/config_loader.py b/ui/config_loader.py index ed82be13..4b18564c 100644 --- a/ui/config_loader.py +++ b/ui/config_loader.py @@ -1,18 +1,18 @@ import os.path -from configparser import ConfigParser -config_file = 'config.rc' +import toml +config_file = 'config.rc' def load(config_file=config_file): - config = ConfigParser() - c = config.read(config_file) - if len(c) == 0: + if not os.path.exists(config_file): raise ValueError('Please provide a config.rc') - media_dir = config.get('files', 'media_dir') - config.set('files', 'media_dir', os.path.expanduser(media_dir)) - if not config.has_section('comms'): - config.add_section('comms') - if not config.has_option('comms', 'timeout'): - config.set('comms', 'timeout', 60) + + config = toml.load(config_file) + + # expand any ~ home dirs in library paths + library = config.get('files', {}).get('library', []) + for entry in library: + entry['path'] = os.path.expanduser(entry['path']) + return config diff --git a/ui/initial_state.py b/ui/initial_state.py index 6c04634b..4010e636 100644 --- a/ui/initial_state.py +++ b/ui/initial_state.py @@ -28,44 +28,53 @@ def to_state_file(book_path): def configured_source_dirs(): config = config_loader.load() - state_sources = [('sd_card_dir', 'SD')] - if config.has_option('files', 'additional_lib_1'): - state_sources.append(('additional_lib_1', 'USB1')) - if config.has_option('files', 'additional_lib_2'): - state_sources.append(('additional_lib_2', 'USB2')) - return [(config.get('files', source), name) for source, name in state_sources] - - -def mounted_source_paths(media_dir): - for source_dir, name in configured_source_dirs(): - source_path = os.path.join(media_dir, source_dir) - if os.path.ismount(source_path) or 'TRAVIS' in os.environ: + return config.get('files', {}).get('library', []) + +def mounted_source_paths(): + for source_dir in configured_source_dirs(): + source_path = source_dir.get('path') + if not source_dir.get('mountpoint', False) or os.path.ismount(source_path): yield source_path -def swap_library(current_book): +def swap_library(current_book, books): + """ + The current_book path includes a mount-point path (if on removable media) + so try to cope with user accidentally swapping slots + """ config = config_loader.load() - if config.has_option('files', 'additional_lib_1') and \ - config.has_option('files', 'additional_lib_2'): - lib1 = config.get('files', 'additional_lib_1') - lib2 = config.get('files', 'additional_lib_2') - if current_book.startswith(lib1): - return lib2 + current_book[len(lib1):] - elif current_book.startswith(lib2): - return lib1 + current_book[len(lib2):] + library = config.get('files', {}).get('library', []) + + for lib in library: + path = lib['path'] + if current_book.startswith(path): + rel_book = current_book[len(path):] + break + # provide backwards compatibility, where the media_dir isn't included + path = os.path.relpath(path, '/media') + if current_book.startswith(path): + rel_book = current_book[len(path):] + break + + for lib in library: + path = lib['path'] + book = os.path.join(path, rel_book) + if book in books: + return book + return current_book -async def read_user_state(media_dir, state): +async def read_user_state(state): global manual current_book = manual_filename current_language = None # walk the available filesystems for directories and braille files - library = Library(media_dir, configured_source_dirs(), ('brf', 'pef')) + library = Library(configured_source_dirs(), ('brf', 'pef')) book_files = library.book_files() - source_paths = mounted_source_paths(media_dir) + source_paths = mounted_source_paths() for source_path in source_paths: main_toml = os.path.join(source_path, USER_STATE_FILE) if os.path.exists(main_toml): @@ -86,7 +95,7 @@ async def read_user_state(media_dir, state): install(current_language) manual = Manual.create() - manual_toml = os.path.join(media_dir, to_state_file(manual_filename)) + manual_toml = to_state_file(manual_filename) if os.path.exists(manual_toml): try: t = toml.load(manual_toml) @@ -103,7 +112,7 @@ async def read_user_state(media_dir, state): books = OrderedDict({manual_filename: manual}) for book_file in book_files: - filename = os.path.join(media_dir, book_file) + filename = book_file toml_file = to_state_file(filename) book = BookFile(filename=filename, width=width, height=height) if os.path.exists(toml_file): @@ -129,22 +138,21 @@ async def read_user_state(media_dir, state): if current_book not in books: # let's check that they're not just using a different USB port log.info('current book not in original library {}'.format(current_book)) - current_book = swap_library(current_book) + current_book = swap_library(current_book, books) if current_book not in books: log.warn('current book not found {}, ignoring'.format(current_book)) current_book = manual_filename state.app.user.books = books - state.app.library.media_dir = media_dir state.app.library.dirs = library.dirs state.app.user.load(current_book, current_language) -async def read(media_dir, state): - await read_user_state(media_dir, state) +async def read(state): + await read_user_state(state) -async def write(media_dir, queue): +async def write(queue): log.info('save file worker started') while True: (filename, data) = await queue.get() @@ -152,7 +160,7 @@ async def write(media_dir, queue): if filename is None: # main user state file - source_paths = mounted_source_paths(media_dir) + source_paths = mounted_source_paths() for source_path in source_paths: path = os.path.join(source_path, USER_STATE_FILE) log.debug('writing user state file save to {path}') @@ -161,13 +169,11 @@ async def write(media_dir, queue): else: # book state file - if filename == manual_filename: - path = os.path.join(media_dir, path) - else: + if filename != manual_filename: path = to_state_file(filename) - log.debug(f'writing {filename} state file save to {path}') - async with aiofiles.open(path, 'w') as f: - await f.write(s) + log.debug(f'writing {filename} state file save to {path}') + async with aiofiles.open(path, 'w') as f: + await f.write(s) log.debug('state file save complete') queue.task_done() diff --git a/ui/library/explorer.py b/ui/library/explorer.py index 98437491..a9e72460 100644 --- a/ui/library/explorer.py +++ b/ui/library/explorer.py @@ -82,19 +82,18 @@ class Library: DIRS_PAGE_SIZE = 8 FILES_PAGE_SIZE = 7 - def __init__(self, media_dir, source_dirs, file_exts): - self.media_dir = os.path.abspath(media_dir) + def __init__(self, source_dirs, file_exts): self.file_exts = file_exts self.dirs = [] - for source_dir, name in source_dirs: - # if os.path.ismount(os.path.join(self.media_dir, source_dir)): - # not needed as unmounted devices will be empty and so pruned - root = Directory(source_dir, display=name) - self.walk(root) - self.prune(root) - if len(root.dirs) > 0 or root.files_count > 0: - self.flatten(root) + for source_dir in source_dirs: + source_path = source_dir.get('path') + if not source_dir.get('mountpoint', False) or os.path.ismount(source_path): + root = Directory(source_path, display=source_dir.get('name')) + self.walk(root) + self.prune(root) + if len(root.dirs) > 0 or root.files_count > 0: + self.flatten(root) self.dir_count = len(self.dirs) self.files_dir_index = None @@ -107,7 +106,7 @@ def walk(self, root): """ dirs = [] files = [] - for entry in os.scandir(os.path.join(self.media_dir, root.relpath)): + for entry in os.scandir(root.relpath): if entry.is_dir(follow_symlinks=False) and \ entry.name[0] != '.' and \ entry.name != 'RECYCLE' and \ diff --git a/ui/library/state.py b/ui/library/state.py index f71783da..936074c9 100644 --- a/ui/library/state.py +++ b/ui/library/state.py @@ -14,7 +14,6 @@ def __init__(self, root: 'state.RootState'): self.root = root self.page = 0 - self.media_dir = '' self.dirs = [] # index of directory that is currently expanded, if any self.files_dir_index = None @@ -161,14 +160,14 @@ def show_files_dir(self, book): break def open_book(self, book): - self.root.app.user.current_book = book.relpath(self.media_dir) + self.root.app.user.current_book = book.relpath() self.root.app.location = 'book' self.root.app.home_menu_visible = False self.root.refresh_display() self.root.save_state() def add_or_replace(self, book): - relpath = os.path.relpath(book.filename, start=self.media_dir) + relpath = book.filename self.root.app.user.books[relpath] = book def set_book_loading(self, book): diff --git a/ui/main.py b/ui/main.py index 269fc1dd..32a66f2f 100644 --- a/ui/main.py +++ b/ui/main.py @@ -25,7 +25,7 @@ def main(): args = argparser.parser.parse_args() config = config_loader.load() log = setup_logs(config, args.loglevel) - timeout = config.get('comms', 'timeout') + timeout = config.get('comm', {}).get('timeout', 60) if args.fuzz_duration: log.info('running fuzz test') @@ -169,16 +169,14 @@ async def run_async(driver, config, loop): queue, save_worker = handle_save_events(config, state) load_worker = handle_display_events(config, state) - media_dir = config.get('files', 'media_dir') - log.info(f'reading initial state from {media_dir}') - await initial_state.read(media_dir, state) + await initial_state.read(state) log.info('created store') state.refresh_display() try: while 1: - if (await handle_hardware(driver, state, media_dir)): + if (await handle_hardware(driver, state)): media_handler.cancel() try: await media_handler @@ -212,9 +210,8 @@ async def run_async(driver, config, loop): def handle_save_events(config, state): - media_dir = config.get('files', 'media_dir') queue = asyncio.Queue() - worker = asyncio.create_task(initial_state.write(media_dir, queue)) + worker = asyncio.create_task(initial_state.write(queue)) def on_save_state(book=None): # queue a snapshot of the state we want to save, theoretically we @@ -223,7 +220,7 @@ def on_save_state(book=None): # that much faster than a filesystem write if book is None: log.debug('queuing user state file save') - queue.put_nowait((None, state.app.user.to_file(media_dir))) + queue.put_nowait((None, state.app.user.to_file())) else: log.debug(f'queuing {book.filename} state file save') queue.put_nowait((book.filename, book.to_file())) @@ -242,7 +239,6 @@ def on_backup_log(): def handle_display_events(config, state): from .book.handlers import load_book, load_book_worker - media_dir = config.get('files', 'media_dir') queue = asyncio.Queue() worker = asyncio.create_task(load_book_worker(state, queue)) @@ -263,7 +259,7 @@ async def load_render_cache(): # now queue up indexing tasks to cache for book in later: - queue.put_nowait(book.relpath(media_dir)) + queue.put_nowait(book.relpath()) await display.render_to_buffer(state) @@ -283,7 +279,7 @@ async def change_files(config, state): state.app.backup_log('done') -async def handle_hardware(driver, state, media_dir): +async def handle_hardware(driver, state): if not driver.is_ok(): log.debug('shutting down due to GUI closed') state.app.load_books('cancel') @@ -308,18 +304,16 @@ async def handle_hardware(driver, state, media_dir): def backup_log(config): - sd_card_dir = config.get('files', 'sd_card_dir') - media_dir = config.get('files', 'media_dir') - sd_card_dir = os.path.join(media_dir, sd_card_dir) - log_file = config.get('files', 'log_file') - # make a filename based on the date - backup_file = os.path.join(sd_card_dir, time.strftime('%Y%m%d%M_log.txt')) - log.debug('backing up log to USB stick: {}'.format(backup_file)) - try: - import shutil - shutil.copyfile(log_file, backup_file) - except IOError as e: - log.warning("couldn't backup log file: {}".format(e)) + mounted_dirs = initial_state.mounted_source_paths() + if len(mounted_dirs) > 0: + # make a filename based on the date and save to first path + backup_file = os.path.join(mounted_dirs[0], time.strftime('%Y%m%d%M_log.txt')) + log.debug('backing up log to USB stick: {}'.format(backup_file)) + try: + import shutil + shutil.copyfile(log_file, backup_file) + except IOError as e: + log.warning("couldn't backup log file: {}".format(e)) async def log_markers(): diff --git a/ui/manual.py b/ui/manual.py index 926dcbb1..2c5b1d18 100644 --- a/ui/manual.py +++ b/ui/manual.py @@ -46,5 +46,5 @@ def create(): for page in pages) return Manual(manual_filename, 40, 9, pages=pages, load_state=LoadState.DONE) - def relpath(self, media_dir): + def relpath(self): return self.filename From eddd654e338ff973610ee2301f755dcbdf9102a6 Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Thu, 8 Feb 2024 16:48:48 +0000 Subject: [PATCH 02/11] Make the button presses a bit less sensitive to noise and make row actuation logging optional --- config.rc.in | 12 ++++++++++++ ui/driver/driver_pi.py | 22 ++++++++++++++-------- ui/main.py | 23 +++++++++++++++-------- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/config.rc.in b/config.rc.in index 2fcfd9b6..2e288f75 100644 --- a/config.rc.in +++ b/config.rc.in @@ -16,3 +16,15 @@ library = [ [comms] # serial timeout in seconds timeout = 1000 + +[hardware] +# count row actuations (motor wear) when running on Canute 360 +log_duty = true + +# minumum number of samples of button down to count as a press +button_debounce = 1 + +# shutdown mode +shutdown_on_exit = true + +# versions \ No newline at end of file diff --git a/ui/driver/driver_pi.py b/ui/driver/driver_pi.py index c957c885..29c9a706 100644 --- a/ui/driver/driver_pi.py +++ b/ui/driver/driver_pi.py @@ -37,8 +37,9 @@ class Pi(Driver): :param port: the serial port the display is plugged into """ - def __init__(self, port=None, timeout=60): + def __init__(self, port=None, timeout=60, button_threshold=1): self.timeout = timeout + self.button_threshold = button_threshold # get serial connection if port is None: ports = serial.tools.list_ports.comports() @@ -53,7 +54,7 @@ def __init__(self, port=None, timeout=60): else: self.port = None - self.previous_buttons = tuple() + self.previous_buttons = dict() self.row_actuations = [0] * N_ROWS @@ -121,15 +122,20 @@ def get_buttons(self): buttons = {} self.send_data(comms.CMD_SEND_BUTTONS) read_buttons = self.get_data(comms.CMD_SEND_BUTTONS) - down = list(self.previous_buttons) + down = self.previous_buttons for i, n in enumerate(reversed(list('{:0>14b}'.format(read_buttons)))): name = mapping[str(i)] - if n == '1' and (name not in self.previous_buttons): - buttons[name] = 'down' - down.append(name) + if n == '1': + if name in self.previous_buttons: + self.previous_buttons[name] += 1 + else: + self.previous_buttons[name] = 1 + if self.previous_buttons[name] == self.button_threshold: + buttons[name] = 'down' elif n == '0' and (name in self.previous_buttons): - buttons[name] = 'up' - down.remove(name) + if self.previous_buttons[name] > self.button_threshold: + buttons[name] = 'up' + del self.previous_buttons[name] self.previous_buttons = tuple(down) diff --git a/ui/main.py b/ui/main.py index 32a66f2f..d9cad10e 100644 --- a/ui/main.py +++ b/ui/main.py @@ -82,7 +82,8 @@ def main(): log.info('running with real hardware on port %s, timeout %s' % (args.tty, timeout)) try: - with Pi(port=args.tty, timeout=timeout) as driver: + debounce = config.get('hardware', {}).get('button_debounce', 1) + with Pi(port=args.tty, timeout=timeout, button_threshold=debounce) as driver: run(driver, config) except RuntimeError as err: if err.args[0] == 'readFrame timeout': @@ -161,7 +162,11 @@ async def run_async(driver, config, loop): await asyncio.sleep(0.01) log.debug('motion complete') media_handler = asyncio.ensure_future(handle_media_changes()) - duty_logger = asyncio.ensure_future(driver.track_duty()) + + if config.get('hardware', {}).get('log_duty', False): + duty_logger = asyncio.ensure_future(driver.track_duty()) + else: + duty_logger = None width, height = driver.get_dimensions() state.app.set_dimensions((width, height)) @@ -182,11 +187,12 @@ async def run_async(driver, config, loop): await media_handler except asyncio.CancelledError: pass - duty_logger.cancel() - try: - await duty_logger - except asyncio.CancelledError: - pass + if duty_logger is not None: + duty_logger.cancel() + try: + await duty_logger + except asyncio.CancelledError: + pass break @@ -202,7 +208,8 @@ async def run_async(driver, config, loop): await asyncio.sleep(0) except asyncio.CancelledError: media_handler.cancel() - duty_logger.cancel() + if duty_logger is not None: + duty_logger.cancel() await queue.join() save_worker.cancel() load_worker.cancel() From e40565f03a057c68f64f57991d86be3fe78aa794 Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Thu, 8 Feb 2024 16:49:17 +0000 Subject: [PATCH 03/11] Handle not having release and serial numbers available --- ui/system_menu/system_menu.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ui/system_menu/system_menu.py b/ui/system_menu/system_menu.py index d58b23fa..c7798b86 100644 --- a/ui/system_menu/system_menu.py +++ b/ui/system_menu/system_menu.py @@ -43,10 +43,14 @@ def brailleify(rel): # This exists on a Pi and reading it yields a useful board identifier. # But existence will do for right now. if os.path.exists('/sys/firmware/devicetree/base/model'): - with open('/etc/canute_release') as x: - release = brailleify(x.read().strip()) - with open('/etc/canute_serial') as x: - serial = brailleify(x.read().strip()) + if os.path.exists('/etc/canute_release'): + with open('/etc/canute_release') as x: + release = _('release:') + ' ' + brailleify(x.read().strip()) + with open('/etc/canute_serial') as x: + serial = _('serial:') + ' ' + brailleify(x.read().strip()) + else: + release = brailleify(_('run in standalone mode')) + serial = release else: # Assume we're being emulated. release = brailleify(_('emulated')) From b47a484362f831b61df01cb062f204dda3d749ab Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Thu, 8 Feb 2024 17:04:49 +0000 Subject: [PATCH 04/11] Fix tests --- tests/test_book_file.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_book_file.py b/tests/test_book_file.py index 074d2881..ea683774 100644 --- a/tests/test_book_file.py +++ b/tests/test_book_file.py @@ -1,6 +1,6 @@ import unittest from ui.book.book_file import BookFile -from ui.book.handlers import _read_pages, get_page_data +from ui.book.handlers import _read_pages2, get_page_data from .util import async_test @@ -12,7 +12,7 @@ async def setUpClass(self): self.filename = ('books/A_balance_between_technology_and_Braille_Addin' + 'g_Value_and_Creating_a_Love_of_Reading.BRF') book = BookFile(self.filename, 40, 9) - self.book = await _read_pages(book) + self.book = await _read_pages2(book) def test_filename(self): self.assertEqual(self.book.filename, self.filename) @@ -21,7 +21,7 @@ def test_title(self): self.assertIsNotNone(self.book.title) def test_has_len(self): - self.assertGreater(len(self.book.pages), 0) + self.assertGreater(self.book.num_pages, 0) @async_test async def test_get_line(self): @@ -45,7 +45,7 @@ class TestBookFilePef(unittest.TestCase): async def setUpClass(self): self.filename = "books/g2 AESOP'S FABLES.pef" book = BookFile(self.filename, 40, 9) - self.book = await _read_pages(book) + self.book = await _read_pages2(book) def test_filename(self): self.assertEqual(self.book.filename, self.filename) @@ -54,7 +54,7 @@ def test_title(self): self.assertIsNotNone(self.book.title) def test_has_len(self): - self.assertGreater(len(self.book.pages), 0) + self.assertGreater(self.book.num_pages, 0) @async_test async def test_get_line(self): From 872a9d996132d03062d416dbc8af083e4a5a3eff Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Fri, 9 Feb 2024 09:49:17 +0000 Subject: [PATCH 05/11] Revert "Candidate change to make the removable media system more flexible (e.g. for use on the console)" This reverts commit fb8570d6748ddece588d122b5bddba983f226a1a. --- config.rc.in | 27 ++++++++----- config.rc.travis | 14 +++---- tests/test_utility.py | 8 ++-- ui/book/book_file.py | 4 +- ui/book/state.py | 2 +- ui/cleaning_and_testing.py | 2 +- ui/config_loader.py | 22 +++++----- ui/initial_state.py | 82 ++++++++++++++++++-------------------- ui/library/explorer.py | 21 +++++----- ui/library/state.py | 5 ++- ui/main.py | 40 +++++++++++-------- ui/manual.py | 2 +- 12 files changed, 116 insertions(+), 113 deletions(-) diff --git a/config.rc.in b/config.rc.in index 2e288f75..00655e46 100644 --- a/config.rc.in +++ b/config.rc.in @@ -1,17 +1,22 @@ [files] # relative path to where log is kept -log_file = 'canute.log' +log_file = canute.log +# book directory +# where usb sticks and sd-cards will be automounted +media_dir = /media +# the exact sub mount-point directory name of the sd-card +sd_card_dir = sd-card -# Book Directories -# Additional books made available on the USB ports will be made visible -# in the library. The current state will be written to all mount points -# that are active. The first state file found (based on the order in the -# list) will take precedence if they differ. -library = [ - { name = 'SD', path = '/media/sd-card', mountpoint = true }, - { name = 'USB1', path = '/media/front-usb', mountpoint = true }, - { name = 'USB2', path = '/media/back-usb', mountpoint = true } -] +# Additional books made available on the USB ports will (for now at +# least) be visible in the library. The current state will be written +# to all mount points that are active, however the state file on the +# SD card will take precedence, followed by lib_1 and finally lib_2. +# These behaviours may change at any time so shouldn't be relied upon. +# +# These paths are relative to media_dir. +# These config values are optional. +additional_lib_1 = front-usb +additional_lib_2 = back-usb [comms] # serial timeout in seconds diff --git a/config.rc.travis b/config.rc.travis index b20cc57f..01e1419c 100644 --- a/config.rc.travis +++ b/config.rc.travis @@ -1,15 +1,11 @@ [files] # relative path to where log is kept log_file = canute.log - -# Book Directories -# Additional books made available on the USB ports will be made visible -# in the library. The current state will be written to all mount points -# that are active. The first state file found (based on the order in the -# list) will take precedence if they differ. -library = [ - { name = 'SD', path = '~/canute-media/sd-card' } -] +# book directory +# where usb sticks and sd-cards will be automounted +media_dir = ~/canute-media +# the exact sub mount-point directory name of the sd-card +sd_card_dir = sd-card [comms] # serial timeout in seconds diff --git a/tests/test_utility.py b/tests/test_utility.py index cc182af8..004f9a50 100644 --- a/tests/test_utility.py +++ b/tests/test_utility.py @@ -4,14 +4,14 @@ from ui.library.explorer import Library dir_path = os.path.dirname(os.path.realpath(__file__)) -test_books_dir = [{ 'path': os.path.join(dir_path, 'test-books'), 'name': 'test dir' }] +test_books_dir = [('test-books', 'test dir')] class TestUtility(unittest.TestCase): def test_find_files(self): - library = Library(test_books_dir, ('brf',)) + library = Library(dir_path, test_books_dir, ('brf',)) self.assertEqual(len(library.book_files()), 2) - library = Library(test_books_dir, ('pef',)) + library = Library(dir_path, test_books_dir, ('pef',)) self.assertEqual(len(library.book_files()), 1) - library = Library(test_books_dir, ('brf', 'pef')) + library = Library(dir_path, test_books_dir, ('brf', 'pef')) self.assertEqual(len(library.book_files()), 3) diff --git a/ui/book/book_file.py b/ui/book/book_file.py index 42b66cd7..a7ef0d1e 100644 --- a/ui/book/book_file.py +++ b/ui/book/book_file.py @@ -70,5 +70,5 @@ def to_file(self): ['bookmarks', list(bms)] ]) - def relpath(self): - return self.filename + def relpath(self, media_dir): + return os.path.relpath(self.filename, start=media_dir) diff --git a/ui/book/state.py b/ui/book/state.py index 4a7ff6db..8db18184 100644 --- a/ui/book/state.py +++ b/ui/book/state.py @@ -75,7 +75,7 @@ def insert_bookmark(self): self.books[self.current_book] = book self.root.save_state(book) - def to_file(self): + def to_file(self, media_dir): current_book = self.current_book return { 'current_book': current_book, diff --git a/ui/cleaning_and_testing.py b/ui/cleaning_and_testing.py index 67485e0e..6d3b3f39 100644 --- a/ui/cleaning_and_testing.py +++ b/ui/cleaning_and_testing.py @@ -28,5 +28,5 @@ def create(): return CleaningAndTesting(cleaning_filename, NUM_COLS, NUM_ROWS, pages=pages, load_state=LoadState.DONE) - def relpath(self): + def relpath(self, media_dir): return self.filename diff --git a/ui/config_loader.py b/ui/config_loader.py index 4b18564c..ed82be13 100644 --- a/ui/config_loader.py +++ b/ui/config_loader.py @@ -1,18 +1,18 @@ import os.path - -import toml +from configparser import ConfigParser config_file = 'config.rc' + def load(config_file=config_file): - if not os.path.exists(config_file): + config = ConfigParser() + c = config.read(config_file) + if len(c) == 0: raise ValueError('Please provide a config.rc') - - config = toml.load(config_file) - - # expand any ~ home dirs in library paths - library = config.get('files', {}).get('library', []) - for entry in library: - entry['path'] = os.path.expanduser(entry['path']) - + media_dir = config.get('files', 'media_dir') + config.set('files', 'media_dir', os.path.expanduser(media_dir)) + if not config.has_section('comms'): + config.add_section('comms') + if not config.has_option('comms', 'timeout'): + config.set('comms', 'timeout', 60) return config diff --git a/ui/initial_state.py b/ui/initial_state.py index 4010e636..6c04634b 100644 --- a/ui/initial_state.py +++ b/ui/initial_state.py @@ -28,53 +28,44 @@ def to_state_file(book_path): def configured_source_dirs(): config = config_loader.load() - return config.get('files', {}).get('library', []) - -def mounted_source_paths(): - for source_dir in configured_source_dirs(): - source_path = source_dir.get('path') - if not source_dir.get('mountpoint', False) or os.path.ismount(source_path): + state_sources = [('sd_card_dir', 'SD')] + if config.has_option('files', 'additional_lib_1'): + state_sources.append(('additional_lib_1', 'USB1')) + if config.has_option('files', 'additional_lib_2'): + state_sources.append(('additional_lib_2', 'USB2')) + return [(config.get('files', source), name) for source, name in state_sources] + + +def mounted_source_paths(media_dir): + for source_dir, name in configured_source_dirs(): + source_path = os.path.join(media_dir, source_dir) + if os.path.ismount(source_path) or 'TRAVIS' in os.environ: yield source_path -def swap_library(current_book, books): - """ - The current_book path includes a mount-point path (if on removable media) - so try to cope with user accidentally swapping slots - """ +def swap_library(current_book): config = config_loader.load() - library = config.get('files', {}).get('library', []) - - for lib in library: - path = lib['path'] - if current_book.startswith(path): - rel_book = current_book[len(path):] - break - # provide backwards compatibility, where the media_dir isn't included - path = os.path.relpath(path, '/media') - if current_book.startswith(path): - rel_book = current_book[len(path):] - break - - for lib in library: - path = lib['path'] - book = os.path.join(path, rel_book) - if book in books: - return book - + if config.has_option('files', 'additional_lib_1') and \ + config.has_option('files', 'additional_lib_2'): + lib1 = config.get('files', 'additional_lib_1') + lib2 = config.get('files', 'additional_lib_2') + if current_book.startswith(lib1): + return lib2 + current_book[len(lib1):] + elif current_book.startswith(lib2): + return lib1 + current_book[len(lib2):] return current_book -async def read_user_state(state): +async def read_user_state(media_dir, state): global manual current_book = manual_filename current_language = None # walk the available filesystems for directories and braille files - library = Library(configured_source_dirs(), ('brf', 'pef')) + library = Library(media_dir, configured_source_dirs(), ('brf', 'pef')) book_files = library.book_files() - source_paths = mounted_source_paths() + source_paths = mounted_source_paths(media_dir) for source_path in source_paths: main_toml = os.path.join(source_path, USER_STATE_FILE) if os.path.exists(main_toml): @@ -95,7 +86,7 @@ async def read_user_state(state): install(current_language) manual = Manual.create() - manual_toml = to_state_file(manual_filename) + manual_toml = os.path.join(media_dir, to_state_file(manual_filename)) if os.path.exists(manual_toml): try: t = toml.load(manual_toml) @@ -112,7 +103,7 @@ async def read_user_state(state): books = OrderedDict({manual_filename: manual}) for book_file in book_files: - filename = book_file + filename = os.path.join(media_dir, book_file) toml_file = to_state_file(filename) book = BookFile(filename=filename, width=width, height=height) if os.path.exists(toml_file): @@ -138,21 +129,22 @@ async def read_user_state(state): if current_book not in books: # let's check that they're not just using a different USB port log.info('current book not in original library {}'.format(current_book)) - current_book = swap_library(current_book, books) + current_book = swap_library(current_book) if current_book not in books: log.warn('current book not found {}, ignoring'.format(current_book)) current_book = manual_filename state.app.user.books = books + state.app.library.media_dir = media_dir state.app.library.dirs = library.dirs state.app.user.load(current_book, current_language) -async def read(state): - await read_user_state(state) +async def read(media_dir, state): + await read_user_state(media_dir, state) -async def write(queue): +async def write(media_dir, queue): log.info('save file worker started') while True: (filename, data) = await queue.get() @@ -160,7 +152,7 @@ async def write(queue): if filename is None: # main user state file - source_paths = mounted_source_paths() + source_paths = mounted_source_paths(media_dir) for source_path in source_paths: path = os.path.join(source_path, USER_STATE_FILE) log.debug('writing user state file save to {path}') @@ -169,11 +161,13 @@ async def write(queue): else: # book state file - if filename != manual_filename: + if filename == manual_filename: + path = os.path.join(media_dir, path) + else: path = to_state_file(filename) - log.debug(f'writing {filename} state file save to {path}') - async with aiofiles.open(path, 'w') as f: - await f.write(s) + log.debug(f'writing {filename} state file save to {path}') + async with aiofiles.open(path, 'w') as f: + await f.write(s) log.debug('state file save complete') queue.task_done() diff --git a/ui/library/explorer.py b/ui/library/explorer.py index a9e72460..98437491 100644 --- a/ui/library/explorer.py +++ b/ui/library/explorer.py @@ -82,18 +82,19 @@ class Library: DIRS_PAGE_SIZE = 8 FILES_PAGE_SIZE = 7 - def __init__(self, source_dirs, file_exts): + def __init__(self, media_dir, source_dirs, file_exts): + self.media_dir = os.path.abspath(media_dir) self.file_exts = file_exts self.dirs = [] - for source_dir in source_dirs: - source_path = source_dir.get('path') - if not source_dir.get('mountpoint', False) or os.path.ismount(source_path): - root = Directory(source_path, display=source_dir.get('name')) - self.walk(root) - self.prune(root) - if len(root.dirs) > 0 or root.files_count > 0: - self.flatten(root) + for source_dir, name in source_dirs: + # if os.path.ismount(os.path.join(self.media_dir, source_dir)): + # not needed as unmounted devices will be empty and so pruned + root = Directory(source_dir, display=name) + self.walk(root) + self.prune(root) + if len(root.dirs) > 0 or root.files_count > 0: + self.flatten(root) self.dir_count = len(self.dirs) self.files_dir_index = None @@ -106,7 +107,7 @@ def walk(self, root): """ dirs = [] files = [] - for entry in os.scandir(root.relpath): + for entry in os.scandir(os.path.join(self.media_dir, root.relpath)): if entry.is_dir(follow_symlinks=False) and \ entry.name[0] != '.' and \ entry.name != 'RECYCLE' and \ diff --git a/ui/library/state.py b/ui/library/state.py index 936074c9..f71783da 100644 --- a/ui/library/state.py +++ b/ui/library/state.py @@ -14,6 +14,7 @@ def __init__(self, root: 'state.RootState'): self.root = root self.page = 0 + self.media_dir = '' self.dirs = [] # index of directory that is currently expanded, if any self.files_dir_index = None @@ -160,14 +161,14 @@ def show_files_dir(self, book): break def open_book(self, book): - self.root.app.user.current_book = book.relpath() + self.root.app.user.current_book = book.relpath(self.media_dir) self.root.app.location = 'book' self.root.app.home_menu_visible = False self.root.refresh_display() self.root.save_state() def add_or_replace(self, book): - relpath = book.filename + relpath = os.path.relpath(book.filename, start=self.media_dir) self.root.app.user.books[relpath] = book def set_book_loading(self, book): diff --git a/ui/main.py b/ui/main.py index d9cad10e..5ccf14b8 100644 --- a/ui/main.py +++ b/ui/main.py @@ -25,7 +25,7 @@ def main(): args = argparser.parser.parse_args() config = config_loader.load() log = setup_logs(config, args.loglevel) - timeout = config.get('comm', {}).get('timeout', 60) + timeout = config.get('comms', 'timeout') if args.fuzz_duration: log.info('running fuzz test') @@ -174,14 +174,16 @@ async def run_async(driver, config, loop): queue, save_worker = handle_save_events(config, state) load_worker = handle_display_events(config, state) - await initial_state.read(state) + media_dir = config.get('files', 'media_dir') + log.info(f'reading initial state from {media_dir}') + await initial_state.read(media_dir, state) log.info('created store') state.refresh_display() try: while 1: - if (await handle_hardware(driver, state)): + if (await handle_hardware(driver, state, media_dir)): media_handler.cancel() try: await media_handler @@ -217,8 +219,9 @@ async def run_async(driver, config, loop): def handle_save_events(config, state): + media_dir = config.get('files', 'media_dir') queue = asyncio.Queue() - worker = asyncio.create_task(initial_state.write(queue)) + worker = asyncio.create_task(initial_state.write(media_dir, queue)) def on_save_state(book=None): # queue a snapshot of the state we want to save, theoretically we @@ -227,7 +230,7 @@ def on_save_state(book=None): # that much faster than a filesystem write if book is None: log.debug('queuing user state file save') - queue.put_nowait((None, state.app.user.to_file())) + queue.put_nowait((None, state.app.user.to_file(media_dir))) else: log.debug(f'queuing {book.filename} state file save') queue.put_nowait((book.filename, book.to_file())) @@ -246,6 +249,7 @@ def on_backup_log(): def handle_display_events(config, state): from .book.handlers import load_book, load_book_worker + media_dir = config.get('files', 'media_dir') queue = asyncio.Queue() worker = asyncio.create_task(load_book_worker(state, queue)) @@ -266,7 +270,7 @@ async def load_render_cache(): # now queue up indexing tasks to cache for book in later: - queue.put_nowait(book.relpath()) + queue.put_nowait(book.relpath(media_dir)) await display.render_to_buffer(state) @@ -286,7 +290,7 @@ async def change_files(config, state): state.app.backup_log('done') -async def handle_hardware(driver, state): +async def handle_hardware(driver, state, media_dir): if not driver.is_ok(): log.debug('shutting down due to GUI closed') state.app.load_books('cancel') @@ -311,16 +315,18 @@ async def handle_hardware(driver, state): def backup_log(config): - mounted_dirs = initial_state.mounted_source_paths() - if len(mounted_dirs) > 0: - # make a filename based on the date and save to first path - backup_file = os.path.join(mounted_dirs[0], time.strftime('%Y%m%d%M_log.txt')) - log.debug('backing up log to USB stick: {}'.format(backup_file)) - try: - import shutil - shutil.copyfile(log_file, backup_file) - except IOError as e: - log.warning("couldn't backup log file: {}".format(e)) + sd_card_dir = config.get('files', 'sd_card_dir') + media_dir = config.get('files', 'media_dir') + sd_card_dir = os.path.join(media_dir, sd_card_dir) + log_file = config.get('files', 'log_file') + # make a filename based on the date + backup_file = os.path.join(sd_card_dir, time.strftime('%Y%m%d%M_log.txt')) + log.debug('backing up log to USB stick: {}'.format(backup_file)) + try: + import shutil + shutil.copyfile(log_file, backup_file) + except IOError as e: + log.warning("couldn't backup log file: {}".format(e)) async def log_markers(): diff --git a/ui/manual.py b/ui/manual.py index 2c5b1d18..926dcbb1 100644 --- a/ui/manual.py +++ b/ui/manual.py @@ -46,5 +46,5 @@ def create(): for page in pages) return Manual(manual_filename, 40, 9, pages=pages, load_state=LoadState.DONE) - def relpath(self): + def relpath(self, media_dir): return self.filename From 447ad61f3aa6f3910aaa21943d8b835b16695111 Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Fri, 9 Feb 2024 12:08:45 +0000 Subject: [PATCH 06/11] Rewrite of change to make the removable media system more flexible (e.g. for use on the console) while maintaining backwards compatibility with the current standalone ui --- config.rc.in | 31 +++++++++--------- config.rc.travis | 13 +++++--- tests/test_utility.py | 2 +- ui/config_loader.py | 22 ++++++------- ui/initial_state.py | 73 ++++++++++++++++++++++++++++-------------- ui/library/explorer.py | 17 +++++----- ui/main.py | 30 ++++++++--------- 7 files changed, 109 insertions(+), 79 deletions(-) diff --git a/config.rc.in b/config.rc.in index 00655e46..ef50ab5a 100644 --- a/config.rc.in +++ b/config.rc.in @@ -1,22 +1,23 @@ [files] # relative path to where log is kept -log_file = canute.log -# book directory +log_file = 'canute.log' + # where usb sticks and sd-cards will be automounted -media_dir = /media -# the exact sub mount-point directory name of the sd-card -sd_card_dir = sd-card +media_dir = '/media' -# Additional books made available on the USB ports will (for now at -# least) be visible in the library. The current state will be written -# to all mount points that are active, however the state file on the -# SD card will take precedence, followed by lib_1 and finally lib_2. -# These behaviours may change at any time so shouldn't be relied upon. -# -# These paths are relative to media_dir. -# These config values are optional. -additional_lib_1 = front-usb -additional_lib_2 = back-usb +# Book Directories +# Additional books made available on the USB ports will be made visible +# in the library. The current state will be written to all mount points +# that are active. The first state file found (based on the order in the +# list) will take precedence if they differ. +# These paths can be relative to media_dir or absolute. The mountpoint +# flag should be used if they point to mountpoints that may not be present +# and the swappable flag means USB media that is interchangeable. +library = [ + { name = 'SD', path = 'sd-card', mountpoint = true }, + { name = 'USB1', path = 'front-usb', mountpoint = true, swappable = true }, + { name = 'USB2', path = 'back-usb', mountpoint = true, swappable = true } +] [comms] # serial timeout in seconds diff --git a/config.rc.travis b/config.rc.travis index 01e1419c..03d4289c 100644 --- a/config.rc.travis +++ b/config.rc.travis @@ -1,11 +1,16 @@ [files] # relative path to where log is kept log_file = canute.log -# book directory -# where usb sticks and sd-cards will be automounted media_dir = ~/canute-media -# the exact sub mount-point directory name of the sd-card -sd_card_dir = sd-card + +# Book Directories +# Additional books made available on the USB ports will be made visible +# in the library. The current state will be written to all mount points +# that are active. The first state file found (based on the order in the +# list) will take precedence if they differ. +library = [ + { name = 'SD', path = 'sd-card' } +] [comms] # serial timeout in seconds diff --git a/tests/test_utility.py b/tests/test_utility.py index 004f9a50..63a3d7cc 100644 --- a/tests/test_utility.py +++ b/tests/test_utility.py @@ -4,7 +4,7 @@ from ui.library.explorer import Library dir_path = os.path.dirname(os.path.realpath(__file__)) -test_books_dir = [('test-books', 'test dir')] +test_books_dir = [{ 'path': 'test-books', 'name': 'test dir' }] class TestUtility(unittest.TestCase): diff --git a/ui/config_loader.py b/ui/config_loader.py index ed82be13..47abe5ec 100644 --- a/ui/config_loader.py +++ b/ui/config_loader.py @@ -1,18 +1,18 @@ import os.path -from configparser import ConfigParser -config_file = 'config.rc' +import toml +config_file = 'config.rc' def load(config_file=config_file): - config = ConfigParser() - c = config.read(config_file) - if len(c) == 0: + if not os.path.exists(config_file): raise ValueError('Please provide a config.rc') - media_dir = config.get('files', 'media_dir') - config.set('files', 'media_dir', os.path.expanduser(media_dir)) - if not config.has_section('comms'): - config.add_section('comms') - if not config.has_option('comms', 'timeout'): - config.set('comms', 'timeout', 60) + + config = toml.load(config_file) + + # expand any ~ home dirs in media_dir + files_section = config.get('files', {}) + media_dir = files_section.get('media_dir', '/media') + files_section.set('media_dir', os.path.expanduser(media_dir)) + return config diff --git a/ui/initial_state.py b/ui/initial_state.py index 6c04634b..0b40afe1 100644 --- a/ui/initial_state.py +++ b/ui/initial_state.py @@ -28,32 +28,57 @@ def to_state_file(book_path): def configured_source_dirs(): config = config_loader.load() - state_sources = [('sd_card_dir', 'SD')] - if config.has_option('files', 'additional_lib_1'): - state_sources.append(('additional_lib_1', 'USB1')) - if config.has_option('files', 'additional_lib_2'): - state_sources.append(('additional_lib_2', 'USB2')) - return [(config.get('files', source), name) for source, name in state_sources] + return config.get('files', {}).get('library', []) def mounted_source_paths(media_dir): - for source_dir, name in configured_source_dirs(): - source_path = os.path.join(media_dir, source_dir) - if os.path.ismount(source_path) or 'TRAVIS' in os.environ: - yield source_path + for source_dir in configured_source_dirs(): + source_path = os.path.join(media_dir, source_dir.get('path')) + if not source_dir.get('mountpoint', False) or os.path.ismount(source_path): + yield source_path, source_dir.get('swappable', False) + + +def swappable_usb(data): + """ + The state file contains part of the book path (e.g. usb0) and so if we want + to maintain compatibility with the old standalone setup, we need to convert + it to something it understands. So here we fix any 'swappable' path (i.e. + USB removable media) to 'front-usb' and rely on the function below to find + the correct prefix on app startup. + """ + book = data.get('current_book') + for source_dir in configured_source_dirs(): + if source_dir.get('swappable', False): + prefix = source_dir.get('path') + os.sep + if book.startswith(prefix): + book = os.path.join('front-usb', book[len(prefix):]) + # modifying data is safe as a new dict is created each time + data.set('current_book', book) + break + return data -def swap_library(current_book): +def swap_library(current_book, books): + """ + The current_book path includes a mount-point subpath (if on removable media) + so try to cope with user accidentally swapping slots + """ config = config_loader.load() - if config.has_option('files', 'additional_lib_1') and \ - config.has_option('files', 'additional_lib_2'): - lib1 = config.get('files', 'additional_lib_1') - lib2 = config.get('files', 'additional_lib_2') - if current_book.startswith(lib1): - return lib2 + current_book[len(lib1):] - elif current_book.startswith(lib2): - return lib1 + current_book[len(lib2):] - return current_book + library = config.get('files', {}).get('library', []) + + # check for expected path, for backward compatibility with standalone unit + for path in ['front-usb' + os.path.sep, 'back-usb' + os.path.sep]: + if current_book.startswith(path): + rel_book = current_book[len(path):] + break + + # see if we can find it on a different swappable device path + for lib in library: + if lib.get('swappable', False): + path = lib['path'] + book = os.path.join(path, rel_book) + if book in books: + return book async def read_user_state(media_dir, state): @@ -66,7 +91,7 @@ async def read_user_state(media_dir, state): book_files = library.book_files() source_paths = mounted_source_paths(media_dir) - for source_path in source_paths: + for source_path, swappable in source_paths: main_toml = os.path.join(source_path, USER_STATE_FILE) if os.path.exists(main_toml): try: @@ -129,7 +154,7 @@ async def read_user_state(media_dir, state): if current_book not in books: # let's check that they're not just using a different USB port log.info('current book not in original library {}'.format(current_book)) - current_book = swap_library(current_book) + current_book = swap_library(current_book, books) if current_book not in books: log.warn('current book not found {}, ignoring'.format(current_book)) current_book = manual_filename @@ -148,12 +173,12 @@ async def write(media_dir, queue): log.info('save file worker started') while True: (filename, data) = await queue.get() - s = toml.dumps(data) + s = toml.dumps(swappable_usb(data)) if filename is None: # main user state file source_paths = mounted_source_paths(media_dir) - for source_path in source_paths: + for source_path, swappable in source_paths: path = os.path.join(source_path, USER_STATE_FILE) log.debug('writing user state file save to {path}') async with aiofiles.open(path, 'w') as f: diff --git a/ui/library/explorer.py b/ui/library/explorer.py index 98437491..b571496a 100644 --- a/ui/library/explorer.py +++ b/ui/library/explorer.py @@ -87,14 +87,15 @@ def __init__(self, media_dir, source_dirs, file_exts): self.file_exts = file_exts self.dirs = [] - for source_dir, name in source_dirs: - # if os.path.ismount(os.path.join(self.media_dir, source_dir)): - # not needed as unmounted devices will be empty and so pruned - root = Directory(source_dir, display=name) - self.walk(root) - self.prune(root) - if len(root.dirs) > 0 or root.files_count > 0: - self.flatten(root) + for source_dir in source_dirs: + source_path = source_dir.get('path') + if not source_dir.get('mountpoint', False) or \ + os.path.ismount(os.path.join(self.media_dir, source_path)): + root = Directory(source_path, display=source_dir.get('name')) + self.walk(root) + self.prune(root) + if len(root.dirs) > 0 or root.files_count > 0: + self.flatten(root) self.dir_count = len(self.dirs) self.files_dir_index = None diff --git a/ui/main.py b/ui/main.py index 5ccf14b8..3a5a6594 100644 --- a/ui/main.py +++ b/ui/main.py @@ -25,7 +25,7 @@ def main(): args = argparser.parser.parse_args() config = config_loader.load() log = setup_logs(config, args.loglevel) - timeout = config.get('comms', 'timeout') + timeout = config.get('comm', {}).get('timeout', 60) if args.fuzz_duration: log.info('running fuzz test') @@ -174,7 +174,7 @@ async def run_async(driver, config, loop): queue, save_worker = handle_save_events(config, state) load_worker = handle_display_events(config, state) - media_dir = config.get('files', 'media_dir') + media_dir = config.get('files', {}).get('media_dir') log.info(f'reading initial state from {media_dir}') await initial_state.read(media_dir, state) log.info('created store') @@ -219,7 +219,7 @@ async def run_async(driver, config, loop): def handle_save_events(config, state): - media_dir = config.get('files', 'media_dir') + media_dir = config.get('files', {}).get('media_dir') queue = asyncio.Queue() worker = asyncio.create_task(initial_state.write(media_dir, queue)) @@ -249,7 +249,7 @@ def on_backup_log(): def handle_display_events(config, state): from .book.handlers import load_book, load_book_worker - media_dir = config.get('files', 'media_dir') + media_dir = config.get('files', {}).get('media_dir') queue = asyncio.Queue() worker = asyncio.create_task(load_book_worker(state, queue)) @@ -315,18 +315,16 @@ async def handle_hardware(driver, state, media_dir): def backup_log(config): - sd_card_dir = config.get('files', 'sd_card_dir') - media_dir = config.get('files', 'media_dir') - sd_card_dir = os.path.join(media_dir, sd_card_dir) - log_file = config.get('files', 'log_file') - # make a filename based on the date - backup_file = os.path.join(sd_card_dir, time.strftime('%Y%m%d%M_log.txt')) - log.debug('backing up log to USB stick: {}'.format(backup_file)) - try: - import shutil - shutil.copyfile(log_file, backup_file) - except IOError as e: - log.warning("couldn't backup log file: {}".format(e)) + mounted_dirs = initial_state.mounted_source_paths() + if len(mounted_dirs) > 0: + # make a filename based on the date and save to first path + backup_file = os.path.join(mounted_dirs[0], time.strftime('%Y%m%d%M_log.txt')) + log.debug('backing up log to USB stick: {}'.format(backup_file)) + try: + import shutil + shutil.copyfile(log_file, backup_file) + except IOError as e: + log.warning("couldn't backup log file: {}".format(e)) async def log_markers(): From b2cf3a285ad3918515904f706be15af5f00614c7 Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Fri, 9 Feb 2024 12:09:48 +0000 Subject: [PATCH 07/11] Don't show 'please wait...' if not actually shutting down --- ui/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/main.py b/ui/main.py index 3a5a6594..3c593a16 100644 --- a/ui/main.py +++ b/ui/main.py @@ -298,8 +298,8 @@ async def handle_hardware(driver, state, media_dir): if state.app.shutting_down: if isinstance(driver, Pi): driver.port.close() - os.system('/home/pi/util/shutdown-stage-1.py') if not sys.stdout.isatty(): + os.system('/home/pi/util/shutdown-stage-1.py') os.system('sudo shutdown -h now') # never exit from Dummy driver elif isinstance(driver, Dummy): From 0939cb5a88d55a82858d6ab65dca13c622e6b420 Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Fri, 9 Feb 2024 16:05:58 +0000 Subject: [PATCH 08/11] Fixes from initial testing --- ui/config_loader.py | 2 +- ui/driver/driver_pi.py | 3 --- ui/setup_logs.py | 2 +- ui/system_menu/system_menu.py | 4 ++-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/ui/config_loader.py b/ui/config_loader.py index 47abe5ec..78743e16 100644 --- a/ui/config_loader.py +++ b/ui/config_loader.py @@ -13,6 +13,6 @@ def load(config_file=config_file): # expand any ~ home dirs in media_dir files_section = config.get('files', {}) media_dir = files_section.get('media_dir', '/media') - files_section.set('media_dir', os.path.expanduser(media_dir)) + files_section['media_dir'] = os.path.expanduser(media_dir) return config diff --git a/ui/driver/driver_pi.py b/ui/driver/driver_pi.py index 29c9a706..4ada0fb6 100644 --- a/ui/driver/driver_pi.py +++ b/ui/driver/driver_pi.py @@ -122,7 +122,6 @@ def get_buttons(self): buttons = {} self.send_data(comms.CMD_SEND_BUTTONS) read_buttons = self.get_data(comms.CMD_SEND_BUTTONS) - down = self.previous_buttons for i, n in enumerate(reversed(list('{:0>14b}'.format(read_buttons)))): name = mapping[str(i)] if n == '1': @@ -137,8 +136,6 @@ def get_buttons(self): buttons[name] = 'up' del self.previous_buttons[name] - self.previous_buttons = tuple(down) - return buttons def send_error_sound(self): diff --git a/ui/setup_logs.py b/ui/setup_logs.py index 1d4b0a82..45689e42 100644 --- a/ui/setup_logs.py +++ b/ui/setup_logs.py @@ -4,7 +4,7 @@ def setup_logs(config, loglevel): - log_file = config.get('files', 'log_file') + log_file = config.get('files', {}).get('log_file') log_format = logging.Formatter( '%(asctime)s - %(name)-16s - %(levelname)-8s - %(message)s') # configure the client logging diff --git a/ui/system_menu/system_menu.py b/ui/system_menu/system_menu.py index c7798b86..2cff3faf 100644 --- a/ui/system_menu/system_menu.py +++ b/ui/system_menu/system_menu.py @@ -45,9 +45,9 @@ def brailleify(rel): if os.path.exists('/sys/firmware/devicetree/base/model'): if os.path.exists('/etc/canute_release'): with open('/etc/canute_release') as x: - release = _('release:') + ' ' + brailleify(x.read().strip()) + release = brailleify(x.read().strip()) with open('/etc/canute_serial') as x: - serial = _('serial:') + ' ' + brailleify(x.read().strip()) + serial = brailleify(x.read().strip()) else: release = brailleify(_('run in standalone mode')) serial = release From 5b55d05044a0e79752d45ca6ea93890bdee9aa5f Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Fri, 9 Feb 2024 16:11:32 +0000 Subject: [PATCH 09/11] Fix bug with button responsiveness --- ui/driver/driver_pi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/driver/driver_pi.py b/ui/driver/driver_pi.py index 4ada0fb6..0d2ee43b 100644 --- a/ui/driver/driver_pi.py +++ b/ui/driver/driver_pi.py @@ -132,7 +132,7 @@ def get_buttons(self): if self.previous_buttons[name] == self.button_threshold: buttons[name] = 'down' elif n == '0' and (name in self.previous_buttons): - if self.previous_buttons[name] > self.button_threshold: + if self.previous_buttons[name] >= self.button_threshold: buttons[name] = 'up' del self.previous_buttons[name] From 7d85e6e5de14943f418a1c0bdf42efd579ca66a1 Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Fri, 9 Feb 2024 16:13:38 +0000 Subject: [PATCH 10/11] Fix mistake with tuple of path, swappable values --- ui/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/main.py b/ui/main.py index 3c593a16..322d4418 100644 --- a/ui/main.py +++ b/ui/main.py @@ -318,7 +318,7 @@ def backup_log(config): mounted_dirs = initial_state.mounted_source_paths() if len(mounted_dirs) > 0: # make a filename based on the date and save to first path - backup_file = os.path.join(mounted_dirs[0], time.strftime('%Y%m%d%M_log.txt')) + backup_file = os.path.join(mounted_dirs[0][0], time.strftime('%Y%m%d%M_log.txt')) log.debug('backing up log to USB stick: {}'.format(backup_file)) try: import shutil From ad96a605221ef2baf0e8b8171728571726ac8595 Mon Sep 17 00:00:00 2001 From: Andy Wood Date: Fri, 9 Feb 2024 16:26:24 +0000 Subject: [PATCH 11/11] Make the launch of the media helper optional --- config.rc.in | 3 +++ ui/main.py | 28 +++++++++++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/config.rc.in b/config.rc.in index ef50ab5a..3db7bb59 100644 --- a/config.rc.in +++ b/config.rc.in @@ -19,6 +19,9 @@ library = [ { name = 'USB2', path = 'back-usb', mountpoint = true, swappable = true } ] +# if required, a program to look for changes to removable media +media_helper = './media.py' + [comms] # serial timeout in seconds timeout = 1000 diff --git a/ui/main.py b/ui/main.py index 322d4418..f47ac492 100644 --- a/ui/main.py +++ b/ui/main.py @@ -113,12 +113,7 @@ async def run_async_timeout(driver, config, duration, loop): # This is a task in its own right that listens to an external process for media # change notifications, and handles them. -async def handle_media_changes(): - # For now, under Travis, don't launch it. It requires pygi which is hard - # to make accessible to a virtualenv. - media_helper = './media.py' - if 'TRAVIS' in os.environ: - media_helper = '/bin/cat' +async def handle_media_changes(media_helper): proc = await asyncio.create_subprocess_exec( media_helper, stdout=asyncio.subprocess.PIPE) @@ -161,7 +156,12 @@ async def run_async(driver, config, loop): while not driver.is_motion_complete(): await asyncio.sleep(0.01) log.debug('motion complete') - media_handler = asyncio.ensure_future(handle_media_changes()) + + media_helper = config.get('filese', {}).get('media_helper') + if media_helper is not None: + media_handler = asyncio.ensure_future(handle_media_changes(media_helper)) + else: + media_handler = None if config.get('hardware', {}).get('log_duty', False): duty_logger = asyncio.ensure_future(driver.track_duty()) @@ -184,11 +184,12 @@ async def run_async(driver, config, loop): try: while 1: if (await handle_hardware(driver, state, media_dir)): - media_handler.cancel() - try: - await media_handler - except asyncio.CancelledError: - pass + if media_handler is not None: + media_handler.cancel() + try: + await media_handler + except asyncio.CancelledError: + pass if duty_logger is not None: duty_logger.cancel() try: @@ -209,7 +210,8 @@ async def run_async(driver, config, loop): else: await asyncio.sleep(0) except asyncio.CancelledError: - media_handler.cancel() + if media_handler is not None: + media_handler.cancel() if duty_logger is not None: duty_logger.cancel() await queue.join()