diff --git a/Documentation/Development.md b/Documentation/Development.md index b9f94b2e..65e50955 100644 --- a/Documentation/Development.md +++ b/Documentation/Development.md @@ -1,30 +1,86 @@ # Development -Building this software can be performed two ways, using the STMCubeIDE or using command line tools. +Building this software can be performed two ways: using the STM32CubeIDE or using command line tools. -## STM Cube IDE +## STM32CubeIDE -The easiest way to start working using the STM Cube IDE is to create a new project for the STM32F103RCTx. -Then once this is created remove the auto-generated source code. -Next drag the contents of the `source` folder into the project and choose to link to files. -You will need to update the build settings for include paths & point to the new `.ld` linker file +The easiest way to start working with the STM32CubeIDE is to create a new project for the STM32F103RCTx. +Once this is created, remove the auto-generated source code. +Next, drag the contents of the `source` folder into the project and choose to link to files. +You will need to update the build settings for include paths and point to the new `.ld` linker file. -## Developing with command line tools & building a release +## Command line tools and building a release -In the `source` folder there is a makefile that can be used to build the repository using command line tools. -when running the `make` command, specify which model of the device & the language you would like to use. +In the `source` folder there is a `Makefile` that can be used to build the repository using command line tools. +When running the `make` command, specify which model of the device and the language(s) you would like to use. -`make -j8 lang=EN model=TS80` +### macOS -To build a release instead, run the build.sh script. This will update translations and also build every language for both TS100 and TS80 models. +Use the following steps to set up a build environment for IronOS on the command line (in Terminal). + +1. [Follow steps 1 – 3 here to install the toolchain](https://github.com/glegrain/STM32-with-macOS#0---installing-the-toolchain) needed to compile for STM32 microcontrollers. +2. Install `python`: +``` +brew install python +``` +3. (Optional) Update `pip` so it doesn't warn you about being out-of-date: +``` +python3 -m pip install --upgrade pip +``` +4. Change to the `source` directory: +``` +cd source +``` +5. Create a Python virtual environment for IronOS named `ironos-venv` to keep your Python installation clean: +``` +python3 -m venv ironos-venv +``` +6. Activate the Python virtual environment: +``` +source ironos-venv/bin/activate +``` +7. Install the dependencies required to run `make-translation.py`: +``` +pip install bdflib +``` +8. All done! See some examples below for how you can build your own IronOS. + +### Examples + +To build a single language Simplified Chinese firmware for the TS80P with 8 simultaneous jobs: +``` +make -j8 model=TS80P firmware-ZH_CN +``` + +To build a European multi-language firmware for the Pinecil with as many simultaneous jobs as there are logical processors on Linux: +``` +make -j$(nproc) model=Pinecil firmware-multi_European +``` + +To build a Cyrillic compressed multi-language firmware for the Pinecil with as many simultaneous jobs as there are logical processors on macOS: +``` +make -j$(sysctl -n hw.logicalcpu) model=Pinecil firmware-multi_compressed_Bulgarian+Russian+Serbian+Ukrainian +``` + +To build a custom multi-language firmware including English and Simplified Chinese for the TS80: +``` +make -j8 model=TS80 custom_multi_langs="EN ZH_CN" firmware-multi_Custom +``` + +To build a custom compressed multi-language firmware including German, Spanish, and French for the TS100 (note if `model` is unspecified, it will default to `TS100`): +``` +make -j8 custom_multi_langs="DE ES FR" firmware-multi_compressed_Custom +``` + +To build a release instead, run the `build.sh` script. This will update translations and also build every language for all device models. For macOS users, replace `make -j$(nproc)` in the script with `make -j$(sysctl -n hw.logicalcpu)` before running. ## Updating languages -To update the language translation files & associated font map, execute the `make_translation.py` code from the translations directory. +To update the language translation files and their associated font maps, execute the `make_translation.py` code from the `Translations` directory. ## Building Pinecil -I highly recommend using the command line tools and using docker to run the compiler. -It's a bit more fussy on setup than the STM tooling and this is by far the easiest way. -If you _need_ an IDE I have used [Nuclei's IDE](https://nucleisys.com/download.php) -And follow same idea as the STM Cube IDE notes above. +I highly recommend using the command line tools and using Docker to run the compiler. +It's a bit more fussy on setup than the STM tooling, and this is by far the easiest way. +If you _need_ an IDE I have used [Nuclei's IDE](https://nucleisys.com/download.php). +Follow the same idea as the STM Cube IDE notes above. diff --git a/Translations/TranslationEditor.html b/Translations/TranslationEditor.html index 91b982b7..2b57d344 100644 --- a/Translations/TranslationEditor.html +++ b/Translations/TranslationEditor.html @@ -308,6 +308,7 @@ diff --git a/Translations/font_tables.py b/Translations/font_tables.py index e2f8cff9..94c3b210 100755 --- a/Translations/font_tables.py +++ b/Translations/font_tables.py @@ -326,6 +326,70 @@ def get_font_map_latin_extended() -> Dict[str, bytes]: return font +def get_font_map_greek() -> Dict[str, bytes]: + font = { + # U+0370..U+03FF Greek and Coptic + "Έ": b"\x06\xFC\xFC\x8C\x8C\x8C\x8C\x8C\x8C\x0C\x0C\x00\x00\x3F\x3F\x31\x31\x31\x31\x31\x31\x30\x30\x00", + "Α": b"\x00\x00\x00\xE0\xFC\x1F\x1F\xFC\xE0\x00\x00\x00\x00\x38\x3F\x07\x06\x06\x06\x06\x07\x3F\x38\x00", + "Β": b"\x00\xFF\xFF\xC3\xC3\xC3\xC3\xE7\xFE\xBC\x00\x00\x00\x3F\x3F\x30\x30\x30\x30\x30\x39\x1F\x0F\x00", + "Γ": b"\x00\xFF\xFF\x03\x03\x03\x03\x03\x03\x03\x03\x00\x00\x3F\x3F\x00\x00\x00\x00\x00\x00\x00\x00\x00", + "Δ": b"\x00\x00\x00\xE0\xFC\x1F\x1F\xFC\xE0\x00\x00\x00\x00\x38\x3F\x37\x30\x30\x30\x30\x37\x3F\x38\x00", + "Ε": b"\x00\xFF\xFF\xC3\xC3\xC3\xC3\xC3\xC3\x03\x03\x00\x00\x3F\x3F\x30\x30\x30\x30\x30\x30\x30\x30\x00", + "Ζ": b"\x00\x03\x03\x03\x03\xC3\xE3\x33\x1F\x0F\x03\x00\x00\x30\x3C\x3E\x33\x31\x30\x30\x30\x30\x30\x00", + "Η": b"\x00\xFF\xFF\xC0\xC0\xC0\xC0\xC0\xC0\xFF\xFF\x00\x00\x3F\x3F\x00\x00\x00\x00\x00\x00\x3F\x3F\x00", + "Θ": b"\x00\xF0\xFC\x0E\xC7\xC3\xC3\xC7\x0E\xFC\xF0\x00\x00\x03\x0F\x1C\x38\x30\x30\x38\x1C\x0F\x03\x00", + "Ι": b"\x00\x00\x00\x03\x03\xFF\xFF\x03\x03\x00\x00\x00\x00\x00\x00\x30\x30\x3F\x3F\x30\x30\x00\x00\x00", + "Κ": b"\x00\xFF\xFF\xC0\xE0\xF0\x38\x1C\x0E\x07\x03\x00\x00\x3F\x3F\x00\x01\x03\x07\x0E\x1C\x38\x30\x00", + "Λ": b"\x00\x00\x00\xE0\xFC\x1F\x1F\xFC\xE0\x00\x00\x00\x00\x38\x3F\x07\x00\x00\x00\x00\x07\x3F\x38\x00", + "Μ": b"\x00\xFF\xFF\x1E\x78\xE0\xE0\x78\x1E\xFF\xFF\x00\x00\x3F\x3F\x00\x00\x01\x01\x00\x00\x3F\x3F\x00", + "Ν": b"\x00\xFF\xFF\x0E\x38\xF0\xC0\x00\x00\xFF\xFF\x00\x00\x3F\x3F\x00\x00\x00\x03\x07\x1C\x3F\x3F\x00", + "Ξ": b"\x00\x03\x03\xC3\xC3\xC3\xC3\xC3\xC3\x03\x03\x00\x00\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x00", + "Ο": b"\x00\xF0\xFC\x0E\x07\x03\x03\x07\x0E\xFC\xF0\x00\x00\x03\x0F\x1C\x38\x30\x30\x38\x1C\x0F\x03\x00", + "Π": b"\x00\xFF\xFF\x03\x03\x03\x03\x03\x03\xFF\xFF\x00\x00\x3F\x3F\x00\x00\x00\x00\x00\x00\x3F\x3F\x00", + "Ρ": b"\x00\xFF\xFF\x83\x83\x83\x83\x83\xC7\xFE\x7C\x00\x00\x3F\x3F\x01\x01\x01\x01\x01\x01\x00\x00\x00", + "Σ": b"\x00\x03\x0F\x1F\x33\xE3\xE3\x03\x03\x03\x03\x00\x00\x30\x3C\x3E\x33\x31\x30\x30\x30\x30\x30\x00", + "Τ": b"\x00\x03\x03\x03\x03\xFF\xFF\x03\x03\x03\x03\x00\x00\x00\x00\x00\x00\x3F\x3F\x00\x00\x00\x00\x00", + "Υ": b"\x00\x03\x0F\x3C\xF0\xC0\xC0\xF0\x3C\x0F\x03\x00\x00\x00\x00\x00\x00\x3F\x3F\x00\x00\x00\x00\x00", + "Φ": b"\x00\xF8\xFC\x0E\x06\xFF\xFF\x06\x0E\xFC\xF8\x00\x00\x03\x07\x0E\x0C\x3F\x3F\x0C\x0E\x07\x03\x00", + "Χ": b"\x00\x03\x0F\x3C\xF0\xC0\xC0\xF0\x3C\x0F\x03\x00\x00\x30\x3C\x0F\x03\x00\x00\x03\x0F\x3C\x30\x00", + "Ψ": b"\x00\x3F\x7F\xE0\xC0\xFF\xFF\xC0\xE0\x7F\x3F\x00\x00\x00\x00\x00\x00\x3F\x3F\x00\x00\x00\x00\x00", + "Ω": b"\x00\xF0\xFC\x0E\x07\x03\x03\x07\x0E\xFC\xF0\x00\x00\x63\x6F\x7C\x70\x00\x00\x70\x7C\x6F\x63\x00", + "ά": b"\x00\x80\xC0\xE0\x60\x6C\x6E\x66\xC0\xE0\xE0\x00\x00\x0F\x1F\x38\x30\x30\x30\x30\x18\x3F\x3F\x20", + "έ": b"\x00\xE0\xF0\x30\x30\x36\x37\x33\x30\x70\x60\x00\x00\x1D\x3F\x33\x33\x33\x33\x33\x33\x38\x18\x00", + "ή": b"\x00\xE0\xE0\xC0\x60\x6C\x6E\xE6\xE0\xC0\x00\x00\x00\x1F\x1F\x00\x00\x00\x00\x00\x7F\x7F\x00\x00", + "ί": b"\x00\x00\x00\xEC\xEE\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1F\x3F\x30\x30\x30\x00\x00\x00\x00", + "α": b"\x00\x80\xC0\xE0\x60\x60\x60\x60\xC0\xE0\xE0\x00\x00\x0F\x1F\x38\x30\x30\x30\x30\x18\x3F\x3F\x20", + "β": b"\x00\x00\xC0\xE0\x30\x10\x10\x30\xE0\xC0\x00\x00\x00\x00\xFF\xFF\x21\x21\x21\x33\x3F\x1E\x00\x00", + "γ": b"\x00\x60\xE0\x80\x00\x00\x00\x00\x80\xE0\x60\x00\x00\x00\x01\x07\x1E\xF8\xF8\x1E\x07\x01\x00\x00", + "δ": b"\x00\x83\xC7\xEF\x7F\x7B\x73\x63\xE3\xC3\x83\x00\x00\x0F\x1F\x38\x30\x30\x30\x30\x38\x1F\x0F\x00", + "ε": b"\x00\xE0\xF0\x30\x30\x30\x30\x30\x30\x70\x60\x00\x00\x1D\x3F\x33\x33\x33\x33\x33\x33\x38\x18\x00", + "ζ": b"\x00\x83\xC3\xE3\x63\x63\x63\x73\x3F\x1F\x00\x00\x00\x0F\x1F\x38\x30\x30\x30\xF0\xE0\x00\x00\x00", + "η": b"\x00\xE0\xE0\xC0\x60\x60\x60\xE0\xE0\xC0\x00\x00\x00\x1F\x1F\x00\x00\x00\x00\x00\x7F\x7F\x00\x00", + "θ": b"\x00\xF0\xF8\x1C\x8C\x8C\x8C\x8C\x1C\xF8\xF0\x00\x00\x0F\x1F\x38\x31\x31\x31\x31\x38\x1F\x0F\x00", + "ι": b"\x00\x00\x00\xE0\xE0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1F\x3F\x30\x30\x30\x00\x00\x00\x00", + "κ": b"\x00\x00\xC0\xC0\x00\x00\x80\xC0\xC0\x00\x00\x00\x00\x00\x3F\x3F\x06\x0F\x1F\x39\x30\x00\x00\x00", + "λ": b"\x00\x00\x00\x00\xC0\xE0\xE0\xC0\x00\x00\x00\x00\x00\x30\x3C\x0F\x03\x00\x00\x03\x0F\x3C\x30\x00", + "μ": b"\x00\xF0\xF0\x00\x00\x00\x00\x00\xF0\xF0\x00\x00\x00\xFF\xFF\x0E\x0C\x0C\x0C\x06\x0F\x0F\x00\x00", + "ν": b"\x00\x60\xE0\x80\x00\x00\x00\x00\x80\xE0\x60\x00\x00\x00\x01\x07\x1E\x38\x38\x1E\x07\x01\x00\x00", + "ξ": b"\x00\x3C\xFE\xE7\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x0F\x1F\x39\x30\x30\xF0\xE0\x00\x00\x00\x00", + "ο": b"\x00\x80\xC0\xE0\x60\x60\x60\x60\xE0\xC0\x80\x00\x00\x0F\x1F\x38\x30\x30\x30\x30\x38\x1F\x0F\x00", + "π": b"\x00\x60\xE0\xE0\x60\x60\x60\xE0\xE0\x60\x00\x00\x00\x00\x3F\x3F\x00\x00\x00\x3F\x3F\x30\x00\x00", + "ρ": b"\x00\xE0\xE0\x60\x60\x60\x60\x60\xE0\xC0\x80\x00\x00\xFF\xFF\x0C\x18\x18\x18\x18\x1C\x0F\x07\x00", + "ς": b"\x00\x80\xC0\xE0\x60\x60\x60\xE0\xC0\x80\x00\x00\x00\x0F\x1F\x38\x30\x30\xF0\xE0\x03\x03\x00\x00", + "σ": b"\x00\x80\xC0\xE0\x60\x60\x60\xE0\xE0\xE0\x60\x00\x00\x0F\x1F\x38\x30\x30\x30\x38\x1F\x0F\x00\x00", + "τ": b"\x00\x60\x60\xE0\xE0\x60\x60\x60\x00\x00\x00\x00\x00\x00\x00\x1F\x3F\x30\x30\x30\x00\x00\x00\x00", + "υ": b"\x00\xE0\xE0\x00\x00\x00\x00\x00\xE0\xE0\x00\x00\x00\x0F\x1F\x38\x30\x30\x30\x38\x1F\x0F\x00\x00", + "φ": b"\x00\xC0\xE0\x70\x30\xF8\xF8\x30\x70\xE0\xC0\x00\x00\x07\x0F\x1C\x18\x7F\x7F\x18\x1C\x0F\x07\x00", + "χ": b"\x00\x70\xF0\xC0\x80\x00\x80\xC0\xF0\x70\x00\x00\x00\x70\x78\x1D\x0F\x07\x0F\x1D\x78\x70\x00\x00", + "ψ": b"\x00\xE0\xE0\x00\x00\xE0\xE0\x00\x00\xE0\xE0\x00\x00\x07\x0F\x1C\x18\x7F\x7F\x18\x1C\x0F\x07\x00", + "ω": b"\x00\xC0\xE0\x00\x00\x00\x00\x00\x00\xE0\xC0\x00\x00\x0F\x1F\x38\x38\x1E\x1E\x38\x38\x1F\x0F\x00", + "ό": b"\x00\x80\xC0\xE0\x60\x6C\x6E\x66\xE0\xC0\x80\x00\x00\x0F\x1F\x38\x30\x30\x30\x30\x38\x1F\x0F\x00", + "ύ": b"\x00\xE0\xE0\x00\x00\x0C\x0E\x06\xE0\xE0\x00\x00\x00\x0F\x1F\x38\x30\x30\x30\x38\x1F\x0F\x00\x00", + "ώ": b"\x00\xC0\xE0\x00\x00\x18\x1C\x0C\x00\xE0\xC0\x00\x00\x0F\x1F\x38\x38\x1E\x1E\x38\x38\x1F\x0F\x00", + } + return font + + def get_font_map_cyrillic() -> Dict[str, bytes]: font = { # U+0400..U+04FF Cyrillic @@ -750,6 +814,70 @@ def get_small_font_map_latin_extended() -> Dict[str, bytes]: return font +def get_small_font_map_greek() -> Dict[str, bytes]: + font = { + # U+0370..U+03FF Greek and Coptic + "Έ": b"\x03\x7F\x49\x49\x49\x41", + "Α": b"\x7e\x09\x09\x09\x7e\x00", + "Β": b"\x7f\x49\x49\x49\x36\x00", + "Γ": b"\x7f\x01\x01\x01\x01\x00", + "Δ": b"\x70\x4C\x43\x4C\x70\x00", + "Ε": b"\x7f\x49\x49\x49\x41\x00", + "Ζ": b"\x61\x51\x49\x45\x43\x00", + "Η": b"\x7f\x08\x08\x08\x7f\x00", + "Θ": b"\x3E\x49\x49\x49\x3E\x00", + "Ι": b"\x00\x41\x7f\x41\x00\x00", + "Κ": b"\x7f\x08\x14\x22\x41\x00", + "Λ": b"\x70\x0C\x03\x0C\x70\x00", + "Μ": b"\x7f\x02\x0c\x02\x7f\x00", + "Ν": b"\x7f\x04\x08\x10\x7f\x00", + "Ξ": b"\x41\x49\x49\x49\x41\x00", + "Ο": b"\x3e\x41\x41\x41\x3e\x00", + "Π": b"\x7F\x01\x01\x01\x7F\x00", + "Ρ": b"\x7f\x09\x09\x09\x06\x00", + "Σ": b"\x63\x55\x49\x41\x41\x00", + "Τ": b"\x01\x01\x7f\x01\x01\x00", + "Υ": b"\x07\x08\x70\x08\x07\x00", + "Φ": b"\x0c\x12\x7f\x12\x0c\x00", + "Χ": b"\x63\x14\x08\x14\x63\x00", + "Ψ": b"\x07\x08\x7F\x08\x07\x00", + "Ω": b"\x5E\x61\x01\x61\x5E\x00", + "ά": b"\x38\x45\x45\x38\x7C\x40", + "έ": b"\x28\x55\x55\x44\x28\x00", + "ή": b"\x04\x79\x05\x04\xF8\x00", + "ί": b"\x04\x3D\x41\x40\x00\x00", + "α": b"\x38\x44\x44\x38\x7C\x40", + "β": b"\x7E\x21\x25\x25\x1A\x00", + "γ": b"\x0C\x10\x60\x10\x0C\x00", + "δ": b"\x30\x4B\x45\x49\x30\x00", + "ε": b"\x28\x54\x54\x44\x28\x00", + "ζ": b"\x00\x31\x49\x45\xC3\x00", + "η": b"\x04\x78\x04\x04\xF8\x00", + "θ": b"\x3E\x49\x49\x49\x3E\x00", + "ι": b"\x04\x3C\x40\x40\x00\x00", + "κ": b"\x00\x7C\x10\x28\x44\x00", + "λ": b"\x70\x0A\x04\x08\x70\x00", + "μ": b"\xFC\x10\x10\x3C\x20\x00", + "ν": b"\x1C\x20\x40\x20\x1C\x00", + "ξ": b"\x36\x49\x49\xC9\x00\x00", + "ο": b"\x38\x44\x44\x44\x38\x00", + "π": b"\x04\x7C\x04\x7C\x44\x00", + "ρ": b"\xF8\x24\x24\x24\x18\x00", + "ς": b"\x38\x44\x44\xC4\x04\x00", + "σ": b"\x38\x44\x44\x44\x3C\x04", + "τ": b"\x04\x3C\x44\x44\x00\x00", + "υ": b"\x3C\x40\x40\x40\x3C\x00", + "φ": b"\x18\x24\x7e\x24\x18\x00", + "χ": b"\x44\x24\x38\x48\x44\x00", + "ψ": b"\x1C\x20\x7C\x20\x1C\x00", + "ω": b"\x38\x44\x30\x44\x38\x00", + "ό": b"\x38\x45\x45\x44\x38\x00", + "ύ": b"\x3C\x41\x41\x40\x3C\x00", + "ώ": b"\x38\x45\x31\x44\x38\x00", + } + return font + + def get_small_font_map_cyrillic() -> Dict[str, bytes]: font = { # U+0400..U+04FF Cyrillic @@ -859,6 +987,7 @@ NAME_ASCII_BASIC: Final = "ascii_basic" NAME_LATIN_EXTENDED: Final = "latin_extended" NAME_CYRILLIC: Final = "cyrillic" NAME_CJK: Final = "cjk" +NAME_GREEK: Final = "greek" def get_font_maps_for_name( @@ -870,5 +999,7 @@ def get_font_maps_for_name( return get_font_map_latin_extended(), get_small_font_map_latin_extended() elif font_name == NAME_CYRILLIC: return get_font_map_cyrillic(), get_small_font_map_cyrillic() + elif font_name == NAME_GREEK: + return get_font_map_greek(), get_small_font_map_greek() else: raise ValueError("Invalid font name") diff --git a/Translations/make_translation.py b/Translations/make_translation.py index 99145216..085a3ec9 100755 --- a/Translations/make_translation.py +++ b/Translations/make_translation.py @@ -1,1370 +1,1370 @@ -#!/usr/bin/env python3 - -import argparse -import functools -import json -import hashlib -import logging -import os -import pickle -import re -import subprocess -import sys -from datetime import datetime -from itertools import chain -from pathlib import Path -from typing import BinaryIO, Dict, List, Optional, TextIO, Tuple, Union -from dataclasses import dataclass - -from bdflib import reader as bdfreader -from bdflib.model import Font, Glyph - -import font_tables -import brieflz -import objcopy - -logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) - -HERE = Path(__file__).resolve().parent - - -@functools.lru_cache(maxsize=None) -def cjk_font() -> Font: - with open(os.path.join(HERE, "wqy-bitmapsong/wenquanyi_9pt.bdf"), "rb") as f: - return bdfreader.read_bdf(f) - - -# Loading a single JSON file -def load_json(filename: str, skip_first_line: bool) -> dict: - with open(filename) as f: - if skip_first_line: - f.readline() - return json.loads(f.read()) - - -def get_language_unqiue_id(language_ascii_name: str): - """ - Given a language code, it will return a unique (enough) uint16_t id code - When we have a collision here we can tweak this, but language list should be fairly stable from now on - """ - return ( - int(hashlib.sha1(language_ascii_name.encode("utf-8")).hexdigest(), 16) % 0xFFFF - ) - - -def read_translation(json_root: Union[str, Path], lang_code: str) -> dict: - filename = f"translation_{lang_code}.json" - - file_with_path = os.path.join(json_root, filename) - - try: - lang = load_json(file_with_path, skip_first_line=False) - except json.decoder.JSONDecodeError as e: - logging.error(f"Failed to decode {filename}") - logging.exception(str(e)) - sys.exit(2) - - validate_langcode_matches_content(filename, lang) - - return lang - - -def validate_langcode_matches_content(filename: str, content: dict) -> None: - # Extract lang code from file name - lang_code = filename[12:-5].upper() - # ...and the one specified in the JSON file... - try: - lang_code_from_json = content["languageCode"] - except KeyError: - lang_code_from_json = "(missing)" - - # ...cause they should be the same! - if lang_code != lang_code_from_json: - raise ValueError( - f"Invalid languageCode {lang_code_from_json} in file {filename}" - ) - - -def write_start(f: TextIO): - f.write( - "// WARNING: THIS FILE WAS AUTO GENERATED BY make_translation.py. PLEASE DO NOT EDIT.\n" - ) - f.write("\n") - f.write('#include "Translation.h"\n') - - -def get_constants(build_version: str) -> List[Tuple[str, str]]: - # Extra constants that are used in the firmware that are shared across all languages - return [ - ("SymbolPlus", "+"), - ("SymbolMinus", "-"), - ("SymbolSpace", " "), - ("SymbolDot", "."), - ("SymbolDegC", "C"), - ("SymbolDegF", "F"), - ("SymbolMinutes", "M"), - ("SymbolSeconds", "S"), - ("SymbolWatts", "W"), - ("SymbolVolts", "V"), - ("SymbolDC", "DC"), - ("SymbolCellCount", "S"), - ("SymbolVersionNumber", build_version), - ] - - -def get_debug_menu() -> List[str]: - return [ - datetime.today().strftime("%d-%m-%y"), - "HW G ", - "HW M ", - "HW P ", - "Time ", - "Move ", - "RTip ", - "CTip ", - "CHan ", - "Vin ", - "ACC ", - "PWR ", - "Max ", - ] - - -def get_accel_names_list() -> List[str]: - return [ - "Scanning", - "None", - "MMA8652FC", - "LIS2DH12", - "BMA223", - "MSA301", - "SC7A20", - ] - - -def get_power_source_list() -> List[str]: - return [ - "DC", - "QC", - "PD", - ] - - -def get_letter_counts( - defs: dict, lang: dict, build_version: str -) -> Tuple[List[str], Dict[str, int]]: - text_list = [] - # iterate over all strings - obj = lang["menuOptions"] - for mod in defs["menuOptions"]: - eid = mod["id"] - text_list.append(obj[eid]["desc"]) - - obj = lang["messages"] - for mod in defs["messages"]: - eid = mod["id"] - if eid not in obj: - text_list.append(mod["default"]) - else: - text_list.append(obj[eid]) - - obj = lang["messagesWarn"] - for mod in defs["messagesWarn"]: - eid = mod["id"] - if isinstance(obj[eid], list): - text_list.append(obj[eid][0]) - text_list.append(obj[eid][1]) - else: - text_list.append(obj[eid]) - - obj = lang["characters"] - - for mod in defs["characters"]: - eid = mod["id"] - text_list.append(obj[eid]) - - obj = lang["menuOptions"] - for mod in defs["menuOptions"]: - eid = mod["id"] - if isinstance(obj[eid]["text2"], list): - text_list.append(obj[eid]["text2"][0]) - text_list.append(obj[eid]["text2"][1]) - else: - text_list.append(obj[eid]["text2"]) - - obj = lang["menuGroups"] - for mod in defs["menuGroups"]: - eid = mod["id"] - if isinstance(obj[eid]["text2"], list): - text_list.append(obj[eid]["text2"][0]) - text_list.append(obj[eid]["text2"][1]) - else: - text_list.append(obj[eid]["text2"]) - - obj = lang["menuGroups"] - for mod in defs["menuGroups"]: - eid = mod["id"] - text_list.append(obj[eid]["desc"]) - constants = get_constants(build_version) - for x in constants: - text_list.append(x[1]) - text_list.extend(get_debug_menu()) - text_list.extend(get_accel_names_list()) - text_list.extend(get_power_source_list()) - - # collapse all strings down into the composite letters and store totals for these - - symbol_counts: dict[str, int] = {} - for line in text_list: - line = line.replace("\n", "").replace("\r", "") - line = line.replace("\\n", "").replace("\\r", "") - if line: - for letter in line: - symbol_counts[letter] = symbol_counts.get(letter, 0) + 1 - # swap to Big -> little sort order - symbols_by_occurrence = [ - x[0] - for x in sorted( - symbol_counts.items(), key=lambda kv: (kv[1], kv[0]), reverse=True - ) - ] - return symbols_by_occurrence, symbol_counts - - -def get_cjk_glyph(sym: str) -> bytes: - glyph: Glyph = cjk_font()[ord(sym)] - - data = glyph.data - src_left, src_bottom, src_w, src_h = glyph.get_bounding_box() - dst_w = 12 - dst_h = 16 - - # The source data is a per-row list of ints. The first item is the bottom- - # most row. For each row, the LSB is the right-most pixel. - # Here, (x, y) is the coordinates with origin at the top-left. - def get_cell(x: int, y: int) -> bool: - # Adjust x coordinates by actual bounding box. - adj_x = x - src_left - if adj_x < 0 or adj_x >= src_w: - return False - # Adjust y coordinates by actual bounding box, then place the glyph - # baseline 3px above the bottom edge to make it centre-ish. - # This metric is optimized for WenQuanYi Bitmap Song 9pt and assumes - # each glyph is to be placed in a 12x12px box. - adj_y = y - (dst_h - src_h - src_bottom - 3) - if adj_y < 0 or adj_y >= src_h: - return False - if data[src_h - adj_y - 1] & (1 << (src_w - adj_x - 1)): - return True - else: - return False - - # A glyph in the font table is divided into upper and lower parts, each by - # 8px high. Each byte represents half if a column, with the LSB being the - # top-most pixel. The data goes from the left-most to the right-most column - # of the top half, then from the left-most to the right-most column of the - # bottom half. - bs = bytearray() - for block in range(2): - for c in range(dst_w): - b = 0 - for r in range(8): - if get_cell(c, r + 8 * block): - b |= 0x01 << r - bs.append(b) - return bytes(bs) - - -def get_bytes_from_font_index(index: int) -> bytes: - """ - Converts the font table index into its corresponding bytes - """ - - # We want to be able to use more than 254 symbols (excluding \x00 null - # terminator and \x01 new-line) in the font table but without making all - # the chars take 2 bytes. To do this, we use \xF1 to \xFF as lead bytes - # to designate double-byte chars, and leave the remaining as single-byte - # chars. - # - # For the sake of sanity, \x00 always means the end of string, so we skip - # \xF1\x00 and others in the mapping. - # - # Mapping example: - # - # 0x02 => 2 - # 0x03 => 3 - # ... - # 0xEF => 239 - # 0xF0 => 240 - # 0xF1 0x01 => 1 * 0xFF - 15 + 1 = 241 - # 0xF1 0x02 => 1 * 0xFF - 15 + 2 = 242 - # ... - # 0xF1 0xFF => 1 * 0xFF - 15 + 255 = 495 - # 0xF2 0x01 => 2 * 0xFF - 15 + 1 = 496 - # ... - # 0xF2 0xFF => 2 * 0xFF - 15 + 255 = 750 - # 0xF3 0x01 => 3 * 0xFF - 15 + 1 = 751 - # ... - # 0xFF 0xFF => 15 * 0xFF - 15 + 255 = 4065 - - if index < 0: - raise ValueError("index must be positive") - page = (index + 0x0E) // 0xFF - if page > 0x0F: - raise ValueError("page value out of range") - if page == 0: - return bytes([index]) - else: - # Into extended range - # Leader is 0xFz where z is the page number - # Following char is the remainder - leader = page + 0xF0 - value = ((index + 0x0E) % 0xFF) + 0x01 - - if leader > 0xFF or value > 0xFF: - raise ValueError("value is out of range") - return bytes([leader, value]) - - -def bytes_to_escaped(b: bytes) -> str: - return "".join((f"\\x{i:02X}" for i in b)) - - -def bytes_to_c_hex(b: bytes) -> str: - return ", ".join((f"0x{i:02X}" for i in b)) + "," - - -@dataclass -class FontMap: - font12: Dict[str, bytes] - font06: Dict[str, Optional[bytes]] - - -@dataclass -class FontMapsPerFont: - font12_maps: Dict[str, Dict[str, bytes]] - font06_maps: Dict[str, Dict[str, Optional[bytes]]] - sym_lists: Dict[str, List[str]] - - -def get_font_map_per_font(text_list: List[str], fonts: List[str]) -> FontMapsPerFont: - pending_sym_set = set(text_list) - if len(pending_sym_set) != len(text_list): - raise ValueError("`text_list` contains duplicated symbols") - - if fonts[0] != font_tables.NAME_ASCII_BASIC: - raise ValueError( - f'First item in `fonts` must be "{font_tables.NAME_ASCII_BASIC}"' - ) - - total_symbol_count = len(text_list) - # \x00 is for NULL termination and \x01 is for newline, so the maximum - # number of symbols allowed is as follow (see also the comments in - # `get_bytes_from_font_index`): - if total_symbol_count > (0x10 * 0xFF - 15) - 2: # 4063 - raise ValueError( - f"Error, too many used symbols for this version (total {total_symbol_count})" - ) - - logging.info(f"Generating fonts for {total_symbol_count} symbols") - - # Collect font bitmaps by the defined font order: - font12_maps: Dict[str, Dict[str, bytes]] = {} - font06_maps: Dict[str, Dict[str, Optional[bytes]]] = {} - sym_lists: Dict[str, List[str]] = {} - for font in fonts: - font12_maps[font] = {} - font12_map = font12_maps[font] - font06_maps[font] = {} - font06_map = font06_maps[font] - sym_lists[font] = [] - sym_list = sym_lists[font] - - if len(pending_sym_set) == 0: - logging.warning( - f"Font {font} not used because all symbols already have font bitmaps" - ) - continue - - if font == font_tables.NAME_CJK: - is_cjk = True - else: - is_cjk = False - font12: Dict[str, bytes] - font06: Dict[str, bytes] - font12, font06 = font_tables.get_font_maps_for_name(font) - - for sym in text_list: - if sym not in pending_sym_set: - continue - if is_cjk: - font12_line = get_cjk_glyph(sym) - if font12_line is None: - continue - font06_line = None - else: - try: - font12_line = font12[sym] - font06_line = font06[sym] - except KeyError: - continue - font12_map[sym] = font12_line - font06_map[sym] = font06_line - sym_list.append(sym) - pending_sym_set.remove(sym) - - if len(sym_list) == 0: - logging.warning(f"Font {font} not used by any symbols on the list") - if len(pending_sym_set) > 0: - raise KeyError(f"Symbols not found in specified fonts: {pending_sym_set}") - - return FontMapsPerFont(font12_maps, font06_maps, sym_lists) - - -def get_forced_first_symbols() -> List[str]: - forced_first_symbols = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] - return forced_first_symbols - - -def get_sym_list_and_font_map( - text_list: List[str], fonts: List[str] -) -> Tuple[List[str], Dict[str, List[str]], FontMap]: - font_maps = get_font_map_per_font(text_list, fonts) - font12_maps = font_maps.font12_maps - font06_maps = font_maps.font06_maps - - # Build the full font maps - font12_map = {} - font06_map = {} - for font in fonts: - font12_map.update(font12_maps[font]) - font06_map.update(font06_maps[font]) - - # Collect all symbols by the original symbol order, but also making sure - # all symbols with only large font must be placed after all symbols with - # both small and large fonts - sym_list_both_fonts = [] - sym_list_large_only = [] - for sym in text_list: - if font06_map[sym] is None: - sym_list_large_only.append(sym) - else: - sym_list_both_fonts.append(sym) - sym_list = sym_list_both_fonts + sym_list_large_only - - return sym_list, font_maps.sym_lists, FontMap(font12_map, font06_map) - - -def build_symbol_conversion_map(sym_list: List[str]) -> Dict[str, bytes]: - forced_first_symbols = get_forced_first_symbols() - if sym_list[: len(forced_first_symbols)] != forced_first_symbols: - raise ValueError("Symbol list does not start with forced_first_symbols.") - - # the text list is sorted - # allocate out these in their order as number codes - symbol_map: Dict[str, bytes] = {"\n": bytes([1])} - index = 2 # start at 2, as 0= null terminator,1 = new line - - # Assign symbol bytes by font index - for index, sym in enumerate(sym_list, index): - assert sym not in symbol_map - symbol_map[sym] = get_bytes_from_font_index(index) - - return symbol_map - - -def make_font_table_cpp( - sym_list: List[str], font_map: FontMap, symbol_map: Dict[str, bytes] -) -> str: - output_table = make_font_table_named_cpp( - "USER_FONT_12", sym_list, font_map.font12, symbol_map - ) - output_table += make_font_table_06_cpp(sym_list, font_map, symbol_map) - return output_table - - -def make_font_table_named_cpp( - name: Optional[str], - sym_list: List[str], - font_map: Dict[str, bytes], - symbol_map: Dict[str, bytes], -) -> str: - output_table = "" - if name: - output_table = f"const uint8_t {name}[] = {{\n" - for sym in sym_list: - output_table += f"{bytes_to_c_hex(font_map[sym])}//{bytes_to_escaped(symbol_map[sym])} -> {sym}\n" - if name: - output_table += f"}}; // {name}\n" - return output_table - - -def make_font_table_06_cpp( - sym_list: List[str], font_map: FontMap, symbol_map: Dict[str, bytes] -) -> str: - output_table = "const uint8_t USER_FONT_6x8[] = {\n" - for sym in sym_list: - font_bytes = font_map.font06[sym] - if font_bytes: - font_line = bytes_to_c_hex(font_bytes) - else: - font_line = "// " # placeholder - output_table += f"{font_line}//{bytes_to_escaped(symbol_map[sym])} -> {sym}\n" - output_table += "};\n" - return output_table - - -def convert_string_bytes(symbol_conversion_table: Dict[str, bytes], text: str) -> bytes: - # convert all of the symbols from the string into bytes for their content - output_string = b"" - for c in text.replace("\\r", "").replace("\\n", "\n"): - if c not in symbol_conversion_table: - logging.error(f"Missing font definition for {c}") - sys.exit(1) - else: - output_string += symbol_conversion_table[c] - return output_string - - -def convert_string(symbol_conversion_table: Dict[str, bytes], text: str) -> str: - # convert all of the symbols from the string into escapes for their content - return bytes_to_escaped(convert_string_bytes(symbol_conversion_table, text)) - - -def escape(string: str) -> str: - return json.dumps(string, ensure_ascii=False) - - -def write_bytes_as_c_array( - f: TextIO, name: str, data: bytes, indent: int = 2, bytes_per_line: int = 16 -) -> None: - f.write(f"const uint8_t {name}[] = {{\n") - for i in range(0, len(data), bytes_per_line): - f.write(" " * indent) - f.write(", ".join((f"0x{b:02X}" for b in data[i : i + bytes_per_line]))) - f.write(",\n") - f.write(f"}}; // {name}\n\n") - - -@dataclass -class LanguageData: - langs: List[dict] - defs: dict - build_version: str - sym_list: List[str] - sym_lists_by_font: Dict[str, List[str]] - font_map: FontMap - - -def prepare_language(lang: dict, defs: dict, build_version: str) -> LanguageData: - language_code: str = lang["languageCode"] - logging.info(f"Preparing language data for {language_code}") - # Iterate over all of the text to build up the symbols & counts - text_list, _ = get_letter_counts(defs, lang, build_version) - # From the letter counts, need to make a symbol translator & write out the font - fonts = lang["fonts"] - - forced_first_symbols = get_forced_first_symbols() - - # We enforce that numbers come first. - text_list = forced_first_symbols + [ - x for x in text_list if x not in forced_first_symbols - ] - - sym_list, sym_lists_by_font, font_map = get_sym_list_and_font_map(text_list, fonts) - return LanguageData( - [lang], defs, build_version, sym_list, sym_lists_by_font, font_map - ) - - -def prepare_languages( - langs: List[dict], defs: dict, build_version: str -) -> LanguageData: - language_codes: List[str] = [lang["languageCode"] for lang in langs] - logging.info(f"Preparing language data for {language_codes}") - - forced_first_symbols = get_forced_first_symbols() - - all_fonts = [ - font_tables.NAME_ASCII_BASIC, - font_tables.NAME_LATIN_EXTENDED, - font_tables.NAME_CYRILLIC, - font_tables.NAME_CJK, - ] - - # Build the full font maps - font12_map = {} - font06_map = {} - # Calculate total symbol counts per font: - total_sym_counts: Dict[str, Dict[str, int]] = {} - for lang in langs: - text_list, sym_counts = get_letter_counts(defs, lang, build_version) - fonts = lang["fonts"] - text_list = forced_first_symbols + [ - x for x in text_list if x not in forced_first_symbols - ] - font_maps = get_font_map_per_font(text_list, fonts) - for font in fonts: - font12_map.update(font_maps.font12_maps[font]) - font06_map.update(font_maps.font06_maps[font]) - for font, font_sym_list in font_maps.sym_lists.items(): - font_total_sym_counts = total_sym_counts.get(font, {}) - for sym in font_sym_list: - font_total_sym_counts[sym] = font_total_sym_counts.get( - sym, 0 - ) + sym_counts.get(sym, 0) - total_sym_counts[font] = font_total_sym_counts - - sym_lists_by_font: Dict[str, List[str]] = {} - combined_sym_list = [] - for font in all_fonts: - if font not in total_sym_counts: - continue - # swap to Big -> little sort order - current_sym_list = [ - x[0] - for x in sorted( - total_sym_counts[font].items(), - key=lambda kv: (kv[1], kv[0]), - reverse=True, - ) - ] - if font == font_tables.NAME_ASCII_BASIC: - # We enforce that numbers come first. - current_sym_list = forced_first_symbols + [ - x for x in current_sym_list if x not in forced_first_symbols - ] - sym_lists_by_font[font] = current_sym_list - combined_sym_list.extend(current_sym_list) - - return LanguageData( - langs, - defs, - build_version, - combined_sym_list, - sym_lists_by_font, - FontMap(font12_map, font06_map), - ) - - -def write_language( - data: LanguageData, - f: TextIO, - strings_bin: Optional[bytes] = None, - compress_font: bool = False, -) -> None: - if len(data.langs) > 1: - raise ValueError("More than 1 languages are provided") - lang = data.langs[0] - defs = data.defs - build_version = data.build_version - sym_list = data.sym_list - font_map = data.font_map - - symbol_conversion_table = build_symbol_conversion_map(sym_list) - - language_code: str = lang["languageCode"] - logging.info(f"Generating block for {language_code}") - - try: - lang_name = lang["languageLocalName"] - except KeyError: - lang_name = language_code - - if strings_bin or compress_font: - f.write('#include "brieflz.h"\n') - - f.write(f"\n// ---- {lang_name} ----\n\n") - - if not compress_font: - font_table_text = make_font_table_cpp( - sym_list, font_map, symbol_conversion_table - ) - f.write(font_table_text) - f.write( - "const FontSection FontSectionsData[] = {\n" - " {\n" - " .symbol_start = 2,\n" - f" .symbol_end = {len(sym_list) + 2},\n" - " .font12_start_ptr = USER_FONT_12,\n" - " .font06_start_ptr = USER_FONT_6x8,\n" - " },\n" - "};\n" - "const FontSection *const FontSections = FontSectionsData;\n" - "const uint8_t FontSectionsCount = sizeof(FontSectionsData) / sizeof(FontSectionsData[0]);\n" - ) - else: - font12_uncompressed = bytearray() - for sym in sym_list: - font12_uncompressed.extend(font_map.font12[sym]) - font12_compressed = brieflz.compress(bytes(font12_uncompressed)) - logging.info( - f"Font table 12x16 compressed from {len(font12_uncompressed)} to {len(font12_compressed)} bytes (ratio {len(font12_compressed) / len(font12_uncompressed):.3})" - ) - write_bytes_as_c_array(f, "font_12x16_brieflz", font12_compressed) - font_table_text = make_font_table_06_cpp( - sym_list, font_map, symbol_conversion_table - ) - f.write(font_table_text) - f.write( - f"static uint8_t font_out_buffer[{len(font12_uncompressed)}];\n" - "const FontSection FontSectionsData[] = {\n" - " {\n" - " .symbol_start = 2,\n" - f" .symbol_end = {len(sym_list) + 2},\n" - " .font12_start_ptr = font_out_buffer,\n" - " .font06_start_ptr = USER_FONT_6x8,\n" - " },\n" - "};\n" - "const FontSection *const FontSections = FontSectionsData;\n" - "const uint8_t FontSectionsCount = sizeof(FontSectionsData) / sizeof(FontSectionsData[0]);\n" - ) - - f.write(f"\n// ---- {lang_name} ----\n\n") - - translation_common_text = get_translation_common_text( - defs, symbol_conversion_table, build_version - ) - f.write(translation_common_text) - f.write( - f"const bool HasFahrenheit = {('true' if lang.get('tempUnitFahrenheit', True) else 'false')};\n\n" - ) - - if not strings_bin: - translation_strings_and_indices_text = get_translation_strings_and_indices_text( - lang, defs, symbol_conversion_table - ) - f.write(translation_strings_and_indices_text) - f.write( - "const TranslationIndexTable *Tr = &translation.indices;\n" - "const char *TranslationStrings = translation.strings;\n\n" - ) - else: - compressed = brieflz.compress(strings_bin) - logging.info( - f"Strings compressed from {len(strings_bin)} to {len(compressed)} bytes (ratio {len(compressed) / len(strings_bin):.3})" - ) - write_bytes_as_c_array(f, "translation_data_brieflz", compressed) - f.write( - f"static uint8_t translation_data_out_buffer[{len(strings_bin)}] __attribute__((__aligned__(2)));\n\n" - "const TranslationIndexTable *Tr = reinterpret_cast(translation_data_out_buffer);\n" - "const char *TranslationStrings = reinterpret_cast(translation_data_out_buffer) + sizeof(TranslationIndexTable);\n\n" - ) - - if not strings_bin and not compress_font: - f.write("void prepareTranslations() {}\n\n") - else: - f.write("void prepareTranslations() {\n") - if compress_font: - f.write( - " blz_depack_srcsize(font_12x16_brieflz, font_out_buffer, sizeof(font_12x16_brieflz));\n" - ) - if strings_bin: - f.write( - " blz_depack_srcsize(translation_data_brieflz, translation_data_out_buffer, sizeof(translation_data_brieflz));\n" - ) - f.write("}\n\n") - - sanity_checks_text = get_translation_sanity_checks_text(defs) - f.write(sanity_checks_text) - - -def write_languages( - data: LanguageData, - f: TextIO, - strings_obj_path: Optional[str] = None, - compress_font: bool = False, -) -> None: - defs = data.defs - build_version = data.build_version - combined_sym_list = data.sym_list - sym_lists_by_font = data.sym_lists_by_font - font_map = data.font_map - - symbol_conversion_table = build_symbol_conversion_map(combined_sym_list) - - language_codes: List[str] = [lang["languageCode"] for lang in data.langs] - logging.info(f"Generating block for {language_codes}") - - lang_names = [ - lang.get("languageLocalName", lang["languageCode"]) for lang in data.langs - ] - - f.write('#include "Translation_multi.h"') - - f.write(f"\n// ---- {lang_names} ----\n\n") - - max_decompressed_font_size = 0 - if not compress_font: - font_table_text = "" - font_section_info_text = ( - "const FontSectionDataInfo FontSectionDataInfos[] = {\n" - ) - for font, current_sym_list in sym_lists_by_font.items(): - font_table_text += f"const uint8_t font_table_data_{font}[] = {{\n" - font_table_text += "// 12x16:\n" - font_table_text += make_font_table_named_cpp( - None, - current_sym_list, - font_map.font12, - symbol_conversion_table, - ) - if font != font_tables.NAME_CJK: - font_table_text += "// 6x8:\n" - font_table_text += make_font_table_named_cpp( - None, - current_sym_list, - font_map.font06, # type: ignore[arg-type] - symbol_conversion_table, - ) - font_table_text += f"}}; // font_table_data_{font}\n" - current_sym_start = combined_sym_list.index(current_sym_list[0]) + 2 - font_section_info_text += ( - " {\n" - f" .symbol_start = {current_sym_start},\n" - f" .symbol_count = {len(current_sym_list)},\n" - f" .data_size = sizeof(font_table_data_{font}),\n" - " .data_is_compressed = false,\n" - f" .data_ptr = font_table_data_{font},\n" - " },\n" - ) - - f.write(font_table_text) - font_section_info_text += ( - "};\n" - "const uint8_t FontSectionDataCount = sizeof(FontSectionDataInfos) / sizeof(FontSectionDataInfos[0]);\n\n" - ) - f.write(font_section_info_text) - f.write( - "FontSection DynamicFontSections[4] = {};\n" - "const FontSection *const FontSections = DynamicFontSections;\n" - "const uint8_t FontSectionsCount = sizeof(DynamicFontSections) / sizeof(DynamicFontSections[0]);\n" - ) - else: - font_section_info_text = ( - "const FontSectionDataInfo FontSectionDataInfos[] = {\n" - ) - for font, current_sym_list in sym_lists_by_font.items(): - current_sym_start = combined_sym_list.index(current_sym_list[0]) + 2 - font_uncompressed = bytearray() - for sym in current_sym_list: - font_uncompressed.extend(font_map.font12[sym]) - if font != font_tables.NAME_CJK: - for sym in current_sym_list: - font_uncompressed.extend(font_map.font06[sym]) # type: ignore[arg-type] - font_compressed = brieflz.compress(bytes(font_uncompressed)) - logging.info( - f"Font table for {font} compressed from {len(font_uncompressed)} to {len(font_compressed)} bytes (ratio {len(font_compressed) / len(font_uncompressed):.3})" - ) - max_decompressed_font_size += len(font_uncompressed) - write_bytes_as_c_array(f, f"font_data_brieflz_{font}", font_compressed) - font_section_info_text += ( - " {\n" - f" .symbol_start = {current_sym_start},\n" - f" .symbol_count = {len(current_sym_list)},\n" - f" .data_size = sizeof(font_data_brieflz_{font}),\n" - " .data_is_compressed = true,\n" - f" .data_ptr = font_data_brieflz_{font},\n" - " },\n" - ) - font_section_info_text += ( - "};\n" - "const uint8_t FontSectionDataCount = sizeof(FontSectionDataInfos) / sizeof(FontSectionDataInfos[0]);\n\n" - ) - f.write(font_section_info_text) - f.write( - "FontSection DynamicFontSections[4] = {};\n" - "const FontSection *const FontSections = DynamicFontSections;\n" - "const uint8_t FontSectionsCount = sizeof(DynamicFontSections) / sizeof(DynamicFontSections[0]);\n" - ) - - f.write(f"\n// ---- {lang_names} ----\n\n") - - translation_common_text = get_translation_common_text( - defs, symbol_conversion_table, build_version - ) - f.write(translation_common_text) - f.write( - f"const bool HasFahrenheit = {('true' if any([lang.get('tempUnitFahrenheit', True) for lang in data.langs]) else 'false')};\n\n" - ) - - max_decompressed_translation_size = 0 - if not strings_obj_path: - for lang in data.langs: - lang_code = lang["languageCode"] - translation_strings_and_indices_text = ( - get_translation_strings_and_indices_text( - lang, defs, symbol_conversion_table, suffix=f"_{lang_code}" - ) - ) - f.write(translation_strings_and_indices_text) - f.write("const LanguageMeta LanguageMetas[] = {\n") - for lang in data.langs: - lang_code = lang["languageCode"] - lang_id = get_language_unqiue_id(lang_code) - f.write( - " {\n" - f" .uniqueID = {lang_id},\n" - f" .translation_data = reinterpret_cast(&translation_{lang_code}),\n" - f" .translation_size = sizeof(translation_{lang_code}),\n" - f" .translation_is_compressed = false,\n" - " },\n" - ) - f.write("};\n") - else: - for lang in data.langs: - lang_code = lang["languageCode"] - sym_name = objcopy.cpp_var_to_section_name(f"translation_{lang_code}") - strings_bin = objcopy.get_binary_from_obj(strings_obj_path, sym_name) - if len(strings_bin) == 0: - raise ValueError(f"Output for {sym_name} is empty") - max_decompressed_translation_size = max( - max_decompressed_translation_size, len(strings_bin) - ) - compressed = brieflz.compress(strings_bin) - logging.info( - f"Strings for {lang_code} compressed from {len(strings_bin)} to {len(compressed)} bytes (ratio {len(compressed) / len(strings_bin):.3})" - ) - write_bytes_as_c_array( - f, f"translation_data_brieflz_{lang_code}", compressed - ) - f.write("const LanguageMeta LanguageMetas[] = {\n") - for lang in data.langs: - lang_code = lang["languageCode"] - lang_id = get_language_unqiue_id(lang_code) - f.write( - " {\n" - f" .uniqueID = {lang_id},\n" - f" .translation_data = translation_data_brieflz_{lang_code},\n" - f" .translation_size = sizeof(translation_data_brieflz_{lang_code}),\n" - f" .translation_is_compressed = true,\n" - " },\n" - ) - f.write("};\n") - f.write( - "const uint8_t LanguageCount = sizeof(LanguageMetas) / sizeof(LanguageMetas[0]);\n\n" - f"alignas(TranslationData) uint8_t translation_data_out_buffer[{max_decompressed_translation_size + max_decompressed_font_size}];\n" - "const uint16_t translation_data_out_buffer_size = sizeof(translation_data_out_buffer);\n\n" - ) - - sanity_checks_text = get_translation_sanity_checks_text(defs) - f.write(sanity_checks_text) - - -def get_translation_common_text( - defs: dict, symbol_conversion_table: Dict[str, bytes], build_version -) -> str: - translation_common_text = "" - - # Write out firmware constant options - constants = get_constants(build_version) - for x in constants: - translation_common_text += f'const char* {x[0]} = "{convert_string(symbol_conversion_table, x[1])}";//{x[1]} \n' - translation_common_text += "\n" - - # Debug Menu - translation_common_text += "const char* DebugMenu[] = {\n" - - for c in get_debug_menu(): - translation_common_text += ( - f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n' - ) - translation_common_text += "};\n\n" - - # accel names - translation_common_text += "const char* AccelTypeNames[] = {\n" - - for c in get_accel_names_list(): - translation_common_text += ( - f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n' - ) - translation_common_text += "};\n\n" - - # power source types - translation_common_text += "const char* PowerSourceNames[] = {\n" - - for c in get_power_source_list(): - translation_common_text += ( - f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n' - ) - translation_common_text += "};\n\n" - - return translation_common_text - - -@dataclass -class TranslationItem: - info: str - str_index: int - - -def get_translation_strings_and_indices_text( - lang: dict, defs: dict, symbol_conversion_table: Dict[str, bytes], suffix: str = "" -) -> str: - str_table: List[str] = [] - str_group_messages: List[TranslationItem] = [] - str_group_messageswarn: List[TranslationItem] = [] - str_group_characters: List[TranslationItem] = [] - str_group_settingdesc: List[TranslationItem] = [] - str_group_settingshortnames: List[TranslationItem] = [] - str_group_settingmenuentries: List[TranslationItem] = [] - str_group_settingmenuentriesdesc: List[TranslationItem] = [] - - eid: str - - # ----- Reading SettingsDescriptions - obj = lang["menuOptions"] - - for index, mod in enumerate(defs["menuOptions"]): - eid = mod["id"] - str_group_settingdesc.append( - TranslationItem(f"[{index:02d}] {eid}", len(str_table)) - ) - str_table.append(obj[eid]["desc"]) - - # ----- Reading Message strings - - obj = lang["messages"] - - for mod in defs["messages"]: - eid = mod["id"] - source_text = "" - if "default" in mod: - source_text = mod["default"] - if eid in obj: - source_text = obj[eid] - str_group_messages.append(TranslationItem(eid, len(str_table))) - str_table.append(source_text) - - obj = lang["messagesWarn"] - - for mod in defs["messagesWarn"]: - eid = mod["id"] - if isinstance(obj[eid], list): - if not obj[eid][1]: - source_text = obj[eid][0] - else: - source_text = obj[eid][0] + "\n" + obj[eid][1] - else: - source_text = "\n" + obj[eid] - str_group_messageswarn.append(TranslationItem(eid, len(str_table))) - str_table.append(source_text) - - # ----- Reading Characters - - obj = lang["characters"] - - for mod in defs["characters"]: - eid = mod["id"] - str_group_characters.append(TranslationItem(eid, len(str_table))) - str_table.append(obj[eid]) - - # ----- Reading SettingsDescriptions - obj = lang["menuOptions"] - - for index, mod in enumerate(defs["menuOptions"]): - eid = mod["id"] - if isinstance(obj[eid]["text2"], list): - if not obj[eid]["text2"][1]: - source_text = obj[eid]["text2"][0] - else: - source_text = obj[eid]["text2"][0] + "\n" + obj[eid]["text2"][1] - else: - source_text = "\n" + obj[eid]["text2"] - str_group_settingshortnames.append( - TranslationItem(f"[{index:02d}] {eid}", len(str_table)) - ) - str_table.append(source_text) - - # ----- Reading Menu Groups - obj = lang["menuGroups"] - - for index, mod in enumerate(defs["menuGroups"]): - eid = mod["id"] - if isinstance(obj[eid]["text2"], list): - if not obj[eid]["text2"][1]: - source_text = obj[eid]["text2"][0] - else: - source_text = obj[eid]["text2"][0] + "\n" + obj[eid]["text2"][1] - else: - source_text = "\n" + obj[eid]["text2"] - str_group_settingmenuentries.append( - TranslationItem(f"[{index:02d}] {eid}", len(str_table)) - ) - str_table.append(source_text) - - # ----- Reading Menu Groups Descriptions - obj = lang["menuGroups"] - - for index, mod in enumerate(defs["menuGroups"]): - eid = mod["id"] - str_group_settingmenuentriesdesc.append( - TranslationItem(f"[{index:02d}] {eid}", len(str_table)) - ) - str_table.append(obj[eid]["desc"]) - - @dataclass - class RemappedTranslationItem: - str_index: int - str_start_offset: int = 0 - - # ----- Perform suffix merging optimization: - # - # We sort the backward strings so that strings with the same suffix will - # be next to each other, e.g.: - # "ef\0", - # "cdef\0", - # "abcdef\0", - backward_sorted_table: List[Tuple[int, str, bytes]] = sorted( - ( - (i, s, bytes(reversed(convert_string_bytes(symbol_conversion_table, s)))) - for i, s in enumerate(str_table) - ), - key=lambda x: x[2], - ) - str_remapping: List[Optional[RemappedTranslationItem]] = [None] * len(str_table) - for i, (str_index, source_str, converted) in enumerate(backward_sorted_table[:-1]): - j = i - while backward_sorted_table[j + 1][2].startswith(converted): - j += 1 - if j + 1 == len(backward_sorted_table): - break - if j != i: - str_remapping[str_index] = RemappedTranslationItem( - str_index=backward_sorted_table[j][0], - str_start_offset=len(backward_sorted_table[j][2]) - len(converted), - ) - - # ----- Write the string table: - str_offsets = [-1] * len(str_table) - offset = 0 - write_null = False - # NOTE: Cannot specify C99 designator here due to GCC (g++) bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55227 - translation_strings_text = " /* .strings = */ {\n" - for i, source_str in enumerate(str_table): - if str_remapping[i] is not None: - continue - if write_null: - translation_strings_text += ' "\\0"\n' - write_null = True - # Find what items use this string - str_used_by = [i] + [ - j for j, r in enumerate(str_remapping) if r and r.str_index == i - ] - for j in str_used_by: - for group, pre_info in [ - (str_group_messages, "messages"), - (str_group_messageswarn, "messagesWarn"), - (str_group_characters, "characters"), - (str_group_settingdesc, "SettingsDescriptions"), - (str_group_settingshortnames, "SettingsShortNames"), - (str_group_settingmenuentries, "SettingsMenuEntries"), - (str_group_settingmenuentriesdesc, "SettingsMenuEntriesDescriptions"), - ]: - for item in group: - if item.str_index == j: - translation_strings_text += ( - f" // - {pre_info} {item.info}\n" - ) - if j == i: - translation_strings_text += ( - f" // {offset: >4}: {escape(source_str)}\n" - ) - str_offsets[j] = offset - else: - remapped = str_remapping[j] - assert remapped is not None - translation_strings_text += f" // {offset + remapped.str_start_offset: >4}: {escape(str_table[j])}\n" - str_offsets[j] = offset + remapped.str_start_offset - converted_bytes = convert_string_bytes(symbol_conversion_table, source_str) - translation_strings_text += f' "{bytes_to_escaped(converted_bytes)}"' - str_offsets[i] = offset - # Add the length and the null terminator - offset += len(converted_bytes) + 1 - translation_strings_text += "\n }, // .strings\n\n" - - str_total_bytes = offset - - def get_offset(idx: int) -> int: - assert str_offsets[idx] >= 0 - return str_offsets[idx] - - translation_indices_text = " .indices = {\n" - - # ----- Write the messages string indices: - for group in [str_group_messages, str_group_messageswarn, str_group_characters]: - for item in group: - translation_indices_text += f" .{item.info} = {get_offset(item.str_index)}, // {escape(str_table[item.str_index])}\n" - translation_indices_text += "\n" - - # ----- Write the settings index tables: - for group, name in [ - (str_group_settingdesc, "SettingsDescriptions"), - (str_group_settingshortnames, "SettingsShortNames"), - (str_group_settingmenuentries, "SettingsMenuEntries"), - (str_group_settingmenuentriesdesc, "SettingsMenuEntriesDescriptions"), - ]: - max_len = 30 - translation_indices_text += f" .{name} = {{\n" - for item in group: - translation_indices_text += f" /* {item.info.ljust(max_len)[:max_len]} */ {get_offset(item.str_index)}, // {escape(str_table[item.str_index])}\n" - translation_indices_text += f" }}, // {name}\n\n" - - translation_indices_text += " }, // .indices\n\n" - - return ( - "struct {\n" - " TranslationIndexTable indices;\n" - f" char strings[{str_total_bytes}];\n" - f"}} const translation{suffix} = {{\n" - + translation_indices_text - + translation_strings_text - + f"}}; // translation{suffix}\n\n" - ) - - -def get_translation_sanity_checks_text(defs: dict) -> str: - sanity_checks_text = "\n// Verify SettingsItemIndex values:\n" - for i, mod in enumerate(defs["menuOptions"]): - eid = mod["id"] - sanity_checks_text += ( - f"static_assert(static_cast(SettingsItemIndex::{eid}) == {i});\n" - ) - sanity_checks_text += f"static_assert(static_cast(SettingsItemIndex::NUM_ITEMS) == {len(defs['menuOptions'])});\n" - return sanity_checks_text - - -def read_version() -> str: - with open(HERE.parent / "source" / "version.h") as version_file: - for line in version_file: - if re.findall(r"^.*(?<=(#define)).*(?<=(BUILD_VERSION))", line): - matches = re.findall(r"\"(.+?)\"", line) - if matches: - version = matches[0] - try: - version += f".{subprocess.check_output(['git', 'rev-parse', '--short=7', 'HEAD']).strip().decode('ascii').upper()}" - # --short=7: the shorted hash with 7 digits. Increase/decrease if needed! - except OSError: - version += " git" - return version - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - parser.add_argument( - "--output-pickled", - help="Write pickled language data for later reuse", - type=argparse.FileType("wb"), - required=False, - dest="output_pickled", - ) - parser.add_argument( - "--input-pickled", - help="Use previously generated pickled language data", - type=argparse.FileType("rb"), - required=False, - dest="input_pickled", - ) - parser.add_argument( - "--strings-obj", - help="Use generated TranslationData by extracting from object file", - type=argparse.FileType("rb"), - required=False, - dest="strings_obj", - ) - parser.add_argument( - "--compress-font", - help="Compress the font table", - action="store_true", - required=False, - dest="compress_font", - ) - parser.add_argument( - "--output", "-o", help="Target file", type=argparse.FileType("w"), required=True - ) - parser.add_argument( - "languageCodes", - metavar="languageCode", - nargs="+", - help="Language(s) to generate", - ) - return parser.parse_args() - - -def main() -> None: - json_dir = HERE - - args = parse_args() - if args.input_pickled and args.output_pickled: - logging.error("error: Both --output-pickled and --input-pickled are specified") - sys.exit(1) - - language_data: LanguageData - if args.input_pickled: - logging.info(f"Reading pickled language data from {args.input_pickled.name}...") - language_data = pickle.load(args.input_pickled) - language_codes = [lang["languageCode"] for lang in language_data.langs] - if language_codes != args.languageCodes: - logging.error( - f"error: languageCode {args.languageCode} does not match language data {language_codes}" - ) - sys.exit(1) - logging.info(f"Read language data for {language_codes}") - logging.info(f"Build version: {language_data.build_version}") - else: - try: - build_version = read_version() - except FileNotFoundError: - logging.error("error: Could not find version info ") - sys.exit(1) - - logging.info(f"Build version: {build_version}") - logging.info(f"Making {args.languageCodes} from {json_dir}") - - defs_ = load_json(os.path.join(json_dir, "translations_def.js"), True) - if len(args.languageCodes) == 1: - lang_ = read_translation(json_dir, args.languageCodes[0]) - language_data = prepare_language(lang_, defs_, build_version) - else: - langs_ = [ - read_translation(json_dir, lang_code) - for lang_code in args.languageCodes - ] - language_data = prepare_languages(langs_, defs_, build_version) - - out_ = args.output - write_start(out_) - if len(language_data.langs) == 1: - if args.strings_obj: - sym_name = objcopy.cpp_var_to_section_name("translation") - strings_bin = objcopy.get_binary_from_obj(args.strings_obj.name, sym_name) - if len(strings_bin) == 0: - raise ValueError(f"Output for {sym_name} is empty") - write_language( - language_data, - out_, - strings_bin=strings_bin, - compress_font=args.compress_font, - ) - else: - write_language(language_data, out_, compress_font=args.compress_font) - else: - if args.strings_obj: - write_languages( - language_data, - out_, - strings_obj_path=args.strings_obj.name, - compress_font=args.compress_font, - ) - else: - write_languages(language_data, out_, compress_font=args.compress_font) - - if args.output_pickled: - logging.info(f"Writing pickled data to {args.output_pickled.name}") - pickle.dump(language_data, args.output_pickled) - - logging.info("Done") - - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 + +import argparse +import functools +import json +import hashlib +import logging +import os +import pickle +import re +import subprocess +import sys +from datetime import datetime +from itertools import chain +from pathlib import Path +from typing import BinaryIO, Dict, List, Optional, TextIO, Tuple, Union +from dataclasses import dataclass + +from bdflib import reader as bdfreader +from bdflib.model import Font, Glyph + +import font_tables +import brieflz +import objcopy + +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + +HERE = Path(__file__).resolve().parent + + +@functools.lru_cache(maxsize=None) +def cjk_font() -> Font: + with open(os.path.join(HERE, "wqy-bitmapsong/wenquanyi_9pt.bdf"), "rb") as f: + return bdfreader.read_bdf(f) + + +# Loading a single JSON file +def load_json(filename: str, skip_first_line: bool) -> dict: + with open(filename) as f: + if skip_first_line: + f.readline() + return json.loads(f.read()) + + +def get_language_unqiue_id(language_ascii_name: str): + """ + Given a language code, it will return a unique (enough) uint16_t id code + When we have a collision here we can tweak this, but language list should be fairly stable from now on + """ + return ( + int(hashlib.sha1(language_ascii_name.encode("utf-8")).hexdigest(), 16) % 0xFFFF + ) + + +def read_translation(json_root: Union[str, Path], lang_code: str) -> dict: + filename = f"translation_{lang_code}.json" + + file_with_path = os.path.join(json_root, filename) + + try: + lang = load_json(file_with_path, skip_first_line=False) + except json.decoder.JSONDecodeError as e: + logging.error(f"Failed to decode {filename}") + logging.exception(str(e)) + sys.exit(2) + + validate_langcode_matches_content(filename, lang) + + return lang + + +def validate_langcode_matches_content(filename: str, content: dict) -> None: + # Extract lang code from file name + lang_code = filename[12:-5].upper() + # ...and the one specified in the JSON file... + try: + lang_code_from_json = content["languageCode"] + except KeyError: + lang_code_from_json = "(missing)" + + # ...cause they should be the same! + if lang_code != lang_code_from_json: + raise ValueError( + f"Invalid languageCode {lang_code_from_json} in file {filename}" + ) + + +def write_start(f: TextIO): + f.write( + "// WARNING: THIS FILE WAS AUTO GENERATED BY make_translation.py. PLEASE DO NOT EDIT.\n" + ) + f.write("\n") + f.write('#include "Translation.h"\n') + + +def get_constants(build_version: str) -> List[Tuple[str, str]]: + # Extra constants that are used in the firmware that are shared across all languages + return [ + ("SymbolPlus", "+"), + ("SymbolMinus", "-"), + ("SymbolSpace", " "), + ("SymbolDot", "."), + ("SymbolDegC", "C"), + ("SymbolDegF", "F"), + ("SymbolMinutes", "M"), + ("SymbolSeconds", "S"), + ("SymbolWatts", "W"), + ("SymbolVolts", "V"), + ("SymbolDC", "DC"), + ("SymbolCellCount", "S"), + ("SymbolVersionNumber", build_version), + ] + + +def get_debug_menu() -> List[str]: + return [ + datetime.today().strftime("%d-%m-%y"), + "HW G ", + "HW M ", + "HW P ", + "Time ", + "Move ", + "RTip ", + "CTip ", + "CHan ", + "Vin ", + "ACC ", + "PWR ", + "Max ", + ] + + +def get_accel_names_list() -> List[str]: + return [ + "Scanning", + "None", + "MMA8652FC", + "LIS2DH12", + "BMA223", + "MSA301", + "SC7A20", + ] + + +def get_power_source_list() -> List[str]: + return [ + "DC", + "QC", + "PD", + ] + + +def get_letter_counts( + defs: dict, lang: dict, build_version: str +) -> Tuple[List[str], Dict[str, int]]: + text_list = [] + # iterate over all strings + obj = lang["menuOptions"] + for mod in defs["menuOptions"]: + eid = mod["id"] + text_list.append(obj[eid]["desc"]) + + obj = lang["messages"] + for mod in defs["messages"]: + eid = mod["id"] + if eid not in obj: + text_list.append(mod["default"]) + else: + text_list.append(obj[eid]) + + obj = lang["messagesWarn"] + for mod in defs["messagesWarn"]: + eid = mod["id"] + if isinstance(obj[eid], list): + text_list.append(obj[eid][0]) + text_list.append(obj[eid][1]) + else: + text_list.append(obj[eid]) + + obj = lang["characters"] + + for mod in defs["characters"]: + eid = mod["id"] + text_list.append(obj[eid]) + + obj = lang["menuOptions"] + for mod in defs["menuOptions"]: + eid = mod["id"] + if isinstance(obj[eid]["text2"], list): + text_list.append(obj[eid]["text2"][0]) + text_list.append(obj[eid]["text2"][1]) + else: + text_list.append(obj[eid]["text2"]) + + obj = lang["menuGroups"] + for mod in defs["menuGroups"]: + eid = mod["id"] + if isinstance(obj[eid]["text2"], list): + text_list.append(obj[eid]["text2"][0]) + text_list.append(obj[eid]["text2"][1]) + else: + text_list.append(obj[eid]["text2"]) + + obj = lang["menuGroups"] + for mod in defs["menuGroups"]: + eid = mod["id"] + text_list.append(obj[eid]["desc"]) + constants = get_constants(build_version) + for x in constants: + text_list.append(x[1]) + text_list.extend(get_debug_menu()) + text_list.extend(get_accel_names_list()) + text_list.extend(get_power_source_list()) + + # collapse all strings down into the composite letters and store totals for these + + symbol_counts: dict[str, int] = {} + for line in text_list: + line = line.replace("\n", "").replace("\r", "") + line = line.replace("\\n", "").replace("\\r", "") + if line: + for letter in line: + symbol_counts[letter] = symbol_counts.get(letter, 0) + 1 + # swap to Big -> little sort order + symbols_by_occurrence = [ + x[0] + for x in sorted( + symbol_counts.items(), key=lambda kv: (kv[1], kv[0]), reverse=True + ) + ] + return symbols_by_occurrence, symbol_counts + + +def get_cjk_glyph(sym: str) -> bytes: + glyph: Glyph = cjk_font()[ord(sym)] + + data = glyph.data + src_left, src_bottom, src_w, src_h = glyph.get_bounding_box() + dst_w = 12 + dst_h = 16 + + # The source data is a per-row list of ints. The first item is the bottom- + # most row. For each row, the LSB is the right-most pixel. + # Here, (x, y) is the coordinates with origin at the top-left. + def get_cell(x: int, y: int) -> bool: + # Adjust x coordinates by actual bounding box. + adj_x = x - src_left + if adj_x < 0 or adj_x >= src_w: + return False + # Adjust y coordinates by actual bounding box, then place the glyph + # baseline 3px above the bottom edge to make it centre-ish. + # This metric is optimized for WenQuanYi Bitmap Song 9pt and assumes + # each glyph is to be placed in a 12x12px box. + adj_y = y - (dst_h - src_h - src_bottom - 3) + if adj_y < 0 or adj_y >= src_h: + return False + if data[src_h - adj_y - 1] & (1 << (src_w - adj_x - 1)): + return True + else: + return False + + # A glyph in the font table is divided into upper and lower parts, each by + # 8px high. Each byte represents half if a column, with the LSB being the + # top-most pixel. The data goes from the left-most to the right-most column + # of the top half, then from the left-most to the right-most column of the + # bottom half. + bs = bytearray() + for block in range(2): + for c in range(dst_w): + b = 0 + for r in range(8): + if get_cell(c, r + 8 * block): + b |= 0x01 << r + bs.append(b) + return bytes(bs) + + +def get_bytes_from_font_index(index: int) -> bytes: + """ + Converts the font table index into its corresponding bytes + """ + + # We want to be able to use more than 254 symbols (excluding \x00 null + # terminator and \x01 new-line) in the font table but without making all + # the chars take 2 bytes. To do this, we use \xF1 to \xFF as lead bytes + # to designate double-byte chars, and leave the remaining as single-byte + # chars. + # + # For the sake of sanity, \x00 always means the end of string, so we skip + # \xF1\x00 and others in the mapping. + # + # Mapping example: + # + # 0x02 => 2 + # 0x03 => 3 + # ... + # 0xEF => 239 + # 0xF0 => 240 + # 0xF1 0x01 => 1 * 0xFF - 15 + 1 = 241 + # 0xF1 0x02 => 1 * 0xFF - 15 + 2 = 242 + # ... + # 0xF1 0xFF => 1 * 0xFF - 15 + 255 = 495 + # 0xF2 0x01 => 2 * 0xFF - 15 + 1 = 496 + # ... + # 0xF2 0xFF => 2 * 0xFF - 15 + 255 = 750 + # 0xF3 0x01 => 3 * 0xFF - 15 + 1 = 751 + # ... + # 0xFF 0xFF => 15 * 0xFF - 15 + 255 = 4065 + + if index < 0: + raise ValueError("index must be positive") + page = (index + 0x0E) // 0xFF + if page > 0x0F: + raise ValueError("page value out of range") + if page == 0: + return bytes([index]) + else: + # Into extended range + # Leader is 0xFz where z is the page number + # Following char is the remainder + leader = page + 0xF0 + value = ((index + 0x0E) % 0xFF) + 0x01 + + if leader > 0xFF or value > 0xFF: + raise ValueError("value is out of range") + return bytes([leader, value]) + + +def bytes_to_escaped(b: bytes) -> str: + return "".join((f"\\x{i:02X}" for i in b)) + + +def bytes_to_c_hex(b: bytes) -> str: + return ", ".join((f"0x{i:02X}" for i in b)) + "," + + +@dataclass +class FontMap: + font12: Dict[str, bytes] + font06: Dict[str, Optional[bytes]] + + +@dataclass +class FontMapsPerFont: + font12_maps: Dict[str, Dict[str, bytes]] + font06_maps: Dict[str, Dict[str, Optional[bytes]]] + sym_lists: Dict[str, List[str]] + + +def get_font_map_per_font(text_list: List[str], fonts: List[str]) -> FontMapsPerFont: + pending_sym_set = set(text_list) + if len(pending_sym_set) != len(text_list): + raise ValueError("`text_list` contains duplicated symbols") + + if fonts[0] != font_tables.NAME_ASCII_BASIC: + raise ValueError( + f'First item in `fonts` must be "{font_tables.NAME_ASCII_BASIC}"' + ) + + total_symbol_count = len(text_list) + # \x00 is for NULL termination and \x01 is for newline, so the maximum + # number of symbols allowed is as follow (see also the comments in + # `get_bytes_from_font_index`): + if total_symbol_count > (0x10 * 0xFF - 15) - 2: # 4063 + raise ValueError( + f"Error, too many used symbols for this version (total {total_symbol_count})" + ) + + logging.info(f"Generating fonts for {total_symbol_count} symbols") + + # Collect font bitmaps by the defined font order: + font12_maps: Dict[str, Dict[str, bytes]] = {} + font06_maps: Dict[str, Dict[str, Optional[bytes]]] = {} + sym_lists: Dict[str, List[str]] = {} + for font in fonts: + font12_maps[font] = {} + font12_map = font12_maps[font] + font06_maps[font] = {} + font06_map = font06_maps[font] + sym_lists[font] = [] + sym_list = sym_lists[font] + + if len(pending_sym_set) == 0: + logging.warning( + f"Font {font} not used because all symbols already have font bitmaps" + ) + continue + + if font == font_tables.NAME_CJK: + is_cjk = True + else: + is_cjk = False + font12: Dict[str, bytes] + font06: Dict[str, bytes] + font12, font06 = font_tables.get_font_maps_for_name(font) + + for sym in text_list: + if sym not in pending_sym_set: + continue + if is_cjk: + font12_line = get_cjk_glyph(sym) + if font12_line is None: + continue + font06_line = None + else: + try: + font12_line = font12[sym] + font06_line = font06[sym] + except KeyError: + continue + font12_map[sym] = font12_line + font06_map[sym] = font06_line + sym_list.append(sym) + pending_sym_set.remove(sym) + + if len(sym_list) == 0: + logging.warning(f"Font {font} not used by any symbols on the list") + if len(pending_sym_set) > 0: + raise KeyError(f"Symbols not found in specified fonts: {pending_sym_set}") + + return FontMapsPerFont(font12_maps, font06_maps, sym_lists) + + +def get_forced_first_symbols() -> List[str]: + forced_first_symbols = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + return forced_first_symbols + + +def get_sym_list_and_font_map( + text_list: List[str], fonts: List[str] +) -> Tuple[List[str], Dict[str, List[str]], FontMap]: + font_maps = get_font_map_per_font(text_list, fonts) + font12_maps = font_maps.font12_maps + font06_maps = font_maps.font06_maps + + # Build the full font maps + font12_map = {} + font06_map = {} + for font in fonts: + font12_map.update(font12_maps[font]) + font06_map.update(font06_maps[font]) + + # Collect all symbols by the original symbol order, but also making sure + # all symbols with only large font must be placed after all symbols with + # both small and large fonts + sym_list_both_fonts = [] + sym_list_large_only = [] + for sym in text_list: + if font06_map[sym] is None: + sym_list_large_only.append(sym) + else: + sym_list_both_fonts.append(sym) + sym_list = sym_list_both_fonts + sym_list_large_only + + return sym_list, font_maps.sym_lists, FontMap(font12_map, font06_map) + + +def build_symbol_conversion_map(sym_list: List[str]) -> Dict[str, bytes]: + forced_first_symbols = get_forced_first_symbols() + if sym_list[: len(forced_first_symbols)] != forced_first_symbols: + raise ValueError("Symbol list does not start with forced_first_symbols.") + + # the text list is sorted + # allocate out these in their order as number codes + symbol_map: Dict[str, bytes] = {"\n": bytes([1])} + index = 2 # start at 2, as 0= null terminator,1 = new line + + # Assign symbol bytes by font index + for index, sym in enumerate(sym_list, index): + assert sym not in symbol_map + symbol_map[sym] = get_bytes_from_font_index(index) + + return symbol_map + + +def make_font_table_cpp( + sym_list: List[str], font_map: FontMap, symbol_map: Dict[str, bytes] +) -> str: + output_table = make_font_table_named_cpp( + "USER_FONT_12", sym_list, font_map.font12, symbol_map + ) + output_table += make_font_table_06_cpp(sym_list, font_map, symbol_map) + return output_table + + +def make_font_table_named_cpp( + name: Optional[str], + sym_list: List[str], + font_map: Dict[str, bytes], + symbol_map: Dict[str, bytes], +) -> str: + output_table = "" + if name: + output_table = f"const uint8_t {name}[] = {{\n" + for sym in sym_list: + output_table += f"{bytes_to_c_hex(font_map[sym])}//{bytes_to_escaped(symbol_map[sym])} -> {sym}\n" + if name: + output_table += f"}}; // {name}\n" + return output_table + + +def make_font_table_06_cpp( + sym_list: List[str], font_map: FontMap, symbol_map: Dict[str, bytes] +) -> str: + output_table = "const uint8_t USER_FONT_6x8[] = {\n" + for sym in sym_list: + font_bytes = font_map.font06[sym] + if font_bytes: + font_line = bytes_to_c_hex(font_bytes) + else: + font_line = "// " # placeholder + output_table += f"{font_line}//{bytes_to_escaped(symbol_map[sym])} -> {sym}\n" + output_table += "};\n" + return output_table + + +def convert_string_bytes(symbol_conversion_table: Dict[str, bytes], text: str) -> bytes: + # convert all of the symbols from the string into bytes for their content + output_string = b"" + for c in text.replace("\\r", "").replace("\\n", "\n"): + if c not in symbol_conversion_table: + logging.error(f"Missing font definition for {c}") + sys.exit(1) + else: + output_string += symbol_conversion_table[c] + return output_string + + +def convert_string(symbol_conversion_table: Dict[str, bytes], text: str) -> str: + # convert all of the symbols from the string into escapes for their content + return bytes_to_escaped(convert_string_bytes(symbol_conversion_table, text)) + + +def escape(string: str) -> str: + return json.dumps(string, ensure_ascii=False) + + +def write_bytes_as_c_array( + f: TextIO, name: str, data: bytes, indent: int = 2, bytes_per_line: int = 16 +) -> None: + f.write(f"const uint8_t {name}[] = {{\n") + for i in range(0, len(data), bytes_per_line): + f.write(" " * indent) + f.write(", ".join((f"0x{b:02X}" for b in data[i : i + bytes_per_line]))) + f.write(",\n") + f.write(f"}}; // {name}\n\n") + + +@dataclass +class LanguageData: + langs: List[dict] + defs: dict + build_version: str + sym_list: List[str] + sym_lists_by_font: Dict[str, List[str]] + font_map: FontMap + + +def prepare_language(lang: dict, defs: dict, build_version: str) -> LanguageData: + language_code: str = lang["languageCode"] + logging.info(f"Preparing language data for {language_code}") + # Iterate over all of the text to build up the symbols & counts + text_list, _ = get_letter_counts(defs, lang, build_version) + # From the letter counts, need to make a symbol translator & write out the font + fonts = lang["fonts"] + + forced_first_symbols = get_forced_first_symbols() + + # We enforce that numbers come first. + text_list = forced_first_symbols + [ + x for x in text_list if x not in forced_first_symbols + ] + + sym_list, sym_lists_by_font, font_map = get_sym_list_and_font_map(text_list, fonts) + return LanguageData( + [lang], defs, build_version, sym_list, sym_lists_by_font, font_map + ) + + +def prepare_languages( + langs: List[dict], defs: dict, build_version: str +) -> LanguageData: + language_codes: List[str] = [lang["languageCode"] for lang in langs] + logging.info(f"Preparing language data for {language_codes}") + + forced_first_symbols = get_forced_first_symbols() + + all_fonts = [ + font_tables.NAME_ASCII_BASIC, + font_tables.NAME_LATIN_EXTENDED, + font_tables.NAME_CYRILLIC, + font_tables.NAME_CJK, + ] + + # Build the full font maps + font12_map = {} + font06_map = {} + # Calculate total symbol counts per font: + total_sym_counts: Dict[str, Dict[str, int]] = {} + for lang in langs: + text_list, sym_counts = get_letter_counts(defs, lang, build_version) + fonts = lang["fonts"] + text_list = forced_first_symbols + [ + x for x in text_list if x not in forced_first_symbols + ] + font_maps = get_font_map_per_font(text_list, fonts) + for font in fonts: + font12_map.update(font_maps.font12_maps[font]) + font06_map.update(font_maps.font06_maps[font]) + for font, font_sym_list in font_maps.sym_lists.items(): + font_total_sym_counts = total_sym_counts.get(font, {}) + for sym in font_sym_list: + font_total_sym_counts[sym] = font_total_sym_counts.get( + sym, 0 + ) + sym_counts.get(sym, 0) + total_sym_counts[font] = font_total_sym_counts + + sym_lists_by_font: Dict[str, List[str]] = {} + combined_sym_list = [] + for font in all_fonts: + if font not in total_sym_counts: + continue + # swap to Big -> little sort order + current_sym_list = [ + x[0] + for x in sorted( + total_sym_counts[font].items(), + key=lambda kv: (kv[1], kv[0]), + reverse=True, + ) + ] + if font == font_tables.NAME_ASCII_BASIC: + # We enforce that numbers come first. + current_sym_list = forced_first_symbols + [ + x for x in current_sym_list if x not in forced_first_symbols + ] + sym_lists_by_font[font] = current_sym_list + combined_sym_list.extend(current_sym_list) + + return LanguageData( + langs, + defs, + build_version, + combined_sym_list, + sym_lists_by_font, + FontMap(font12_map, font06_map), + ) + + +def write_language( + data: LanguageData, + f: TextIO, + strings_bin: Optional[bytes] = None, + compress_font: bool = False, +) -> None: + if len(data.langs) > 1: + raise ValueError("More than 1 languages are provided") + lang = data.langs[0] + defs = data.defs + build_version = data.build_version + sym_list = data.sym_list + font_map = data.font_map + + symbol_conversion_table = build_symbol_conversion_map(sym_list) + + language_code: str = lang["languageCode"] + logging.info(f"Generating block for {language_code}") + + try: + lang_name = lang["languageLocalName"] + except KeyError: + lang_name = language_code + + if strings_bin or compress_font: + f.write('#include "brieflz.h"\n') + + f.write(f"\n// ---- {lang_name} ----\n\n") + + if not compress_font: + font_table_text = make_font_table_cpp( + sym_list, font_map, symbol_conversion_table + ) + f.write(font_table_text) + f.write( + "const FontSection FontSectionsData[] = {\n" + " {\n" + " .symbol_start = 2,\n" + f" .symbol_end = {len(sym_list) + 2},\n" + " .font12_start_ptr = USER_FONT_12,\n" + " .font06_start_ptr = USER_FONT_6x8,\n" + " },\n" + "};\n" + "const FontSection *const FontSections = FontSectionsData;\n" + "const uint8_t FontSectionsCount = sizeof(FontSectionsData) / sizeof(FontSectionsData[0]);\n" + ) + else: + font12_uncompressed = bytearray() + for sym in sym_list: + font12_uncompressed.extend(font_map.font12[sym]) + font12_compressed = brieflz.compress(bytes(font12_uncompressed)) + logging.info( + f"Font table 12x16 compressed from {len(font12_uncompressed)} to {len(font12_compressed)} bytes (ratio {len(font12_compressed) / len(font12_uncompressed):.3})" + ) + write_bytes_as_c_array(f, "font_12x16_brieflz", font12_compressed) + font_table_text = make_font_table_06_cpp( + sym_list, font_map, symbol_conversion_table + ) + f.write(font_table_text) + f.write( + f"static uint8_t font_out_buffer[{len(font12_uncompressed)}];\n" + "const FontSection FontSectionsData[] = {\n" + " {\n" + " .symbol_start = 2,\n" + f" .symbol_end = {len(sym_list) + 2},\n" + " .font12_start_ptr = font_out_buffer,\n" + " .font06_start_ptr = USER_FONT_6x8,\n" + " },\n" + "};\n" + "const FontSection *const FontSections = FontSectionsData;\n" + "const uint8_t FontSectionsCount = sizeof(FontSectionsData) / sizeof(FontSectionsData[0]);\n" + ) + + f.write(f"\n// ---- {lang_name} ----\n\n") + + translation_common_text = get_translation_common_text( + defs, symbol_conversion_table, build_version + ) + f.write(translation_common_text) + f.write( + f"const bool HasFahrenheit = {('true' if lang.get('tempUnitFahrenheit', True) else 'false')};\n\n" + ) + + if not strings_bin: + translation_strings_and_indices_text = get_translation_strings_and_indices_text( + lang, defs, symbol_conversion_table + ) + f.write(translation_strings_and_indices_text) + f.write( + "const TranslationIndexTable *Tr = &translation.indices;\n" + "const char *TranslationStrings = translation.strings;\n\n" + ) + else: + compressed = brieflz.compress(strings_bin) + logging.info( + f"Strings compressed from {len(strings_bin)} to {len(compressed)} bytes (ratio {len(compressed) / len(strings_bin):.3})" + ) + write_bytes_as_c_array(f, "translation_data_brieflz", compressed) + f.write( + f"static uint8_t translation_data_out_buffer[{len(strings_bin)}] __attribute__((__aligned__(2)));\n\n" + "const TranslationIndexTable *Tr = reinterpret_cast(translation_data_out_buffer);\n" + "const char *TranslationStrings = reinterpret_cast(translation_data_out_buffer) + sizeof(TranslationIndexTable);\n\n" + ) + + if not strings_bin and not compress_font: + f.write("void prepareTranslations() {}\n\n") + else: + f.write("void prepareTranslations() {\n") + if compress_font: + f.write( + " blz_depack_srcsize(font_12x16_brieflz, font_out_buffer, sizeof(font_12x16_brieflz));\n" + ) + if strings_bin: + f.write( + " blz_depack_srcsize(translation_data_brieflz, translation_data_out_buffer, sizeof(translation_data_brieflz));\n" + ) + f.write("}\n\n") + + sanity_checks_text = get_translation_sanity_checks_text(defs) + f.write(sanity_checks_text) + + +def write_languages( + data: LanguageData, + f: TextIO, + strings_obj_path: Optional[str] = None, + compress_font: bool = False, +) -> None: + defs = data.defs + build_version = data.build_version + combined_sym_list = data.sym_list + sym_lists_by_font = data.sym_lists_by_font + font_map = data.font_map + + symbol_conversion_table = build_symbol_conversion_map(combined_sym_list) + + language_codes: List[str] = [lang["languageCode"] for lang in data.langs] + logging.info(f"Generating block for {language_codes}") + + lang_names = [ + lang.get("languageLocalName", lang["languageCode"]) for lang in data.langs + ] + + f.write('#include "Translation_multi.h"') + + f.write(f"\n// ---- {lang_names} ----\n\n") + + max_decompressed_font_size = 0 + if not compress_font: + font_table_text = "" + font_section_info_text = ( + "const FontSectionDataInfo FontSectionDataInfos[] = {\n" + ) + for font, current_sym_list in sym_lists_by_font.items(): + font_table_text += f"const uint8_t font_table_data_{font}[] = {{\n" + font_table_text += "// 12x16:\n" + font_table_text += make_font_table_named_cpp( + None, + current_sym_list, + font_map.font12, + symbol_conversion_table, + ) + if font != font_tables.NAME_CJK: + font_table_text += "// 6x8:\n" + font_table_text += make_font_table_named_cpp( + None, + current_sym_list, + font_map.font06, # type: ignore[arg-type] + symbol_conversion_table, + ) + font_table_text += f"}}; // font_table_data_{font}\n" + current_sym_start = combined_sym_list.index(current_sym_list[0]) + 2 + font_section_info_text += ( + " {\n" + f" .symbol_start = {current_sym_start},\n" + f" .symbol_count = {len(current_sym_list)},\n" + f" .data_size = sizeof(font_table_data_{font}),\n" + " .data_is_compressed = false,\n" + f" .data_ptr = font_table_data_{font},\n" + " },\n" + ) + + f.write(font_table_text) + font_section_info_text += ( + "};\n" + "const uint8_t FontSectionDataCount = sizeof(FontSectionDataInfos) / sizeof(FontSectionDataInfos[0]);\n\n" + ) + f.write(font_section_info_text) + f.write( + "FontSection DynamicFontSections[4] = {};\n" + "const FontSection *const FontSections = DynamicFontSections;\n" + "const uint8_t FontSectionsCount = sizeof(DynamicFontSections) / sizeof(DynamicFontSections[0]);\n" + ) + else: + font_section_info_text = ( + "const FontSectionDataInfo FontSectionDataInfos[] = {\n" + ) + for font, current_sym_list in sym_lists_by_font.items(): + current_sym_start = combined_sym_list.index(current_sym_list[0]) + 2 + font_uncompressed = bytearray() + for sym in current_sym_list: + font_uncompressed.extend(font_map.font12[sym]) + if font != font_tables.NAME_CJK: + for sym in current_sym_list: + font_uncompressed.extend(font_map.font06[sym]) # type: ignore[arg-type] + font_compressed = brieflz.compress(bytes(font_uncompressed)) + logging.info( + f"Font table for {font} compressed from {len(font_uncompressed)} to {len(font_compressed)} bytes (ratio {len(font_compressed) / len(font_uncompressed):.3})" + ) + max_decompressed_font_size += len(font_uncompressed) + write_bytes_as_c_array(f, f"font_data_brieflz_{font}", font_compressed) + font_section_info_text += ( + " {\n" + f" .symbol_start = {current_sym_start},\n" + f" .symbol_count = {len(current_sym_list)},\n" + f" .data_size = sizeof(font_data_brieflz_{font}),\n" + " .data_is_compressed = true,\n" + f" .data_ptr = font_data_brieflz_{font},\n" + " },\n" + ) + font_section_info_text += ( + "};\n" + "const uint8_t FontSectionDataCount = sizeof(FontSectionDataInfos) / sizeof(FontSectionDataInfos[0]);\n\n" + ) + f.write(font_section_info_text) + f.write( + "FontSection DynamicFontSections[4] = {};\n" + "const FontSection *const FontSections = DynamicFontSections;\n" + "const uint8_t FontSectionsCount = sizeof(DynamicFontSections) / sizeof(DynamicFontSections[0]);\n" + ) + + f.write(f"\n// ---- {lang_names} ----\n\n") + + translation_common_text = get_translation_common_text( + defs, symbol_conversion_table, build_version + ) + f.write(translation_common_text) + f.write( + f"const bool HasFahrenheit = {('true' if any([lang.get('tempUnitFahrenheit', True) for lang in data.langs]) else 'false')};\n\n" + ) + + max_decompressed_translation_size = 0 + if not strings_obj_path: + for lang in data.langs: + lang_code = lang["languageCode"] + translation_strings_and_indices_text = ( + get_translation_strings_and_indices_text( + lang, defs, symbol_conversion_table, suffix=f"_{lang_code}" + ) + ) + f.write(translation_strings_and_indices_text) + f.write("const LanguageMeta LanguageMetas[] = {\n") + for lang in data.langs: + lang_code = lang["languageCode"] + lang_id = get_language_unqiue_id(lang_code) + f.write( + " {\n" + f" .uniqueID = {lang_id},\n" + f" .translation_data = reinterpret_cast(&translation_{lang_code}),\n" + f" .translation_size = sizeof(translation_{lang_code}),\n" + f" .translation_is_compressed = false,\n" + " },\n" + ) + f.write("};\n") + else: + for lang in data.langs: + lang_code = lang["languageCode"] + sym_name = objcopy.cpp_var_to_section_name(f"translation_{lang_code}") + strings_bin = objcopy.get_binary_from_obj(strings_obj_path, sym_name) + if len(strings_bin) == 0: + raise ValueError(f"Output for {sym_name} is empty") + max_decompressed_translation_size = max( + max_decompressed_translation_size, len(strings_bin) + ) + compressed = brieflz.compress(strings_bin) + logging.info( + f"Strings for {lang_code} compressed from {len(strings_bin)} to {len(compressed)} bytes (ratio {len(compressed) / len(strings_bin):.3})" + ) + write_bytes_as_c_array( + f, f"translation_data_brieflz_{lang_code}", compressed + ) + f.write("const LanguageMeta LanguageMetas[] = {\n") + for lang in data.langs: + lang_code = lang["languageCode"] + lang_id = get_language_unqiue_id(lang_code) + f.write( + " {\n" + f" .uniqueID = {lang_id},\n" + f" .translation_data = translation_data_brieflz_{lang_code},\n" + f" .translation_size = sizeof(translation_data_brieflz_{lang_code}),\n" + f" .translation_is_compressed = true,\n" + " },\n" + ) + f.write("};\n") + f.write( + "const uint8_t LanguageCount = sizeof(LanguageMetas) / sizeof(LanguageMetas[0]);\n\n" + f"alignas(TranslationData) uint8_t translation_data_out_buffer[{max_decompressed_translation_size + max_decompressed_font_size}];\n" + "const uint16_t translation_data_out_buffer_size = sizeof(translation_data_out_buffer);\n\n" + ) + + sanity_checks_text = get_translation_sanity_checks_text(defs) + f.write(sanity_checks_text) + + +def get_translation_common_text( + defs: dict, symbol_conversion_table: Dict[str, bytes], build_version +) -> str: + translation_common_text = "" + + # Write out firmware constant options + constants = get_constants(build_version) + for x in constants: + translation_common_text += f'const char* {x[0]} = "{convert_string(symbol_conversion_table, x[1])}";//{x[1]} \n' + translation_common_text += "\n" + + # Debug Menu + translation_common_text += "const char* DebugMenu[] = {\n" + + for c in get_debug_menu(): + translation_common_text += ( + f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n' + ) + translation_common_text += "};\n\n" + + # accel names + translation_common_text += "const char* AccelTypeNames[] = {\n" + + for c in get_accel_names_list(): + translation_common_text += ( + f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n' + ) + translation_common_text += "};\n\n" + + # power source types + translation_common_text += "const char* PowerSourceNames[] = {\n" + + for c in get_power_source_list(): + translation_common_text += ( + f'\t "{convert_string(symbol_conversion_table, c)}",//{c} \n' + ) + translation_common_text += "};\n\n" + + return translation_common_text + + +@dataclass +class TranslationItem: + info: str + str_index: int + + +def get_translation_strings_and_indices_text( + lang: dict, defs: dict, symbol_conversion_table: Dict[str, bytes], suffix: str = "" +) -> str: + str_table: List[str] = [] + str_group_messages: List[TranslationItem] = [] + str_group_messageswarn: List[TranslationItem] = [] + str_group_characters: List[TranslationItem] = [] + str_group_settingdesc: List[TranslationItem] = [] + str_group_settingshortnames: List[TranslationItem] = [] + str_group_settingmenuentries: List[TranslationItem] = [] + str_group_settingmenuentriesdesc: List[TranslationItem] = [] + + eid: str + + # ----- Reading SettingsDescriptions + obj = lang["menuOptions"] + + for index, mod in enumerate(defs["menuOptions"]): + eid = mod["id"] + str_group_settingdesc.append( + TranslationItem(f"[{index:02d}] {eid}", len(str_table)) + ) + str_table.append(obj[eid]["desc"]) + + # ----- Reading Message strings + + obj = lang["messages"] + + for mod in defs["messages"]: + eid = mod["id"] + source_text = "" + if "default" in mod: + source_text = mod["default"] + if eid in obj: + source_text = obj[eid] + str_group_messages.append(TranslationItem(eid, len(str_table))) + str_table.append(source_text) + + obj = lang["messagesWarn"] + + for mod in defs["messagesWarn"]: + eid = mod["id"] + if isinstance(obj[eid], list): + if not obj[eid][1]: + source_text = obj[eid][0] + else: + source_text = obj[eid][0] + "\n" + obj[eid][1] + else: + source_text = "\n" + obj[eid] + str_group_messageswarn.append(TranslationItem(eid, len(str_table))) + str_table.append(source_text) + + # ----- Reading Characters + + obj = lang["characters"] + + for mod in defs["characters"]: + eid = mod["id"] + str_group_characters.append(TranslationItem(eid, len(str_table))) + str_table.append(obj[eid]) + + # ----- Reading SettingsDescriptions + obj = lang["menuOptions"] + + for index, mod in enumerate(defs["menuOptions"]): + eid = mod["id"] + if isinstance(obj[eid]["text2"], list): + if not obj[eid]["text2"][1]: + source_text = obj[eid]["text2"][0] + else: + source_text = obj[eid]["text2"][0] + "\n" + obj[eid]["text2"][1] + else: + source_text = "\n" + obj[eid]["text2"] + str_group_settingshortnames.append( + TranslationItem(f"[{index:02d}] {eid}", len(str_table)) + ) + str_table.append(source_text) + + # ----- Reading Menu Groups + obj = lang["menuGroups"] + + for index, mod in enumerate(defs["menuGroups"]): + eid = mod["id"] + if isinstance(obj[eid]["text2"], list): + if not obj[eid]["text2"][1]: + source_text = obj[eid]["text2"][0] + else: + source_text = obj[eid]["text2"][0] + "\n" + obj[eid]["text2"][1] + else: + source_text = "\n" + obj[eid]["text2"] + str_group_settingmenuentries.append( + TranslationItem(f"[{index:02d}] {eid}", len(str_table)) + ) + str_table.append(source_text) + + # ----- Reading Menu Groups Descriptions + obj = lang["menuGroups"] + + for index, mod in enumerate(defs["menuGroups"]): + eid = mod["id"] + str_group_settingmenuentriesdesc.append( + TranslationItem(f"[{index:02d}] {eid}", len(str_table)) + ) + str_table.append(obj[eid]["desc"]) + + @dataclass + class RemappedTranslationItem: + str_index: int + str_start_offset: int = 0 + + # ----- Perform suffix merging optimization: + # + # We sort the backward strings so that strings with the same suffix will + # be next to each other, e.g.: + # "ef\0", + # "cdef\0", + # "abcdef\0", + backward_sorted_table: List[Tuple[int, str, bytes]] = sorted( + ( + (i, s, bytes(reversed(convert_string_bytes(symbol_conversion_table, s)))) + for i, s in enumerate(str_table) + ), + key=lambda x: x[2], + ) + str_remapping: List[Optional[RemappedTranslationItem]] = [None] * len(str_table) + for i, (str_index, source_str, converted) in enumerate(backward_sorted_table[:-1]): + j = i + while backward_sorted_table[j + 1][2].startswith(converted): + j += 1 + if j + 1 == len(backward_sorted_table): + break + if j != i: + str_remapping[str_index] = RemappedTranslationItem( + str_index=backward_sorted_table[j][0], + str_start_offset=len(backward_sorted_table[j][2]) - len(converted), + ) + + # ----- Write the string table: + str_offsets = [-1] * len(str_table) + offset = 0 + write_null = False + # NOTE: Cannot specify C99 designator here due to GCC (g++) bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55227 + translation_strings_text = " /* .strings = */ {\n" + for i, source_str in enumerate(str_table): + if str_remapping[i] is not None: + continue + if write_null: + translation_strings_text += ' "\\0"\n' + write_null = True + # Find what items use this string + str_used_by = [i] + [ + j for j, r in enumerate(str_remapping) if r and r.str_index == i + ] + for j in str_used_by: + for group, pre_info in [ + (str_group_messages, "messages"), + (str_group_messageswarn, "messagesWarn"), + (str_group_characters, "characters"), + (str_group_settingdesc, "SettingsDescriptions"), + (str_group_settingshortnames, "SettingsShortNames"), + (str_group_settingmenuentries, "SettingsMenuEntries"), + (str_group_settingmenuentriesdesc, "SettingsMenuEntriesDescriptions"), + ]: + for item in group: + if item.str_index == j: + translation_strings_text += ( + f" // - {pre_info} {item.info}\n" + ) + if j == i: + translation_strings_text += ( + f" // {offset: >4}: {escape(source_str)}\n" + ) + str_offsets[j] = offset + else: + remapped = str_remapping[j] + assert remapped is not None + translation_strings_text += f" // {offset + remapped.str_start_offset: >4}: {escape(str_table[j])}\n" + str_offsets[j] = offset + remapped.str_start_offset + converted_bytes = convert_string_bytes(symbol_conversion_table, source_str) + translation_strings_text += f' "{bytes_to_escaped(converted_bytes)}"' + str_offsets[i] = offset + # Add the length and the null terminator + offset += len(converted_bytes) + 1 + translation_strings_text += "\n }, // .strings\n\n" + + str_total_bytes = offset + + def get_offset(idx: int) -> int: + assert str_offsets[idx] >= 0 + return str_offsets[idx] + + translation_indices_text = " .indices = {\n" + + # ----- Write the messages string indices: + for group in [str_group_messages, str_group_messageswarn, str_group_characters]: + for item in group: + translation_indices_text += f" .{item.info} = {get_offset(item.str_index)}, // {escape(str_table[item.str_index])}\n" + translation_indices_text += "\n" + + # ----- Write the settings index tables: + for group, name in [ + (str_group_settingdesc, "SettingsDescriptions"), + (str_group_settingshortnames, "SettingsShortNames"), + (str_group_settingmenuentries, "SettingsMenuEntries"), + (str_group_settingmenuentriesdesc, "SettingsMenuEntriesDescriptions"), + ]: + max_len = 30 + translation_indices_text += f" .{name} = {{\n" + for item in group: + translation_indices_text += f" /* {item.info.ljust(max_len)[:max_len]} */ {get_offset(item.str_index)}, // {escape(str_table[item.str_index])}\n" + translation_indices_text += f" }}, // {name}\n\n" + + translation_indices_text += " }, // .indices\n\n" + + return ( + "struct {\n" + " TranslationIndexTable indices;\n" + f" char strings[{str_total_bytes}];\n" + f"}} const translation{suffix} = {{\n" + + translation_indices_text + + translation_strings_text + + f"}}; // translation{suffix}\n\n" + ) + + +def get_translation_sanity_checks_text(defs: dict) -> str: + sanity_checks_text = "\n// Verify SettingsItemIndex values:\n" + for i, mod in enumerate(defs["menuOptions"]): + eid = mod["id"] + sanity_checks_text += ( + f"static_assert(static_cast(SettingsItemIndex::{eid}) == {i});\n" + ) + sanity_checks_text += f"static_assert(static_cast(SettingsItemIndex::NUM_ITEMS) == {len(defs['menuOptions'])});\n" + return sanity_checks_text + + +def read_version() -> str: + with open(HERE.parent / "source" / "version.h") as version_file: + for line in version_file: + if re.findall(r"^.*(?<=(#define)).*(?<=(BUILD_VERSION))", line): + matches = re.findall(r"\"(.+?)\"", line) + if matches: + version = matches[0] + try: + version += f".{subprocess.check_output(['git', 'rev-parse', '--short=7', 'HEAD']).strip().decode('ascii').upper()}" + # --short=7: the shorted hash with 7 digits. Increase/decrease if needed! + except OSError: + version += " git" + return version + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "--output-pickled", + help="Write pickled language data for later reuse", + type=argparse.FileType("wb"), + required=False, + dest="output_pickled", + ) + parser.add_argument( + "--input-pickled", + help="Use previously generated pickled language data", + type=argparse.FileType("rb"), + required=False, + dest="input_pickled", + ) + parser.add_argument( + "--strings-obj", + help="Use generated TranslationData by extracting from object file", + type=argparse.FileType("rb"), + required=False, + dest="strings_obj", + ) + parser.add_argument( + "--compress-font", + help="Compress the font table", + action="store_true", + required=False, + dest="compress_font", + ) + parser.add_argument( + "--output", "-o", help="Target file", type=argparse.FileType("w"), required=True + ) + parser.add_argument( + "languageCodes", + metavar="languageCode", + nargs="+", + help="Language(s) to generate", + ) + return parser.parse_args() + + +def main() -> None: + json_dir = HERE + + args = parse_args() + if args.input_pickled and args.output_pickled: + logging.error("error: Both --output-pickled and --input-pickled are specified") + sys.exit(1) + + language_data: LanguageData + if args.input_pickled: + logging.info(f"Reading pickled language data from {args.input_pickled.name}...") + language_data = pickle.load(args.input_pickled) + language_codes = [lang["languageCode"] for lang in language_data.langs] + if language_codes != args.languageCodes: + logging.error( + f"error: languageCode {args.languageCode} does not match language data {language_codes}" + ) + sys.exit(1) + logging.info(f"Read language data for {language_codes}") + logging.info(f"Build version: {language_data.build_version}") + else: + try: + build_version = read_version() + except FileNotFoundError: + logging.error("error: Could not find version info ") + sys.exit(1) + + logging.info(f"Build version: {build_version}") + logging.info(f"Making {args.languageCodes} from {json_dir}") + + defs_ = load_json(os.path.join(json_dir, "translations_def.js"), True) + if len(args.languageCodes) == 1: + lang_ = read_translation(json_dir, args.languageCodes[0]) + language_data = prepare_language(lang_, defs_, build_version) + else: + langs_ = [ + read_translation(json_dir, lang_code) + for lang_code in args.languageCodes + ] + language_data = prepare_languages(langs_, defs_, build_version) + + out_ = args.output + write_start(out_) + if len(language_data.langs) == 1: + if args.strings_obj: + sym_name = objcopy.cpp_var_to_section_name("translation") + strings_bin = objcopy.get_binary_from_obj(args.strings_obj.name, sym_name) + if len(strings_bin) == 0: + raise ValueError(f"Output for {sym_name} is empty") + write_language( + language_data, + out_, + strings_bin=strings_bin, + compress_font=args.compress_font, + ) + else: + write_language(language_data, out_, compress_font=args.compress_font) + else: + if args.strings_obj: + write_languages( + language_data, + out_, + strings_obj_path=args.strings_obj.name, + compress_font=args.compress_font, + ) + else: + write_languages(language_data, out_, compress_font=args.compress_font) + + if args.output_pickled: + logging.info(f"Writing pickled data to {args.output_pickled.name}") + pickle.dump(language_data, args.output_pickled) + + logging.info("Done") + + +if __name__ == "__main__": + main() diff --git a/Translations/translation_BE.json b/Translations/translation_BE.json new file mode 100644 index 00000000..4a848704 --- /dev/null +++ b/Translations/translation_BE.json @@ -0,0 +1,213 @@ +{ + "languageCode": "BE", + "languageLocalName": "Беларуская", + "fonts": ["ascii_basic", "latin_extended", "cyrillic"], + "messages": { + "SettingsCalibrationDone": "Каліброўка закончана!", + "SettingsCalibrationWarning": "Перад тым, як працягнуць, каліласка, упэўнецеся, што джала мае комнатную тэмпературу!", + "SettingsResetWarning": "Вы ўпэннены, што жадаеце зкінуць налады да першапачатковых значэнняў?", + "UVLOWarningString": "НАПРУГА--", + "UndervoltageString": "Нізкая напруга", + "InputVoltageString": "Сілкаванне В: ", + "WarningTipTempString": "Тэмп. джала: ", + "BadTipString": "ДЖАЛА--", + "SleepingSimpleString": "Zzzz", + "SleepingAdvancedString": "Чаканне...", + "WarningSimpleString": "ГОРАЧА!", + "WarningAdvancedString": "!!! ГАРАЧАЕ !!!\n!!! ДЖАЛА !!!", + "SleepingTipAdvancedString": "Джала:", + "IdleTipString": "Джала:", + "IdleSetString": " ->", + "TipDisconnectedString": "ДЖАЛА АДЛУЧАНА", + "SolderingAdvancedPowerPrompt": "Сілкаванне: ", + "OffString": "Выкл.", + "YourGainMessage": "Прырост:" + }, + "messagesWarn": { + "ResetOKMessage": "Скід OK", + "SettingsResetMessage": ["Налады", "зкінуты!"], + "NoAccelerometerMessage": ["Ня вызначаны", "акселерометр!"], + "NoPowerDeliveryMessage": ["Няма USB-PD IC", "выяўлены!"], + "LockingKeysString": "ЗАМКНУТЫ", + "UnlockingKeysString": "АДЫМКНУТЫ", + "WarningKeysLockedString": "!ЗАМКНУТЫ!", + "WarningThermalRunaway": ["Цеплавы", "Уцякач"] + }, + "characters": { + "SettingRightChar": "П", + "SettingLeftChar": "Л", + "SettingAutoChar": "А", + "SettingOffChar": "O", + "SettingSlowChar": "М", + "SettingMediumChar": "С", + "SettingFastChar": "Х", + "SettingStartNoneChar": "В", + "SettingStartSolderingChar": "П", + "SettingStartSleepChar": "Ч", + "SettingStartSleepOffChar": "К", + "SettingSensitivityOff": "А", + "SettingSensitivityLow": "Н", + "SettingSensitivityMedium": "С", + "SettingSensitivityHigh": "В", + "SettingLockDisableChar": "А", + "SettingLockBoostChar": "Т", + "SettingLockFullChar": "П", + "SettingNAChar": "Н/Д" + }, + "menuGroups": { + "PowerMenu": { + "text2": ["Налады", "сілкавання"], + "desc": "Крыніца сілкавання і мінімальная напруга на ячэйку" + }, + "SolderingMenu": { + "text2": ["Налады", "пайкі"], + "desc": "Налады пайкі" + }, + "PowerSavingMenu": { + "text2": ["Рэжымы", "сну"], + "desc": "Налады рэжыму чакання" + }, + "UIMenu": { + "text2": ["Налады", "інтэрфейсу"], + "desc": "Карыстальніцкія налады знешняга выгляду" + }, + "AdvancedMenu": { + "text2": ["Дадатковыя", "налады"], + "desc": "Дадатковыя налады" + } + }, + "menuOptions": { + "DCInCutoff": { + "text2": ["Крыніца", "сілкавання"], + "desc": "Крыніца сілкавання. Усталюе напругу адсечкі. (DC 10В) (S 3,3В на ячэйку, без абмежавання магутнасці)" + }, + "SleepTemperature": { + "text2": ["Тэмп.", "чакання"], + "desc": "Тэмпература рэжыму чакання" + }, + "SleepTimeout": { + "text2": ["Таймаўт", "чакання"], + "desc": "Час да пераходу ў рэжым чакання (Хвіліны | Секунды)" + }, + "ShutdownTimeout": { + "text2": ["Таймаут", "выключэння"], + "desc": "Час да адключэння паяльніка (Хвіліны)" + }, + "MotionSensitivity": { + "text2": ["Адчувальнасць", "акселерометра"], + "desc": "Адчувальнасць акселерометра (0=Выкл. | 1=Мін. | ... | 9=Макс.)" + }, + "TemperatureUnit": { + "text2": ["Адзінкі", "тэмпературы"], + "desc": "Адзінкі вымярэння тэмпературы (C=Цэльcія | F=Фарэнгейта)" + }, + "AdvancedIdle": { + "text2": ["Падрабязны", "рэжым чакання"], + "desc": "Адлюстроўваць дэталёвую инфармацыю паменьшаным шрыфтом на экране чакання" + }, + "DisplayRotation": { + "text2": ["Арыентацыя", "экрану"], + "desc": "Арыентацыя экрану (П=Правая рука | Л=Левая рука | А=Аўта)" + }, + "BoostTemperature": { + "text2": ["t° турба", "рэжыму"], + "desc": "Тэмпература джала ў турба-рэжыме" + }, + "AutoStart": { + "text2": ["Аўта", "старт"], + "desc": "Рэжым, у якім запускаецца паяльнік пры падачы сілкавання (В=Выкл. | П=Пайка | Ч=Чаканне | К=Чаканне пры комн. тэмп.)" + }, + "CooldownBlink": { + "text2": ["Мігценне t°", "пры астуджэнні"], + "desc": "Міргаць тэмпературай на экране астуджэння, пакуль джага яшчэ гарачае" + }, + "TemperatureCalibration": { + "text2": ["Каліброўка", "тэмпературы"], + "desc": "Каліброўка тэрмадатчыка джала" + }, + "SettingsReset": { + "text2": ["Скід", "наладаў"], + "desc": "Скід наладаў да першапачатковых значэнняў" + }, + "VoltageCalibration": { + "text2": ["Каліброўка", "напругі"], + "desc": "Каліброўка ўваходнай напругі (падоўжаны націск для выхаду)" + }, + "AdvancedSoldering": { + "text2": ["Падрабязны", "экран пайкі"], + "desc": "Паказваць дэталёвую інформацыю на экране пайкі" + }, + "ScrollingSpeed": { + "text2": ["Хуткацсь", "тексту"], + "desc": "Хуткасць гартання тэксту (М=марудна | Х=хутка)" + }, + "QCMaxVoltage": { + "text2": ["Магутнасць", "сілкавання"], + "desc": "Магутнасць выкарыстоўваемай крыніцы сілкавання" + }, + "PDNegTimeout": { + "text2": ["PD", "прыпынак"], + "desc": "Час чакання ўзгаднення PD з крокам 100 мс для сумяшчальнасці з некаторымі зараднымі зараднымі прыладамі QC (0: адключана)" + }, + "PowerLimit": { + "text2": ["Межы", "магутнасці"], + "desc": "Максімальная магутнасць, якую можа выкарыстоўваць паяльнік (Ватт)" + }, + "ReverseButtonTempChange": { + "text2": ["Інвертаваць", "кнопкі"], + "desc": "Інвертаваць кнопкі вымярэння тэмпературы" + }, + "TempChangeShortStep": { + "text2": ["Крок тэмп.", "кар. нац."], + "desc": "Крок вымярэння тэмпературы пры кароткім націску кнопак" + }, + "TempChangeLongStep": { + "text2": ["Крок тэмп.", "пад. нац."], + "desc": "Крок вымярэння тэмпературы пры падоўжаным націску кнопак" + }, + "PowerPulsePower": { + "text2": ["Сіла імп.", "сілкав. Вт"], + "desc": "Сіла імпульса щтрымливаючага ад сну павербанку ці іншай крыніцы сілкавання" + }, + "HallEffSensitivity": { + "text2": ["Эфект Хола", "адчувальнасць"], + "desc": "Узровень адчувальнасці датчыка хола ў рэжыме сну (А=Адключана | Н=Нізкі | С=Сярэдні | В=Высокі)" + }, + "LockingMode": { + "text2": ["Дазволіць", "блок. кнопак"], + "desc": "Пры рабоце падоўжаны націск дзьвух кнопак блакуе іх (А=Адключана | Т=Толькі турба | П=Поўная блакіроўка)" + }, + "MinVolCell": { + "text2": ["Мін.", "напр."], + "desc": "Мінімальная дазволеная напруга на ячэйку (3S: 3 - 3,7V | 4S: 2,4 - 3,7V)" + }, + "AnimLoop": { + "text2": ["Зацыкленая", "анімацыя"], + "desc": "Зацыкленая анімацыя гузікаў у галоўным меню" + }, + "AnimSpeed": { + "text2": ["Хуткасць", "анімацыі"], + "desc": "Хуткасць анімацыі гузікаў у галоўным меню (Мілісекунды) (А=Адключана | Н=Нізкі | С=Сярэдні | В=Высокі)" + }, + "PowerPulseWait": { + "text2": ["Імпульс магутнасці", "час чакання"], + "desc": "Час чакання перад запускам кожнага імпульсу няспання (x 2.5 с)" + }, + "PowerPulseDuration": { + "text2": ["Імпульс магутнасці", "працягласць"], + "desc": "Працягласць імпульсу няспання (x 250 мс)" + }, + "LanguageSwitch": { + "text2": ["Мова:", " BY Беларуская"], + "desc": "" + }, + "Brightness": { + "text2": ["Экран", "Яркасць"], + "desc": "Адрэгулюйце кантраснасць / яркасць OLED-экрана" + }, + "ColourInversion": { + "text2": ["Экран", "Інвертаваць"], + "desc": "Інвертаваць колеры OLED-экрана" + } + } +} diff --git a/Translations/translation_EL.json b/Translations/translation_EL.json new file mode 100644 index 00000000..10d4ae86 --- /dev/null +++ b/Translations/translation_EL.json @@ -0,0 +1,340 @@ +{ + "languageCode": "EL", + "languageLocalName": "Greek", + "fonts": [ + "ascii_basic", + "greek" + ], + "tempUnitFahrenheit": true, + "messages": { + "SettingsCalibrationWarning": "Πριν προχωρήσετε, παρακαλώ σιγουρευτείτε πως η μύτη βρίσκεται σε θερμοκρασία δωματίου!", + "SettingsResetWarning": "Σίγουρα θέλετε επαναφορά αρχικών ρυθμίσεων;", + "UVLOWarningString": "Χαμηλ DC", + "UndervoltageString": "Υπόταση", + "InputVoltageString": "Είσοδος V: ", + "SleepingSimpleString": "Zzzz", + "SleepingAdvancedString": "Υπνος...", + "SleepingTipAdvancedString": "Μύτη:", + "IdleTipString": "Μύτη:", + "IdleSetString": " Set:", + "TipDisconnectedString": "ΧΩΡΙΣ ΜΥΤΗ", + "SolderingAdvancedPowerPrompt": "Ενέργεια: ", + "OffString": "Απ." + }, + "messagesWarn": { + "ResetOKMessage": "Επαν. OK", + "SettingsResetMessage": [ + "Κάποιες ρυθμ.", + "άλλαξαν" + ], + "NoAccelerometerMessage": [ + "Δεν εντοπίστηκε", + "επιταχυνσιόμετρο" + ], + "NoPowerDeliveryMessage": [ + "Δεν εντοπίστηκε", + "κύκλωμα USB-PD" + ], + "LockingKeysString": "ΚΛΕΙΔ.", + "UnlockingKeysString": "ΞΕΚΛΕΙΔ.", + "WarningKeysLockedString": [ + "ΚΛΕΙΔΩΜΕΝΑ", + "ΠΛΗΚΤΡΑ!" + ], + "WarningThermalRunaway": [ + "Θερμική", + "Φυγή" + ] + }, + "characters": { + "SettingRightChar": "R", + "SettingLeftChar": "L", + "SettingAutoChar": "Α", + "SettingOffChar": "0", + "SettingSlowChar": "Α", + "SettingMediumChar": "Μ", + "SettingFastChar": "Γ", + "SettingStartNoneChar": "0", + "SettingStartSolderingChar": "Κ", + "SettingStartSleepChar": "Ζ", + "SettingStartSleepOffChar": "Υ", + "SettingSensitivityOff": "0", + "SettingSensitivityLow": "Χ", + "SettingSensitivityMedium": "Μ", + "SettingSensitivityHigh": "Υ", + "SettingLockDisableChar": "Α", + "SettingLockBoostChar": "B", + "SettingLockFullChar": "Π", + "SettingNAChar": "Δ/Δ" + }, + "menuGroups": { + "PowerMenu": { + "text2": [ + "Ρυθμίσεις", + "ενέργειας" + ], + "desc": "Ρυθμίσεις ενέργειας" + }, + "SolderingMenu": { + "text2": [ + "Ρυθμίσεις", + "κόλλησης" + ], + "desc": "Ρυθμίσεις κόλλησης" + }, + "PowerSavingMenu": { + "text2": [ + "Λειτουργία", + "ύπνου" + ], + "desc": "Ρυθμίσεις εξοικ. ενέργ." + }, + "UIMenu": { + "text2": [ + "Διεπαφή", + "χρήστη" + ], + "desc": "Ρυθμίσεις διεπαφής χρήστη" + }, + "AdvancedMenu": { + "text2": [ + "Προηγμένες", + "ρυθμίσεις" + ], + "desc": "Προηγμένες ρυθμίσεις" + } + }, + "menuOptions": { + "DCInCutoff": { + "text2": [ + "Πηγή", + "ενέργειας" + ], + "desc": "Πηγή ενέργειας. Oρισμός τάσης απενεργοποίησης. (DC 10V) (S 3.3V ανα κυψέλη, απενεργοποίηση ενεργειακού ορίου)" + }, + "SleepTemperature": { + "text2": [ + "Θερμοκρ.", + "ύπνου" + ], + "desc": "Θερμοκρασία μύτης σε λειτ. ύπνου" + }, + "SleepTimeout": { + "text2": [ + "Έναρξη", + "ύπνου" + ], + "desc": "Χρονικό διάστημα πρίν την ενεργοποίηση λειτουργίας ύπνου (Δ=δευτ. | Λ=λεπτά)" + }, + "ShutdownTimeout": { + "text2": [ + "Έναρξη", + "απενεργ." + ], + "desc": "Χρονικό διάστημα πρίν την απενεργοποίηση του κολλητηριού (Λ=λεπτά)" + }, + "MotionSensitivity": { + "text2": [ + "Ευαισθησία", + "κίνησης" + ], + "desc": "0=off | 1=λιγότερο ευαίσθητο | ... | 9=περισσότερο ευαίσθητο" + }, + "TemperatureUnit": { + "text2": [ + "Μονάδες", + "θερμοκρασίας" + ], + "desc": "C=Κελσίου | F=Φαρενάιτ" + }, + "AdvancedIdle": { + "text2": [ + "Λεπτομερής", + "οθ. αδράνειας" + ], + "desc": "Προβολή λεπτομερών πληροφοριών σε μικρότερη γραμματοσειρά στην οθόνη αδράνειας" + }, + "DisplayRotation": { + "text2": [ + "Διάταξη", + "οθόνης" + ], + "desc": "R=δεξιόχειρες | L=αριστερόχειρες | Α=αυτόματο" + }, + "BoostTemperature": { + "text2": [ + "Θερμοκ.", + "boost" + ], + "desc": "Θερμοκρασία στη \"λειτουργία boost\"" + }, + "AutoStart": { + "text2": [ + "Ζέσταμα", + "κατά την εν." + ], + "desc": "0=off | Κ=θερμ. κόλλησης | Z=αναμονή σε θερμοκρασία ύπνου μέχρι την κίνηση | Υ=αναμονή χωρίς ζέσταμα μέχρι την κίνηση" + }, + "CooldownBlink": { + "text2": [ + "Αναβοσβήσιμο", + "ψύξης" + ], + "desc": "Αναβοσβήσιμο της ενδειξης θερμοκρασίας κατά την παύση θέρμανσης όταν η μύτη είναι ακόμα καυτή" + }, + "TemperatureCalibration": { + "text2": [ + "Βαθμονόμηση", + "θερμοκρασίας;" + ], + "desc": "Βαθμονόμηση αρχικής θερμοκρασίας μύτης" + }, + "SettingsReset": { + "text2": [ + "Επαναφορά", + "εργ. ρυθμίσεων;" + ], + "desc": "Επαναφορά στις προεπιλεγμένες ρυθμίσεις" + }, + "VoltageCalibration": { + "text2": [ + "Βαθμονόμηση", + "τάσης εισόδου;" + ], + "desc": "Έναρξη βαθμονόμησης τάσης εισόδου (κράτημα για έξοδο)" + }, + "AdvancedSoldering": { + "text2": [ + "Λεπτομερής", + "οθ. κόλλησης" + ], + "desc": "Προβολή λεπτομερών πληροφοριών σε μικρότερη γραμματοσειρά στην οθόνη κόλλησης" + }, + "ScrollingSpeed": { + "text2": [ + "Ταχύτητα", + "κύλισης" + ], + "desc": "Ταχύτητα κύλισης κειμένου (Α=αργά | Γ=γρήγορα)" + }, + "QCMaxVoltage": { + "text2": [ + "Τάση", + "QC" + ], + "desc": "Μέγιστη τάση QC που να ζητά το κολλητήρι από το τροφοδοτικό" + }, + "PDNegTimeout": { + "text2": [ + "χρονικό όριο", + "PD" + ], + "desc": "Χρονικό όριο διαπραγμάτευσης PD σε βήματα 100ms για συμβατότητα με κάποιους φορτιστές QC" + }, + "PowerLimit": { + "text2": [ + "Ενεργειακό", + "όριο" + ], + "desc": "Μέγιστη ενέργεια που μπορεί να χρησιμοποιεί το κολλητήρι (W=watt)" + }, + "ReverseButtonTempChange": { + "text2": [ + "Αντιστροφή", + "πλήκτρων + -" + ], + "desc": "Αντιστροφή διάταξης πλήκτρων στη ρύθμιση θερμοκρασίας" + }, + "TempChangeShortStep": { + "text2": [ + "Αλλαγή θερμοκ.", + "στιγμιαίο" + ], + "desc": "Βήμα αλλαγής θερμοκρασίας σε στιγμιαίο πάτημα πλήκτρου" + }, + "TempChangeLongStep": { + "text2": [ + "Αλλαγή θερμοκ.", + "παρατεταμένο" + ], + "desc": "Βήμα αλλαγής θερμοκρασίας σε παρατεταμένο πάτημα πλήκτρου" + }, + "PowerPulsePower": { + "text2": [ + "Παλμός", + "ενέργειας" + ], + "desc": "Ένταση ενέργειας παλμού διατήρησης λειτουργίας (watt)" + }, + "HallEffSensitivity": { + "text2": [ + "Ευαισθ. αισθ. ", + "φαιν. Hall" + ], + "desc": "Ευαισθησία του αισθητήρα φαινομένου Hall για εντοπισμό αδράνειας (0=off | Χ=χαμηλή | Μ=μέτρια | Υ=υψηλή)" + }, + "LockingMode": { + "text2": [ + "Κλείδωμα", + "πλήκτρων" + ], + "desc": "Κατά την κόλληση, κρατήστε και τα δύο πλήκτρα για κλείδωμα (A=απενεργοποίηση | B=μόνο λειτ. boost | Π=πλήρες κλείδωμα)" + }, + "MinVolCell": { + "text2": [ + "Ελάχιστη", + "τάση" + ], + "desc": "Ελάχιστη επιτρεπτή τάση ανα κυψέλη (3 σε σειρά: 3 - 3.7V | 4-6 σε σειρά: 2.4 - 3.7V)" + }, + "AnimLoop": { + "text2": [ + "Επανάληψη", + "κιν. εικονιδ." + ], + "desc": "Επανάληψη κίνησης εικονιδίων στο αρχικό μενού" + }, + "AnimSpeed": { + "text2": [ + "Ταχύτητα", + "κιν. εικονιδ." + ], + "desc": "Ρυθμός κίνησης εικονιδίων στο μενού (0=off | Α=αργός | Μ=μέτριος | Γ=γρήγορος" + }, + "PowerPulseWait": { + "text2": [ + "Καθυστέρηση", + "παλμού ενέργ." + ], + "desc": "Καθυστέρηση πριν την ενεργοποίση παλμού διατήρησης λειτουργίας (x 2.5s)" + }, + "PowerPulseDuration": { + "text2": [ + "Διάρκεια", + "παλμού ενέργ." + ], + "desc": "Διάρκεια παλμού διατήρησης ενέργειας (x 250ms)" + }, + "LanguageSwitch": { + "text2": [ + "Γλώσσα", + " GR Ελληνικά" + ], + "desc": "Τρέχουσα γλώσσα λογισμικού" + }, + "Brightness": { + "text2": [ + "Αντίθεση", + "οθόνης" + ], + "desc": "Ρύθμιση φωτεινότητας οθόνης OLED" + }, + "ColourInversion": { + "text2": [ + "Αντιστροφή", + "χρωμάτων" + ], + "desc": "Αντιστροφή χρωμάτων οθόνης OLED" + } + } +} \ No newline at end of file diff --git a/Translations/translation_YUE_HK.json b/Translations/translation_YUE_HK.json index d5b8ed78..8def9cbb 100644 --- a/Translations/translation_YUE_HK.json +++ b/Translations/translation_YUE_HK.json @@ -4,7 +4,7 @@ "fonts": ["ascii_basic", "cjk"], "tempUnitFahrenheit": true, "messages": { - "SettingsCalibrationWarning": "開始温度校正之前,請先確定辣雞咀係處於室温!", + "SettingsCalibrationWarning": "開始温度校正之前,請先確定焫雞咀係處於室温!", "SettingsResetWarning": "你係咪確定要將全部設定重設到預設值?", "UVLOWarningString": "電壓過低", "UndervoltageString": "Undervoltage", @@ -21,8 +21,8 @@ "messagesWarn": { "ResetOKMessage": "已重設!", "SettingsResetMessage": "設定已被重設!", - "NoAccelerometerMessage": ["No accelerometer", "detected!"], - "NoPowerDeliveryMessage": ["No USB-PD IC", "detected!"], + "NoAccelerometerMessage": "未能偵測加速度計", + "NoPowerDeliveryMessage": "未能偵測PD晶片", "LockingKeysString": "已鎖定", "UnlockingKeysString": "已解除鎖定", "WarningKeysLockedString": "!撳掣鎖定!", @@ -78,7 +78,7 @@ }, "SleepTemperature": { "text2": "待機温度", - "desc": "喺待機模式時嘅辣雞咀温度" + "desc": "喺待機模式時嘅焫雞咀温度" }, "SleepTimeout": { "text2": "待機延時", @@ -114,11 +114,11 @@ }, "CooldownBlink": { "text2": "降温時閃爍", - "desc": "停止加熱之後,當辣雞咀仲係熱嗰陣閃爍畫面" + "desc": "停止加熱之後,當焫雞咀仲係熱嗰陣閃爍畫面" }, "TemperatureCalibration": { "text2": "温度校正?", - "desc": "開始校正辣雞咀温度位移" + "desc": "開始校正焫雞咀温度位移" }, "SettingsReset": { "text2": "全部重設?", @@ -146,7 +146,7 @@ }, "PowerLimit": { "text2": "功率限制", - "desc": "限制辣雞可用嘅最大功率 " + "desc": "限制焫雞可用嘅最大功率 " }, "ReverseButtonTempChange": { "text2": "反轉加減掣", diff --git a/Translations/translation_ZH_CN.json b/Translations/translation_ZH_CN.json index 18a6527a..7c5c0333 100644 --- a/Translations/translation_ZH_CN.json +++ b/Translations/translation_ZH_CN.json @@ -4,27 +4,27 @@ "fonts": ["ascii_basic", "cjk"], "tempUnitFahrenheit": true, "messages": { - "SettingsCalibrationWarning": "开始温度校正前,请先确定铬铁头正处于室温!", - "SettingsResetWarning": "你是否确定要将全部设置重置为默认值?", + "SettingsCalibrationWarning": "开始温度校正前,请先确定烙铁头正处于室温!", + "SettingsResetWarning": "你是否确定要将全部设定重置为默认值?", "UVLOWarningString": "电压过低", "UndervoltageString": "Undervoltage", - "InputVoltageString": "Input V: ", + "InputVoltageString": "VIN: ", "SleepingSimpleString": "Zzzz", - "SleepingAdvancedString": "Sleeping...", - "SleepingTipAdvancedString": "Tip:", - "IdleTipString": "Tip:", - "IdleSetString": " Set:", - "TipDisconnectedString": "NO TIP", + "SleepingAdvancedString": "Zzzz...", + "SleepingTipAdvancedString": "--->", + "IdleTipString": "--->", + "IdleSetString": " /", + "TipDisconnectedString": "<-X-", "SolderingAdvancedPowerPrompt": "Power: ", "OffString": "关" }, "messagesWarn": { "ResetOKMessage": "已重置!", - "SettingsResetMessage": "设置已被重置!", - "NoAccelerometerMessage": ["No accelerometer", "detected!"], - "NoPowerDeliveryMessage": ["No USB-PD IC", "detected!"], + "SettingsResetMessage": "设定已被重置!", + "NoAccelerometerMessage": "未检测到加速度计", + "NoPowerDeliveryMessage": "未检测到PD电路", "LockingKeysString": "已锁定", - "UnlockingKeysString": "已解除锁定", + "UnlockingKeysString": "已解锁", "WarningKeysLockedString": "!按键锁定!", "WarningThermalRunaway": "加热失控" }, @@ -63,78 +63,78 @@ "desc": "自动待机省电设置" }, "UIMenu": { - "text2": "使用者介面", - "desc": "使用者介面设置" + "text2": "用户界面", + "desc": "用户界面设置" }, "AdvancedMenu": { - "text2": "进阶设置", - "desc": "进阶设置" + "text2": "高级设置", + "desc": "高级设置" } }, "menuOptions": { "DCInCutoff": { - "text2": "电源", - "desc": "输入电源;设置自动停机电压 " + "text2": "下限电压", + "desc": "设置自动停机电压 " }, "SleepTemperature": { "text2": "待机温度", - "desc": "于待机模式时的铬铁头温度" + "desc": "待机模式时的烙铁头温度" }, "SleepTimeout": { - "text2": "待机延时", - "desc": "自动进入待机模式前的闲置等候时间 " + "text2": "待机超时", + "desc": "自动进入待机模式前的等候时间 " }, "ShutdownTimeout": { "text2": "自动关机", - "desc": "自动关机前的闲置等候时间 " + "desc": "自动关机前的等候时间 " }, "MotionSensitivity": { "text2": "动作灵敏度", "desc": "0=禁用 | 1=最低灵敏度 | ... | 9=最高灵敏度" }, "TemperatureUnit": { - "text2": "温标", + "text2": "温度单位", "desc": "C=摄氏 | F=华氏" }, "AdvancedIdle": { - "text2": "详细闲置画面", - "desc": "于闲置画面以英语小字显示详细信息" + "text2": "闲置画面详情", + "desc": "闲置画面以英语小字体显示详情" }, "DisplayRotation": { - "text2": "画面方向", - "desc": "右=使用右手 | 左=使用左手 | 自=自动" + "text2": "显示方向", + "desc": "右=右手 | 左=左手 | 自=自动" }, "BoostTemperature": { "text2": "增热温度", - "desc": "于增热模式时使用的温度" + "desc": "增热模式时使用的温度" }, "AutoStart": { - "text2": "自动启用", - "desc": "开机时自动启用 <无=禁用 | 焊=焊接模式 | 待=待机模式 | 室=室温待机>" + "text2": "自动启动", + "desc": "开机时自动启动 <无=禁用 | 焊=焊接模式 | 待=待机模式 | 室=室温待机>" }, "CooldownBlink": { - "text2": "降温时闪烁", - "desc": "停止加热之后,当铬铁头仍处于高温时闪烁画面" + "text2": "降温时闪显", + "desc": "停止加热之后,闪动温度显示提醒烙铁头仍处于高温状态" }, "TemperatureCalibration": { "text2": "温度校正?", - "desc": "开始校正铬铁头温度位移" + "desc": "开始校正烙铁头温度偏移" }, "SettingsReset": { "text2": "全部重置?", - "desc": "将所有设置重置为默认值" + "desc": "将所有设定重置为默认值" }, "VoltageCalibration": { "text2": "输入电压校正?", - "desc": "开始校正VIN输入电压 <长按以退出>" + "desc": "开始校正输入电压(VIN)<长按以退出>" }, "AdvancedSoldering": { - "text2": "详细焊接画面", - "desc": "于焊接模式画面以英语小字显示详细信息" + "text2": "焊接画面详情", + "desc": "焊接模式画面以英语小字体显示详请" }, "ScrollingSpeed": { - "text2": "卷动速度", - "desc": "解说文字的卷动速度" + "text2": "滚动速度", + "desc": "解说文字的滚动速度" }, "QCMaxVoltage": { "text2": "QC电压", @@ -142,55 +142,55 @@ }, "PDNegTimeout": { "text2": "PD超时", - "desc": "设定USB PD协议交涉的超时时限;为兼容某些QC电源而设 " + "desc": "设定USB-PD协议交涉的超时时限;为兼容某些QC电源而设 " }, "PowerLimit": { "text2": "功率限制", - "desc": "限制铬铁可用的最大功率 " + "desc": "限制烙铁可用的最大功率 " }, "ReverseButtonTempChange": { "text2": "调换加减键", - "desc": "调校温度时调换加减键的方向" + "desc": "调校温度时更换加减键的方向" }, "TempChangeShortStep": { - "text2": "温度调整 短", - "desc": "调校温度时短按一下的温度变幅" + "text2": "短按温度调整", + "desc": "调校温度时短按按键的温度变幅" }, "TempChangeLongStep": { - "text2": "温度调整 长", + "text2": "长按温度调整", "desc": "调校温度时长按按键的温度变幅" }, "PowerPulsePower": { "text2": "电源脉冲", - "desc": "为保持电源唤醒而通电所用的功率 " + "desc": "为保持电源处于唤醒状态所用的功率 " }, "HallEffSensitivity": { "text2": "磁场灵敏度", - "desc": "磁场感应器用作启动待机模式的灵敏度 <关=禁用 | 低=最低灵敏度 | 中=中等灵敏度 | 高=最高灵敏度>" + "desc": "霍尔效应传感器用作启动待机模式的灵敏度 <关=禁用 | 低=低灵敏度 | 中=中灵敏度 | 高=高灵敏度>" }, "LockingMode": { "text2": "按键锁定", - "desc": "于焊接模式时,同时长按两个按键启用按键锁定 <无=禁用 | 增=只容许增热模式 | 全=锁定全部>" + "desc": "焊接模式时,同时长按两个按键启用按键锁定 <无=禁用 | 增=只容许增热模式 | 全=完全锁定>" }, "MinVolCell": { "text2": "最低电压", - "desc": "每颗电池的最低可用电压 <伏特> <3S: 3.0V - 3.7V, 4/5/6S: 2.4V - 3.7V>" + "desc": "每节电池的最低允许电压 <3S: 3.0V - 3.7V, 4/5/6S: 2.4V - 3.7V>" }, "AnimLoop": { "text2": "动画循环", - "desc": "循环显示功能表图示动画" + "desc": "主菜单中循环播放功能图标动画" }, "AnimSpeed": { "text2": "动画速度", - "desc": "功能表图示动画的速度 <关=不显示动画 | 慢=慢速 | 中=中速 | 快=快速>" + "desc": "主菜单中功能图标动画的播放速度 <关=不显示动画 | 慢=慢速 | 中=中速 | 快=快速>" }, "PowerPulseWait": { "text2": "电源脉冲间隔", - "desc": "为保持电源唤醒,每次通电之间的间隔时间 " + "desc": "为保持电源处于唤醒状态,每次通电之间的间隔时间 " }, "PowerPulseDuration": { "text2": "电源脉冲时长", - "desc": "为保持电源唤醒,每次通电脉冲的时间长度 " + "desc": "为保持电源处于唤醒状态,每次通电脉冲的时间长度 " }, "LanguageSwitch": { "text2": "语言:简体中文", @@ -198,11 +198,11 @@ }, "Brightness": { "text2": "屏幕亮度", - "desc": "设定OLED屏幕的亮度" + "desc": "调整OLED屏幕的亮度" }, "ColourInversion": { - "text2": "螢幕反轉色", - "desc": "反转OLED屏幕的黑白色彩" + "text2": "反转屏幕颜色", + "desc": "反转OLED黑/白屏幕" } } } diff --git a/Translations/translation_ZH_TW.json b/Translations/translation_ZH_TW.json index 34727748..b28835e5 100644 --- a/Translations/translation_ZH_TW.json +++ b/Translations/translation_ZH_TW.json @@ -4,7 +4,7 @@ "fonts": ["ascii_basic", "cjk"], "tempUnitFahrenheit": true, "messages": { - "SettingsCalibrationWarning": "開始溫度校正前,請先確定鉻鐵頭正處於室溫!", + "SettingsCalibrationWarning": "開始溫度校正前,請先確定烙鐵頭正處於室溫!", "SettingsResetWarning": "你是否確定要將全部設定重設到預設值?", "UVLOWarningString": "電壓過低", "UndervoltageString": "Undervoltage", @@ -21,8 +21,8 @@ "messagesWarn": { "ResetOKMessage": "已重設!", "SettingsResetMessage": "設定已被重設!", - "NoAccelerometerMessage": ["No accelerometer", "detected!"], - "NoPowerDeliveryMessage": ["No USB-PD IC", "detected!"], + "NoAccelerometerMessage": "未能偵測加速度計", + "NoPowerDeliveryMessage": "未能偵測PD晶片", "LockingKeysString": "已鎖定", "UnlockingKeysString": "已解除鎖定", "WarningKeysLockedString": "!按鍵鎖定!", @@ -78,7 +78,7 @@ }, "SleepTemperature": { "text2": "待機溫度", - "desc": "於待機模式時的鉻鐵頭溫度" + "desc": "於待機模式時的烙鐵頭溫度" }, "SleepTimeout": { "text2": "待機延時", @@ -114,11 +114,11 @@ }, "CooldownBlink": { "text2": "降溫時閃爍", - "desc": "停止加熱之後,當鉻鐵頭仍處於高溫時閃爍畫面" + "desc": "停止加熱之後,當烙鐵頭仍處於高溫時閃爍畫面" }, "TemperatureCalibration": { "text2": "溫度校正?", - "desc": "開始校正鉻鐵頭溫度位移" + "desc": "開始校正烙鐵頭溫度位移" }, "SettingsReset": { "text2": "全部重設?", @@ -146,7 +146,7 @@ }, "PowerLimit": { "text2": "功率限制", - "desc": "限制鉻鐵可用的最大功率 " + "desc": "限制烙鐵可用的最大功率 " }, "ReverseButtonTempChange": { "text2": "調換加減鍵", diff --git a/source/Makefile b/source/Makefile index dfae3739..51373f1b 100644 --- a/source/Makefile +++ b/source/Makefile @@ -1,4 +1,3 @@ - ifndef model model:=TS100 endif @@ -11,18 +10,30 @@ ifneq ($(model),$(filter $(model),$(ALL_MODELS))) $(error Invalid model '$(model)', valid options are: $(ALL_MODELS)) endif +# output folder +HEXFILE_DIR=Hexfile +# temporary objects folder +OUTPUT_DIR_BASE=Objects +OUTPUT_DIR=Objects/$(model) + ALL_LANGUAGES=BG CS DA DE EN ES FI FR HR HU IT JA_JP LT NL NL_BE NO PL PT RU SK SL SR_CYRL SR_LATN SV TR UK YUE_HK ZH_CN ZH_TW LANGUAGE_GROUP_CJK_LANGS=EN JA_JP YUE_HK ZH_TW ZH_CN LANGUAGE_GROUP_CJK_NAME=Chinese+Japanese +ifdef custom_multi_langs +RUN_SHELL_CMD := $(shell rm -Rf {Core/Gen,$(OUTPUT_DIR)/Core/Gen,$(HEXFILE_DIR)/*_Custom.*}) +LANGUAGE_GROUP_CUSTOM_LANGS=$(custom_multi_langs) +LANGUAGE_GROUP_CUSTOM_NAME=Custom +endif + LANGUAGE_GROUP_CYRILLIC_LANGS=EN BG RU SR_CYRL SR_LATN UK LANGUAGE_GROUP_CYRILLIC_NAME=Bulgarian+Russian+Serbian+Ukrainian LANGUAGE_GROUP_EUR_LANGS=EN $(filter-out $(LANGUAGE_GROUP_CJK_LANGS) $(LANGUAGE_GROUP_CYRILLIC_LANGS),$(ALL_LANGUAGES)) LANGUAGE_GROUP_EUR_NAME=European -LANGUAGE_GROUPS=CJK CYRILLIC EUR +LANGUAGE_GROUPS=CUSTOM CJK CYRILLIC EUR # Defines for host tools @@ -184,12 +195,7 @@ $(shell find $(SOURCE_CORE_DIR) -type f -name '*.cpp') \ $(shell find $(SOURCE_DRIVERS_DIR) -path $(PD_DRIVER_TESTS_DIR) -prune -false -o -type f -name '*.cpp') \ $(shell find $(DEVICE_BSP_DIR) -type f -name '*.cpp') \ $(shell find $(SOURCE_MIDDLEWARES_DIR) -type f -name '*.cpp') -# output folder -HEXFILE_DIR=Hexfile -# temporary objects folder -OUTPUT_DIR_BASE=Objects -OUTPUT_DIR=Objects/$(model) # code optimisation ------------------------------------------------------------ OPTIM=-Os -flto -finline-small-functions -findirect-inlining -fdiagnostics-color -ffunction-sections -fdata-sections -fshort-enums