From a828a52b6e0bdeed43bece24987f8c678d5d128d Mon Sep 17 00:00:00 2001 From: wortelus Date: Thu, 21 Jul 2022 13:12:45 +0200 Subject: [PATCH 1/2] added image caching functionality --- .gitignore | 1 + config.yaml | 3 +++ main.py | 44 +++++++++++++++++++++++++++++++++++++++++-- preprocessing.py | 49 +++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 1dbe0d5..7d85e84 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__/ venv /ms33558.ttf +img-cache.pkl diff --git a/config.yaml b/config.yaml index 61e50d6..60dcea6 100644 --- a/config.yaml +++ b/config.yaml @@ -4,6 +4,9 @@ xp-port: 49000 server-ip: 127.0.0.1 server-port: 49008 default-font: ms33558.ttf +# comment out following line to disable image caching +# the images will have to load for several seconds every time, but it is suitable for tweaking +cache-path: ./img-cache.pkl stream-decks: - serial: CL41I1A00651 keys: 32 diff --git a/main.py b/main.py index 7bf55fb..ed57574 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,8 @@ import math +import pickle import threading import time +from os.path import exists import numpy as np import yaml @@ -203,12 +205,48 @@ def main(): "Have you installed it correctly?") print(e) + cache_path = None + caching_enabled = False + load_cached_img = False + if "cache-path" in global_cfg: + cache_path = global_cfg["cache-path"] + # check cache_path if is not False or None -> implicating it is enabled in config + # and check if it exists + # then we will load the cache file, thus skipping the loader load_images_datarefs_all + if cache_path: + caching_enabled = True + if exists(global_cfg["cache-path"]): + load_cached_img = True + global presets_all - presets_all = preprocessing.load_all_presets(keys_dir, key_count) + presets_all = preprocessing.load_all_presets(keys_dir, key_count, preload_labels=load_cached_img) global datarefs_all datarefs_all = preprocessing.load_datarefs(presets_all) global images_all - images_all = preprocessing.load_images_datarefs_all(current_deck, presets_all) + + if not caching_enabled: + print("note: caching is disabled, loading will be noticeably slower") + print("you can enable it by setting the field 'cache-path' in config.yaml") + images_all = preprocessing.load_images_datarefs_all(current_deck, presets_all) + elif load_cached_img: + # images are stored as cache, open and load + print("cache file {} is present, skipping pre-generation.".format(cache_path)) + print("note: if you changed configuration or icon set, you should delete the {} cache file".format(cache_path)) + print("loading cache...") + with open(cache_path, 'rb') as f: + # load and convert it to runtime format + images_save_format = pickle.load(f) + images_all = preprocessing.convert_to_runtime_format(images_save_format) + else: + # caching is enabled, but cache file not found + print("cache file {} not found, starting the pre-generation.".format(cache_path)) + images_all = preprocessing.load_images_datarefs_all(current_deck, presets_all) + # save images to cache-path + print("saving the pregen to {}".format(cache_path)) + with open(cache_path, 'wb') as f: + # convert to save format and save it + images_save_format = preprocessing.convert_to_save_format(images_all) + pickle.dump(images_save_format, f) global directory_stack directory_stack = [] @@ -228,6 +266,8 @@ def main(): deck_show(current_deck, current_datarefs) deck_show_static(current_deck) + print("xplane-streamdeck ready...") + try: while True: time.sleep(0.05) diff --git a/preprocessing.py b/preprocessing.py index ad6ed84..e14ce7f 100644 --- a/preprocessing.py +++ b/preprocessing.py @@ -155,7 +155,7 @@ def count_presets(target_dir): return len(glob.glob1(target_dir, "*.yaml")) -def load_preset(target_dir, yaml_keyset, deck_key_count): +def load_preset(target_dir, yaml_keyset, deck_key_count, preload_labels=False): with open(os.path.join(target_dir, yaml_keyset)) as stream: try: preset_cfg = safe_load(stream) @@ -196,6 +196,19 @@ def load_preset(target_dir, yaml_keyset, deck_key_count): key.get("gauge"), key.get("display"), ) + + # restoring images from cache file (preload_labels flag) + # this applies to buttons with 'label' parameter set + # If set to True, we must 'correct' image file_names here, because the + # image post-loader load_images_datarefs_all is not called during current session + if preload_labels: + btn = preset[index] + for i, state_name in enumerate(btn.file_names): + # change state name for storing, allowing same icons with different labels + if btn.label: + state_name = btn.label + state_name + preset[index].file_names[i] = state_name + if cmd_type == "dir": other_keysets = np.append(other_keysets, name) @@ -206,16 +219,18 @@ def add_yaml_suffix(filename): return filename + ".yaml" -def load_all_presets(target_dir, deck_key_count): +def load_all_presets(target_dir, deck_key_count, preload_labels=False): presets_all = {} # read root - preset, keysets = load_preset(target_dir, ACTION_CFG, deck_key_count) + preset, keysets = load_preset(target_dir, ACTION_CFG, deck_key_count, + preload_labels=preload_labels) presets_all[ACTION_CFG_NAME] = preset # execute while there are keysets to be read and loaded into presets while keysets.size > 0: for _, key_set in enumerate(keysets): if key_set not in presets_all and key_set != "return": - preset, other_keysets = load_preset(target_dir, add_yaml_suffix(key_set), deck_key_count) + preset, other_keysets = load_preset(target_dir, add_yaml_suffix(key_set), deck_key_count, + preload_labels=preload_labels) presets_all[key_set] = preset keysets = np.unique(np.concatenate((keysets, other_keysets), 0)) @@ -311,7 +326,9 @@ def load_images_datarefs(deck, presets_dir): if state_name not in set_images: state_image = render_key_image(deck, state_name, button.label) - # change state name for storing, allowing same icons with different labels + # change file_names in preset according to images_all, allowing same icons with different labels + # notice how this is executed in the post-processing stage + # i.e. after the presets have long been generated if button.label: state_name = button.label + state_name button.file_names[i] = state_name @@ -328,3 +345,25 @@ def load_images_datarefs_all(deck, presets_all): set_images_all.update(images_single_dir) return set_images_all + +# +# pickle helpers +# + +# pickle is unable to handler 'memoryview' objects, we must convert them to bytearray and vice versa + + +def convert_to_save_format(images_all): + images_bytearray = {} + for key, img in images_all.items(): + images_bytearray[key] = img.tobytes() + + return images_bytearray + + +def convert_to_runtime_format(images_save_format): + images_all = {} + for key, img in images_save_format.items(): + images_all[key] = memoryview(img) + + return images_all From 1534801b8f78153b4ca72fa52f2fedb0808625f2 Mon Sep 17 00:00:00 2001 From: wortelus Date: Thu, 21 Jul 2022 13:25:01 +0200 Subject: [PATCH 2/2] corrected README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 20afdff..f200cd9 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,11 @@ Instructions for **Windows** running `python .\main.py` under the *xplane-streamdeck* directory, while having the Stream Deck plugged in already +The program supports image caching, which saves several seconds of image preloading during launch +- To enable it, set `cache-path` field in `config.yaml` +- NOTE: If you are tweaking your image set or configuration, it is recommended disable this feature +to always see the up-to-date configuration state and avoid runtime errors + ### Additional Info **Refer to the `B737-800X/README.md` for a guide on how to create/edit buttons.** @@ -118,7 +123,6 @@ you're going to reference the directories by their name in key configs with keys **Refer to the `B737-800X/README.md` for a guide on how to create/edit buttons.** ### What is planned / WIP? -- Caching of preloaded graphics - More types of labels - Multi deck support