diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9365a790b..4c6548b6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,48 @@
+# Changelog 24.04.beta8 - November 20, 2023
+
+## Changes
+
+### IRQ Interfaces
+Button and touch presses are now detected by the application through IO interrupts. Meaning inputs events will be registered and handled even if they happened when other tasks were being executed by the processor, resulting in a better UX.
+
+### Save and Load Wallet Output Descriptor from SD card
+Create or load from a wallet output descriptor file on a SD card. The backup file format is compatible with most coordinators.
+
+### Restore Default Settings
+Option to restore the device's settings to its factory state.
+
+### Wipe Device
+Option on settings to wipe the device, permanently removing settings and stored encrypted mnemonics by erasing every single bit of user's flash space.
+
+### Screensaver
+Optional screensaver to reduce pixels' burn-in and grab attention of the user when the device is left powered on.
+
+### Maix Dock Simulator
+Now Krux PC simulator can also run in Maix Dock mode, mimetizing appearance and characteristics of the most DIY Krux device
+
+### Update Embit to version 0.7
+Use latest Embit release
+
+### Optimized QR codes
+QR codes rendering is faster and uses less RAM
+
+### Mnemonic Numbers
+To match the input options, export mnemonics as decimal, hexadecimal or octal numbers
+When loading from numbers, a new numbers confirmation screen was added.
+
+### Addresses Exploring
+More receive and change addresses per page are shown on bigger screens
+
+### Export QR Codes as Images to SD Card
+Some QR codes can be exported as images to SD card
+
+### Optimized Settings Storage
+Device's storage is now used more efficiently, data is stored less frequently, only in case a setting is changed from defaults.
+
+### Other Small Fixes and Code Optimizations
+Many other small fixes and optimizations under the hood
+
+
# Version 23.09.1 - November 18, 2023
This release contain bugfixes:
diff --git a/README.md b/README.md
index 5d543fa10..480795748 100644
--- a/README.md
+++ b/README.md
@@ -66,25 +66,25 @@ Note that you can run `poetry install` after making a change to the krux code if
## Format code
```bash
-poetry run black ./src
-poetry run black ./tests
+poetry run poe format
```
## Run pylint
```bash
-poetry run pylint ./src
+poetry run poe lint
```
## Run tests
```bash
-poetry run pytest --cache-clear --cov src/krux --cov-report html ./tests
+poetry run poe test
```
+
This will run all tests and generate a coverage report you can browse to locally in your browser at `file:///path/to/krux/htmlcov/index.html`.
For more verbose test output (e.g., to see the output of print statements), run:
```bash
-poetry run pytest --cache-clear --cov src/krux --cov-report html --show-capture all --capture tee-sys -r A ./tests
+poetry run poe test-verbose
```
To run just a specific test from a specific file, run:
diff --git a/docs/faq.en.md b/docs/faq.en.md
index 7a1af5e9a..20ae002b7 100644
--- a/docs/faq.en.md
+++ b/docs/faq.en.md
@@ -72,4 +72,7 @@ If after flashing `maixpy_amigo_tft` to your device you notice that the buttons
Starting from version 23.09.0, Krux supports SD card hot plugging. If you are using older versions, it may only detect the SD card at boot, so make sure Krux is turned off when inserting the microSD into it. To test the card compatibility use Krux [Tools>Check SD Card](getting-started/features/tools.md/#check-sd-card).
Make sure the SD card is using MBR/DOS partition table and FAT32 format.
-Here is some [supported microSD cards](https://github.com/m5stack/m5-docs/blob/master/docs/en/core/m5stickv.md#tf-cardmicrosd-test), and here is the MaixPy FAQ explaining [Why my micro SD card cannot be read](https://wiki.sipeed.com/soft/maixpy/en/others/maixpy_faq.html#Micro-SD-card-cannot-be-read).
\ No newline at end of file
+Here is some [supported microSD cards](https://github.com/m5stack/m5-docs/blob/master/docs/en/core/m5stickv.md#tf-cardmicrosd-test), and here is the MaixPy FAQ explaining [Why my micro SD card cannot be read](https://wiki.sipeed.com/soft/maixpy/en/others/maixpy_faq.html#Micro-SD-card-cannot-be-read).
+
+## Why insert an SD card into my device? What is it for? Does it save something?
+SD card use is optional, most people use Krux only with QR codes. But you can use SD card to to upgrade the firmware, save settings, cnc/file, QR codes, XPUBs, encrypted mnemonics, and to save and load PSBTs, messages and wallet output descriptors.
\ No newline at end of file
diff --git a/docs/getting-started/usage/generating-a-mnemonic.en.md b/docs/getting-started/usage/generating-a-mnemonic.en.md
index 533338ed1..1d5cdbd16 100644
--- a/docs/getting-started/usage/generating-a-mnemonic.en.md
+++ b/docs/getting-started/usage/generating-a-mnemonic.en.md
@@ -53,8 +53,13 @@ Note: For 12-word mnemonics, only the first half of the SHA256 hash is used (128
+### How to verify
+
+Don't trust, verify. We encourage you not to trust any claim you cannot verify yourself. Therefore, there are wallets that use compatible algorithms to calculate the entropy derived from dice rolls. You can use the [SeedSigner](https://seedsigner.com/) or Coldcard hardware wallets, or even the [Bitcoiner Guide website](https://bitcoiner.guide/seed/), they share the same logic that Krux uses and will give the same mnemonic.
+
## Alternatives
-See [here](https://vault12.com/securemycrypto/cryptocurrency-security-how-to/seed-phrase-creation/) for good methods to generate a mnemonic manually, or visit [Ian Coleman's BIP-39 Tool](https://iancoleman.io/bip39/) offline or on an airgapped device to generate one automatically.
-It's worth noting that Ian's tool is able to take a mnemonic and generate a QR code that Krux can read in via the QR input method mentioned on the next page.
+You can use any other offline airgapped devices to generate your mnemonic. If you want to use a regular PC, a common strategy is to boot the PC using [Tails](https://tails.boum.org/) from a USB stick, without connecting the device to the internet, and then use a copy of the the [Bitcoiner Guide website](https://bitcoiner.guide/seed/) or even [Ian Coleman's BIP-39 Tool](https://iancoleman.io/bip39/). It's worth noting that both generate a QR code that Krux can read via the QR input method mentioned on the next page (Loading a Mnemonic).
+
+See [here](https://vault12.com/securemycrypto/cryptocurrency-security-how-to/seed-phrase-creation/) for other good methods to generate a mnemonic manually.
diff --git a/docs/getting-started/usage/navigating-the-main-menu.en.md b/docs/getting-started/usage/navigating-the-main-menu.en.md
index 8139f326c..ae66608d2 100644
--- a/docs/getting-started/usage/navigating-the-main-menu.en.md
+++ b/docs/getting-started/usage/navigating-the-main-menu.en.md
@@ -62,7 +62,7 @@ The encrypted mnemonic will be converted to a QR code. When you scan this QR cod
-This option displays your master extended public key (xpub) as text as well as a QR code.
+This option displays your master extended public key (xpub) as text as well as a QR code. The extended public key (xpub) can also be stored on a SD card if available.
After the xpub, a zpub or Zpub is shown depending on if a single-sig or multisig wallet was chosen. This z/Zpub is usually not necessary unless you are using a wallet coordinator that either cannot parse or ignores [key origin information in key expressions](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#Key_Expressions).
diff --git a/firmware/MaixPy b/firmware/MaixPy
index 198dac960..4cdf15f1a 160000
--- a/firmware/MaixPy
+++ b/firmware/MaixPy
@@ -1 +1 @@
-Subproject commit 198dac9608bad51affdde0d3bf48e94db678d8d7
+Subproject commit 4cdf15f1a4d7cb496adce5da7314623f552a75a1
diff --git a/firmware/font/README.md b/firmware/font/README.md
index 45a7c44d0..a7b575d95 100644
--- a/firmware/font/README.md
+++ b/firmware/font/README.md
@@ -1,14 +1,14 @@
# Updating the built-in font
-Krux uses a [custom fork](https://github.com/bachan/terminus-font-vietnamese) of the [Terminus](http://terminus-font.sourceforge.net/) font for its glyphs that includes Vietnamese characters, the Bitcoin currency symbol (βΏ) and the PokΓ©mon Dollar symbol (β½ with 2 horizontal lines).
+Krux uses a [custom fork](https://github.com/bachan/terminus-font-vietnamese) of the [Terminus](http://terminus-font.sourceforge.net/) font for its glyphs that includes Vietnamese characters, the Bitcoin currency symbol (βΏ) and custom icons.
To rebuild the font for all devices, run:
```
./bdftokff.sh ter-u14n 8 14 > m5stickv.kff
-./bdftokff.sh ter-u16n 8 16 > bit_dock.kff
+./bdftokff.sh ter-u16n 8 16 > bit_dock_yahboom.kff
./bdftokff.sh ter-u24b 12 24 > amigo.kff
```
-Once you have a `.kff` file, locate the project that you want to use the updated font under `firmware/MaixPy/projects/` (`maixpy_amigo_tft/` for ex.), open its `compile/overrides/components/micropython/port/src/omv/img/font.c` file and replace the array contents in the `unicode` variable with the byte array found within the `.kff` file, then rebuild the firmware.
+Once you have `.kff` files, for each project that you want to use the updated fonts, edit `../MaixPy/projects/*/compile/overrides/components/micropython/port/src/omv/img/font.c` (substituting `maixpy_amigo_tft` for `*` if only for an amigo) and replace the array contents in the `unicode` variable with the byte array found within the appropriate `.kff` file, then rebuild the firmware.
# How it works
Krux uses bitmap fonts that are custom-built for each device it runs on. The format that the firmware expects fonts to be in is a custom format referred to as "krux font format," or `.kff`.
diff --git a/firmware/font/font_icons/Derivation_amigo.pbm b/firmware/font/font_icons/Derivation_amigo.pbm
new file mode 100644
index 000000000..0f4a1c22e
Binary files /dev/null and b/firmware/font/font_icons/Derivation_amigo.pbm differ
diff --git a/firmware/font/font_icons/Derivation_bit_dock_yahboom.pbm b/firmware/font/font_icons/Derivation_bit_dock_yahboom.pbm
new file mode 100644
index 000000000..7d5d3e932
--- /dev/null
+++ b/firmware/font/font_icons/Derivation_bit_dock_yahboom.pbm
@@ -0,0 +1,4 @@
+P4
+# Created by GIMP version 2.10.34 PNM plug-in
+8 16
+ππππ`dffd`df
\ No newline at end of file
diff --git a/firmware/font/font_icons/Derivation_m5stickv.pbm b/firmware/font/font_icons/Derivation_m5stickv.pbm
new file mode 100644
index 000000000..1fffa8a52
--- /dev/null
+++ b/firmware/font/font_icons/Derivation_m5stickv.pbm
@@ -0,0 +1,4 @@
+P4
+# Created by GIMP version 2.10.34 PNM plug-in
+8 14
+πππτffd`df
\ No newline at end of file
diff --git a/firmware/font/font_icons/Fingerprint_amigo.pbm b/firmware/font/font_icons/Fingerprint_amigo.pbm
new file mode 100644
index 000000000..b8c4a6716
Binary files /dev/null and b/firmware/font/font_icons/Fingerprint_amigo.pbm differ
diff --git a/firmware/font/font_icons/Fingerprint_bit_dock_yahboom.pbm b/firmware/font/font_icons/Fingerprint_bit_dock_yahboom.pbm
new file mode 100644
index 000000000..4b4c01a7f
Binary files /dev/null and b/firmware/font/font_icons/Fingerprint_bit_dock_yahboom.pbm differ
diff --git a/firmware/font/font_icons/Fingerprint_m5stickv.pbm b/firmware/font/font_icons/Fingerprint_m5stickv.pbm
new file mode 100644
index 000000000..681d08ffb
--- /dev/null
+++ b/firmware/font/font_icons/Fingerprint_m5stickv.pbm
@@ -0,0 +1,4 @@
+P4
+# Created by GIMP version 2.10.34 PNM plug-in
+8 14
+> 3) << 11
+
+ # 6 significant bits of green shifted to middle
+ green = (rgb[1] >> 2) << 5
+
+ # 5 significant bits of blue on the right
+ blue = rgb[2] >> 3
+
+ return int(red + green + blue).to_bytes(2, "big" if big else "little")
+
+
+def rgb16_to_rgb24(rgb, big=False):
+ """
+ convert 2 bytes of rgb565 into 3 bytes of rgb888
+ default from little-endian, so rgb565 becomes gbrg3553
+ """
+ def maxv(number_of_bits):
+ return (2 ** number_of_bits) -1
+
+ assert type(rgb) == bytes and len(rgb) == 2
+
+ rgb_int = int.from_bytes(rgb, "big" if big else "little")
+
+ # left 5 bits of red multiplied to fill 8 bit space
+ red = round((rgb_int >> 11) * maxv(8) / maxv(5))
+
+ # middle 6 bits of green multiplied to fill 8 bit space
+ green = round(((rgb_int & maxv(11)) >> 5) * maxv(8) / maxv(6))
+
+ # right 5 bits of blue multiplied to fill 8 bit space
+ blue = round((rgb_int & maxv(5)) * maxv(8) / maxv(5))
+
+ return b''.join([x.to_bytes(1, "big") for x in [red, green, blue]])
+
+
+def main(*args):
+ rgb = None
+
+ if len(args) == 1:
+ arg = args[0]
+ if arg[:2] == "0x":
+ rgb = unhexlify(arg[2:])
+ else:
+ rgb = (abs(int(args[0])) % 65536).to_bytes(2, 'big')
+ else:
+ rgb = b''.join([(abs(int(x)) % 256).to_bytes(1, 'big') for x in args])
+
+ if rgb:
+ if len(rgb) == 2:
+ answer = rgb16_to_rgb24(rgb)
+ elif len(rgb) == 3:
+ answer = rgb24_to_rgb16(rgb)
+ else:
+ answer = None
+
+ return(answer)
+
+
+if __name__ == '__main__':
+ import sys
+
+ def help():
+ print("syntax: {} integer-bytes-or-0xhex-color".format(sys.argv[0]))
+
+ if len(sys.argv) > 1:
+ try:
+ print('0x' + main(*sys.argv[1:]).hex())
+ except:
+ help()
+ exit(1)
+ else:
+ help()
+ exit(1)
diff --git a/i18n/i18n.py b/i18n/i18n.py
index 7e24941ec..c01c1bc5f 100644
--- a/i18n/i18n.py
+++ b/i18n/i18n.py
@@ -87,14 +87,13 @@ def validate_translation_files():
def fill_missing():
- """Uses googletrans==4.0.0rc1 to automaticalyy fill missing translations"""
+ """Uses translate 3.6.1 to automaticalyy fill missing translations"""
if len(sys.argv) > 2:
force_target = sys.argv[2]
else:
force_target = None
- from googletrans import Translator
+ from translate import Translator
- translator = Translator()
slugs = find_translation_slugs()
translation_filenames = [
f
@@ -102,12 +101,11 @@ def fill_missing():
if isfile(join(TRANSLATION_FILES_DIR, f))
]
for translation_filename in translation_filenames:
- target = translation_filename[:5].replace("-", "_")
+ target = translation_filename[:5]
if force_target:
if force_target != translation_filename:
continue
- if translation_filename.startswith("poke"):
- continue
+ translator = Translator(to_lang=target)
print("Translating %s...\n" % translation_filename)
complete = True
with open(
@@ -117,21 +115,17 @@ def fill_missing():
for slug in slugs:
if slug not in translations or translations[slug] == "":
try:
- translated = '"%s",' % translator.translate(
- slug, src="en", dest=target
- ).text.replace(" \ n", "\\n")
- except:
- # some languages fail to translate with a space at end of string
- translated = (
- '"%s",'
- % translator.translate(
- slug + "]", src="en", dest=target
- ).text.replace(" \ n", "\\n")[:-1]
+ translated = '"%s",' % translator.translate(slug).replace(
+ " \ n", "\\n"
)
- print('"%s":' % slug, translated)
+ print('"%s":' % slug, translated)
+ except Exception as e:
+ print("Error:", e)
+ print("Failed to translate:", slug)
+ break
complete = False
if complete:
- print("Notthing to add")
+ print("Nothing to add")
else:
print("Please review and copy items above")
print("\n\n")
diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json
index a43f322a8..1305e33ed 100644
--- a/i18n/translations/de-DE.json
+++ b/i18n/translations/de-DE.json
@@ -19,7 +19,6 @@
"Anti-glare disabled": "Blendschutz deaktiviert",
"Anti-glare enabled": "Blendschutz aktiviert",
"Are you sure?": "Bist Du sicher?",
- "BIP39 Mnemonic": "BIP39 Mnemonic",
"Back": "ZurΓΌck",
"Backing up bootloader..\n\n%d%%": "Bootloader wird gesichert..\n\n%d%%",
"Bad signature": "UngΓΌltige Signatur",
@@ -49,6 +48,7 @@
"Cut Method": "Cut-Methode",
"Decimal": "Dezimal",
"Decrypt?": "EntschlΓΌsseln?",
+ "Delete %s?": "LΓΆschen %s?",
"Delete File?": "Datei lΓΆschen?",
"Delete Mnemonic": "Mnemonic lΓΆschen",
"Depth Per Pass": "Tiefe pro Durchgang",
@@ -94,11 +94,10 @@
"GRBL": "GRBL",
"Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "Dieser Mnemonic eine benutzerdefinierte ID zuteilen? Andernfalls wird der aktuelle Fingerabdruck verwendet",
"Go": "Go",
- "Heat Interval": "WΓ€rmeintervall",
- "Heat Time": "Hitzezeit",
"Hex Public Key": "Hex ΓΆffentlicher SchlΓΌssel",
"Hexadecimal": "Hexadezimal",
"ID already exists\n": "ID existiert bereits\n",
+ "Incomplete output descriptor": "UnvollstΓ€ndiger Ausgabedeskriptor",
"Inputs (%d): ": "Input (%d): ",
"Invalid address": "UngΓΌltige Adresse",
"Invalid bootloader": "UngΓΌltiger Bootloader",
@@ -199,6 +198,7 @@
"Signed PSBT": "Signierte PSBT",
"Single-sig": "Single-Sig",
"Size: ": "GrΓΆΓe: ",
+ "Some checks cannot be performed.": "Einige Schecks kΓΆnnen nicht durchgefΓΌhrt werden.",
"Spend (%d): ": "Ausgabe (%d): ",
"Stackbit 1248": "Stackbit 1248",
"Store on Flash": "Auf Flash speichern",
@@ -235,7 +235,7 @@
"Wallet output descriptor": "Wallet Ausgabedeskriptor",
"Wallet output descriptor loaded!": "Wallet Ausgabedeskriptor geladen!",
"Wallet output descriptor not found.": "Wallet Ausgabedeskriptor nicht gefunden.",
- "Warning:\nIncomplete output descriptor": "Warnung:\nUnvollstΓ€ndiger Ausgabedeskriptor",
+ "Warning:": "Warnung:",
"Word %d": "Wort %d",
"Word Numbers": "Wortnummern",
"Words": "WΓΆrter",
diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json
index 7e9ac20eb..dbff6bd65 100644
--- a/i18n/translations/es-MX.json
+++ b/i18n/translations/es-MX.json
@@ -19,7 +19,6 @@
"Anti-glare disabled": "Antideslumbrante desactivado",
"Anti-glare enabled": "Antideslumbrante habilitado",
"Are you sure?": "ΒΏEstas seguro?",
- "BIP39 Mnemonic": "BIP39 MnemΓ³nico",
"Back": "AtrΓ‘s",
"Backing up bootloader..\n\n%d%%": "Copia de seguridad del cargador de arranque..\n\n%d%%",
"Bad signature": "Mala asignatura",
@@ -49,6 +48,7 @@
"Cut Method": "MΓ©todo de corte",
"Decimal": "Decimal",
"Decrypt?": "Descifrar?",
+ "Delete %s?": "Eliminar %s?",
"Delete File?": "ΒΏBorrar archivo?",
"Delete Mnemonic": "Eliminar mnemΓ³nico",
"Depth Per Pass": "Profundidad por pasada",
@@ -94,11 +94,10 @@
"GRBL": "GRBL",
"Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "ΒΏDarle a este mnemΓ³nico una identificaciΓ³n personalizada?De lo contrario se utilizarΓ‘ la huella digital actual",
"Go": "Ir",
- "Heat Interval": "Intervalo de calor",
- "Heat Time": "Tiempo de calor",
"Hex Public Key": "Clave pΓΊblica hexadecimal",
"Hexadecimal": "Hexadecimal",
"ID already exists\n": "ID ya existe\n",
+ "Incomplete output descriptor": "Descriptor incompleto",
"Inputs (%d): ": "Entradas (%d): ",
"Invalid address": "DirecciΓ³n invΓ‘lida",
"Invalid bootloader": "Cargador de arranque invΓ‘lido",
@@ -199,6 +198,7 @@
"Signed PSBT": "PSBT firmado",
"Single-sig": "Single-sig",
"Size: ": "Espacio: ",
+ "Some checks cannot be performed.": "No se pueden realizar algunos cheques.",
"Spend (%d): ": "Gastos (%d): ",
"Stackbit 1248": "Stackbit 1248",
"Store on Flash": "Almacenar en flash",
@@ -235,7 +235,7 @@
"Wallet output descriptor": "Descriptor de salida de billetera",
"Wallet output descriptor loaded!": "Β‘Se ha cargado el descriptor de salida de la cartera!",
"Wallet output descriptor not found.": "No se encontrΓ³ el descriptor de salida de la cartera.",
- "Warning:\nIncomplete output descriptor": "Advertencia:\nDescriptor de salida incompleto",
+ "Warning:": "Advertencia:",
"Word %d": "Palabra %d",
"Word Numbers": "NΓΊmeros de palabra",
"Words": "Palabras",
diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json
index 95da68cb8..3f2ef4339 100644
--- a/i18n/translations/fr-FR.json
+++ b/i18n/translations/fr-FR.json
@@ -19,7 +19,6 @@
"Anti-glare disabled": "Anti-Γ©blouissement dΓ©sactivΓ©",
"Anti-glare enabled": "Anti-Γ©blouissement activΓ©",
"Are you sure?": "Es-tu sΓ»r?",
- "BIP39 Mnemonic": "BIP39 MnΓ©monique",
"Back": "Retour",
"Backing up bootloader..\n\n%d%%": "Sauvegarde du chargeur de dΓ©marrage..\n\n%d%%",
"Bad signature": "Mauvaise signature",
@@ -49,6 +48,7 @@
"Cut Method": "MΓ©thode de coupe",
"Decimal": "DΓ©cimal",
"Decrypt?": "DΓ©crypter?",
+ "Delete %s?": "Supprimer %s?",
"Delete File?": "Supprimer le fichier?",
"Delete Mnemonic": "Supprimer mnΓ©monique",
"Depth Per Pass": "Profondeur par passage",
@@ -94,11 +94,10 @@
"GRBL": "GRBL",
"Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "Donnez Γ ce mnΓ©monique un identifiant personnalisΓ©?Sinon l'empreinte actuelle sera utilisΓ©e",
"Go": "Go",
- "Heat Interval": "Intervalle de chauffe",
- "Heat Time": "Temps de chauffe",
"Hex Public Key": "ClΓ© public hexadΓ©cimal",
"Hexadecimal": "HexadΓ©cimal",
"ID already exists\n": "Id existe dΓ©jΓ \n",
+ "Incomplete output descriptor": "Descripteur de sortie incomplet",
"Inputs (%d): ": "EntrΓ©es (%d) : ",
"Invalid address": "Adresse invalide",
"Invalid bootloader": "Chargeur de dΓ©marrage invalide",
@@ -199,6 +198,7 @@
"Signed PSBT": "PSBT signΓ©",
"Single-sig": "ClΓ© unique",
"Size: ": "CapacitΓ©: ",
+ "Some checks cannot be performed.": "Certains chΓ¨ques ne peuvent pas Γͺtre effectuΓ©s.",
"Spend (%d): ": "DΓ©pense (%d) : ",
"Stackbit 1248": "Stackbit 1248",
"Store on Flash": "Stocker sur flash",
@@ -225,7 +225,7 @@
"Use camera's entropy to create a new mnemonic": "Utilisez l'entropie de la camΓ©ra pour crΓ©er un nouveau mnΓ©monique",
"Used: ": "UtilisΓ©: ",
"Value %s out of range: [%s, %s]": "Valeur %s hors de portΓ©e: [%s, %s]",
- "Value must be multiple of %s": "La valeur doit Γͺtre multiple de %s",
+ "Value must be multiple of %s": "La valeur doit Γͺtre un multiple de %s",
"Via Camera": "Par camΓ©ra",
"Via D20": "Via D20",
"Via D6": "Via D6",
@@ -235,7 +235,7 @@
"Wallet output descriptor": "Descripteur de sortie du portefeuille",
"Wallet output descriptor loaded!": "Descripteur de sortie du portefeuille chargΓ©!",
"Wallet output descriptor not found.": "Descripteur de sortie du portefeuille introuvable.",
- "Warning:\nIncomplete output descriptor": "Attention:\nDescripteur de sortie incomplet",
+ "Warning:": "Avertissement:",
"Word %d": "Mot %d",
"Word Numbers": "NumΓ©ros de mots",
"Words": "Mots",
diff --git a/i18n/translations/nl-NL.json b/i18n/translations/nl-NL.json
index 9ec906282..112100180 100644
--- a/i18n/translations/nl-NL.json
+++ b/i18n/translations/nl-NL.json
@@ -19,7 +19,6 @@
"Anti-glare disabled": "Anti reflecterend uitgeschakeld",
"Anti-glare enabled": "Anti reflecterend ingeschakeld",
"Are you sure?": "Weet je het zeker?",
- "BIP39 Mnemonic": "BIP-39 geheugensteun",
"Back": "Terug",
"Backing up bootloader..\n\n%d%%": "Backup van de bootloader...\n\n%d%%",
"Bad signature": "Ongeldige handtekening",
@@ -49,6 +48,7 @@
"Cut Method": "Snijmethode",
"Decimal": "Decimaal",
"Decrypt?": "Ontsleutelen?",
+ "Delete %s?": "Verwijderen %s?",
"Delete File?": "Bestand verwijderen?",
"Delete Mnemonic": "Geheugensteun verwijderen",
"Depth Per Pass": "Diepte per pas",
@@ -94,11 +94,10 @@
"GRBL": "GRBL",
"Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "Eigen ID gebruiken voor geheugensteun? Anders vingerafdruk gebruiken",
"Go": "Ga",
- "Heat Interval": "Warmte interval",
- "Heat Time": "Warmte tijd",
"Hex Public Key": "Hex publieke sleutel",
"Hexadecimal": "Hexadecimaal",
"ID already exists\n": "ID bestaat al\n",
+ "Incomplete output descriptor": "Incomplete portemonnee descriptor",
"Inputs (%d): ": "Invoer (%d): ",
"Invalid address": "Ongeldig adres",
"Invalid bootloader": "Ongeldige bootloader",
@@ -199,6 +198,7 @@
"Signed PSBT": "PSBT ondertekend",
"Single-sig": "Enkele sleutel",
"Size: ": "Grootte: ",
+ "Some checks cannot be performed.": "Sommige controles kunnen niet worden uitgevoerd.",
"Spend (%d): ": "Uitgaven (%d): ",
"Stackbit 1248": "Stackbit 1248",
"Store on Flash": "Opslaan op apparaat",
@@ -235,7 +235,7 @@
"Wallet output descriptor": "Portemonnee descriptor",
"Wallet output descriptor loaded!": "Portemonnee descriptor geladen!",
"Wallet output descriptor not found.": "Portemonnee descriptor niet gevonden.",
- "Warning:\nIncomplete output descriptor": "Waarschuwing:\nIncomplete portemonnee descriptor",
+ "Warning:": "Waarschuwing:",
"Word %d": "Woord %d",
"Word Numbers": "Woord nummers",
"Words": "Woorden",
diff --git a/i18n/translations/pl-PL.json b/i18n/translations/pl-PL.json
new file mode 100644
index 000000000..45f270a75
--- /dev/null
+++ b/i18n/translations/pl-PL.json
@@ -0,0 +1,244 @@
+{
+ "%d of %d multisig": "%d z %d multisig",
+ "%d. Change: \n\n%s\n\n": "%D.Zmiana:\n\n%s\n\n",
+ "%d. Self-transfer: \n\n%s\n\n": "%D.Samo-transfer:\n\n%s\n\n",
+ "%d. Spend: \n\n%s\n\n": "%D.WydaΔ:\n\n%s\n\n",
+ "%s\n\nis a valid change address!": "%s\n\nis prawidΕowy adres zmiany!",
+ "%s\n\nis a valid receive address!": "%s\n\nis waΕΌny adres odbierania!",
+ "%s\n\nwas NOT FOUND in the first %d change addresses": "%s\n\nwas nie znaleziono w pierwszych adresach zmiany",
+ "%s\n\nwas NOT FOUND in the first %d receive addresses": "%s\n\nwas nie znaleziono w pierwszych %D Otrzymuj adresy",
+ "(Experimental)": "(Eksperymentalny)",
+ "12 words": "12 sΕΓ³w",
+ "24 words": "24 sΕowa",
+ "ABC": "ABC",
+ "About": "O",
+ "Adafruit": "Adafruit",
+ "Address": "Adres",
+ "Aditional entropy from camera required for AES-CBC mode": "Dodatkowa entropia z kamery wymaganej dla trybu AES-CBC",
+ "Align camera and Tiny Seed properly.": "WΕaΕciwie wyrΓ³wnaj kamerΔ i maleΕkie nasiona.",
+ "Anti-glare disabled": "NiepeΕnosprawne przeciwblokowane",
+ "Anti-glare enabled": "WΕΔ
czona anty-zabawa",
+ "Are you sure?": "JesteΕ pewny?",
+ "Back": "Z powrotem",
+ "Backing up bootloader..\n\n%d%%": "Kopie zapasowe bootloader ..\n\n%d %%",
+ "Bad signature": "zΕy podpis",
+ "Baudrate": "Baudrate",
+ "Bitcoin": "Bitcoin",
+ "Border Padding": "WyΕciΓ³Εka graniczna",
+ "CNC": "CNC",
+ "Change": "Zmiana",
+ "Change Addresses": "ZmieΕ adresy",
+ "Change theme and reboot?": "ZmieniΔ motyw i ponownie uruchomiΔ?",
+ "Changes persisted to SD card!": "Zmiany utrzymywaΕy siΔ na karcie SD!",
+ "Changes will last until shutdown.": "Zmiany bΔdΔ
trwaΔ do zamkniΔcia.",
+ "Check SD Card": "SprawdΕΊ kartΔ SD",
+ "Check that address belongs to this wallet?": "SprawdΕΊ, czy adres naleΕΌy do tego portfela?",
+ "Checked %d change addresses with no matches.": "Sprawdzone %d adresy zmiany bez dopasowaΕ.",
+ "Checked %d receive addresses with no matches.": "Sprawdzone %D Otrzymuj adresy bez zapaΕek.",
+ "Checking change address %d for match..": "Sprawdzanie Zmieniania Adres %D dla dopasowania.",
+ "Checking for SD card..": "Sprawdzanie karty SD ..",
+ "Checking receive address %d for match..": "Sprawdzanie adresu Otrzymaj %D dla meczu.",
+ "Compact SeedQR": "Kompaktowy seedqr",
+ "Continue?": "KontynuowaΔ?",
+ "Create QR Code": "UtwΓ³rz kod QR",
+ "Create QR code from text?": "UtwΓ³rz kod QR z tekstu?",
+ "Created: ": "Utworzony:",
+ "Custom QR Code": "Niestandardowy kod QR",
+ "Cut Depth": "Wytnij gΕΔbokoΕΔ",
+ "Cut Method": "Metoda ciΔcia",
+ "Decimal": "DziesiΔtny",
+ "Decrypt?": "OdszyfrowaΔ?",
+ "Delete %s?": "UsuΕ %s?",
+ "Delete File?": "UsunΔ
Δ plik?",
+ "Delete Mnemonic": "UsuΕ mnemonic",
+ "Depth Per Pass": "GΕΔbokoΕΔ na przepustkΔ",
+ "Derivation: %s": "Pochodzenie: %s",
+ "Device flash storage not detected.": "Nie wykryto pamiΔci flash urzΔ
dzenia.",
+ "Done?": "Zrobione?",
+ "Driver": "Kierowca",
+ "Encoder": "Enkoder",
+ "Encoder Debounce": "Encoder Debunet",
+ "Encrypt Mnemonic": "Szyfrowanie mnemoniki",
+ "Encrypted QR Code": "Zaszyfrowany kod QR",
+ "Encrypted mnemonic was not stored": "Szyfrowana mnemonika nie byΕa przechowywana",
+ "Encrypted mnemonic was stored with ID: ": "Szyfrowana mnemonika byΕa przechowywana z ID:",
+ "Encryption": "Szyfrowanie",
+ "Encryption Mode": "Tryb szyfrowania",
+ "Enter each word of your BIP-39 mnemonic as a number from 1 to 2048.": "WprowadΕΊ kaΕΌde sΕowo swojego mnemonika BIP-39 jako liczbΔ od 1 do 2048 r.",
+ "Enter each word of your BIP-39 mnemonic as a number in hexadecimal from 1 to 800.": "WprowadΕΊ kaΕΌde sΕowo swojego mnemonika BIP-39 jako liczbΔ w heksadecimal od 1 do 800.",
+ "Enter each word of your BIP-39 mnemonic as a number in octal from 1 to 4000.": "WprowadΕΊ kaΕΌde sΕowo swojego mnemonika BIP-39 jako liczbΔ w wysokoΕci od 1 do 4000.",
+ "Enter each word of your BIP-39 mnemonic.": "WprowadΕΊ kaΕΌde sΕowo swojego mnemonika BIP-39.",
+ "Error:\n%s": "BΕΔ
d:\n%s",
+ "Esc": "wyjΕcie",
+ "Explore files?": "EksplorowaΔ pliki?",
+ "Exporting to SD card..": "Eksportowanie do karty SD ..",
+ "Extended Public Key": "Rozszerzony klucz publiczny",
+ "Failed to decrypt": "Nie udaΕo siΔ odszyfrowaΔ",
+ "Failed to load PSBT": "Nie udaΕo siΔ zaΕadowaΔ PSBT",
+ "Failed to load address": "Nie udaΕo siΔ zaΕadowaΔ adresu",
+ "Failed to load key": "Nie udaΕo siΔ zaΕadowaΔ klucza",
+ "Failed to load message": "Nie udaΕo siΔ zaΕadowaΔ wiadomoΕci",
+ "Failed to load mnemonic": "Nie udaΕo siΔ zaΕadowaΔ mnemonika",
+ "Failed to load output descriptor": "Nie udaΕo siΔ zaΕadowaΔ deskryptora wyjΕciowego",
+ "Failed to load passphrase": "Nie udaΕo siΔ zaΕadowaΔ pseudonim",
+ "Failed to store mnemonic": "Nie udaΕo siΔ przechowywaΔ mnemonika",
+ "Fee: ": "OpΕata:",
+ "Feed Rate": "SzybkoΕΔ pasz",
+ "Filename": "Nazwa pliku",
+ "Filename %s exists on SD card, overwrite?": "Nazwa pliku %s istnieje na karcie SD, zastΔ
p?",
+ "Fingerprint: %s": "Odcisk palca: %s",
+ "Firmware exceeds max size: %d": "Oprogramowanie ukΕadowe przekracza maksymalny rozmiar: %D",
+ "Flute Diameter": "Εrednica fletu",
+ "Free: ": "BezpΕatny:",
+ "From Storage": "Z przechowywania",
+ "GRBL": "Grbl",
+ "Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "Podaj temu mnemonicznemu identyfikatorowi?W przeciwnym razie zostanie uΕΌyty obecny odcisk palca",
+ "Go": "IΕΔ",
+ "Hex Public Key": "Hex Key Public",
+ "Hexadecimal": "Szesnastkowy",
+ "ID already exists\n": "ID juΕΌ istnieje\n",
+ "Incomplete output descriptor": "inComplete Descriptor",
+ "Inputs (%d): ": "WejΕcia (%d):",
+ "Invalid address": "BΕΔdny adres",
+ "Invalid bootloader": "NieprawidΕowy bootloader",
+ "Invalid mnemonic length": "NieprawidΕowa dΕugoΕΔ mnemoniczna",
+ "Invalid public key": "NieprawidΕowy klucz publiczny",
+ "Invalid wallet:\n%s": "NieprawidΕowy portfel:\n%s",
+ "Invert": "OdwracaΔ",
+ "Key": "Klucz",
+ "Key: ": "Klucz:",
+ "Krux\n\n\nVersion\n%s": "Krux\n\n\neversion\n%s",
+ "Krux Printer Test QR": "Test drukarki Krux QR",
+ "Language": "JΔzyk",
+ "Line Delay": "OpΓ³ΕΊnienie linii",
+ "Line: ": "Linia:",
+ "Load Mnemonic": "Εaduj mnemoniczne",
+ "Load from SD card?": "ZaΕaduj z karty SD?",
+ "Load one?": "ZaΕaduj jeden?",
+ "Load?": "ObciΔ
ΕΌenie?",
+ "Loading Camera..": "Εadowanie aparatu ..",
+ "Loading change address %d..": "Εadowanie ZMIANY Adres %D ..",
+ "Loading printer..": "Εadowanie drukarki ..",
+ "Loading receive address %d..": "Εadowanie adresu odbierania %d ..",
+ "Loading..": "Εadowanie..",
+ "Locale": "Widownia",
+ "Location": "Lokalizacja",
+ "Log Level": "Poziom dziennika",
+ "Logging": "Logowanie",
+ "Maximum length exceeded (%s)": "Maksymalna dΕugoΕΔ przekroczona (%s)",
+ "Message": "WiadomoΕΔ",
+ "Missing signature file": "BrakujΔ
cy plik podpisu",
+ "Mnemonic": "Mnemoniczny",
+ "Mnemonic ID": "Mnemoniczne id",
+ "Mnemonic Storage ID": "Mnemoniczny identyfikator przechowywania",
+ "Mnemonic was not decrypted": "Mnemonik nie zostaΕ odszyfrowany",
+ "Mnemonic was not encrypted": "Mnemoniczny nie byΕ szyfrowany",
+ "Modified: ": "Zmodyfikowany:",
+ "Multisig": "Multisig",
+ "Network": "SieΔ",
+ "New Mnemonic": "Nowy Mnemonic",
+ "New firmware detected.\n\nSHA256:\n%s\n\n\n\nInstall?": "Wykryto nowe oprogramowanie.\n\nsha256:\n%s\n\n\n\ninstall?",
+ "No": "NIE",
+ "No BIP39 passphrase": "Brak frazy BIP39",
+ "Not enough rolls!": "Za maΕo rolki!",
+ "Octal": "Octal",
+ "PBKDF2 Iter.": "PBKDF2 ITER.",
+ "PSBT": "PSBT",
+ "Paint punched dots black so they can be detected.": "Parzyj kropki czarne, aby moΕΌna je byΕo wykryΔ.",
+ "Paper Width": "SzerokoΕΔ papieru",
+ "Part\n%d / %d": "CzΔΕΔ\n %d / %d",
+ "Part Size": "Rozmiar czΔΕci",
+ "Passphrase": "Fraza",
+ "Passphrase: ": "FRASSE:",
+ "Persist": "TrwaΔ",
+ "Plaintext QR": "PlainText QR",
+ "Please load a wallet output descriptor": "ZaΕaduj deskryptor wyjΕciowy portfela",
+ "Plunge Rate": "SzybkoΕΔ spadku",
+ "Print Test QR": "Test wydrukuj QR",
+ "Print to QR?\n\n%s\n\n": "Drukuj do qr?\n\n%s\n\n",
+ "Print?\n\n%s\n\n": "WydrukowaΔ?\n\n%s\n\n",
+ "Printer": "Drukarka",
+ "Printer Driver not set!": "Sterownik drukarki nie jest ustawiony!",
+ "Printing\n%d / %d": "Drukowanie\n %d / %d",
+ "Printing ...": "Drukowanie ...",
+ "Proceed?": "PrzystΔpowaΔ?",
+ "Processing ...": "Przetwarzanie ...",
+ "QR Code": "Kod QR",
+ "RX Pin": "Pin Rx",
+ "Receive": "OdbieraΔ",
+ "Receive Addresses": "OdbieraΔ adresy",
+ "Region: ": "Region:",
+ "Review scanned data, edit if necessary": "Przejrzyj zeskanowane dane, w razie potrzeby edytuj",
+ "Roll dice at least %d times to generate a mnemonic.": "RzuΔ kostkΔ co najmniej %d, aby wygenerowaΔ mnemoniczny.",
+ "Rolls:\n\n%s": "Rolls:\n\n%s",
+ "Rolls: %d\n": "Rolls: %d\n",
+ "SD card": "karta SD",
+ "SD card not detected": "Karta SD nie zostaΕa wykryta",
+ "SD card not detected.": "Karta SD nie zostaΕa wykryta.",
+ "SHA256 of rolls:\n\n%s": "SHA256 Rolls:\n\n%s",
+ "SHA256 of snapshot:\n\n%s": "SHA256 migawki:\n\n%s",
+ "SHA256:\n%s": "SHA256:\n%s",
+ "Save to SD card?": "Zapisz na karcie SD?",
+ "Saved to SD card:\n%s": "Zapisano na karcie SD:\n%s",
+ "Scale": "Skala",
+ "Scan Address": "Adres skanowania",
+ "Scan BIP39 passphrase": "Scan BIP39 Passhraz",
+ "Scan Key QR code": "Skanuj kod QR",
+ "Scanning words 1-12 again": "Znowu skanowanie sΕΓ³w 1-12",
+ "Scanning words 13-24": "Skanowanie sΕΓ³w 13-24",
+ "SeedQR": "Seedqr",
+ "Self-transfer or Change (%d): ": "Samo-transfer lub zmiana (%d):",
+ "Settings": "Ustawienia",
+ "Shutdown": "ZamkniΔcie",
+ "Shutting down..": "WyΕΔ
czanie..",
+ "Sign": "PodpisaΔ",
+ "Sign?": "PodpisaΔ?",
+ "Signature": "Podpis",
+ "Signed Message": "Podpisana wiadomoΕΔ",
+ "Signed PSBT": "Podpisano PSBT",
+ "Single-sig": "Pojedyncze Sig",
+ "Size: ": "Rozmiar:",
+ "Some checks cannot be performed.": "Nie moΕΌna wykonaΔ niektΓ³rych kontroli.",
+ "Spend (%d): ": "WydaΔ (%d):",
+ "Stackbit 1248": "Stackbit 1248",
+ "Store on Flash": "Przechowuj na Flash",
+ "Store on SD Card": "Przechowuj na karcie SD",
+ "Swipe to change mode": "PrzesuΕ tryb zmiany",
+ "TOUCH or ENTER to capture": "Dotknij lub wejdΕΊ do przechwytywania",
+ "TX Pin": "Pin TX",
+ "Text": "Tekst",
+ "Theme": "Temat",
+ "Thermal": "Termiczny",
+ "Tiny Seed": "MaΕe ziarno",
+ "Tiny Seed (Bits)": "MaΕe nasiona (bity)",
+ "Tools": "NarzΔdzia",
+ "Touch Threshold": "PrΓ³g dotykowy",
+ "Touchscreen": "Ekran dotykowy",
+ "Try more?": "PrΓ³buj bardziej?",
+ "Type BIP39 passphrase": "PassΓ³wka typu BIP39",
+ "Type Key": "Klucz typu",
+ "Unit": "Jednostka",
+ "Updating bootloader..\n\n%d%%": "Aktualizacja bootloader ..\n\n%d %%",
+ "Upgrade complete.\n\nShutting down..": "Uaktualnienie zakoΕczone.\n\nshutting w dΓ³Ε ..",
+ "Upgrading firmware..\n\n%d%%": "Aktualizacja oprogramowania ukΕadowego ..\n\n%d %%",
+ "Use a black background surface.": "UΕΌyj czarnej powierzchni tΕa.",
+ "Use camera's entropy to create a new mnemonic": "UΕΌyj entropii aparatu, aby stworzyΔ nowy mnemonik",
+ "Used: ": "UΕΌywany:",
+ "Value %s out of range: [%s, %s]": "WartoΕΔ %s z zakresu: [ %s, %s]",
+ "Via Camera": "Za poΕrednictwem aparatu",
+ "Via D20": "Via D20",
+ "Via D6": "Przez D6",
+ "Via Manual Input": "Poprzez rΔczne wejΕcie",
+ "Wait for the capture": "Poczekaj na schwytanie",
+ "Wallet Descriptor": "Deskryptor portfela",
+ "Wallet output descriptor": "Deskryptor wyjΕciowy portfela",
+ "Wallet output descriptor loaded!": "ZaΕadowany deskryptor wyjΕciowy portfela!",
+ "Wallet output descriptor not found.": "Nie znaleziono deskryptora wyjΕciowego portfela.",
+ "Warning:": "OstrzeΕΌenie:",
+ "Word %d": "SΕowo %d",
+ "Word Numbers": "Numery sΕΓ³w",
+ "Words": "SΕowa",
+ "Yes": "Tak",
+ "Your changes will be kept on device flash storage.": "Twoje zmiany bΔdΔ
przechowywane w pamiΔci flash urzΔ
dzenia.",
+ "Your changes will be kept on the SD card.": "Twoje zmiany bΔdΔ
przechowywane na karcie SD."
+}
\ No newline at end of file
diff --git a/i18n/translations/pt-BR.json b/i18n/translations/pt-BR.json
index 1597a86a4..a9f049226 100644
--- a/i18n/translations/pt-BR.json
+++ b/i18n/translations/pt-BR.json
@@ -19,7 +19,6 @@
"Anti-glare disabled": "Antirreflexo desativado",
"Anti-glare enabled": "Antirreflexo ativado",
"Are you sure?": "Tem certeza?",
- "BIP39 Mnemonic": "MnemΓ΄nico BIP39",
"Back": "Voltar",
"Backing up bootloader..\n\n%d%%": "Backing up bootloader..\n\n%d%%",
"Bad signature": "Assinatura InvΓ‘lida",
@@ -49,6 +48,7 @@
"Cut Method": "MΓ©todo de Corte",
"Decimal": "Decimal",
"Decrypt?": "Descriptografar?",
+ "Delete %s?": "Excluir %s?",
"Delete File?": "Excluir Arquivo?",
"Delete Mnemonic": "Excluir MnemΓ΄nico",
"Depth Per Pass": "Profundidade da Passagem",
@@ -94,11 +94,10 @@
"GRBL": "GRBL",
"Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "DΓͺ a este mnemΓ΄nico um ID personalizado? Caso contrΓ‘rio, a impressΓ£o digital atual serΓ‘ usada",
"Go": "Ir",
- "Heat Interval": "Intervalo de Aquecimento",
- "Heat Time": "Tempo de Aquecimento",
"Hex Public Key": "Chave pΓΊblica hexadecimal",
"Hexadecimal": "Hexadecimal",
"ID already exists\n": "Id jΓ‘ existe\n",
+ "Incomplete output descriptor": "Descritor incompleto",
"Inputs (%d): ": "Entradas (%d): ",
"Invalid address": "Endereço invÑlido",
"Invalid bootloader": "Bootloader invΓ‘lido",
@@ -199,6 +198,7 @@
"Signed PSBT": "PSBT Assinada",
"Single-sig": "Single-sig",
"Size: ": "Total: ",
+ "Some checks cannot be performed.": "Algumas verificaçáes não podem ser realizadas.",
"Spend (%d): ": "Gastos (%d): ",
"Stackbit 1248": "Stackbit 1248",
"Store on Flash": "Armazene na Flash",
@@ -235,7 +235,7 @@
"Wallet output descriptor": "Descritor da carteira",
"Wallet output descriptor loaded!": "Descritor de saΓda da carteira carregado!",
"Wallet output descriptor not found.": "O descritor de saΓda da carteira nΓ£o foi encontrado.",
- "Warning:\nIncomplete output descriptor": "Atenção:\nDescritor de saΓda incompleto",
+ "Warning:": "Aviso:",
"Word %d": "Palavra %d",
"Word Numbers": "NΓΊmeros das Palavras",
"Words": "Palavras",
diff --git a/i18n/translations/ru-RU.json b/i18n/translations/ru-RU.json
new file mode 100644
index 000000000..2a4e4cb60
--- /dev/null
+++ b/i18n/translations/ru-RU.json
@@ -0,0 +1,244 @@
+{
+ "%d of %d multisig": "%d ΠΈΠ· %d ΠΌΡΠ»ΡΡΠΈΠΏΠΎΠ΄ΠΏΠΈΡΡ",
+ "%d. Change: \n\n%s\n\n": "%d. Π‘Π΄Π°ΡΠ°: \n\n%s\n\n",
+ "%d. Self-transfer: \n\n%s\n\n": "%d. ΠΠ΅ΡΠ΅Π²ΠΎΠ΄ ΡΠ°ΠΌΠΎΠΌΡ ΡΠ΅Π±Π΅: \n\n%s\n\n",
+ "%d. Spend: \n\n%s\n\n": "%d. Π Π°ΡΡ
ΠΎΠ΄: \n\n%s\n\n",
+ "%s\n\nis a valid change address!": "%s\n\nΠ²Π°Π»ΠΈΠ΄Π½ΡΠΉ Π°Π΄ΡΠ΅Ρ ΡΠ΄Π°ΡΠΈ!",
+ "%s\n\nis a valid receive address!": "%s\n\nΠ²Π°Π»ΠΈΠ΄Π½ΡΠΉ Π°Π΄ΡΠ΅Ρ ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ!",
+ "%s\n\nwas NOT FOUND in the first %d change addresses": "%s\n\nΠΠ ΠΠΠΠΠΠ Π² ΠΏΠ΅ΡΠ²ΡΡ
%d Π°Π΄ΡΠ΅ΡΠ°Ρ
ΡΠ΄Π°ΡΠΈ",
+ "%s\n\nwas NOT FOUND in the first %d receive addresses": "%s\n\nΠΠ ΠΠΠΠΠΠ Π² ΠΏΠ΅ΡΠ²ΡΡ
%d Π°Π΄ΡΠ΅ΡΠ°Ρ
ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ",
+ "(Experimental)": "(ΠΠΊΡΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½ΡΠ°Π»ΡΠ½ΡΠΉ)",
+ "12 words": "12 ΡΠ»ΠΎΠ²",
+ "24 words": "24 ΡΠ»ΠΎΠ²Π°",
+ "ABC": "ABC",
+ "About": "Π ΠΡΠΎΠ³ΡΠ°ΠΌΠΌΠ΅",
+ "Adafruit": "Adafruit",
+ "Address": "ΠΠ΄ΡΠ΅Ρ",
+ "Aditional entropy from camera required for AES-CBC mode": "ΠΠ»Ρ ΡΠ΅ΠΆΠΈΠΌΠ° AES-CBC ΡΡΠ΅Π±ΡΠ΅ΡΡΡ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡΠ΅Π»ΡΠ½Π°Ρ ΡΠ½ΡΡΠΎΠΏΠΈΡ ΠΎΡ ΠΊΠ°ΠΌΠ΅ΡΡ",
+ "Align camera and Tiny Seed properly.": "ΠΡΠ°Π²ΠΈΠ»ΡΠ½ΠΎ ΡΠΎΠ²ΠΌΠ΅ΡΡΠΈΡΠ΅ ΠΊΠ°ΠΌΠ΅ΡΡ ΠΈ ΠΠΈΠ½ΠΈ Π‘ΠΈΠ΄-ΡΡΠ°Π·Ρ.",
+ "Anti-glare disabled": "ΠΠ½ΡΠΈΠ±Π»ΠΈΠΊ ΠΎΡΠΊΠ»ΡΡΠ΅Π½",
+ "Anti-glare enabled": "ΠΠ½ΡΠΈΠ±Π»ΠΈΠΊ Π²ΠΊΠ»ΡΡΠ΅Π½",
+ "Are you sure?": "ΠΡ ΡΠ²Π΅ΡΠ΅Π½Ρ?",
+ "Back": "ΠΠ°Π·Π°Π΄",
+ "Backing up bootloader..\n\n%d%%": "Π Π΅Π·Π΅ΡΠ²Π½ΠΎΠ΅ ΠΊΠΎΠΏΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ Π·Π°Π³ΡΡΠ·ΡΠΈΠΊΠ°..\n\n%d%%",
+ "Bad signature": "ΠΠ»ΠΎΡ
Π°Ρ ΠΏΠΎΠ΄ΠΏΠΈΡΡ",
+ "Baudrate": "Π‘ΠΊΠΎΡΠΎΡΡΡ ΠΠ΅ΡΠ΅Π΄Π°ΡΠΈ ΠΠ°Π½Π½ΡΡ
",
+ "Bitcoin": "ΠΠΈΡΠΊΠΎΠΈΠ½",
+ "Border Padding": "ΠΠ°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ Π³ΡΠ°Π½ΠΈΡ",
+ "CNC": "CNC",
+ "Change": "Π‘Π΄Π°ΡΠ°",
+ "Change Addresses": "ΠΠ΄ΡΠ΅Ρ Π‘Π΄Π°ΡΠΈ",
+ "Change theme and reboot?": "Π‘ΠΌΠ΅Π½ΠΈΡΡ ΡΠ΅ΠΌΡ ΠΈ ΠΏΠ΅ΡΠ΅Π·Π°Π³ΡΡΠ·ΠΈΡΡ?",
+ "Changes persisted to SD card!": "ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ ΡΠΎΡ
ΡΠ°Π½Π΅Π½Ρ Π½Π° SD ΠΊΠ°ΡΡΠ΅!",
+ "Changes will last until shutdown.": "ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ Π±ΡΠ΄ΡΡ Ρ
ΡΠ°Π½ΠΈΡΡΡΡ Π΄ΠΎ Π²ΡΠΊΠ»ΡΡΠ΅Π½ΠΈΡ.",
+ "Check SD Card": "ΠΡΠΎΠ²Π΅ΡΠΈΡΡ SD ΠΠ°ΡΡΡ",
+ "Check that address belongs to this wallet?": "ΠΡΠΎΠ²Π΅ΡΠΈΡΡ, ΡΡΠΎ Π°Π΄ΡΠ΅Ρ ΠΏΡΠΈΠ½Π°Π΄Π»Π΅ΠΆΠΈΡ ΡΡΠΎΠΌΡ ΠΊΠΎΡΠ΅Π»ΡΠΊΡ?",
+ "Checked %d change addresses with no matches.": "ΠΡΠΎΠ²Π΅ΡΠ΅Π½ΠΎ %d Π°Π΄ΡΠ΅ΡΠΎΠ² ΡΠ΄Π°ΡΠΈ Π±Π΅Π· ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠΉ.",
+ "Checked %d receive addresses with no matches.": "ΠΡΠΎΠ²Π΅ΡΠ΅Π½ΠΎ %d Π°Π΄ΡΠ΅ΡΠΎΠ² ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ Π±Π΅Π· ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠΉ.",
+ "Checking change address %d for match..": "ΠΡΠΎΠ²Π΅ΡΡΠ΅ΠΌ Π°Π΄ΡΠ΅Ρ ΡΠ΄Π°ΡΠΈ %d Π½Π° ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠ΅..",
+ "Checking for SD card..": "ΠΡΠΎΠ²Π΅ΡΠΊΠ° SD ΠΊΠ°ΡΡΡ..",
+ "Checking receive address %d for match..": "ΠΡΠΎΠ²Π΅ΡΡΠ΅ΠΌ Π°Π΄ΡΠ΅Ρ ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ %d Π½Π° ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠ΅..",
+ "Compact SeedQR": "ΠΠΎΠΌΠΏΠ°ΠΊΡΠ½ΡΠΉ SeedQR",
+ "Continue?": "ΠΡΠΎΠ΄ΠΎΠ»ΠΆΠΈΡΡ?",
+ "Create QR Code": "Π‘ΠΎΠ·Π΄Π°ΡΡ QR ΠΠΎΠ΄",
+ "Create QR code from text?": "Π‘ΠΎΠ·Π΄Π°ΡΡ QR ΠΊΠΎΠ΄ ΠΈΠ· ΡΠ΅ΠΊΡΡΠ°?",
+ "Created: ": "Π‘ΠΎΠ·Π΄Π°Π½ΠΎ",
+ "Custom QR Code": "ΠΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»ΡΡΠΊΠΈΠΉ QR ΠΠΎΠ΄",
+ "Cut Depth": "ΠΠ»ΡΠ±ΠΈΠ½Π° Π Π΅Π·ΠΊΠΈ",
+ "Cut Method": "ΠΠ΅ΡΠΎΠ΄ Π Π΅Π·ΠΊΠΈ",
+ "Decimal": "ΠΠ΅ΡΡΡΠΈΡΠ½ΡΠΉ",
+ "Decrypt?": "Π Π°ΡΡΠΈΡΡΠΎΠ²Π°ΡΡ?",
+ "Delete %s?": "Π£Π΄Π°Π»ΠΈΡΡ %s?",
+ "Delete File?": "Π£Π΄Π°Π»ΠΈΡΡ Π€Π°ΠΉΠ»?",
+ "Delete Mnemonic": "Π£Π΄Π°Π»ΠΈΡΡ ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ "Depth Per Pass": "ΠΠ»ΡΠ±ΠΈΠ½Π° ΠΠ° ΠΡΠΎΡ
ΠΎΠ΄",
+ "Derivation: %s": "ΠΡΠΎΠΈΠ·Π²ΠΎΠ΄Π½ΡΠΉ ΠΏΡΡΡ: %s",
+ "Device flash storage not detected.": "Π€Π»ΡΡ ΠΏΠ°ΠΌΡΡΡ ΡΡΡΡΠΎΠΉΡΡΠ²Π° Π½Π΅ ΠΎΠ±Π½Π°ΡΡΠΆΠ΅Π½Π°.",
+ "Done?": "ΠΠΎΡΠΎΠ²ΠΎ?",
+ "Driver": "ΠΡΠ°ΠΉΠ²Π΅Ρ",
+ "Encoder": "ΠΠΎΠ΄Π΅Ρ",
+ "Encoder Debounce": "ΠΠ΅Π±Π°ΡΠ½Ρ ΠΠΎΠ΄Π΅ΡΠ°",
+ "Encrypt Mnemonic": "ΠΠ°ΡΠΈΡΡΠΎΠ²Π°ΡΡ ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ "Encrypted QR Code": "ΠΠ°ΡΠΈΡΡΠΎΠ²Π°Π½Π½ΡΠΉ QR ΠΠΎΠ΄",
+ "Encrypted mnemonic was not stored": "ΠΠ°ΡΠΈΡΡΠΎΠ²Π°Π½Π½Π°Ρ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π½Π΅ Π±ΡΠ»Π° ΡΠΎΡ
ΡΠ°Π½Π΅Π½Π°",
+ "Encrypted mnemonic was stored with ID: ": "ΠΠ°ΡΠΈΡΡΠΎΠ²Π°Π½Π½Π°Ρ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π±ΡΠ»Π° ΡΠΎΡ
ΡΠ°Π½Π΅Π½Π° Ρ ID: ",
+ "Encryption": "Π¨ΠΈΡΡΠΎΠ²Π°Π½ΠΈΠ΅",
+ "Encryption Mode": "ΠΠ΅ΡΠΎΠ΄ ΡΠΈΡΡΠΎΠ²Π°Π½ΠΈΡ",
+ "Enter each word of your BIP-39 mnemonic as a number from 1 to 2048.": "ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ ΡΠ»ΠΎΠ²ΠΎ Π²Π°ΡΠ΅ΠΉ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ ΡΠΈΡΠ»Π° ΠΎΡ 1 Π΄ΠΎ 2048.",
+ "Enter each word of your BIP-39 mnemonic as a number in hexadecimal from 1 to 800.": "ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ ΡΠ»ΠΎΠ²ΠΎ Π²Π°ΡΠ΅ΠΉ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ ΡΠ΅ΡΡΠ½Π°Π΄ΡΠ°ΡΠ΅ΡΠΈΡΠ½ΠΎΠ³ΠΎ ΡΠΈΡΠ»Π° ΠΎΡ 1 Π΄ΠΎ 800.",
+ "Enter each word of your BIP-39 mnemonic as a number in octal from 1 to 4000.": "ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ ΡΠ»ΠΎΠ²ΠΎ Π²Π°ΡΠ΅ΠΉ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ Π²ΠΎΡΡΠΌΠ΅ΡΠΈΡΠ½ΠΎΠ³ΠΎ ΡΠΈΡΠ»Π° ΠΎΡ 1 Π΄ΠΎ 4000.",
+ "Enter each word of your BIP-39 mnemonic.": "ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ ΡΠ»ΠΎΠ²ΠΎ Π²Π°ΡΠ΅ΠΉ BIP-39 ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ.",
+ "Error:\n%s": "ΠΡΠΈΠ±ΠΊΠ°:\n%s",
+ "Esc": "ΠΡΠΉΡΠΈ",
+ "Explore files?": "ΠΡΡΠ»Π΅Π΄ΠΎΠ²Π°ΡΡ ΡΠ°ΠΉΠ»Ρ?",
+ "Exporting to SD card..": "ΠΠΊΡΠΏΠΎΡΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ Π½Π° SD ΠΊΠ°ΡΡΡ..",
+ "Extended Public Key": "Π Π°ΡΡΠΈΡΠ΅Π½Π½ΡΠΉ ΠΡΠ±Π»ΠΈΡΠ½ΡΠΉ ΠΠ»ΡΡ",
+ "Failed to decrypt": "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΡΠ°ΡΡΠΈΡΡΠΎΠ²Π°ΡΡ",
+ "Failed to load PSBT": "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ PSBT",
+ "Failed to load address": "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ Π°Π΄ΡΠ΅Ρ",
+ "Failed to load key": "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ ΠΊΠ»ΡΡ",
+ "Failed to load message": "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅",
+ "Failed to load mnemonic": "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ "Failed to load output descriptor": "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ Π²ΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ",
+ "Failed to load passphrase": "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ ΡΡΠ°Π·Ρ-ΠΏΠ°ΡΠΎΠ»Ρ",
+ "Failed to store mnemonic": "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΡΠΎΡ
ΡΠ°Π½ΠΈΡΡ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ "Fee: ": "ΠΠΎΠΌΠΈΡΡΠΈΡ: ",
+ "Feed Rate": "Π‘ΠΊΠΎΡΠΎΡΡΡ ΠΏΠΎΠ΄Π°ΡΠΈ",
+ "Filename": "ΠΠΌΡ ΡΠ°ΠΉΠ»Π°",
+ "Filename %s exists on SD card, overwrite?": "Π€Π°ΠΉΠ» %s ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ Π½Π° SD ΠΊΠ°ΡΡΠ΅, ΠΏΠ΅ΡΠ΅Π·Π°ΠΏΠΈΡΠ°ΡΡ?",
+ "Fingerprint: %s": "Π€ΠΈΠ½Π³Π΅ΡΠΏΡΠΈΠ½Ρ: %s",
+ "Firmware exceeds max size: %d": "ΠΡΠΎΡΠΈΠ²ΠΊΠ° ΠΏΡΠ΅Π²ΡΡΠ°Π΅Ρ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠΉ ΡΠ°Π·ΠΌΠ΅Ρ: %d",
+ "Flute Diameter": "Flute ΠΠΈΠ°ΠΌΠ΅ΡΡ",
+ "Free: ": "ΠΠ΅ΡΠΏΠ»Π°ΡΠ½ΠΎ: ",
+ "From Storage": "ΠΠ· ΠΠ°ΠΌΡΡΠΈ",
+ "GRBL": "GRBL",
+ "Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "ΠΠ°Π·Π½Π°ΡΠΈΡΡ ΡΡΠΎΠΉ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ ΠΊΠ°ΡΡΠΎΠΌΠ½ΡΠΉ ID? Π ΠΈΠ½ΠΎΠΌ ΡΠ»ΡΡΠ°Π΅ Π±ΡΠ΄Π΅Ρ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ ΡΠ΅ΠΊΡΡΠΈΠΉ ΡΠΈΠ½Π³Π΅ΡΠΏΡΠΈΠ½Ρ",
+ "Go": "OK",
+ "Hex Public Key": "Π¨Π΅ΡΡΠ½Π°Π΄ΡΠ°ΡΠ΅ΡΠΈΡΠ½ΡΠΉ ΠΡΠ±Π»ΠΈΡΠ½ΡΠΉ ΠΠ»ΡΡ",
+ "Hexadecimal": "Π¨Π΅ΡΡΠ½Π°Π΄ΡΠ°ΡΠ΅ΡΠΈΡΠ½ΡΠΉ",
+ "ID already exists\n": "ID ΡΠΆΠ΅ ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ\n",
+ "Incomplete output descriptor": "ΠΠ΅ΠΏΠΎΠ»Π½ΡΠΉ Π²ΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ",
+ "Inputs (%d): ": "ΠΡ
ΠΎΠ΄Ρ (%d): ",
+ "Invalid address": "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ Π°Π΄ΡΠ΅Ρ",
+ "Invalid bootloader": "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ Π·Π°Π³ΡΡΠ·ΡΠΈΠΊ",
+ "Invalid mnemonic length": "ΠΠ΅Π²Π΅ΡΠ½Π°Ρ Π΄Π»ΠΈΠ½Π° ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ",
+ "Invalid public key": "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΠΏΡΠ±Π»ΠΈΡΠ½ΡΠΉ ΠΊΠ»ΡΡ",
+ "Invalid wallet:\n%s": "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΠΊΠΎΡΠ΅Π»Π΅ΠΊ:\n%s",
+ "Invert": "ΠΠ½Π²Π΅ΡΡΠΈΡΠΎΠ²Π°ΡΡ",
+ "Key": "ΠΠ»ΡΡ",
+ "Key: ": "ΠΠ»ΡΡ: ",
+ "Krux\n\n\nVersion\n%s": "Krux\n\n\nΠΠ΅ΡΡΠΈΡ\n%s",
+ "Krux Printer Test QR": "Π’Π΅ΡΡΠΎΠ²ΡΠΉ QR ΠΡΠΈΠ½ΡΠ΅ΡΠ° Krux",
+ "Language": "Π―Π·ΡΠΊ",
+ "Line Delay": "ΠΠ°Π΄Π΅ΡΠΆΠΊΠ° ΠΠΈΠ½ΠΈΠΈ",
+ "Line: ": "ΠΠΈΠ½ΠΈΡ: ",
+ "Load Mnemonic": "ΠΠ°Π³ΡΡΠ·ΠΈΡΡ ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ "Load from SD card?": "ΠΠ°Π³ΡΡΠ·ΠΈΡΡ Ρ SD ΠΊΠ°ΡΡΡ?",
+ "Load one?": "ΠΠ°Π³ΡΡΠ·ΠΈΡΡ ΠΎΠ΄Π½Ρ?",
+ "Load?": "ΠΠ°Π³ΡΡΠ·ΠΈΡΡ?",
+ "Loading Camera..": "ΠΠ°Π³ΡΡΠ·ΠΊΠ° ΠΠ°ΠΌΠ΅ΡΡ..",
+ "Loading change address %d..": "ΠΠ°Π³ΡΡΠ·ΠΊΠ° Π°Π΄ΡΠ΅ΡΠ° ΡΠ΄Π°ΡΠΈ %d..",
+ "Loading printer..": "ΠΠ°Π³ΡΡΠ·ΠΊΠ° ΠΏΡΠΈΠ½ΡΠ΅ΡΠ°..",
+ "Loading receive address %d..": "ΠΠ°Π³ΡΡΠ·ΠΊΠ° Π°Π΄ΡΠ΅ΡΠ° ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ %d..",
+ "Loading..": "ΠΠ°Π³ΡΡΠ·ΠΊΠ°..",
+ "Locale": "ΠΠΎΠΊΠ°Π»Ρ",
+ "Location": "Π Π°ΡΠΏΠΎΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅",
+ "Log Level": "Π£ΡΠΎΠ²Π΅Π½Ρ Π»ΠΎΠ³ΠΈΡΠΎΠ²Π°Π½ΠΈΡ",
+ "Logging": "ΠΠΎΠ³ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅",
+ "Maximum length exceeded (%s)": "ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½Π°Ρ Π΄Π»ΠΈΠ½Π° ΠΏΡΠ΅Π²ΡΡΠ΅Π½Π° (%s)",
+ "Message": "Π‘ΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅",
+ "Missing signature file": "ΠΡΡΡΡΡΡΠ²ΡΠ΅Ρ ΡΠ°ΠΉΠ» ΠΏΠΎΠ΄ΠΏΠΈΡΠΈ",
+ "Mnemonic": "ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ°",
+ "Mnemonic ID": "ID ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ",
+ "Mnemonic Storage ID": "ID ΠΏΠ°ΠΌΡΡΠΈ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ",
+ "Mnemonic was not decrypted": "ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π½Π΅ Π±ΡΠ»Π° ΡΠ°ΡΡΠΈΡΡΠΎΠ²Π°Π½Π°",
+ "Mnemonic was not encrypted": "ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π½Π΅ Π±ΡΠ»Π° Π·Π°ΡΠΈΡΡΠΎΠ²Π°Π½Π°",
+ "Modified: ": "ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΎ: ",
+ "Multisig": "ΠΡΠ»ΡΡΠΈΠΏΠΎΠ΄ΠΏΠΈΡΡ",
+ "Network": "Π‘Π΅ΡΡ",
+ "New Mnemonic": "ΠΠΎΠ²Π°Ρ ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ°",
+ "New firmware detected.\n\nSHA256:\n%s\n\n\n\nInstall?": "ΠΠ±Π½Π°ΡΡΠΆΠ΅Π½Π° Π½ΠΎΠ²Π°Ρ ΠΏΡΠΎΡΠΈΠ²ΠΊΠ°.\n\nSHA256:\n%s\n\n\n\nΠ£ΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ?",
+ "No": "ΠΠ΅Ρ",
+ "No BIP39 passphrase": "ΠΠ΅Π· BIP39 ΡΡΠ°Π·Ρ-ΠΏΠ°ΡΠΎΠ»Ρ",
+ "Not enough rolls!": "ΠΠ΅Π΄ΠΎΡΡΠ°ΡΠΎΡΠ½ΠΎ Π±ΡΠΎΡΠΊΠΎΠ²!",
+ "Octal": "ΠΠΎΡΡΠΌΠ΅ΡΠΈΡΠ½ΡΠΉ",
+ "PBKDF2 Iter.": "PBKDF2 ΠΡΠ΅ΡΠ°ΡΠΈΠΈ",
+ "PSBT": "PSBT",
+ "Paint punched dots black so they can be detected.": "ΠΠ°ΠΊΡΠ°ΡΡΡΠ΅ ΠΏΠ΅ΡΡΠΎΡΠΈΡΠΎΠ²Π°Π½Π½ΡΠ΅ ΡΠΎΡΠΊΠΈ ΡΠ΅ΡΠ½ΡΠΌ ΡΠ²Π΅ΡΠΎΠΌ, ΡΡΠΎΠ±Ρ ΠΈΡ
ΠΌΠΎΠΆΠ½ΠΎ Π±ΡΠ»ΠΎ ΠΎΠ±Π½Π°ΡΡΠΆΠΈΡΡ.",
+ "Paper Width": "Π¨ΠΈΡΠΈΠ½Π° ΠΡΠΌΠ°Π³ΠΈ",
+ "Part\n%d / %d": "Π§Π°ΡΡΡ\n%d / %d",
+ "Part Size": "Π Π°Π·ΠΌΠ΅Ρ Π§Π°ΡΡΠΈ",
+ "Passphrase": "Π€ΡΠ°Π·Π°-ΠΏΠ°ΡΠΎΠ»Ρ",
+ "Passphrase: ": "Π€ΡΠ°Π·Π°-ΠΏΠ°ΡΠΎΠ»Ρ: ",
+ "Persist": "ΠΠΎΡΡΠΎΡΠ½Π½Π°Ρ ΠΠ°ΠΌΡΡΡ",
+ "Plaintext QR": "QR ΠΎΡΠΊΡΡΡΡΠΌ ΡΠ΅ΠΊΡΡΠΎΠΌ",
+ "Please load a wallet output descriptor": "ΠΠΎΠΆΠ°Π»ΡΠΉΡΡΠ° Π·Π°Π³ΡΡΠ·ΠΈΡΠ΅ Π²ΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ ΠΊΠΎΡΠ΅Π»ΡΠΊΠ°",
+ "Plunge Rate": "Π‘ΠΊΠΎΡΠΎΡΡΡ ΠΠΎΠ³ΡΡΠΆΠ΅Π½ΠΈΡ",
+ "Print Test QR": "ΠΠ°ΠΏΠ΅ΡΠ°ΡΠ°ΡΡ Π’Π΅ΡΡΠΎΠ²ΡΠΉ QR",
+ "Print to QR?\n\n%s\n\n": "ΠΠ°ΠΏΠ΅ΡΠ°ΡΠ°ΡΡ Π² Π²ΠΈΠ΄Π΅ QR?\n\n%s\n\n",
+ "Print?\n\n%s\n\n": "ΠΠ΅ΡΠ°ΡΠ°ΡΡ?\n\n%s\n\n",
+ "Printer": "ΠΡΠΈΠ½ΡΠ΅Ρ",
+ "Printer Driver not set!": "ΠΡΠ°ΠΉΠ²Π΅Ρ ΠΡΠΈΠ½ΡΠ΅ΡΠ° Π½Π΅ ΡΡΡΠ°Π½ΠΎΠ²Π»Π΅Π½!",
+ "Printing\n%d / %d": "ΠΠ΄Π΅Ρ ΠΏΠ΅ΡΠ°ΡΡ\n%d / %d",
+ "Printing ...": "ΠΠ΄Π΅Ρ ΠΏΠ΅ΡΠ°ΡΡ ...",
+ "Proceed?": "ΠΡΠΎΠ΄ΠΎΠ»ΠΆΠΈΡΡ?",
+ "Processing ...": "ΠΠ±ΡΠ°Π±ΠΎΡΠΊΠ° ...",
+ "QR Code": "QR ΠΠΎΠ΄",
+ "RX Pin": "RX ΠΠΈΠ½",
+ "Receive": "ΠΠΎΠ»ΡΡΠΈΡΡ",
+ "Receive Addresses": "ΠΠ΄ΡΠ΅Ρ ΠΠΎΠ»ΡΡΠ΅Π½ΠΈΡ",
+ "Region: ": "Π Π΅Π³ΠΈΠΎΠ½: ",
+ "Review scanned data, edit if necessary": "ΠΡΠΎΡΠΌΠΎΡΡΠΈΡΠ΅ ΠΎΡΡΠΊΠ°Π½ΠΈΡΠΎΠ²Π°Π½Π½ΡΠ΅ Π΄Π°Π½Π½ΡΠ΅, ΠΎΡΡΠ΅Π΄Π°ΠΊΡΠΈΡΡΠΉΡΠ΅ ΠΏΡΠΈ Π½Π΅ΠΎΠ±Ρ
ΠΎΠ΄ΠΈΠΌΠΎΡΡΠΈ",
+ "Roll dice at least %d times to generate a mnemonic.": "ΠΡΠΎΡΡΡΠ΅ ΠΊΡΠ±ΠΈΠΊ Π½Π΅ ΠΌΠ΅Π½Π΅Π΅ %d ΡΠ°Π·, ΡΡΠΎΠ±Ρ ΡΠ³Π΅Π½Π΅ΡΠΈΡΠΎΠ²Π°ΡΡ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ.",
+ "Rolls:\n\n%s": "ΠΡΠΎΡΠΊΠΈ:\n\n%s",
+ "Rolls: %d\n": "ΠΡΠΎΡΠΊΠΈ: %d\n",
+ "SD card": "SD ΠΊΠ°ΡΡΠ°",
+ "SD card not detected": "SD ΠΊΠ°ΡΡΠ° Π½Π΅ ΠΎΠ±Π½Π°ΡΡΠΆΠ΅Π½Π°",
+ "SD card not detected.": "SD ΠΊΠ°ΡΡΠ° Π½Π΅ ΠΎΠ±Π½Π°ΡΡΠΆΠ΅Π½Π°.",
+ "SHA256 of rolls:\n\n%s": "SHA256 Π±ΡΠΎΡΠΊΠΎΠ²:\n\n%s",
+ "SHA256 of snapshot:\n\n%s": "SHA256 ΡΠ½ΡΠΏΡΠΎΡΠ°:\n\n%s",
+ "SHA256:\n%s": "SHA256:\n%s",
+ "Save to SD card?": "Π‘ΠΎΡ
ΡΠ°Π½ΠΈΡΡ Π½Π° SD ΠΊΠ°ΡΡΡ?",
+ "Saved to SD card:\n%s": "Π‘ΠΎΡ
ΡΠ°Π½Π΅Π½ΠΎ Π½Π° SD ΠΊΠ°ΡΡΡ:\n%s",
+ "Scale": "Π¨ΠΊΠ°Π»Π°",
+ "Scan Address": "ΠΡΡΠΊΠ°Π½ΠΈΡΠΎΠ²Π°ΡΡ ΠΠ΄ΡΠ΅Ρ",
+ "Scan BIP39 passphrase": "ΠΡΡΠΊΠ°Π½ΠΈΡΠΎΠ²Π°ΡΡ BIP39 ΡΡΠ°Π·Ρ-ΠΏΠ°ΡΠΎΠ»Ρ",
+ "Scan Key QR code": "ΠΡΡΠΊΠ°Π½ΠΈΡΠΎΠ²Π°ΡΡ ΠΠ»ΡΡ QR ΠΊΠΎΠ΄",
+ "Scanning words 1-12 again": "Π‘ΠΊΠ°Π½ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΡΠ»ΠΎΠ² 1-12 ΡΠ½ΠΎΠ²Π°",
+ "Scanning words 13-24": "Π‘ΠΊΠ°Π½ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΡΠ»ΠΎΠ² 13-24",
+ "SeedQR": "SeedQR",
+ "Self-transfer or Change (%d): ": "Π’ΡΠ°Π½ΡΡΠ΅Ρ ΡΠ°ΠΌΠΎΠΌΡ ΡΠ΅Π±Π΅ ΠΈΠ»ΠΈ Π‘Π΄Π°ΡΠ° (%d): ",
+ "Settings": "ΠΠ°ΡΡΡΠΎΠΉΠΊΠΈ",
+ "Shutdown": "ΠΡΠΊΠ»ΡΡΠΈΡΡ",
+ "Shutting down..": "ΠΡΠΊΠ»ΡΡΠ΅Π½ΠΈΠ΅..",
+ "Sign": "ΠΠΎΠ΄ΠΏΠΈΡΠ°ΡΡ",
+ "Sign?": "ΠΠΎΠ΄ΠΏΠΈΡΠ°ΡΡ?",
+ "Signature": "ΠΠΎΠ΄ΠΏΠΈΡΡ",
+ "Signed Message": "ΠΠΎΠ΄ΠΏΠΈΡΠ°Π½Π½ΠΎΠ΅ Π‘ΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅",
+ "Signed PSBT": "ΠΠΎΠ΄ΠΏΠΈΡΠ°Π½Π½ΠΎΠ΅ PSBT",
+ "Single-sig": "ΠΠ΄Π½Π° ΠΏΠΎΠ΄ΠΏΠΈΡΡ",
+ "Size: ": "Π Π°Π·ΠΌΠ΅Ρ: ",
+ "Some checks cannot be performed.": "ΠΠ΅ΠΊΠΎΡΠΎΡΡΠ΅ ΠΏΡΠΎΠ²Π΅ΡΠΊΠΈ Π½Π΅ ΠΌΠΎΠ³ΡΡ Π±ΡΡΡ Π²ΡΠΏΠΎΠ»Π½Π΅Π½Ρ.",
+ "Spend (%d): ": "Π Π°ΡΡ
ΠΎΠ΄ (%d): ",
+ "Stackbit 1248": "Π‘ΡΡΠΊΠ±ΠΈΡ 1248",
+ "Store on Flash": "Π‘ΠΎΡ
ΡΠ°Π½ΠΈΡΡ Π½Π° Π€Π»ΡΡ ΠΠ°ΠΌΡΡΡ",
+ "Store on SD Card": "Π‘ΠΎΡ
ΡΠ°Π½ΠΈΡΡ Π½Π° SD ΠΠ°ΡΡΡ",
+ "Swipe to change mode": "Π‘Π²Π°ΠΉΠΏΠ½ΠΈΡΠ΅, ΡΡΠΎΠ±Ρ ΡΠΌΠ΅Π½ΠΈΡΡ ΡΠ΅ΠΆΠΈΠΌ",
+ "TOUCH or ENTER to capture": "ΠΠ ΠΠΠΠ‘ΠΠΠ’ΠΠ‘Π¬ ΠΈΠ»ΠΈ Π½Π°ΠΆΠΌΠΈΡΠ΅ ΠΠΠΠ, ΡΡΠΎΠ±Ρ Π·Π°Ρ
Π²Π°ΡΠΈΡΡ",
+ "TX Pin": "TX ΠΠΈΠ½",
+ "Text": "Π’Π΅ΠΊΡΡ",
+ "Theme": "Π’Π΅ΠΌΠ°",
+ "Thermal": "Π’Π΅ΡΠΌΠ°Π»ΡΠ½ΡΠΉ",
+ "Tiny Seed": "ΠΠΈΠ½ΠΈ Π‘ΠΈΠ΄-ΡΡΠ°Π·Π°",
+ "Tiny Seed (Bits)": "ΠΠΈΠ½ΠΈ Π‘ΠΈΠ΄-ΡΡΠ°Π·Π° (ΠΠΈΡΡ)",
+ "Tools": "ΠΠ½ΡΡΡΡΠΌΠ΅Π½ΡΡ",
+ "Touch Threshold": "ΠΡΠΈΠΊΠΎΡΠ½ΠΈΡΠ΅ΡΡ ΠΡΠ°Π½ΠΈΡΡ",
+ "Touchscreen": "Π’Π°ΡΡΠΊΡΠΈΠ½",
+ "Try more?": "ΠΠΎΠΏΡΠΎΠ±ΠΎΠ²Π°ΡΡ Π΅ΡΡ?",
+ "Type BIP39 passphrase": "ΠΠ²Π΅Π΄ΠΈΡΠ΅ BIP39 ΡΡΠ°Π·Ρ-ΠΏΠ°ΡΠΎΠ»Ρ",
+ "Type Key": "ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΠ»ΡΡ",
+ "Unit": "ΠΠ½ΠΈΡ",
+ "Updating bootloader..\n\n%d%%": "ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ Π·Π°Π³ΡΡΠ·ΡΠΈΠΊΠ°..\n\n%d%%",
+ "Upgrade complete.\n\nShutting down..": "ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ Π·Π°Π²Π΅ΡΡΠ΅Π½ΠΎ.\n\nΠΡΠΊΠ»ΡΡΠ΅Π½ΠΈΠ΅..",
+ "Upgrading firmware..\n\n%d%%": "ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΏΡΠΎΡΠΈΠ²ΠΊΠΈ..\n\n%d%%",
+ "Use a black background surface.": "ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΡ ΡΠ΅ΡΠ½ΡΡ ΡΠΎΠ½ΠΎΠ²ΡΡ ΠΏΠΎΠ²Π΅ΡΡ
Π½ΠΎΡΡΡ.",
+ "Use camera's entropy to create a new mnemonic": "ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΡ ΡΠ½ΡΡΠΎΠΏΠΈΡ ΠΊΠ°ΠΌΠ΅ΡΡ, ΡΡΠΎΠ±Ρ ΡΠΎΠ·Π΄Π°ΡΡ Π½ΠΎΠ²ΡΡ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ "Used: ": "ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΎ: ",
+ "Value %s out of range: [%s, %s]": "ΠΠ½Π°ΡΠ΅Π½ΠΈΠ΅ %s Π²Π½Π΅ Π΄ΠΈΠ°ΠΏΠΎΠ·ΠΎΠ½Π°: [%s, %s]",
+ "Via Camera": "Π‘ ΠΠΎΠΌΠΎΡΡΡ ΠΠ°ΠΌΠ΅ΡΡ",
+ "Via D20": "Π‘ ΠΠΎΠΌΠΎΡΡΡ D20",
+ "Via D6": "Π‘ ΠΠΎΠΌΠΎΡΡΡ D6",
+ "Via Manual Input": "Π‘ ΠΠΎΠΌΠΎΡΡΡ Π ΡΡΠ½ΠΎΠ³ΠΎ ΠΠ²ΠΎΠ΄Π°",
+ "Wait for the capture": "ΠΠΎΠΆΠ΄ΠΈΡΠ΅ΡΡ ΠΠ°Ρ
Π²Π°ΡΠ°",
+ "Wallet Descriptor": "ΠΠ΅ΡΠΊΡΠΈΠΏΡΠΎΡ ΠΠΎΡΠ΅Π»ΡΠΊΠ°",
+ "Wallet output descriptor": "ΠΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ ΠΊΠΎΡΠ΅Π»ΡΠΊΠ°",
+ "Wallet output descriptor loaded!": "ΠΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ ΠΊΠΎΡΠ΅Π»ΡΠΊΠ° Π·Π°Π³ΡΡΠΆΠ΅Π½!",
+ "Wallet output descriptor not found.": "ΠΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ ΠΊΠΎΡΠ΅Π»ΡΠΊΠ° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½.",
+ "Warning:": "ΠΡΠ΅Π΄ΡΠΏΡΠ΅ΠΆΠ΄Π΅Π½ΠΈΠ΅:",
+ "Word %d": "Π‘Π»ΠΎΠ²ΠΎ %d",
+ "Word Numbers": "Π§ΠΈΡΠ»Π° Π‘Π»ΠΎΠ²",
+ "Words": "Π‘Π»ΠΎΠ²Π°",
+ "Yes": "ΠΠ°",
+ "Your changes will be kept on device flash storage.": "ΠΠ°ΡΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ Π±ΡΠ΄ΡΡ ΡΠΎΡ
ΡΠ°Π½Π΅Π½Ρ Π½Π° ΡΠ»ΡΡ ΠΏΠ°ΠΌΡΡΠΈ ΡΡΡΡΠΎΠΉΡΡΠ²Π°.",
+ "Your changes will be kept on the SD card.": "ΠΠ°ΡΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ Π±ΡΠ΄ΡΡ ΡΠΎΡ
ΡΠ°Π½Π΅Π½Ρ Π½Π° SD ΠΊΠ°ΡΡΠ΅."
+}
\ No newline at end of file
diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json
index 8e5de7b8b..ce8294faf 100644
--- a/i18n/translations/vi-VN.json
+++ b/i18n/translations/vi-VN.json
@@ -19,7 +19,6 @@
"Anti-glare disabled": "Chα»ng lΓ³a bα» vΓ΄ hiα»u hΓ³a",
"Anti-glare enabled": "ΔΓ£ bαΊt chα»ng lΓ³a",
"Are you sure?": "BαΊ‘n cΓ³ chαΊ―c khΓ΄ng?",
- "BIP39 Mnemonic": "MΓ£ mnemonic dαΊ‘ng chuαΊ©n BIP39",
"Back": "Trα» lαΊ‘i",
"Backing up bootloader..\n\n%d%%": "Sao lΖ°u bα» tαΊ£i khα»i Δα»ng..\n\n%d%%",
"Bad signature": "Chα»― kΓ½ xαΊ₯u",
@@ -49,6 +48,7 @@
"Cut Method": "PhΖ°Ζ‘ng phΓ‘p cαΊ―t",
"Decimal": "Sα» thαΊp phΓ’n",
"Decrypt?": "PhαΊ£n Δα»i?",
+ "Delete %s?": "XΓ³a %s?",
"Delete File?": "XΓ³a tΓ i liα»u?",
"Delete Mnemonic": "XΓ³a ghi nhα»",
"Depth Per Pass": "Δα» sΓ’u mα»i lαΊ§n vượt qua",
@@ -94,11 +94,10 @@
"GRBL": "GRBL",
"Give this mnemonic a custom ID? Otherwise current fingerprint will be used": "Cung cαΊ₯p cho Mnemonic nΓ y mα»t ID tΓΉy chα»nh?NαΊΏu khΓ΄ng thΓ¬ dαΊ₯u vΓ’n tay hiα»n tαΊ‘i sαΊ½ Δược sα» dα»₯ng",
"Go": "ΔαΊΏn",
- "Heat Interval": "KhoαΊ£ng thα»i gian nhiα»t",
- "Heat Time": "Thα»i gian nhiα»t",
"Hex Public Key": "KhΓ³a cΓ΄ng khai Hex",
"Hexadecimal": "ThαΊp lα»₯c phΓ’n",
"ID already exists\n": "Id ΔΓ£ tα»n tαΊ‘i\n",
+ "Incomplete output descriptor": "Bα» mΓ΄ tαΊ£ ΔαΊ§u ra chΖ°a hoΓ n chα»nh",
"Inputs (%d): ": "ΔαΊ§u vΓ o (%d): ",
"Invalid address": "Δα»a chα» khΓ΄ng hợp lα»",
"Invalid bootloader": "Bα» tαΊ£i khα»i Δα»ng khΓ΄ng hợp lα»",
@@ -199,6 +198,7 @@
"Signed PSBT": "ΔΓ£ kΓ½ PSBT",
"Single-sig": "KhΓ³a ΔΖ‘n",
"Size: ": "Dung lượng: ",
+ "Some checks cannot be performed.": "Mα»t sα» kiα»m tra khΓ΄ng thα» Δược thα»±c hiα»n.",
"Spend (%d): ": "Chi tiΓͺu (%d): ",
"Stackbit 1248": "Stackbit 1248",
"Store on Flash": "LΖ°u trα»― trΓͺn flash",
@@ -235,7 +235,7 @@
"Wallet output descriptor": "VΓ ΔαΊ§u ra mΓ΄ tαΊ£",
"Wallet output descriptor loaded!": "ΔΓ£ tαΊ£i bα» mΓ΄ tαΊ£ ΔαΊ§u ra của vΓ!",
"Wallet output descriptor not found.": "KhΓ΄ng tΓ¬m thαΊ₯y bα» mΓ΄ tαΊ£ ΔαΊ§u ra vΓ.",
- "Warning:\nIncomplete output descriptor": "CαΊ£nh bΓ‘o:\nBα» mΓ΄ tαΊ£ ΔαΊ§u ra chΖ°a hoΓ n chα»nh",
+ "Warning:": "CαΊ£nh bΓ‘o:",
"Word %d": "KΓ tα»± %d",
"Word Numbers": "Tα»« sα»",
"Words": "Tα»« ngα»―",
diff --git a/krux b/krux
index 950aee6ba..8a562122a 100755
--- a/krux
+++ b/krux
@@ -74,7 +74,7 @@ elif [ "$1" == "flash" ]; then
elif [ "$device" == "maixpy_amigo_ips" ]||[ "$device" == "maixpy_amigo_tft" ]||[ "$device" == "maixpy_bit" ]; then
# https://devicehunt.com/view/type/usb/vendor/0403/device/6010
device_vendor_product_id="0403:6010"
- elif [ "$device" == "maixpy_dock" ]; then
+ elif [ "$device" == "maixpy_dock" ]||[ "$device" == "maixpy_yahboom" ]; then
# https://devicehunt.com/view/type/usb/vendor/1a86/device/7523
device_vendor_product_id="1a86:7523"
fi
@@ -191,7 +191,7 @@ elif [ "$1" == "build-release" ]; then
cp -f ktool-win.exe ../$release_dir/ktool-win.exe
cd ..
- devices=("maixpy_m5stickv" "maixpy_amigo_ips" "maixpy_amigo_tft" "maixpy_bit" "maixpy_dock")
+ devices=("maixpy_m5stickv" "maixpy_amigo_ips" "maixpy_amigo_tft" "maixpy_bit" "maixpy_dock" "maixpy_yahboom")
for device in ${devices[@]}; do
./krux build $device
mkdir -p $release_dir/$device
diff --git a/odudex.PEM b/odudex.PEM
new file mode 100644
index 000000000..2ce42000d
--- /dev/null
+++ b/odudex.PEM
@@ -0,0 +1,3 @@
+-----BEGIN PUBLIC KEY-----
+MDYwEAYHKoZIzj0CAQYFK4EEAAoDIgACgw+uOhd7ie6I08S4q3vIZtYBP8TmBuQJkVDbAP4sty0=
+-----END PUBLIC KEY-----
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index ea20943c4..b6aca2e77 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,15 +1,14 @@
-# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]]
name = "astroid"
-version = "2.15.6"
+version = "2.15.8"
description = "An abstract syntax tree for Python with inference support."
-category = "dev"
optional = false
python-versions = ">=3.7.2"
files = [
- {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"},
- {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"},
+ {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"},
+ {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"},
]
[package.dependencies]
@@ -21,65 +20,47 @@ wrapt = [
]
[[package]]
-name = "atomicwrites"
-version = "1.4.1"
-description = "Atomic file writes."
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-files = [
- {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
-]
-
-[[package]]
-name = "attrs"
-version = "23.1.0"
-description = "Classes Without Boilerplate"
-category = "dev"
-optional = false
+name = "babel"
+version = "2.13.1"
+description = "Internationalization utilities"
+optional = true
python-versions = ">=3.7"
files = [
- {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
- {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
+ {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"},
+ {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"},
]
+[package.dependencies]
+setuptools = {version = "*", markers = "python_version >= \"3.12\""}
+
[package.extras]
-cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
-dev = ["attrs[docs,tests]", "pre-commit"]
-docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
-tests = ["attrs[tests-no-zope]", "zope-interface"]
-tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "black"
-version = "23.7.0"
+version = "23.11.0"
description = "The uncompromising code formatter."
-category = "dev"
optional = false
python-versions = ">=3.8"
files = [
- {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"},
- {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"},
- {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"},
- {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"},
- {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"},
- {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"},
- {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"},
- {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"},
- {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"},
- {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"},
- {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"},
- {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"},
- {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"},
- {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"},
- {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"},
- {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"},
- {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"},
- {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"},
- {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"},
- {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"},
- {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"},
- {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"},
+ {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"},
+ {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"},
+ {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"},
+ {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"},
+ {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"},
+ {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"},
+ {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"},
+ {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"},
+ {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"},
+ {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"},
+ {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"},
+ {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"},
+ {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"},
+ {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"},
+ {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"},
+ {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"},
+ {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"},
+ {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"},
]
[package.dependencies]
@@ -89,7 +70,7 @@ packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
+typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
@@ -99,123 +80,123 @@ uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "certifi"
-version = "2023.7.22"
+version = "2023.11.17"
description = "Python package for providing Mozilla's CA Bundle."
-category = "main"
optional = false
python-versions = ">=3.6"
files = [
- {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
- {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
-]
-
-[[package]]
-name = "chardet"
-version = "3.0.4"
-description = "Universal encoding detector for Python 2 and 3"
-category = "main"
-optional = false
-python-versions = "*"
-files = [
- {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
- {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
+ {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
+ {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
]
[[package]]
name = "charset-normalizer"
-version = "3.2.0"
+version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "main"
-optional = true
+optional = false
python-versions = ">=3.7.0"
files = [
- {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"},
- {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"},
+ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
[[package]]
name = "click"
-version = "8.1.6"
+version = "8.1.7"
description = "Composable command line interface toolkit"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"},
- {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"},
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
@@ -225,7 +206,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
-category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@@ -235,72 +215,63 @@ files = [
[[package]]
name = "coverage"
-version = "7.2.7"
+version = "7.3.2"
description = "Code coverage measurement for Python"
-category = "dev"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
- {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
- {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
- {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"},
- {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"},
- {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"},
- {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"},
- {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"},
- {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"},
- {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"},
- {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"},
- {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"},
- {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"},
- {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"},
- {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"},
- {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"},
- {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"},
- {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"},
- {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"},
- {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"},
- {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"},
- {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"},
- {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"},
- {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"},
- {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"},
- {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"},
- {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"},
- {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"},
- {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"},
- {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"},
- {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"},
- {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"},
- {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"},
- {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"},
- {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"},
- {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"},
- {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"},
- {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"},
- {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"},
- {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"},
- {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"},
- {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"},
- {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"},
- {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"},
- {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"},
- {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"},
- {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"},
- {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"},
- {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"},
- {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"},
- {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"},
- {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"},
- {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"},
- {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"},
- {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"},
- {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"},
- {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"},
- {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"},
- {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
- {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
+ {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"},
+ {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"},
+ {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"},
+ {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"},
+ {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"},
+ {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"},
+ {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"},
+ {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"},
+ {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"},
+ {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"},
+ {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"},
+ {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"},
+ {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"},
+ {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"},
+ {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"},
+ {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"},
+ {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"},
+ {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"},
+ {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"},
+ {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"},
+ {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"},
+ {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"},
+ {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"},
+ {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"},
+ {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"},
+ {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"},
+ {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"},
+ {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"},
+ {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"},
+ {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"},
+ {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"},
+ {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"},
+ {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"},
+ {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"},
+ {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"},
+ {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"},
+ {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"},
+ {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"},
+ {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"},
+ {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"},
+ {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"},
+ {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"},
+ {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"},
+ {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"},
+ {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"},
+ {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"},
+ {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"},
+ {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"},
+ {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"},
+ {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"},
+ {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"},
+ {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"},
]
[package.dependencies]
@@ -313,7 +284,6 @@ toml = ["tomli"]
name = "dill"
version = "0.3.7"
description = "serialize all of Python"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -326,23 +296,38 @@ graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "embit"
-version = "0.5.0"
-description = "yet another bitcoin library"
-category = "main"
+version = "0.7.0"
+description = "A minimal bitcoin library for MicroPython and Python3 with a focus on embedded systems."
optional = false
python-versions = "*"
files = []
develop = true
+[package.extras]
+dev = ["black", "mkdocs", "mkdocs-material", "mkdocstrings[python]", "mypy", "pre-commit", "pytest", "pytest-cov", "requests"]
+
[package.source]
type = "directory"
url = "vendor/embit"
+[[package]]
+name = "exceptiongroup"
+version = "1.2.0"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
+ {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
[[package]]
name = "ghp-import"
version = "2.1.0"
description = "Copy your docs directly to the gh-pages branch."
-category = "main"
optional = true
python-versions = "*"
files = [
@@ -356,139 +341,21 @@ python-dateutil = ">=2.8.1"
[package.extras]
dev = ["flake8", "markdown", "twine", "wheel"]
-[[package]]
-name = "googletrans"
-version = "4.0.0rc1"
-description = "Free Google Translate API for Python. Translates totally free of charge."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "googletrans-4.0.0rc1.tar.gz", hash = "sha256:74df47b092e2d566522019d149e3f1d75732570ad76eaf8e14aebeffc126c372"},
-]
-
-[package.dependencies]
-httpx = "0.13.3"
-
-[[package]]
-name = "h11"
-version = "0.9.0"
-description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
-category = "main"
-optional = false
-python-versions = "*"
-files = [
- {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"},
- {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"},
-]
-
-[[package]]
-name = "h2"
-version = "3.2.0"
-description = "HTTP/2 State-Machine based protocol implementation"
-category = "main"
-optional = false
-python-versions = "*"
-files = [
- {file = "h2-3.2.0-py2.py3-none-any.whl", hash = "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5"},
- {file = "h2-3.2.0.tar.gz", hash = "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"},
-]
-
-[package.dependencies]
-hpack = ">=3.0,<4"
-hyperframe = ">=5.2.0,<6"
-
-[[package]]
-name = "hpack"
-version = "3.0.0"
-description = "Pure-Python HPACK header compression"
-category = "main"
-optional = false
-python-versions = "*"
-files = [
- {file = "hpack-3.0.0-py2.py3-none-any.whl", hash = "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89"},
- {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"},
-]
-
-[[package]]
-name = "hstspreload"
-version = "2023.1.1"
-description = "Chromium HSTS Preload list as a Python package"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "hstspreload-2023.1.1-py3-none-any.whl", hash = "sha256:ac8a56dd603b4bf55292fc7a157e0deea18ee5e2e5c114d131da8949cc7a54bb"},
- {file = "hstspreload-2023.1.1.tar.gz", hash = "sha256:b2330a88b3fe3344c9eb431257e1ff3ae06c3bc2ff87ca686a5f253e2881a6c1"},
-]
-
-[[package]]
-name = "httpcore"
-version = "0.9.1"
-description = "A minimal low-level HTTP client."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "httpcore-0.9.1-py3-none-any.whl", hash = "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0"},
- {file = "httpcore-0.9.1.tar.gz", hash = "sha256:ecc5949310d9dae4de64648a4ce529f86df1f232ce23dcfefe737c24d21dfbe9"},
-]
-
-[package.dependencies]
-h11 = ">=0.8,<0.10"
-h2 = ">=3.0.0,<4.0.0"
-sniffio = ">=1.0.0,<2.0.0"
-
-[[package]]
-name = "httpx"
-version = "0.13.3"
-description = "The next generation HTTP client."
-category = "main"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "httpx-0.13.3-py3-none-any.whl", hash = "sha256:32d930858eab677bc29a742aaa4f096de259f1c78c68a90ad11f5c3c04f08335"},
- {file = "httpx-0.13.3.tar.gz", hash = "sha256:3642bd13e90b80ba8a243a730275eb10a4c26ec96f5fc16b87e458d4ab21efae"},
-]
-
-[package.dependencies]
-certifi = "*"
-chardet = ">=3.0.0,<4.0.0"
-hstspreload = "*"
-httpcore = ">=0.9.0,<0.10.0"
-idna = ">=2.0.0,<3.0.0"
-rfc3986 = ">=1.3,<2"
-sniffio = "*"
-
-[[package]]
-name = "hyperframe"
-version = "5.2.0"
-description = "HTTP/2 framing layer for Python"
-category = "main"
-optional = false
-python-versions = "*"
-files = [
- {file = "hyperframe-5.2.0-py2.py3-none-any.whl", hash = "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40"},
- {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"},
-]
-
[[package]]
name = "idna"
-version = "2.10"
+version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
-category = "main"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.5"
files = [
- {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
- {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
+ {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
+ {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
]
[[package]]
name = "importlib-metadata"
version = "6.8.0"
description = "Read metadata from Python packages"
-category = "main"
optional = true
python-versions = ">=3.8"
files = [
@@ -508,7 +375,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -520,7 +386,6 @@ files = [
name = "isort"
version = "5.12.0"
description = "A Python utility / library to sort Python imports."
-category = "dev"
optional = false
python-versions = ">=3.8.0"
files = [
@@ -538,7 +403,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
-category = "main"
optional = true
python-versions = ">=3.7"
files = [
@@ -556,7 +420,6 @@ i18n = ["Babel (>=2.7)"]
name = "lazy-object-proxy"
version = "1.9.0"
description = "A fast and thorough lazy object proxy."
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -598,30 +461,146 @@ files = [
{file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"},
]
+[[package]]
+name = "libretranslatepy"
+version = "2.1.1"
+description = "Python bindings for LibreTranslate API"
+optional = false
+python-versions = "*"
+files = [
+ {file = "libretranslatepy-2.1.1-py3-none-any.whl", hash = "sha256:bf9f8b0003c94f34e141553b7ec0a02876297c06955f5efea49afbc9f85303a9"},
+ {file = "libretranslatepy-2.1.1.tar.gz", hash = "sha256:3f28e1b990ba5f514ae215c08ace0c4e2327eeccaa356983aefbca3a25ecc568"},
+]
+
+[[package]]
+name = "lxml"
+version = "4.9.3"
+description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
+files = [
+ {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"},
+ {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"},
+ {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"},
+ {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"},
+ {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"},
+ {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"},
+ {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"},
+ {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"},
+ {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"},
+ {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"},
+ {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"},
+ {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"},
+ {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"},
+ {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"},
+ {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"},
+ {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"},
+ {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"},
+ {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"},
+ {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"},
+ {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"},
+ {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"},
+ {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"},
+ {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"},
+ {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"},
+ {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"},
+ {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"},
+ {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"},
+ {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"},
+ {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"},
+ {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"},
+ {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"},
+ {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"},
+ {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"},
+ {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"},
+ {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"},
+ {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"},
+ {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"},
+ {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"},
+ {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"},
+ {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"},
+ {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"},
+ {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"},
+ {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"},
+ {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"},
+ {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"},
+ {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"},
+ {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"},
+ {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"},
+ {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"},
+ {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"},
+ {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"},
+ {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"},
+ {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"},
+ {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"},
+ {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"},
+ {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"},
+ {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"},
+ {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"},
+ {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"},
+ {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"},
+ {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"},
+ {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"},
+ {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"},
+ {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"},
+ {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"},
+ {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"},
+ {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"},
+ {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"},
+]
+
+[package.extras]
+cssselect = ["cssselect (>=0.7)"]
+html5 = ["html5lib"]
+htmlsoup = ["BeautifulSoup4"]
+source = ["Cython (>=0.29.35)"]
+
[[package]]
name = "markdown"
-version = "3.4.4"
+version = "3.5.1"
description = "Python implementation of John Gruber's Markdown."
-category = "main"
optional = true
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"},
- {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"},
+ {file = "Markdown-3.5.1-py3-none-any.whl", hash = "sha256:5874b47d4ee3f0b14d764324d2c94c03ea66bee56f2d929da9f2508d65e722dc"},
+ {file = "Markdown-3.5.1.tar.gz", hash = "sha256:b65d7beb248dc22f2e8a31fb706d93798093c308dc1aba295aedeb9d41a813bd"},
]
[package.dependencies]
importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
[package.extras]
-docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"]
+docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
testing = ["coverage", "pyyaml"]
[[package]]
name = "markupsafe"
version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
-category = "main"
optional = true
python-versions = ">=3.7"
files = [
@@ -681,7 +660,6 @@ files = [
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
-category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@@ -693,7 +671,6 @@ files = [
name = "mergedeep"
version = "1.3.4"
description = "A deep merge function for π."
-category = "main"
optional = true
python-versions = ">=3.6"
files = [
@@ -703,14 +680,13 @@ files = [
[[package]]
name = "mkdocs"
-version = "1.5.2"
+version = "1.5.3"
description = "Project documentation with Markdown."
-category = "main"
optional = true
python-versions = ">=3.7"
files = [
- {file = "mkdocs-1.5.2-py3-none-any.whl", hash = "sha256:60a62538519c2e96fe8426654a67ee177350451616118a41596ae7c876bb7eac"},
- {file = "mkdocs-1.5.2.tar.gz", hash = "sha256:70d0da09c26cff288852471be03c23f0f521fc15cf16ac89c7a3bfb9ae8d24f9"},
+ {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"},
+ {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"},
]
[package.dependencies]
@@ -735,42 +711,48 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
[[package]]
name = "mkdocs-material"
-version = "8.5.11"
+version = "9.4.14"
description = "Documentation that simply works"
-category = "main"
optional = true
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "mkdocs_material-8.5.11-py3-none-any.whl", hash = "sha256:c907b4b052240a5778074a30a78f31a1f8ff82d7012356dc26898b97559f082e"},
- {file = "mkdocs_material-8.5.11.tar.gz", hash = "sha256:b0ea0513fd8cab323e8a825d6692ea07fa83e917bb5db042e523afecc7064ab7"},
+ {file = "mkdocs_material-9.4.14-py3-none-any.whl", hash = "sha256:dbc78a4fea97b74319a6aa9a2f0be575a6028be6958f813ba367188f7b8428f6"},
+ {file = "mkdocs_material-9.4.14.tar.gz", hash = "sha256:a511d3ff48fa8718b033e7e37d17abd9cc1de0fdf0244a625ca2ae2387e2416d"},
]
[package.dependencies]
-jinja2 = ">=3.0.2"
-markdown = ">=3.2"
-mkdocs = ">=1.4.0"
-mkdocs-material-extensions = ">=1.1"
-pygments = ">=2.12"
-pymdown-extensions = ">=9.4"
-requests = ">=2.26"
+babel = ">=2.10,<3.0"
+colorama = ">=0.4,<1.0"
+jinja2 = ">=3.0,<4.0"
+markdown = ">=3.2,<4.0"
+mkdocs = ">=1.5.3,<2.0"
+mkdocs-material-extensions = ">=1.3,<2.0"
+paginate = ">=0.5,<1.0"
+pygments = ">=2.16,<3.0"
+pymdown-extensions = ">=10.2,<11.0"
+regex = ">=2022.4"
+requests = ">=2.26,<3.0"
+
+[package.extras]
+git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2,<2.0)"]
+imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=9.4,<10.0)"]
+recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"]
[[package]]
name = "mkdocs-material-extensions"
-version = "1.1.1"
+version = "1.3.1"
description = "Extension pack for Python Markdown and MkDocs Material."
-category = "main"
optional = true
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"},
- {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"},
+ {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"},
+ {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"},
]
[[package]]
name = "mkdocs-static-i18n"
version = "0.46"
description = "MkDocs i18n plugin using static translation markdown files"
-category = "main"
optional = true
python-versions = ">=3.7"
files = [
@@ -784,7 +766,6 @@ mkdocs = ">=1.2.3"
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
-category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@@ -794,54 +775,63 @@ files = [
[[package]]
name = "numpy"
-version = "1.25.2"
+version = "1.26.2"
description = "Fundamental package for array computing in Python"
-category = "main"
optional = true
python-versions = ">=3.9"
files = [
- {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"},
- {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"},
- {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"},
- {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"},
- {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"},
- {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"},
- {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"},
- {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"},
- {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"},
- {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"},
- {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"},
- {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"},
- {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"},
- {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"},
- {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"},
- {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"},
- {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"},
- {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"},
- {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"},
- {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"},
- {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"},
- {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"},
- {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"},
- {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"},
- {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"},
+ {file = "numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f"},
+ {file = "numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440"},
+ {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75"},
+ {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00"},
+ {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe"},
+ {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523"},
+ {file = "numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9"},
+ {file = "numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919"},
+ {file = "numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841"},
+ {file = "numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1"},
+ {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a"},
+ {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b"},
+ {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7"},
+ {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8"},
+ {file = "numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186"},
+ {file = "numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d"},
+ {file = "numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0"},
+ {file = "numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75"},
+ {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7"},
+ {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6"},
+ {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6"},
+ {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec"},
+ {file = "numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167"},
+ {file = "numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e"},
+ {file = "numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef"},
+ {file = "numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2"},
+ {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3"},
+ {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818"},
+ {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210"},
+ {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36"},
+ {file = "numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80"},
+ {file = "numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060"},
+ {file = "numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79"},
+ {file = "numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d"},
+ {file = "numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841"},
+ {file = "numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea"},
]
[[package]]
name = "opencv-python"
-version = "4.8.0.74"
+version = "4.8.1.78"
description = "Wrapper package for OpenCV python bindings."
-category = "main"
optional = true
python-versions = ">=3.6"
files = [
- {file = "opencv-python-4.8.0.74.tar.gz", hash = "sha256:009e3ce356a0cd2d7423723e00a32fd3d3cc5bb5970ed27a9a1f8a8f221d1db5"},
- {file = "opencv_python-4.8.0.74-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:31d0d59fc8fdf703de4cec46c79b9f8d026fdde9d23d6e2e6a66809feeebbda9"},
- {file = "opencv_python-4.8.0.74-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:66eadb5882ee56848b67f9fb57aadcaca2f4c9d9d00a0ef11043041925b51291"},
- {file = "opencv_python-4.8.0.74-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:038ba7075e55cb8e2846663ae970f0fb776a45b48ee69a887bf4ee15e2570083"},
- {file = "opencv_python-4.8.0.74-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43dd0dfe331fb95767af581bf3b2781d7a72cf6560ddf2f55949fe547f3e5c9f"},
- {file = "opencv_python-4.8.0.74-cp37-abi3-win32.whl", hash = "sha256:458e5dc377f15fcf769d80314f3d885bd95457b1a2891bee67df2eb24a1d3a52"},
- {file = "opencv_python-4.8.0.74-cp37-abi3-win_amd64.whl", hash = "sha256:8fe0018d0056a5187c57120b6b3f6c3e706c13b45c48e54e86d245a9a16fac84"},
+ {file = "opencv-python-4.8.1.78.tar.gz", hash = "sha256:cc7adbbcd1112877a39274106cb2752e04984bc01a031162952e97450d6117f6"},
+ {file = "opencv_python-4.8.1.78-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:91d5f6f5209dc2635d496f6b8ca6573ecdad051a09e6b5de4c399b8e673c60da"},
+ {file = "opencv_python-4.8.1.78-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31f47e05447da8b3089faa0a07ffe80e114c91ce0b171e6424f9badbd1c5cd"},
+ {file = "opencv_python-4.8.1.78-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9814beca408d3a0eca1bae7e3e5be68b07c17ecceb392b94170881216e09b319"},
+ {file = "opencv_python-4.8.1.78-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c406bdb41eb21ea51b4e90dfbc989c002786c3f601c236a99c59a54670a394"},
+ {file = "opencv_python-4.8.1.78-cp37-abi3-win32.whl", hash = "sha256:a7aac3900fbacf55b551e7b53626c3dad4c71ce85643645c43e91fcb19045e47"},
+ {file = "opencv_python-4.8.1.78-cp37-abi3-win_amd64.whl", hash = "sha256:b983197f97cfa6fcb74e1da1802c7497a6f94ed561aba6980f1f33123f904956"},
]
[package.dependencies]
@@ -849,29 +839,37 @@ numpy = [
{version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""},
{version = ">=1.21.2", markers = "python_version >= \"3.10\""},
{version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""},
+ {version = ">=1.23.5", markers = "python_version >= \"3.11\""},
{version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""},
{version = ">=1.17.0", markers = "python_version >= \"3.7\""},
{version = ">=1.17.3", markers = "python_version >= \"3.8\""},
- {version = ">=1.23.5", markers = "python_version >= \"3.11\""},
]
[[package]]
name = "packaging"
-version = "23.1"
+version = "23.2"
description = "Core utilities for Python packages"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
- {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
+ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
+]
+
+[[package]]
+name = "paginate"
+version = "0.5.6"
+description = "Divides large result sets into pages for easier browsing"
+optional = true
+python-versions = "*"
+files = [
+ {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"},
]
[[package]]
name = "pathspec"
version = "0.11.2"
description = "Utility library for gitignore style pattern matching of file paths."
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -881,78 +879,65 @@ files = [
[[package]]
name = "pillow"
-version = "9.5.0"
+version = "10.1.0"
description = "Python Imaging Library (Fork)"
-category = "main"
optional = true
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"},
- {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"},
- {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"},
- {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"},
- {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"},
- {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"},
- {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"},
- {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"},
- {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"},
- {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"},
- {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"},
- {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"},
- {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"},
- {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"},
- {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"},
- {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"},
- {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"},
- {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"},
- {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"},
- {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"},
- {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"},
- {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"},
- {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"},
- {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"},
- {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"},
- {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"},
- {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"},
- {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"},
- {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"},
- {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"},
- {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"},
- {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"},
- {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"},
- {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"},
- {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"},
- {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"},
- {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"},
- {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"},
- {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"},
- {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"},
- {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"},
- {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"},
- {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"},
- {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"},
- {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"},
- {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"},
- {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"},
- {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"},
- {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"},
- {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"},
- {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"},
- {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"},
- {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"},
- {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"},
- {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"},
- {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"},
- {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"},
- {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"},
- {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"},
- {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"},
- {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"},
- {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"},
- {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"},
- {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"},
- {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"},
- {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"},
+ {file = "Pillow-10.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106"},
+ {file = "Pillow-10.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273"},
+ {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666"},
+ {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2"},
+ {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593"},
+ {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db"},
+ {file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f"},
+ {file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818"},
+ {file = "Pillow-10.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57"},
+ {file = "Pillow-10.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7"},
+ {file = "Pillow-10.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7"},
+ {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610"},
+ {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839"},
+ {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172"},
+ {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061"},
+ {file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262"},
+ {file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992"},
+ {file = "Pillow-10.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a"},
+ {file = "Pillow-10.1.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b"},
+ {file = "Pillow-10.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d"},
+ {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27"},
+ {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312"},
+ {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de"},
+ {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651"},
+ {file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b"},
+ {file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f"},
+ {file = "Pillow-10.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996"},
+ {file = "Pillow-10.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793"},
+ {file = "Pillow-10.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e"},
+ {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2"},
+ {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a"},
+ {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01"},
+ {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d"},
+ {file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80"},
+ {file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212"},
+ {file = "Pillow-10.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14"},
+ {file = "Pillow-10.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099"},
+ {file = "Pillow-10.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616"},
+ {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb"},
+ {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219"},
+ {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34"},
+ {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd"},
+ {file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28"},
+ {file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2"},
+ {file = "Pillow-10.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256"},
+ {file = "Pillow-10.1.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7"},
+ {file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba"},
+ {file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4"},
+ {file = "Pillow-10.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9"},
+ {file = "Pillow-10.1.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e"},
+ {file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412"},
+ {file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b"},
+ {file = "Pillow-10.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f"},
+ {file = "Pillow-10.1.0.tar.gz", hash = "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38"},
]
[package.extras]
@@ -961,14 +946,13 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
[[package]]
name = "platformdirs"
-version = "3.10.0"
+version = "4.0.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"},
- {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"},
+ {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"},
+ {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"},
]
[package.extras]
@@ -977,168 +961,154 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
[[package]]
name = "pluggy"
-version = "1.2.0"
+version = "1.3.0"
description = "plugin and hook calling mechanisms for python"
-category = "dev"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"},
- {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"},
+ {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
+ {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
-[[package]]
-name = "py"
-version = "1.11.0"
-description = "library with cross-python path, ini-parsing, io, code, log facilities"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-files = [
- {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
- {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
-]
-
[[package]]
name = "pycryptodome"
-version = "3.18.0"
+version = "3.19.0"
description = "Cryptographic library for Python"
-category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
- {file = "pycryptodome-3.18.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:d1497a8cd4728db0e0da3c304856cb37c0c4e3d0b36fcbabcc1600f18504fc54"},
- {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:928078c530da78ff08e10eb6cada6e0dff386bf3d9fa9871b4bbc9fbc1efe024"},
- {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:157c9b5ba5e21b375f052ca78152dd309a09ed04703fd3721dce3ff8ecced148"},
- {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:d20082bdac9218649f6abe0b885927be25a917e29ae0502eaf2b53f1233ce0c2"},
- {file = "pycryptodome-3.18.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e8ad74044e5f5d2456c11ed4cfd3e34b8d4898c0cb201c4038fe41458a82ea27"},
- {file = "pycryptodome-3.18.0-cp27-cp27m-win32.whl", hash = "sha256:62a1e8847fabb5213ccde38915563140a5b338f0d0a0d363f996b51e4a6165cf"},
- {file = "pycryptodome-3.18.0-cp27-cp27m-win_amd64.whl", hash = "sha256:16bfd98dbe472c263ed2821284118d899c76968db1a6665ade0c46805e6b29a4"},
- {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7a3d22c8ee63de22336679e021c7f2386f7fc465477d59675caa0e5706387944"},
- {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:78d863476e6bad2a592645072cc489bb90320972115d8995bcfbee2f8b209918"},
- {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:b6a610f8bfe67eab980d6236fdc73bfcdae23c9ed5548192bb2d530e8a92780e"},
- {file = "pycryptodome-3.18.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:422c89fd8df8a3bee09fb8d52aaa1e996120eafa565437392b781abec2a56e14"},
- {file = "pycryptodome-3.18.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:9ad6f09f670c466aac94a40798e0e8d1ef2aa04589c29faa5b9b97566611d1d1"},
- {file = "pycryptodome-3.18.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:53aee6be8b9b6da25ccd9028caf17dcdce3604f2c7862f5167777b707fbfb6cb"},
- {file = "pycryptodome-3.18.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:10da29526a2a927c7d64b8f34592f461d92ae55fc97981aab5bbcde8cb465bb6"},
- {file = "pycryptodome-3.18.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f21efb8438971aa16924790e1c3dba3a33164eb4000106a55baaed522c261acf"},
- {file = "pycryptodome-3.18.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4944defabe2ace4803f99543445c27dd1edbe86d7d4edb87b256476a91e9ffa4"},
- {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:51eae079ddb9c5f10376b4131be9589a6554f6fd84f7f655180937f611cd99a2"},
- {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:83c75952dcf4a4cebaa850fa257d7a860644c70a7cd54262c237c9f2be26f76e"},
- {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:957b221d062d5752716923d14e0926f47670e95fead9d240fa4d4862214b9b2f"},
- {file = "pycryptodome-3.18.0-cp35-abi3-win32.whl", hash = "sha256:795bd1e4258a2c689c0b1f13ce9684fa0dd4c0e08680dcf597cf9516ed6bc0f3"},
- {file = "pycryptodome-3.18.0-cp35-abi3-win_amd64.whl", hash = "sha256:b1d9701d10303eec8d0bd33fa54d44e67b8be74ab449052a8372f12a66f93fb9"},
- {file = "pycryptodome-3.18.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:cb1be4d5af7f355e7d41d36d8eec156ef1382a88638e8032215c215b82a4b8ec"},
- {file = "pycryptodome-3.18.0-pp27-pypy_73-win32.whl", hash = "sha256:fc0a73f4db1e31d4a6d71b672a48f3af458f548059aa05e83022d5f61aac9c08"},
- {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f022a4fd2a5263a5c483a2bb165f9cb27f2be06f2f477113783efe3fe2ad887b"},
- {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:363dd6f21f848301c2dcdeb3c8ae5f0dee2286a5e952a0f04954b82076f23825"},
- {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12600268763e6fec3cefe4c2dcdf79bde08d0b6dc1813887e789e495cb9f3403"},
- {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4604816adebd4faf8810782f137f8426bf45fee97d8427fa8e1e49ea78a52e2c"},
- {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:01489bbdf709d993f3058e2996f8f40fee3f0ea4d995002e5968965fa2fe89fb"},
- {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3811e31e1ac3069988f7a1c9ee7331b942e605dfc0f27330a9ea5997e965efb2"},
- {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4b967bb11baea9128ec88c3d02f55a3e338361f5e4934f5240afcb667fdaec"},
- {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9c8eda4f260072f7dbe42f473906c659dcbadd5ae6159dfb49af4da1293ae380"},
- {file = "pycryptodome-3.18.0.tar.gz", hash = "sha256:c9adee653fc882d98956e33ca2c1fb582e23a8af7ac82fee75bd6113c55a0413"},
+ {file = "pycryptodome-3.19.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3006c44c4946583b6de24fe0632091c2653d6256b99a02a3db71ca06472ea1e4"},
+ {file = "pycryptodome-3.19.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:7c760c8a0479a4042111a8dd2f067d3ae4573da286c53f13cf6f5c53a5c1f631"},
+ {file = "pycryptodome-3.19.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:08ce3558af5106c632baf6d331d261f02367a6bc3733086ae43c0f988fe042db"},
+ {file = "pycryptodome-3.19.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45430dfaf1f421cf462c0dd824984378bef32b22669f2635cb809357dbaab405"},
+ {file = "pycryptodome-3.19.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:a9bcd5f3794879e91970f2bbd7d899780541d3ff439d8f2112441769c9f2ccea"},
+ {file = "pycryptodome-3.19.0-cp27-cp27m-win32.whl", hash = "sha256:190c53f51e988dceb60472baddce3f289fa52b0ec38fbe5fd20dd1d0f795c551"},
+ {file = "pycryptodome-3.19.0-cp27-cp27m-win_amd64.whl", hash = "sha256:22e0ae7c3a7f87dcdcf302db06ab76f20e83f09a6993c160b248d58274473bfa"},
+ {file = "pycryptodome-3.19.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7822f36d683f9ad7bc2145b2c2045014afdbbd1d9922a6d4ce1cbd6add79a01e"},
+ {file = "pycryptodome-3.19.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:05e33267394aad6db6595c0ce9d427fe21552f5425e116a925455e099fdf759a"},
+ {file = "pycryptodome-3.19.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:829b813b8ee00d9c8aba417621b94bc0b5efd18c928923802ad5ba4cf1ec709c"},
+ {file = "pycryptodome-3.19.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:fc7a79590e2b5d08530175823a242de6790abc73638cc6dc9d2684e7be2f5e49"},
+ {file = "pycryptodome-3.19.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:542f99d5026ac5f0ef391ba0602f3d11beef8e65aae135fa5b762f5ebd9d3bfb"},
+ {file = "pycryptodome-3.19.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:61bb3ccbf4bf32ad9af32da8badc24e888ae5231c617947e0f5401077f8b091f"},
+ {file = "pycryptodome-3.19.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d49a6c715d8cceffedabb6adb7e0cbf41ae1a2ff4adaeec9432074a80627dea1"},
+ {file = "pycryptodome-3.19.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e249a784cc98a29c77cea9df54284a44b40cafbfae57636dd2f8775b48af2434"},
+ {file = "pycryptodome-3.19.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d033947e7fd3e2ba9a031cb2d267251620964705a013c5a461fa5233cc025270"},
+ {file = "pycryptodome-3.19.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:84c3e4fffad0c4988aef0d5591be3cad4e10aa7db264c65fadbc633318d20bde"},
+ {file = "pycryptodome-3.19.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:139ae2c6161b9dd5d829c9645d781509a810ef50ea8b657e2257c25ca20efe33"},
+ {file = "pycryptodome-3.19.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5b1986c761258a5b4332a7f94a83f631c1ffca8747d75ab8395bf2e1b93283d9"},
+ {file = "pycryptodome-3.19.0-cp35-abi3-win32.whl", hash = "sha256:536f676963662603f1f2e6ab01080c54d8cd20f34ec333dcb195306fa7826997"},
+ {file = "pycryptodome-3.19.0-cp35-abi3-win_amd64.whl", hash = "sha256:04dd31d3b33a6b22ac4d432b3274588917dcf850cc0c51c84eca1d8ed6933810"},
+ {file = "pycryptodome-3.19.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:8999316e57abcbd8085c91bc0ef75292c8618f41ca6d2b6132250a863a77d1e7"},
+ {file = "pycryptodome-3.19.0-pp27-pypy_73-win32.whl", hash = "sha256:a0ab84755f4539db086db9ba9e9f3868d2e3610a3948cbd2a55e332ad83b01b0"},
+ {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0101f647d11a1aae5a8ce4f5fad6644ae1b22bb65d05accc7d322943c69a74a6"},
+ {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c1601e04d32087591d78e0b81e1e520e57a92796089864b20e5f18c9564b3fa"},
+ {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:506c686a1eee6c00df70010be3b8e9e78f406af4f21b23162bbb6e9bdf5427bc"},
+ {file = "pycryptodome-3.19.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7919ccd096584b911f2a303c593280869ce1af9bf5d36214511f5e5a1bed8c34"},
+ {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:560591c0777f74a5da86718f70dfc8d781734cf559773b64072bbdda44b3fc3e"},
+ {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cc2f2ae451a676def1a73c1ae9120cd31af25db3f381893d45f75e77be2400"},
+ {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17940dcf274fcae4a54ec6117a9ecfe52907ed5e2e438fe712fe7ca502672ed5"},
+ {file = "pycryptodome-3.19.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d04f5f623a280fbd0ab1c1d8ecbd753193ab7154f09b6161b0f857a1a676c15f"},
+ {file = "pycryptodome-3.19.0.tar.gz", hash = "sha256:bc35d463222cdb4dbebd35e0784155c81e161b9284e567e7e933d722e533331e"},
]
[[package]]
name = "pygame"
-version = "2.5.0"
+version = "2.5.2"
description = "Python Game Development"
-category = "main"
optional = true
python-versions = ">=3.6"
files = [
- {file = "pygame-2.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e34a2b5660acc298d0a66ce16f13a7ca1c56c2a685e40afef3a0cf6eaf3f44b3"},
- {file = "pygame-2.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:875dbde88b899fb7f48d6f0e87f70c3dcc8ee87a947c3df817d949a9741dbcf5"},
- {file = "pygame-2.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:854dc9106210d1a3a83914af53fc234c0bed65a39f5e6098a8eb489da354ad0c"},
- {file = "pygame-2.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e1898db0fd7b868a31c29204813f447c59390350fd806904d80bebde094f3f8"},
- {file = "pygame-2.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d5eac9b9936c7dc2813a750bc8efd53234ad1afc32eb99d6f64bb403c2b9aa"},
- {file = "pygame-2.5.0-cp310-cp310-win32.whl", hash = "sha256:e9eed550b8921080a3c7524202822fc2cf7226e0ffd3c4e4d16510ba44b24e6f"},
- {file = "pygame-2.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:93128beb1154c443f05a66bfbf3f1d4eb8659157ab3b45e4a0454e5905440431"},
- {file = "pygame-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c71d5b3ec232113cbd2e23a19eb01eef1818db41892d0d5efbac3901f940da66"},
- {file = "pygame-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b763062b1996de26a28600e7a8f138d5b36ba0ddd63c65ccbd06124cd95bab70"},
- {file = "pygame-2.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b6a42109f922352c524565fceb22bf8f8b6e4b00d38306e6f5b4c673048f4a"},
- {file = "pygame-2.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bcb19c8ee3fc794ab3a7cb5b5fb1ad38da6866dfbba4af3699a84a828c8a4b9"},
- {file = "pygame-2.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c66b7abc38511c7ca08c5bb58a3bfc14fa51b4e5f85a1786777afc9e584a14dd"},
- {file = "pygame-2.5.0-cp311-cp311-win32.whl", hash = "sha256:46cf1c9b20fb11c7d836c02dd5fc2ca843b699c0e2bc4130cf4ad2f855db5f7f"},
- {file = "pygame-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:f7b77b5019a9a6342535f53c75cef912b218cd24e98505828418f135aacc0a1b"},
- {file = "pygame-2.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ddec0c823fd0869fe4a75ba906dcb7889db0e0c289ce8c03d4ce0a67351ab66"},
- {file = "pygame-2.5.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bae93ce29b8337a5e02507603094c51740c9f496272ef070e2624e9647776568"},
- {file = "pygame-2.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c80a1ad38d11102b4dfa0519aa2a26fea534503b259872609acc9adb1860884e"},
- {file = "pygame-2.5.0-cp312-cp312-win32.whl", hash = "sha256:8ffebcafda0add8072f82999498113be37494694fa36e02155cfaf1a0ba56fe2"},
- {file = "pygame-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:574e310ba708da0c34b71c4158aa7cdca3cf3e16c4100dcd1d3c931a9c6705b4"},
- {file = "pygame-2.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:275e4fab379620c3b262cd58c457eea80001e91bc2e04d306ddb0ba548c969bf"},
- {file = "pygame-2.5.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c116a96a2784bd1724476dbf9c48bfea466ee493a736bdfa04ecbc3f193de0bc"},
- {file = "pygame-2.5.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4a0787ade8723323a3ba874bb725010bb08990a77327fc85f42474f3a840447"},
- {file = "pygame-2.5.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb39b660da1b56a1704ec4aa72bac538030786e23607fb25b8a66f357ffe3a"},
- {file = "pygame-2.5.0-cp36-cp36m-win32.whl", hash = "sha256:d051420667dd9fc8103b3cf994c03e46ee90b1c4a72c174737b8c14729ddf68e"},
- {file = "pygame-2.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6b58356510b7c38836eb81cf08983b58f280da99580d4f17e89ed0ddb707c29c"},
- {file = "pygame-2.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:80f167d8fcec7cd3107f829784ad721b1b7532c19fdf42b3aabbb51f7347850f"},
- {file = "pygame-2.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef96e9a2d8fd9526b89657d192c42dd7c551bfa381fa98ec52d45443e9713818"},
- {file = "pygame-2.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e21d53279fb504b267ae06b565b72d9f95ecbf1f2dd8c705329b287f38295d98"},
- {file = "pygame-2.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147cc0256a5df1316590f351febf6205ef2907564fb0d902935834b91e183486"},
- {file = "pygame-2.5.0-cp37-cp37m-win32.whl", hash = "sha256:e47696154d689180e4eea8c1d6f2bac923986119219db6ad0d2e60ab1f525e8c"},
- {file = "pygame-2.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4c87fa8fa1f3ea94069119accd6d4b5fbf869c2b2954a19b45162dfb3b7c885e"},
- {file = "pygame-2.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:143550078ab10f290cd7c8715a46853e0dc598fd6cdd1561ecb4d6e3116a6b26"},
- {file = "pygame-2.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:969de806bed49b28972862acba652f05ece9420bbdf5f925c970c6a18a9fd1f9"},
- {file = "pygame-2.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d3b0da31ea341b86ef96d6b13c0ddcb25f5320186b7215bc870f08119d2f80"},
- {file = "pygame-2.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23effd50121468f1dc41022230485bff515154191a9d343224850aa3ed3b7f0"},
- {file = "pygame-2.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce4116db6924b544ff8ff03f7ef681c8baf9c6e039a1ec21e26b4ebdaa0e994"},
- {file = "pygame-2.5.0-cp38-cp38-win32.whl", hash = "sha256:50a89c15412506d95e98792435f49a73101788db30ad9c562f611c7aa7b437fa"},
- {file = "pygame-2.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:df1d8affdbe9f417cc7141581e3d08e4b09f708060d3127d2016fd591b2e4f68"},
- {file = "pygame-2.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:99965da24d0bf138d9ac6b7494b9a12781c1510cf936616d1d0c46a736777f6a"},
- {file = "pygame-2.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:daa81057c1beb71a9fb96253457197ad03ee988ba546a166f253bd92a98a9a11"},
- {file = "pygame-2.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ca85da605f6621c99c05f272a5dcf85bf0badcdca45f16ff2bee9a9d41ae042"},
- {file = "pygame-2.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:603d403997d46b07022097861c8b0ff76c6192f8a2f5f89f1a6a978d4411b715"},
- {file = "pygame-2.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7babaeac11544f3e4d7a15756a27f943dc5fff276481fdc9d90415a903ad31a9"},
- {file = "pygame-2.5.0-cp39-cp39-win32.whl", hash = "sha256:9d2126f91699223f0c36845d1c7b2cdfe2f1753ef85b8410ea613e8bd212ca33"},
- {file = "pygame-2.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:8062adc409f0b2742d7996b9b470494025c5e3b73d0d03e3798708dcf5d195cd"},
- {file = "pygame-2.5.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:1bd14adf6151b6ac2f617a8fd71621f1c125209c41a359d3c1cf4bf3904dba5f"},
- {file = "pygame-2.5.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11708f1c7b1671db15246275adcb18cf384f5f7e73532e26999968876c5099"},
- {file = "pygame-2.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6830e431575697f7a11f7731798445242e37eb07ae9007f7be33083f700e9b1e"},
- {file = "pygame-2.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7dd80addfdf7dc1f0e04f81c98acb96580726783172256f2ebc955a967e84124"},
- {file = "pygame-2.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c2cbd9d1a0a3969d6e1c6b0741279c843b4a36ef3804d324874d0a2f0e49816"},
- {file = "pygame-2.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef373b9865c740f18236f2324e17e7f2111e27c6a4a5b67c490c72a8a8b8de77"},
- {file = "pygame-2.5.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3959038a3e2034cee3f15471786a3bac35baeaa1f7503dc7402bb49d25b5ddbc"},
- {file = "pygame-2.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583d9c8ad033ad51da8485427139d047afb649f49e42d4fa88477f73734ad4b0"},
- {file = "pygame-2.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b650e925d2e8c82c16bdeae6e7fc5d6ca4ac659a1412da4ecd923ef9d688cb"},
- {file = "pygame-2.5.0.tar.gz", hash = "sha256:edd5745b79435976d92c0a7318aedcafcb7ac4567125ac6ba88aa473559ef9ab"},
+ {file = "pygame-2.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a0769eb628c818761755eb0a0ca8216b95270ea8cbcbc82227e39ac9644643da"},
+ {file = "pygame-2.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed9a3d98adafa0805ccbaaff5d2996a2b5795381285d8437a4a5d248dbd12b4a"},
+ {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30d1618672a55e8c6669281ba264464b3ab563158e40d89e8c8b3faa0febebd"},
+ {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39690e9be9baf58b7359d1f3b2336e1fd6f92fedbbce42987be5df27f8d30718"},
+ {file = "pygame-2.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03879ec299c9f4ba23901b2649a96b2143f0a5d787f0b6c39469989e2320caf1"},
+ {file = "pygame-2.5.2-cp310-cp310-win32.whl", hash = "sha256:74e1d6284100e294f445832e6f6343be4fe4748decc4f8a51131ae197dae8584"},
+ {file = "pygame-2.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:485239c7d32265fd35b76ae8f64f34b0637ae11e69d76de15710c4b9edcc7c8d"},
+ {file = "pygame-2.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34646ca20e163dc6f6cf8170f1e12a2e41726780112594ac061fa448cf7ccd75"},
+ {file = "pygame-2.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b8a6e351665ed26ea791f0e1fd649d3f483e8681892caef9d471f488f9ea5ee"},
+ {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc346965847aef00013fa2364f41a64f068cd096dcc7778fc306ca3735f0eedf"},
+ {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35632035fd81261f2d797fa810ea8c46111bd78ceb6089d52b61ed7dc3c5d05f"},
+ {file = "pygame-2.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e24d05184e4195fe5ebcdce8b18ecb086f00182b9ae460a86682d312ce8d31f"},
+ {file = "pygame-2.5.2-cp311-cp311-win32.whl", hash = "sha256:f02c1c7505af18d426d355ac9872bd5c916b27f7b0fe224749930662bea47a50"},
+ {file = "pygame-2.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6d58c8cf937815d3b7cdc0fa9590c5129cb2c9658b72d00e8a4568dea2ff1d42"},
+ {file = "pygame-2.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1a2a43802bb5e89ce2b3b775744e78db4f9a201bf8d059b946c61722840ceea8"},
+ {file = "pygame-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1c289f2613c44fe70a1e40769de4a49c5ab5a29b9376f1692bb1a15c9c1c9bfa"},
+ {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:074aa6c6e110c925f7f27f00c7733c6303407edc61d738882985091d1eb2ef17"},
+ {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe0228501ec616779a0b9c4299e837877783e18df294dd690b9ab0eed3d8aaab"},
+ {file = "pygame-2.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31648d38ecdc2335ffc0e38fb18a84b3339730521505dac68514f83a1092e3f4"},
+ {file = "pygame-2.5.2-cp312-cp312-win32.whl", hash = "sha256:224c308856334bc792f696e9278e50d099a87c116f7fc314cd6aa3ff99d21592"},
+ {file = "pygame-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:dd2d2650faf54f9a0f5bd0db8409f79609319725f8f08af6507a0609deadcad4"},
+ {file = "pygame-2.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b30bc1220c457169571aac998e54b013aaeb732d2fd8744966cb1cfab1f61d1"},
+ {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78fcd7643358b886a44127ff7dec9041c056c212b3a98977674f83f99e9b12d3"},
+ {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cf093a51cb294ede56c29d4acf41538c00f297fcf78a9b186fb7d23c0577b6"},
+ {file = "pygame-2.5.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe323acbf53a0195c8c98b1b941eba7ac24e3e2b28ae48e8cda566f15fc4945"},
+ {file = "pygame-2.5.2-cp36-cp36m-win32.whl", hash = "sha256:5697528266b4716d9cdd44a5a1d210f4d86ef801d0f64ca5da5d0816704009d9"},
+ {file = "pygame-2.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edda1f7cff4806a4fa39e0e8ccd75f38d1d340fa5fc52d8582ade87aca247d92"},
+ {file = "pygame-2.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9bd738fd4ecc224769d0b4a719f96900a86578e26e0105193658a32966df2aae"},
+ {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30a8d7cf12363b4140bf2f93b5eec4028376ca1d0fe4b550588f836279485308"},
+ {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc12e4dea3e88ea8a553de6d56a37b704dbe2aed95105889f6afeb4b96e62097"},
+ {file = "pygame-2.5.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b34c73cb328024f8db3cb6487a37e54000148988275d8d6e5adf99d9323c937"},
+ {file = "pygame-2.5.2-cp37-cp37m-win32.whl", hash = "sha256:7d0a2794649defa57ef50b096a99f7113d3d0c2e32d1426cafa7d618eadce4c7"},
+ {file = "pygame-2.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:41f8779f52e0f6e6e6ccb8f0b5536e432bf386ee29c721a1c22cada7767b0cef"},
+ {file = "pygame-2.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:677e37bc0ea7afd89dde5a88ced4458aa8656159c70a576eea68b5622ee1997b"},
+ {file = "pygame-2.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47a8415d2bd60e6909823b5643a1d4ef5cc29417d817f2a214b255f6fa3a1e4c"},
+ {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ff21201df6278b8ca2e948fb148ffe88f5481fd03760f381dd61e45954c7dff"},
+ {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29a84b2e02814b9ba925357fd2e1df78efe5e1aa64dc3051eaed95d2b96eafd"},
+ {file = "pygame-2.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d78485c4d21133d6b2fbb504cd544ca655e50b6eb551d2995b3aa6035928adda"},
+ {file = "pygame-2.5.2-cp38-cp38-win32.whl", hash = "sha256:d851247239548aa357c4a6840fb67adc2d570ce7cb56988d036a723d26b48bff"},
+ {file = "pygame-2.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:88d1cdacc2d3471eceab98bf0c93c14d3a8461f93e58e3d926f20d4de3a75554"},
+ {file = "pygame-2.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4f1559e7efe4efb9dc19d2d811d702f325d9605f9f6f9ececa39ee6890c798f5"},
+ {file = "pygame-2.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cf2191b756ceb0e8458a761d0c665b0c70b538570449e0d39b75a5ba94ac5cf0"},
+ {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cf2257447ce7f2d6de37e5fb019d2bbe32ed05a5721ace8bc78c2d9beaf3aee"},
+ {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cbbfaba2b81434d62631d0b08b85fab16cf4a36e40b80298d3868927e1299"},
+ {file = "pygame-2.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:daca456d5b9f52e088e06a127dec182b3638a775684fb2260f25d664351cf1ae"},
+ {file = "pygame-2.5.2-cp39-cp39-win32.whl", hash = "sha256:3b3e619e33d11c297d7a57a82db40681f9c2c3ae1d5bf06003520b4fe30c435d"},
+ {file = "pygame-2.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:1822d534bb7fe756804647b6da2c9ea5d7a62d8796b2e15d172d3be085de28c6"},
+ {file = "pygame-2.5.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:e708fc8f709a0fe1d1876489345f2e443d47f3976d33455e2e1e937f972f8677"},
+ {file = "pygame-2.5.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c13edebc43c240fb0532969e914f0ccefff5ae7e50b0b788d08ad2c15ef793e4"},
+ {file = "pygame-2.5.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:263b4a7cbfc9fe2055abc21b0251cc17dea6dff750f0e1c598919ff350cdbffe"},
+ {file = "pygame-2.5.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e58e2b0c791041e4bccafa5bd7650623ba1592b8fe62ae0a276b7d0ecb314b6c"},
+ {file = "pygame-2.5.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0bd67426c02ffe6c9827fc4bcbda9442fbc451d29b17c83a3c088c56fef2c90"},
+ {file = "pygame-2.5.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcff6cbba1584cf7732ce1dbdd044406cd4f6e296d13bcb7fba963fb4aeefc9"},
+ {file = "pygame-2.5.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce4b6c0bfe44d00bb0998a6517bd0cf9455f642f30f91bc671ad41c05bf6f6ae"},
+ {file = "pygame-2.5.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68c4e8e60b725ffc7a6c6ecd9bb5fcc5ed2d6e0e2a2c4a29a8454856ef16ad63"},
+ {file = "pygame-2.5.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f3849f97372a3381c66955f99a0d58485ccd513c3d00c030b869094ce6997a6"},
+ {file = "pygame-2.5.2.tar.gz", hash = "sha256:c1b89eb5d539e7ac5cf75513125fb5f2f0a2d918b1fd6e981f23bf0ac1b1c24a"},
]
[[package]]
name = "pygments"
-version = "2.16.1"
+version = "2.17.2"
description = "Pygments is a syntax highlighting package written in Python."
-category = "main"
optional = true
python-versions = ">=3.7"
files = [
- {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"},
- {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
+ {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
+ {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
]
[package.extras]
plugins = ["importlib-metadata"]
+windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pylint"
-version = "2.17.5"
+version = "2.17.7"
description = "python code static checker"
-category = "dev"
optional = false
python-versions = ">=3.7.2"
files = [
- {file = "pylint-2.17.5-py3-none-any.whl", hash = "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413"},
- {file = "pylint-2.17.5.tar.gz", hash = "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"},
+ {file = "pylint-2.17.7-py3-none-any.whl", hash = "sha256:27a8d4c7ddc8c2f8c18aa0050148f89ffc09838142193fdbe98f172781a3ff87"},
+ {file = "pylint-2.17.7.tar.gz", hash = "sha256:f4fcac7ae74cfe36bc8451e931d8438e4a476c20314b1101c458ad0f05191fad"},
]
[package.dependencies]
-astroid = ">=2.15.6,<=2.17.0-dev0"
+astroid = ">=2.15.8,<=2.17.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
@@ -1157,25 +1127,26 @@ testutils = ["gitpython (>3)"]
[[package]]
name = "pymdown-extensions"
-version = "9.11"
+version = "10.5"
description = "Extension pack for Python Markdown."
-category = "main"
optional = true
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "pymdown_extensions-9.11-py3-none-any.whl", hash = "sha256:a499191d8d869f30339de86fcf072a787e86c42b6f16f280f5c2cf174182b7f3"},
- {file = "pymdown_extensions-9.11.tar.gz", hash = "sha256:f7e86c1d3981f23d9dc43294488ecb54abadd05b0be4bf8f0e15efc90f7853ff"},
+ {file = "pymdown_extensions-10.5-py3-none-any.whl", hash = "sha256:1f0ca8bb5beff091315f793ee17683bc1390731f6ac4c5eb01e27464b80fe879"},
+ {file = "pymdown_extensions-10.5.tar.gz", hash = "sha256:1b60f1e462adbec5a1ed79dac91f666c9c0d241fa294de1989f29d20096cfd0b"},
]
[package.dependencies]
-markdown = ">=3.2"
+markdown = ">=3.5"
pyyaml = "*"
+[package.extras]
+extra = ["pygments (>=2.12)"]
+
[[package]]
name = "pyqrcode"
version = "1.2.1"
description = "A QR code generator written purely in Python with SVG, EPS, PNG and terminal output."
-category = "dev"
optional = false
python-versions = "*"
files = [
@@ -1188,34 +1159,30 @@ png = ["pypng (>=0.0.13)"]
[[package]]
name = "pytest"
-version = "6.2.5"
+version = "7.4.3"
description = "pytest: simple powerful testing with Python"
-category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
files = [
- {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
- {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
+ {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
+ {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
]
[package.dependencies]
-atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
-attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
-py = ">=1.8.2"
-toml = "*"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-cov"
version = "3.0.0"
description = "Pytest plugin for measuring coverage."
-category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@@ -1232,14 +1199,13 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
[[package]]
name = "pytest-mock"
-version = "3.11.1"
+version = "3.12.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
-category = "dev"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"},
- {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"},
+ {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"},
+ {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"},
]
[package.dependencies]
@@ -1252,7 +1218,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
-category = "main"
optional = true
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
@@ -1267,7 +1232,6 @@ six = ">=1.5"
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
-category = "main"
optional = true
python-versions = ">=3.6"
files = [
@@ -1276,6 +1240,7 @@ files = [
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@@ -1283,8 +1248,15 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+ {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@@ -1301,6 +1273,7 @@ files = [
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@@ -1308,6 +1281,7 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@@ -1317,7 +1291,6 @@ files = [
name = "pyyaml-env-tag"
version = "0.1"
description = "A custom YAML tag for referencing environment variables in YAML files. "
-category = "main"
optional = true
python-versions = ">=3.6"
files = [
@@ -1332,7 +1305,6 @@ pyyaml = "*"
name = "pyzbar"
version = "0.1.9"
description = "Read one-dimensional barcodes and QR codes from Python 2 and 3."
-category = "main"
optional = true
python-versions = "*"
files = [
@@ -1344,12 +1316,108 @@ files = [
[package.extras]
scripts = ["Pillow (>=3.2.0)"]
+[[package]]
+name = "regex"
+version = "2023.10.3"
+description = "Alternative regular expression module, to replace re."
+optional = true
+python-versions = ">=3.7"
+files = [
+ {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"},
+ {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"},
+ {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"},
+ {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"},
+ {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"},
+ {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"},
+ {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"},
+ {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"},
+ {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"},
+ {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"},
+ {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"},
+ {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"},
+ {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"},
+ {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"},
+ {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"},
+ {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"},
+ {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"},
+ {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"},
+ {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"},
+ {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"},
+ {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"},
+ {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"},
+ {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"},
+ {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"},
+ {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"},
+ {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"},
+ {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"},
+ {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"},
+ {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"},
+ {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"},
+ {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"},
+ {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"},
+ {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"},
+ {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"},
+ {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"},
+ {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"},
+ {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"},
+ {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"},
+ {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"},
+ {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"},
+ {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"},
+ {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"},
+ {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"},
+ {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"},
+ {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"},
+ {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"},
+ {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"},
+ {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"},
+ {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"},
+ {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"},
+ {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"},
+ {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"},
+ {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"},
+ {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"},
+ {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"},
+ {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"},
+ {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"},
+ {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"},
+ {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"},
+ {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"},
+ {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"},
+ {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"},
+ {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"},
+ {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"},
+ {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"},
+ {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"},
+ {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"},
+ {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"},
+]
+
[[package]]
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
-category = "main"
-optional = true
+optional = false
python-versions = ">=3.7"
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
@@ -1367,25 +1435,25 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
-name = "rfc3986"
-version = "1.5.0"
-description = "Validating URI References per RFC 3986"
-category = "main"
-optional = false
-python-versions = "*"
+name = "setuptools"
+version = "69.0.2"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = true
+python-versions = ">=3.8"
files = [
- {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
- {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
+ {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"},
+ {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"},
]
[package.extras]
-idna2008 = ["idna"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
-category = "main"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@@ -1393,35 +1461,10 @@ files = [
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
-[[package]]
-name = "sniffio"
-version = "1.3.0"
-description = "Sniff out which async library your code is running under"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
- {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
-]
-
-[[package]]
-name = "toml"
-version = "0.10.2"
-description = "Python Library for Tom's Obvious, Minimal Language"
-category = "dev"
-optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
-files = [
- {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
- {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
-]
-
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1431,33 +1474,47 @@ files = [
[[package]]
name = "tomlkit"
-version = "0.12.1"
+version = "0.12.3"
description = "Style preserving TOML library"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"},
- {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"},
+ {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"},
+ {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"},
+]
+
+[[package]]
+name = "translate"
+version = "3.6.1"
+description = "This is a simple, yet powerful command line translator with google translate behind it. You can also use it as a Python module in your code."
+optional = false
+python-versions = "*"
+files = [
+ {file = "translate-3.6.1-py2.py3-none-any.whl", hash = "sha256:cebfb004989d9a2ab0d24c0c5805783c7f4e07243ea4ed2a8f1809d072bf712b"},
+ {file = "translate-3.6.1.tar.gz", hash = "sha256:7e70ffa46f193cc744be7c88b8e1323f10f6b2bb90d24bb5d29fdf1e56618783"},
]
+[package.dependencies]
+click = "*"
+libretranslatepy = "2.1.1"
+lxml = "*"
+requests = "*"
+
[[package]]
name = "typing-extensions"
-version = "4.7.1"
-description = "Backported and Experimental Type Hints for Python 3.7+"
-category = "dev"
+version = "4.8.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
- {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
+ {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
+ {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
]
[[package]]
name = "ur"
version = "0.1.0"
description = "UR Implementation in Python -- ported from the C++ Reference Implementation by Blockchain Commons"
-category = "main"
optional = false
python-versions = "*"
files = []
@@ -1469,19 +1526,17 @@ url = "vendor/foundation-ur-py"
[[package]]
name = "urllib3"
-version = "2.0.4"
+version = "2.1.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "main"
-optional = true
-python-versions = ">=3.7"
+optional = false
+python-versions = ">=3.8"
files = [
- {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"},
- {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"},
+ {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"},
+ {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"},
]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
-secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
@@ -1489,7 +1544,6 @@ zstd = ["zstandard (>=0.18.0)"]
name = "urtypes"
version = "0.1.0"
description = "Python implementation of the Blockchain Commons UR Types specification"
-category = "main"
optional = false
python-versions = "^3.9.1"
files = []
@@ -1503,7 +1557,6 @@ url = "vendor/urtypes"
name = "watchdog"
version = "3.0.0"
description = "Filesystem events monitoring"
-category = "main"
optional = true
python-versions = ">=3.7"
files = [
@@ -1541,103 +1594,96 @@ watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "wrapt"
-version = "1.15.0"
+version = "1.16.0"
description = "Module for decorators, wrappers and monkey patching."
-category = "dev"
optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+python-versions = ">=3.6"
files = [
- {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"},
- {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"},
- {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"},
- {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"},
- {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"},
- {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"},
- {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"},
- {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"},
- {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"},
- {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"},
- {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"},
- {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"},
- {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"},
- {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"},
- {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"},
- {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"},
- {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"},
- {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"},
- {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"},
- {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"},
- {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"},
- {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"},
- {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"},
- {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"},
- {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"},
- {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"},
- {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"},
- {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"},
- {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"},
- {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"},
- {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"},
- {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"},
- {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"},
- {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"},
- {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"},
- {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"},
- {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"},
- {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"},
- {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"},
- {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"},
- {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"},
- {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"},
- {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"},
- {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"},
- {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"},
- {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"},
- {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"},
- {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"},
- {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"},
- {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"},
- {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"},
- {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"},
- {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"},
- {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"},
- {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"},
- {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"},
- {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"},
- {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"},
- {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"},
- {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"},
- {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"},
- {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"},
- {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"},
- {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"},
- {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"},
- {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"},
- {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"},
- {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"},
- {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"},
- {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"},
- {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"},
- {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"},
- {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"},
- {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"},
- {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"},
+ {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"},
+ {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"},
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"},
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"},
+ {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"},
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"},
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"},
+ {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"},
+ {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"},
+ {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"},
+ {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"},
+ {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"},
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"},
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"},
+ {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"},
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"},
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"},
+ {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"},
+ {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"},
+ {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"},
+ {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"},
+ {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"},
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"},
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"},
+ {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"},
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"},
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"},
+ {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"},
+ {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"},
+ {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"},
+ {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"},
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"},
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"},
+ {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"},
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"},
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"},
+ {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"},
+ {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"},
+ {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"},
+ {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"},
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"},
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"},
+ {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"},
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"},
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"},
+ {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"},
+ {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"},
+ {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"},
+ {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"},
+ {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"},
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"},
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"},
+ {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"},
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"},
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"},
+ {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"},
+ {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"},
+ {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"},
+ {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"},
+ {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"},
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"},
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"},
+ {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"},
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"},
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"},
+ {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"},
+ {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"},
+ {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"},
+ {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"},
+ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"},
]
[[package]]
name = "zipp"
-version = "3.16.2"
+version = "3.17.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "main"
optional = true
python-versions = ">=3.8"
files = [
- {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"},
- {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"},
+ {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
+ {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
]
[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
[extras]
@@ -1647,4 +1693,4 @@ simulator = ["Pillow", "numpy", "opencv-python", "pygame", "pyzbar"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.1"
-content-hash = "61aec9aaffffc7193a66fbf8d873f82bd9c062accf5a1099e02fea82a84c1d39"
+content-hash = "74804059f1ad9da0ddd388e482b1f65a474558e44d4c07734724896cd4108351"
diff --git a/pyproject.toml b/pyproject.toml
index e0cf73730..c704a07a7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,39 +22,59 @@
[tool.poetry]
name = "krux"
-version = "23.09.1"
+version = "24.04.beta13"
description = "Open-source signing device firmware for Bitcoin"
authors = ["Jeff S "]
[tool.poetry.dependencies]
python = "^3.9.1"
-googletrans = "^4.0.0rc1"
+translate = "^3.6.1"
embit = { path = "./vendor/embit/", develop = true }
ur = { path = "./vendor/foundation-ur-py/", develop = true }
urtypes = { path = "./vendor/urtypes/", develop = true }
# Docs site dependencies. Optional extras
mkdocs = { version = "^1.5.2", optional = true }
-mkdocs-material = { version = "^8.5.11", optional = true }
+mkdocs-material = { version = "^9.4.14", optional = true }
+urllib3 = "^2.1.0" # Used by mkdocs-material, forces updated secure version
mkdocs-static-i18n = { version = "^0.46", optional = true }
-pymdown-extensions = { version = "^9.11", optional = true }
+pymdown-extensions = { version = "^10.0", optional = true }
# Simulator dependencies. Optional extras
numpy = { version = "^1.25.2", optional = true }
opencv-python = { version = "^4.8.0.74", optional = true }
-Pillow = { version = "^9.0.1", optional = true }
+Pillow = { version = "^10.0.1", optional = true }
pygame = { version = "^2.5.0", optional = true }
pyzbar = { version = "^0.1.9", optional = true }
[tool.poetry.group.dev.dependencies]
black = "^23.3.0"
pylint = "^2.12.2"
-pytest = "^6.2.5"
+pytest = "^7.4.3"
pytest-cov = "^3.0.0"
pytest-mock = "^3.6.1"
PyQRCode = "^1.2.1"
pycryptodome = "^3.17.0"
+poethepoet = "^0.24.4"
[tool.poetry.extras]
docs = ["mkdocs", "mkdocs-material", "mkdocs-static-i18n", "pymdown-extensions"]
simulator = ["numpy", "opencv-python", "Pillow", "pygame", "pyzbar"]
+
+[tool.poe.tasks]
+# tasks for format code with black
+# all tasks that start with `_` are'nt callable
+format-src = "black ./src"
+format-tests = "black ./tests"
+format = ["format-src", "format-tests"]
+
+# tasks for lint code with pylint
+# all tasks that start with `_` are'nt callable
+lint = "pylint ./src"
+
+# tasks for test code
+test = "poetry run pytest --cache-clear --cov src/krux --cov-report html ./tests"
+test-verbose = "poetry run pytest --cache-clear --cov src/krux --cov-report html --show-capture all --capture tee-sys -r A ./tests"
+
+# pre commit (do formatting, linting and tests)
+pre-commit = ["format", "lint", "test"]
diff --git a/sign_release.py b/sign_release.py
new file mode 100644
index 000000000..db40b3124
--- /dev/null
+++ b/sign_release.py
@@ -0,0 +1,142 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# This script batch signs firmware releases through air-gapped interaction with
+# a Krux signing device.
+# Load a 24 words mnemonic in testnet to sign the firmware hashes as messages.
+
+import os
+import cv2
+import hashlib
+from io import StringIO
+import base64
+from qrcode import QRCode
+import subprocess
+
+DEVICES = [
+ "maixpy_m5stickv",
+ "maixpy_amigo_ips",
+ "maixpy_amigo_tft",
+ "maixpy_bit",
+ "maixpy_dock",
+ "maixpy_yahboom",
+]
+
+
+def release_folder():
+ """Returns release folder specified on pyproject.toml"""
+ with open("pyproject.toml", "r") as project_file:
+ lines = project_file.readlines()
+ for line in lines:
+ if line.startswith("version"):
+ version = line.split('"')[-2]
+ release_folder_name = "krux-v" + version
+ print("Signing firmwares in:", release_folder_name)
+ return release_folder_name
+
+
+def print_qr_code(data):
+ """Prints ascii QR code"""
+ qr_code = QRCode()
+ qr_code.add_data(data)
+ qr_string = StringIO()
+ qr_code.print_ascii(out=qr_string, invert=True)
+ print(qr_string.getvalue())
+
+
+def scan():
+ """Opens a scan window and uses cv2 to detect and decode a QR code, returning its data"""
+ vid = cv2.VideoCapture(0)
+ detector = cv2.QRCodeDetector()
+ qr_data = None
+ while True:
+ # Capture the video frame by frame
+ _, frame = vid.read()
+ qr_data, _, _ = detector.detectAndDecode(frame)
+ if len(qr_data) > 0:
+ break
+ # Display the resulting frame
+ cv2.imshow("frame", frame)
+ # the 'q' button is set as the
+ # quitting button you may use any
+ # desired button of your choice
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+ # After the loop release the cap object
+ vid.release()
+ # Destroy all the windows
+ cv2.destroyAllWindows()
+ return qr_data
+
+
+def verify(file_to_verify, key_to_verify, signature_to_verify):
+ """Uses openssl to verify the signature and public key"""
+ print("Verifying signature of",file_to_verify.split("/")[1] )
+ verification = subprocess.run(
+ "openssl sha256 <%s -binary | openssl pkeyutl -verify -pubin -inkey %s -sigfile %s"
+ % (file_to_verify, key_to_verify, signature_to_verify),
+ shell=True,
+ capture_output=True,
+ text=True,
+ check=True
+ )
+ print(verification.stdout)
+
+
+# Main routine
+folder = release_folder()
+for device in DEVICES:
+ print("Signing", device)
+ file_name = os.path.join(folder, device, "firmware.bin")
+ hash_string = ""
+ try:
+ with open(file_name, "rb") as f:
+ sig_bytes = f.read() # read file as bytes
+ hash_string = hashlib.sha256(sig_bytes).hexdigest()
+ except:
+ print("Unable to read target file")
+ # Prints the hash of the file
+ print("Hash of", file_name, ":")
+ print(hash_string + "\n")
+
+ # Prints the QR code
+ print_qr_code(hash_string)
+
+ # Scans the signature QR code
+ _ = input("Press enter to scan signature")
+ signature = scan()
+ binary_signature = base64.b64decode(signature.encode())
+ # Prints signature
+ print("Signature:", signature)
+ # Saves a signature file
+ signature_file = os.path.join(folder, device, "firmware.bin.sig")
+
+ print("Saving a signature file:", signature_file, "\n\n")
+ with open(signature_file, "wb") as f:
+ f.write(binary_signature)
+
+# Verify signatures
+PUBLIC_KEY_FILE = "odudex.PEM" # "selfcustody.PEM"
+for device in DEVICES:
+ file_name = os.path.join(folder, device, "firmware.bin")
+ signature_file = os.path.join(folder, device, "firmware.bin.sig")
+ verify(file_name, PUBLIC_KEY_FILE, signature_file)
diff --git a/simulator/assets/maixpy_dock.png b/simulator/assets/maixpy_dock.png
new file mode 100644
index 000000000..985f47c43
Binary files /dev/null and b/simulator/assets/maixpy_dock.png differ
diff --git a/simulator/kruxsim/devices.py b/simulator/kruxsim/devices.py
index 6df384c20..cbbbdeac8 100644
--- a/simulator/kruxsim/devices.py
+++ b/simulator/kruxsim/devices.py
@@ -33,7 +33,7 @@
AMIGO_IPS: (480, 768),
AMIGO_TFT: (480, 768),
PC: (480, 640),
- DOCK: (440, 640),
+ DOCK: (440, 820),
}
@@ -48,8 +48,6 @@ def load_image(device):
device = with_prefix(device)
if device == PC:
return None
- if device == DOCK:
- return None
if device not in images:
images[device] = pg.image.load(
os.path.join("assets", "%s.png" % device)
diff --git a/simulator/kruxsim/mocks/Maix.py b/simulator/kruxsim/mocks/Maix.py
index 1ba92e2ea..6b446b111 100644
--- a/simulator/kruxsim/mocks/Maix.py
+++ b/simulator/kruxsim/mocks/Maix.py
@@ -29,13 +29,6 @@
PRESSED = 0
RELEASED = 1
-sequence_executor = None
-
-
-def register_sequence_executor(s):
- global sequence_executor
- sequence_executor = s
-
class GPIO:
IN = 0
@@ -83,6 +76,7 @@ class GPIO:
GPIO5 = 37
GPIO6 = 38
GPIO7 = 39
+ IRQ_FALLING = 0
def __init__(self, gpio_num, dir=None, val=None):
self.key = None
@@ -96,27 +90,12 @@ def __init__(self, gpio_num, dir=None, val=None):
def value(self, val=1):
if not self.key:
- return 1
- if (
- sequence_executor
- and sequence_executor.key is not None
- and sequence_executor.key == self.key
- ):
- sequence_executor.key_checks += 1
- # wait for release
- if sequence_executor.key_checks == 1:
- return RELEASED
- # wait for press
- # if pressed
- elif sequence_executor.key_checks == 2 or sequence_executor.key_checks == 3:
- return PRESSED
- # released
- elif sequence_executor.key_checks == 4:
- sequence_executor.key = None
- sequence_executor.key_checks = 0
- return RELEASED
+ return RELEASED
return PRESSED if pg.key.get_pressed()[self.key] else RELEASED
+ def irq(self, pin, mode):
+ pass
+
if "Maix" not in sys.modules:
sys.modules["Maix"] = mock.MagicMock(
diff --git a/simulator/kruxsim/mocks/board.py b/simulator/kruxsim/mocks/board.py
index 0666ca3e2..613d10203 100644
--- a/simulator/kruxsim/mocks/board.py
+++ b/simulator/kruxsim/mocks/board.py
@@ -58,8 +58,10 @@ def register_device(device):
BUTTON_A = BOARD_CONFIG["krux"]["pins"]["BUTTON_A"]
if "ENCODER" in BOARD_CONFIG["krux"]["pins"]:
+ # replace encoder with regular buttons in simulator
del BOARD_CONFIG["krux"]["pins"]["ENCODER"]
BOARD_CONFIG["krux"]["pins"]["BUTTON_B"] = 37
+ BOARD_CONFIG["krux"]["pins"]["BUTTON_C"] = 38
BUTTON_B = BOARD_CONFIG["krux"]["pins"]["BUTTON_B"]
if "BUTTON_C" in BOARD_CONFIG["krux"]["pins"]:
BUTTON_C = BOARD_CONFIG["krux"]["pins"]["BUTTON_C"]
diff --git a/simulator/kruxsim/mocks/buttons.py b/simulator/kruxsim/mocks/buttons.py
new file mode 100644
index 000000000..dc6d54a45
--- /dev/null
+++ b/simulator/kruxsim/mocks/buttons.py
@@ -0,0 +1,175 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+import sys
+from unittest import mock
+from Maix import GPIO
+from fpioa_manager import fm
+import pygame as pg
+
+PRESSED = 0
+RELEASED = 1
+
+
+class TactileButtons:
+ """Interface with Buttons"""
+
+ def __init__(self):
+ self.enter = None
+ self.page = None
+ self.page_prev = None
+ self.enter_event_flag = False
+ self.page_event_flag = False
+ self.page_prev_event_flag = False
+
+ def init_enter(self, pin):
+ """Register ENTER button IO"""
+ fm.register(pin, fm.fpioa.GPIOHS21)
+ self.enter = GPIO(GPIO.GPIOHS21, GPIO.IN, GPIO.PULL_UP)
+
+ def init_page(self, pin):
+ """Register PAGE button IO"""
+ fm.register(pin, fm.fpioa.GPIOHS22)
+ self.page = GPIO(GPIO.GPIOHS22, GPIO.IN, GPIO.PULL_UP)
+
+ def init_page_prev(self, pin):
+ """Register PAGE_PREV button IO"""
+ fm.register(pin, fm.fpioa.GPIOHS0)
+ self.page_prev = GPIO(GPIO.GPIOHS0, GPIO.IN, GPIO.PULL_UP)
+
+
+buttons_control = TactileButtons() # Singleton
+
+sequence_executor = None
+
+def register_sequence_executor(s):
+ global sequence_executor
+ sequence_executor = s
+
+class Button:
+ """Generic button handler format"""
+
+ def __init__(self) -> None:
+ pass
+
+ def value(self):
+ """Returns IO state"""
+ return RELEASED
+
+ def event(self):
+ """Returns event state"""
+ return False
+
+
+class ButtonEnter:
+ """Class to manage button ENTER state and events"""
+
+ def __init__(self, pin) -> None:
+ buttons_control.init_enter(pin)
+
+ def value(self):
+ """Returns the ENTER IO state"""
+ if buttons_control.enter is not None:
+ return buttons_control.enter.value()
+ return RELEASED
+
+ def event(self):
+ """Returns the ENTER event state"""
+ if buttons_control.enter is not None:
+
+ if (
+ sequence_executor
+ and sequence_executor.key is not None
+ and sequence_executor.key == pg.K_RETURN
+ ):
+ sequence_executor.key = None
+ return True
+ if buttons_control.enter_event_flag:
+ buttons_control.enter_event_flag = False
+ return True
+ return False
+
+
+class ButtonPage:
+ """Class to manage button PAGE state and events"""
+
+ def __init__(self, pin) -> None:
+ buttons_control.init_page(pin)
+
+ def value(self):
+ """Returns the PAGE IO state"""
+ if buttons_control.page is not None:
+ return buttons_control.page.value()
+ return RELEASED
+
+ def event(self):
+ """Returns the PAGE event state"""
+ if buttons_control.page is not None:
+ if (
+ sequence_executor
+ and sequence_executor.key is not None
+ and sequence_executor.key == pg.K_DOWN
+ ):
+ sequence_executor.key = None
+ return True
+ if buttons_control.page_event_flag:
+ buttons_control.page_event_flag = False
+ return True
+ return False
+
+
+class ButtonPagePrev:
+ """Class to manage button PAGE_PREV state and events"""
+
+ def __init__(self, pin) -> None:
+ buttons_control.init_page_prev(pin)
+
+ def value(self):
+ """Returns the PAGE_PREV IO state"""
+ if buttons_control.page_prev is not None:
+ return buttons_control.page_prev.value()
+ return RELEASED
+
+ def event(self):
+ """Returns the PAGE_PREV event state"""
+ if buttons_control.page_prev is not None:
+ if (
+ sequence_executor
+ and sequence_executor.key is not None
+ and sequence_executor.key == pg.K_UP
+ ):
+ sequence_executor.key = None
+ return True
+ if buttons_control.page_prev_event_flag:
+ buttons_control.page_prev_event_flag = False
+ return True
+ return False
+
+
+if "krux.buttons" not in sys.modules:
+ sys.modules["krux.buttons"] = mock.MagicMock(
+ TactileButtons=TactileButtons,
+ ButtonEnter=ButtonEnter,
+ ButtonPage=ButtonPage,
+ ButtonPagePrev=ButtonPagePrev,
+ PRESSED=PRESSED,
+ RELEASED=RELEASED,
+ )
diff --git a/simulator/kruxsim/mocks/ft6x36.py b/simulator/kruxsim/mocks/ft6x36.py
index 21dc08d9f..3fe6b198b 100644
--- a/simulator/kruxsim/mocks/ft6x36.py
+++ b/simulator/kruxsim/mocks/ft6x36.py
@@ -19,9 +19,7 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-import imp
import sys
-import time
from unittest import mock
import pygame as pg
from . import lcd
@@ -34,45 +32,49 @@ def register_sequence_executor(s):
sequence_executor = s
-def to_screen_pos(pos):
- if lcd.screen:
- rect = lcd.screen.get_rect()
- rect.center = pg.display.get_surface().get_rect().center
- if rect.collidepoint(pos):
- out = pos[0] - rect.left, pos[1] - rect.top
- return out
- return None
-
-
class FT6X36:
def __init__(self):
+ self.event_flag = False
+
+ def to_screen_pos(self, pos):
+ if lcd.screen:
+ rect = lcd.screen.get_rect()
+ rect.center = pg.display.get_surface().get_rect().center
+ if rect.collidepoint(pos):
+ out = pos[0] - rect.left, pos[1] - rect.top
+ return out
+ return None
+
+ def activate_irq(self, irq_pin):
pass
def current_point(self):
+ return (
+ self.to_screen_pos(pg.mouse.get_pos())
+ if pg.mouse.get_pressed()[0]
+ else None
+ )
+
+ def trigger_event(self):
+ self.event_flag = True
+ self.irq_point = self.current_point()
+
+ def event(self):
if sequence_executor and sequence_executor.touch_pos is not None:
- sequence_executor.touch_checks += 1
- # wait for release
- if sequence_executor.touch_checks == 1:
- return None
- # wait for press
- # if pressed
- elif (
- sequence_executor.touch_checks == 2
- or sequence_executor.touch_checks == 3
- ):
- return sequence_executor.touch_pos
- # released
- elif sequence_executor.touch_checks == 4:
- sequence_executor.touch_pos = None
- sequence_executor.touch_checks = 0
- return None
- return to_screen_pos(pg.mouse.get_pos()) if pg.mouse.get_pressed()[0] else None
+ sequence_executor.touch_pos = None
+ return True
+ flag = self.event_flag
+ self.event_flag = False # Always clean event flag
+ return flag
def threshold(self, value):
pass
+touch_control = FT6X36()
+
+
if "krux.touchscreens.ft6x36" not in sys.modules:
sys.modules["krux.touchscreens.ft6x36"] = mock.MagicMock(
- FT6X36=FT6X36,
+ touch_control=touch_control,
)
diff --git a/simulator/kruxsim/mocks/lcd.py b/simulator/kruxsim/mocks/lcd.py
index 555ff5436..5e0772f27 100644
--- a/simulator/kruxsim/mocks/lcd.py
+++ b/simulator/kruxsim/mocks/lcd.py
@@ -21,6 +21,7 @@
# THE SOFTWARE.
import sys
import os
+import math
from unittest import mock
import pygame as pg
import cv2
@@ -174,6 +175,50 @@ def run():
light_color = rgb565torgb888(light_color)
pg.event.post(pg.event.Event(events.LCD_DRAW_QR_CODE_EVENT, {"f": run}))
+def draw_qr_code_binary(offset_y, code_bin, max_width, dark_color, light_color, background):
+ def run():
+ starting_size = int(math.sqrt(len(code_bin) * 8))
+ block_size_divisor = starting_size + 2; # adds 2 to create room for a 1 block border
+ scale = max_width // block_size_divisor
+ width = starting_size * scale
+ border_size = (max_width - width) // 2
+ opposite_border_offset = border_size + width - 1
+ # Top border
+ for rx in range(max_width):
+ for ry in range(border_size):
+ screen.set_at((rx, ry),light_color)
+
+ # Bottom border
+ for rx in range(max_width):
+ for ry in range(opposite_border_offset, max_width):
+ screen.set_at((rx, ry),light_color)
+
+ # Left border
+ for rx in range(border_size):
+ for ry in range(border_size, opposite_border_offset):
+ screen.set_at((rx, ry),light_color)
+
+ # Right border
+ for rx in range(opposite_border_offset, max_width):
+ for ry in range(border_size, opposite_border_offset):
+ screen.set_at((rx, ry),light_color)
+ # QR code rendering
+ for og_y in range(starting_size):
+ for og_x in range(starting_size):
+ og_yx_index = og_y * starting_size + og_x
+ color_byte = code_bin[og_yx_index >> 3]
+ color_byte &= (1 << (og_yx_index % 8))
+ color = dark_color if color_byte else light_color
+ for i in range(scale):
+ y = border_size + og_y * scale + i
+ for j in range(scale):
+ x = border_size + og_x * scale + j
+ screen.set_at((x, y),color)
+
+ dark_color = rgb565torgb888(dark_color)
+ light_color = rgb565torgb888(light_color)
+ pg.event.post(pg.event.Event(events.LCD_DRAW_QR_CODE_EVENT, {"f": run}))
+
def fill_rectangle(x, y, w, h, color):
def run():
@@ -205,6 +250,7 @@ def run():
height=height,
draw_string=draw_string,
draw_qr_code=draw_qr_code,
+ draw_qr_code_binary=draw_qr_code_binary,
fill_rectangle=fill_rectangle,
BLACK=COLOR_BLACK,
WHITE=COLOR_WHITE,
diff --git a/simulator/kruxsim/mocks/machine.py b/simulator/kruxsim/mocks/machine.py
index f5197128f..21f1a2683 100644
--- a/simulator/kruxsim/mocks/machine.py
+++ b/simulator/kruxsim/mocks/machine.py
@@ -30,7 +30,7 @@
def simulate_printer():
global simulating_printer
simulating_printer = True
- Settings().printer.driver = "thermal/adafruit"
+ Settings().hardware.printer.driver = "thermal/adafruit"
def reset():
@@ -49,14 +49,14 @@ def __init__(self, pin, baudrate):
def read(self, num_bytes):
if simulating_printer:
- module, cls = PrinterSettings.PRINTERS[Settings().printer.driver]
+ module, cls = PrinterSettings.PRINTERS[Settings().hardware.printer.driver]
if module == "thermal" and cls == "AdafruitPrinter":
return chr(0b00000000)
return None
def readline(self):
if simulating_printer:
- module, cls = PrinterSettings.PRINTERS[Settings().printer.driver]
+ module, cls = PrinterSettings.PRINTERS[Settings().hardware.printer.driver]
if module == "cnc" and cls == "FilePrinter":
return "ok\n".encode()
return None
diff --git a/simulator/kruxsim/mocks/pmu.py b/simulator/kruxsim/mocks/pmu.py
index 183245ebc..9493b465b 100644
--- a/simulator/kruxsim/mocks/pmu.py
+++ b/simulator/kruxsim/mocks/pmu.py
@@ -38,46 +38,51 @@ def register_sequence_executor(s):
class PMU_Button:
def __init__(self):
self.key = pg.K_UP
+ self.state = RELEASED
def value(self):
- if (
- sequence_executor
- and sequence_executor.key is not None
- and sequence_executor.key == self.key
- ):
- sequence_executor.key_checks += 1
- # wait for release
- if sequence_executor.key_checks == 1:
- return RELEASED
- # wait for press
- # if pressed
- elif sequence_executor.key_checks == 2 or sequence_executor.key_checks == 3:
- return PRESSED
- # released
- elif sequence_executor.key_checks == 4:
+ return PRESSED if pg.key.get_pressed()[self.key] else RELEASED
+
+ def event(self):
+ if self.state == RELEASED:
+ if (
+ sequence_executor
+ and sequence_executor.key is not None
+ and sequence_executor.key == pg.K_UP
+ ):
sequence_executor.key = None
- sequence_executor.key_checks = 0
- return RELEASED
- return 0 if pg.key.get_pressed()[self.key] else 1
+ return True
+ if self.value() == PRESSED:
+ self.state = PRESSED
+ return True
+ self.state = self.value()
+ return False
-class Battery:
- def getVbatVoltage(self):
+class PMUController:
+ def __init__(self, i2c_bus):
+ pass
+
+ def get_battery_voltage(self):
return 3400
- def getUSBVoltage(self):
+ def get_usb_voltage(self):
return 0
- def enablePMICSleepMode(self, val):
+ def enable_pek_button_monitor(self, val):
pass
- def setEnterSleepMode(self):
+ def enter_sleep_mode(self):
pass
+ def enable_adcs(self, on_off):
+ pass
+
+ def set_screen_brightness(self, level):
+ pass
if "pmu" not in sys.modules:
sys.modules["pmu"] = mock.MagicMock(
PMU_Button=PMU_Button,
- axp192=Battery,
- axp173=Battery,
+ PMUController=PMUController,
)
diff --git a/simulator/kruxsim/mocks/qrcode.py b/simulator/kruxsim/mocks/qrcode.py
index b727de39e..61adb39e9 100644
--- a/simulator/kruxsim/mocks/qrcode.py
+++ b/simulator/kruxsim/mocks/qrcode.py
@@ -23,38 +23,33 @@
from unittest import mock
import pyqrcode
+def encode(data):
+ # Uses string encoded qr as it already cleaned up the frames
+ # PyQRcode also doesn't offer any binary output
-def encode_to_string(data):
try:
- code_str = pyqrcode.create(data, error="L", mode="binary").text()
+ code_str = pyqrcode.create(data, error="L", mode="binary").text(quiet_zone=0)
except:
# pre-decode if binary (SeedQR)
data = data.decode("latin-1")
- code_str = pyqrcode.create(data, error="L", mode="binary").text()
+ code_str = pyqrcode.create(data, error="L", mode="binary").text(quiet_zone=0)
size = 0
while code_str[size] != "\n":
size += 1
- i = 0
- padding = 0
- while code_str[i] != "1":
- if code_str[i] == "\n":
- padding += 1
- i += 1
- code_str = code_str[(padding) * (size + 1) : -(padding) * (size + 1)]
- size -= 2 * padding
-
- new_code_str = ""
- for i in range(size):
- for j in range(size + 2 * padding + 1):
- if padding <= j < size + padding:
- index = i * (size + 2 * padding + 1) + j
- new_code_str += code_str[index]
- new_code_str += "\n"
-
- return new_code_str
+ binary_qr = bytearray(b"\x00" * ((size * size + 7) // 8))
+ for y in range(size):
+ for x in range(size):
+ bit_index = y * size + x
+ bit_string_index = y * (size + 1) + x
+ if code_str[bit_string_index] == "1":
+ binary_qr[bit_index>>3] |= 1 << (bit_index % 8)
+ return binary_qr
+
+
+
if "qrcode" not in sys.modules:
sys.modules["qrcode"] = mock.MagicMock(
- encode_to_string=encode_to_string,
+ encode = encode,
)
diff --git a/simulator/kruxsim/mocks/rotary.py b/simulator/kruxsim/mocks/rotary.py
index 00ff8e383..c04127bb8 100644
--- a/simulator/kruxsim/mocks/rotary.py
+++ b/simulator/kruxsim/mocks/rotary.py
@@ -22,6 +22,7 @@
import sys
from unittest import mock
+
class RotaryEncoder:
def __init__(self):
self.debounce = 0
diff --git a/simulator/kruxsim/sequence.py b/simulator/kruxsim/sequence.py
index f6187bb81..f4f541350 100644
--- a/simulator/kruxsim/sequence.py
+++ b/simulator/kruxsim/sequence.py
@@ -28,6 +28,7 @@
from kruxsim.mocks.board import BOARD_CONFIG
COMMANDS = ["press", "touch", "qrcode", "screenshot", "wait", "include", "x"]
+THREAD_PERIOD = 0.1
class SequenceExecutor:
@@ -50,9 +51,9 @@ def __init__(self, sequence_filepath):
def execute(self):
if self.command_fn:
- if time.time() - self.command_timer > 0.1:
+ if time.time() > self.command_timer + THREAD_PERIOD:
print("Executing (%s, %r)" % (self.command, self.command_params))
- self.command_timer = 0
+ self.command_timer = time.time()
self.command_fn()
self.command_fn = None
self.command_params = []
diff --git a/simulator/sequences/amigo_tft/_load-12-word-mnemonic.txt b/simulator/sequences/amigo_tft/_load-12-word-mnemonic.txt
index 2123b108f..67fe6743d 100644
--- a/simulator/sequences/amigo_tft/_load-12-word-mnemonic.txt
+++ b/simulator/sequences/amigo_tft/_load-12-word-mnemonic.txt
@@ -3,9 +3,6 @@ include _wait-for-logo.txt
# Navigate to via QR
x3 press BUTTON_A
-# Prompt
-press BUTTON_A
-
qrcode 12-word-mnemonic.png
wait 0.5
diff --git a/simulator/sequences/amigo_tft/_load-24-word-mnemonic.txt b/simulator/sequences/amigo_tft/_load-24-word-mnemonic.txt
index a1311d4ac..dc5fbcdfc 100644
--- a/simulator/sequences/amigo_tft/_load-24-word-mnemonic.txt
+++ b/simulator/sequences/amigo_tft/_load-24-word-mnemonic.txt
@@ -3,9 +3,6 @@ include _wait-for-logo.txt
# Navigate to via QR
x3 press BUTTON_A
-# Prompt
-press BUTTON_A
-
qrcode 24-word-mnemonic.png
wait 0.5
diff --git a/simulator/sequences/m5stickv/_load-12-word-mnemonic.txt b/simulator/sequences/m5stickv/_load-12-word-mnemonic.txt
index 2123b108f..67fe6743d 100644
--- a/simulator/sequences/m5stickv/_load-12-word-mnemonic.txt
+++ b/simulator/sequences/m5stickv/_load-12-word-mnemonic.txt
@@ -3,9 +3,6 @@ include _wait-for-logo.txt
# Navigate to via QR
x3 press BUTTON_A
-# Prompt
-press BUTTON_A
-
qrcode 12-word-mnemonic.png
wait 0.5
diff --git a/simulator/sequences/m5stickv/_load-24-word-mnemonic.txt b/simulator/sequences/m5stickv/_load-24-word-mnemonic.txt
index 832ec34e4..c798eca0a 100644
--- a/simulator/sequences/m5stickv/_load-24-word-mnemonic.txt
+++ b/simulator/sequences/m5stickv/_load-24-word-mnemonic.txt
@@ -3,9 +3,6 @@ include _wait-for-logo.txt
# Navigate to via QR
x3 press BUTTON_A
-# Prompt
-press BUTTON_A
-
qrcode 24-word-mnemonic.png
wait 0.5
diff --git a/simulator/simulator.py b/simulator/simulator.py
index 547ea30f3..db4a7ac95 100644
--- a/simulator/simulator.py
+++ b/simulator/simulator.py
@@ -99,24 +99,23 @@
from kruxsim.mocks import secp256k1
from kruxsim.mocks import qrcode
from kruxsim.mocks import sensor
-
from kruxsim.mocks import ft6x36
-from kruxsim.sequence import SequenceExecutor
-
+from kruxsim.mocks import buttons
from kruxsim.mocks import rotary
+from kruxsim.sequence import SequenceExecutor
sequence_executor = None
if args.sequence:
sequence_executor = SequenceExecutor(args.sequence)
-Maix.register_sequence_executor(sequence_executor)
+buttons.register_sequence_executor(sequence_executor)
pmu.register_sequence_executor(sequence_executor)
sensor.register_sequence_executor(sequence_executor)
ft6x36.register_sequence_executor(sequence_executor)
def run_krux():
- with open("../src/boot.py") as boot_file:
+ with open("../src/boot.py", "r", encoding='utf-8') as boot_file:
exec(boot_file.read())
@@ -202,6 +201,16 @@ def shutdown():
)
else:
event.dict["f"]()
+ if event.type == pg.KEYDOWN:
+ if event.key == pg.K_RETURN:
+ buttons.buttons_control.enter_event_flag = True
+ if event.key == pg.K_DOWN:
+ buttons.buttons_control.page_event_flag = True
+ if event.key == pg.K_UP:
+ buttons.buttons_control.page_prev_event_flag = True
+ if event.type == pg.MOUSEBUTTONDOWN:
+ ft6x36.touch_control.trigger_event()
+
if lcd.screen:
lcd_rect = lcd.screen.get_rect()
diff --git a/src/boot.py b/src/boot.py
index 0d8071c4c..48ac45032 100644
--- a/src/boot.py
+++ b/src/boot.py
@@ -31,38 +31,33 @@
from krux.power import power_manager
MIN_SPLASH_WAIT_TIME = 1000
+SPLASH = """
+ββ
+ββ
+ββ
+ββββββ
+ββ
+ ββ ββ
+ββ ββ
+ββββ
+ββ ββ
+ ββ ββ
+ ββ ββ
+"""[
+ 1:-1
+].split(
+ "\n"
+)
-def splash():
+def splash(logo):
"""Display splash while loading modules"""
from krux.display import Display
- SPLASH = """
-
-
-
- ββ
- ββ
- ββ
- ββββββ
- ββ
- ββ ββ
- ββ ββ
- ββββ
- ββ ββ
- ββ ββ
- ββ ββ
-
-
-
-"""[
- 1:-1
- ]
-
disp = Display()
disp.initialize_lcd()
disp.clear()
- disp.draw_centered_text(SPLASH.split("\n"))
+ disp.draw_centered_text(logo)
def check_for_updates():
@@ -112,13 +107,13 @@ def home(ctx_home):
preimport_ticks = time.ticks_ms()
-splash()
+splash(SPLASH)
check_for_updates()
gc.collect()
from krux.context import Context
-ctx = Context()
+ctx = Context(SPLASH)
ctx.power_manager = power_manager
postimport_ticks = time.ticks_ms()
diff --git a/src/krux/buttons.py b/src/krux/buttons.py
new file mode 100644
index 000000000..97484ff2f
--- /dev/null
+++ b/src/krux/buttons.py
@@ -0,0 +1,153 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from Maix import GPIO
+from fpioa_manager import fm
+
+PRESSED = 0
+RELEASED = 1
+
+
+def __handler__(pin_num=None):
+ # pylint: disable=unused-argument
+ """GPIO interrupt handler"""
+ buttons_control.event_handler(pin_num)
+
+
+class TactileButtons:
+ """Interface with Buttons"""
+
+ def __init__(self):
+ self.enter = None
+ self.page = None
+ self.page_prev = None
+ self.enter_event_flag = False
+ self.page_event_flag = False
+ self.page_prev_event_flag = False
+
+ def event_handler(self, pin_num):
+ """Set up a button event flag according to interruption source"""
+ if pin_num == buttons_control.enter:
+ self.enter_event_flag = True
+ elif pin_num == buttons_control.page:
+ self.page_event_flag = True
+ elif pin_num == buttons_control.page_prev:
+ self.page_prev_event_flag = True
+
+ def init_enter(self, pin):
+ """Register ENTER button IO"""
+ fm.register(pin, fm.fpioa.GPIOHS21)
+ self.enter = GPIO(GPIO.GPIOHS21, GPIO.IN, GPIO.PULL_UP)
+ self.enter.irq(__handler__, GPIO.IRQ_FALLING)
+
+ def init_page(self, pin):
+ """Register PAGE button IO"""
+ fm.register(pin, fm.fpioa.GPIOHS22)
+ self.page = GPIO(GPIO.GPIOHS22, GPIO.IN, GPIO.PULL_UP)
+ self.page.irq(__handler__, GPIO.IRQ_FALLING)
+
+ def init_page_prev(self, pin):
+ """Register PAGE_PREV button IO"""
+ fm.register(pin, fm.fpioa.GPIOHS0)
+ self.page_prev = GPIO(GPIO.GPIOHS0, GPIO.IN, GPIO.PULL_UP)
+ self.page_prev.irq(__handler__, GPIO.IRQ_FALLING)
+
+
+buttons_control = TactileButtons() # Singleton
+
+
+class Button:
+ """Generic button handler format"""
+
+ def __init__(self) -> None:
+ pass
+
+ def value(self):
+ """Returns IO state"""
+ return RELEASED
+
+ def event(self):
+ """Returns event state"""
+ return False
+
+
+class ButtonEnter:
+ """Class to manage button ENTER state and events"""
+
+ def __init__(self, pin) -> None:
+ buttons_control.init_enter(pin)
+
+ def value(self):
+ """Returns the ENTER IO state"""
+ if buttons_control.enter is not None:
+ return buttons_control.enter.value()
+ return RELEASED
+
+ def event(self):
+ """Returns the ENTER event state"""
+ if buttons_control.enter is not None:
+ if buttons_control.enter_event_flag:
+ buttons_control.enter_event_flag = False
+ return True
+ return False
+
+
+class ButtonPage:
+ """Class to manage button PAGE state and events"""
+
+ def __init__(self, pin) -> None:
+ buttons_control.init_page(pin)
+
+ def value(self):
+ """Returns the PAGE IO state"""
+ if buttons_control.page is not None:
+ return buttons_control.page.value()
+ return RELEASED
+
+ def event(self):
+ """Returns the PAGE event state"""
+ if buttons_control.page is not None:
+ if buttons_control.page_event_flag:
+ buttons_control.page_event_flag = False
+ return True
+ return False
+
+
+class ButtonPagePrev:
+ """Class to manage button PAGE_PREV state and events"""
+
+ def __init__(self, pin) -> None:
+ buttons_control.init_page_prev(pin)
+
+ def value(self):
+ """Returns the PAGE_PREV IO state"""
+ if buttons_control.page_prev is not None:
+ return buttons_control.page_prev.value()
+ return RELEASED
+
+ def event(self):
+ """Returns the PAGE_PREV event state"""
+ if buttons_control.page_prev is not None:
+ if buttons_control.page_prev_event_flag:
+ buttons_control.page_prev_event_flag = False
+ return True
+ return False
diff --git a/src/krux/camera.py b/src/krux/camera.py
index 722efdd9e..910f69427 100644
--- a/src/krux/camera.py
+++ b/src/krux/camera.py
@@ -36,29 +36,37 @@ class Camera:
"""Camera is a singleton interface for interacting with the device's camera"""
def __init__(self):
+ self.initialized = False
self.cam_id = None
self.antiglare_enabled = False
self.initialize_sensor()
def initialize_sensor(self, grayscale=False):
"""Initializes the camera"""
- sensor.reset(freq=16000000)
+ self.initialized = False
+ self.antiglare_enabled = False
self.cam_id = sensor.get_id()
+ if self.cam_id == OV7740_ID:
+ sensor.reset(freq=18200000)
+ else:
+ sensor.reset()
if grayscale:
sensor.set_pixformat(sensor.GRAYSCALE)
else:
sensor.set_pixformat(sensor.RGB565)
if self.cam_id == OV5642_ID:
- # CIF mode will use central pixels and discard darker periphery
- sensor.set_framesize(sensor.CIF)
sensor.set_hmirror(1)
if self.cam_id == OV2640_ID:
- sensor.set_framesize(sensor.CIF)
sensor.set_vflip(1)
+ if board.config["type"] == "bit":
+ # CIF mode will use central pixels and discard darker periphery
+ sensor.set_framesize(sensor.CIF)
else:
sensor.set_framesize(sensor.QVGA)
if self.cam_id == OV7740_ID:
self.config_ov_7740()
+ if self.cam_id == OV2640_ID:
+ self.config_ov_2640()
sensor.skip_frames()
def config_ov_7740(self):
@@ -80,48 +88,90 @@ def config_ov_7740(self):
# Regions 13,14,15,16
sensor.__write_reg(0x59, 0x0) # pylint: disable=W0212
+ def config_ov_2640(self):
+ """Specialized config for OV2640 sensor"""
+ # Set register bank 0
+ sensor.__write_reg(0xFF, 0x00) # pylint: disable=W0212
+ # Enable AEC
+ sensor.__write_reg(0xC2, 0x8C) # pylint: disable=W0212
+ # Set register bank 1
+ sensor.__write_reg(0xFF, 0x01) # pylint: disable=W0212
+ sensor.__write_reg(0x03, 0xCF) # pylint: disable=W0212
+ # Allowed luminance thresholds:
+ # luminance high threshold, default=0x78
+ sensor.__write_reg(0x24, 0x70) # pylint: disable=W0212
+ # luminance low threshold, default=0x68
+ sensor.__write_reg(0x25, 0x60) # pylint: disable=W0212
+
+ # Average-based sensing window definition
+ # Ingnore periphery and measure luminance only on central area
+ # Regions 1,2,3,4
+ sensor.__write_reg(0x5D, 0xFF) # pylint: disable=W0212
+ # Regions 5,6,7,8
+ sensor.__write_reg(0x5E, 0b11000011) # pylint: disable=W0212
+ # Regions 9,10,11,12
+ sensor.__write_reg(0x5F, 0b11000011) # pylint: disable=W0212
+ # Regions 13,14,15,16
+ sensor.__write_reg(0x60, 0xFF) # pylint: disable=W0212
+
def has_antiglare(self):
"""Returns whether the camera has anti-glare functionality"""
- return self.cam_id == OV7740_ID
+ return self.cam_id in (OV7740_ID, OV2640_ID)
def enable_antiglare(self):
"""Enables anti-glare mode"""
- if self.cam_id == OV7740_ID:
+ if self.cam_id == OV2640_ID:
+ # Set register bank 1
+ sensor.__write_reg(0xFF, 0x01) # pylint: disable=W0212
+ # luminance high level, default=0x78
+ sensor.__write_reg(0x24, 0x28) # pylint: disable=W0212
+ else:
# luminance high level, default=0x78
sensor.__write_reg(0x24, 0x38) # pylint: disable=W0212
- # luminance low level, default=0x68
- sensor.__write_reg(0x25, 0x20) # pylint: disable=W0212
+ # luminance low level, default=0x68
+ sensor.__write_reg(0x25, 0x20) # pylint: disable=W0212
+ if self.cam_id == OV7740_ID:
# Disable frame integrtation (night mode)
sensor.__write_reg(0x15, 0x00) # pylint: disable=W0212
- sensor.skip_frames()
- self.antiglare_enabled = True
+ sensor.skip_frames()
+ self.antiglare_enabled = True
def disable_antiglare(self):
"""Disables anti-glare mode"""
- if self.cam_id == OV7740_ID:
- # luminance high level, default=0x78
- sensor.__write_reg(0x24, 0x70) # pylint: disable=W0212
- # luminance low level, default=0x68
- sensor.__write_reg(0x25, 0x60) # pylint: disable=W0212
- sensor.skip_frames()
- self.antiglare_enabled = False
+ if self.cam_id == OV2640_ID:
+ # Set register bank 1
+ sensor.__write_reg(0xFF, 0x01) # pylint: disable=W0212
+ # luminance high level, default=0x78
+ sensor.__write_reg(0x24, 0x70) # pylint: disable=W0212
+ # luminance low level, default=0x68
+ sensor.__write_reg(0x25, 0x60) # pylint: disable=W0212
+ sensor.skip_frames()
+ self.antiglare_enabled = False
def snapshot(self):
"""Helper to take a customized snapshot from sensor"""
img = sensor.snapshot()
- if self.cam_id in (OV2640_ID, OV5642_ID):
- img.lens_corr(strength=1.1, zoom=0.96)
- if self.cam_id == OV2640_ID:
+ if board.config["type"] == "bit":
+ img.lens_corr(strength=1.1)
img.rotation_corr(z_rotation=180)
return img
+ def initialize_run(self):
+ """Initializes and runs sensor"""
+ self.initialize_sensor()
+ sensor.run(1)
+
+ def stop_sensor(self):
+ """Stops capturing from sensor"""
+ gc.collect()
+ sensor.run(0)
+
def capture_qr_code_loop(self, callback):
"""Captures either singular or animated QRs and parses their contents until
all parts of the message have been captured. The part data are then ordered
and assembled into one message and returned.
"""
- self.initialize_sensor()
- sensor.run(1)
+ self.initialize_run()
parser = QRPartParser()
@@ -130,6 +180,10 @@ def capture_qr_code_loop(self, callback):
while True:
wdt.feed()
command = callback(parser.total_count(), parser.parsed_count(), new_part)
+ if not self.initialized:
+ # Ignores first callback as it may contain unintentional events
+ self.initialized = True
+ command = 0
if command == 1:
break
new_part = False
@@ -151,14 +205,13 @@ def capture_qr_code_loop(self, callback):
parser.parse(data)
- if parser.parsed_count() > prev_parsed_count:
- prev_parsed_count = parser.parsed_count()
+ if parser.processed_parts_count() > prev_parsed_count:
+ prev_parsed_count = parser.processed_parts_count()
new_part = True
if parser.is_complete():
break
- gc.collect()
- sensor.run(0)
+ self.stop_sensor()
if parser.is_complete():
return (parser.result(), parser.format)
@@ -168,8 +221,7 @@ def capture_entropy(self, callback):
"""Captures camera's entropy as the hash of image buffer"""
import hashlib
- self.initialize_sensor()
- sensor.run(1)
+ self.initialize_run()
command = 0
while True:
@@ -178,6 +230,10 @@ def capture_entropy(self, callback):
img = self.snapshot()
command = callback()
+ if not self.initialized:
+ # Ignores first callback as it may contain unintentional events
+ self.initialized = True
+ command = 0
if command > 0:
break
@@ -185,8 +241,7 @@ def capture_entropy(self, callback):
img.lens_corr(strength=1.0, zoom=0.56)
lcd.display(img)
- gc.collect()
- sensor.run(0)
+ self.stop_sensor()
# User cancelled
if command == 2:
diff --git a/src/krux/context.py b/src/krux/context.py
index ac9c030af..ce6890123 100644
--- a/src/krux/context.py
+++ b/src/krux/context.py
@@ -20,12 +20,16 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import gc
+import time
import board
from .logging import logger
from .display import Display
from .input import Input
from .camera import Camera
from .light import Light
+from .themes import theme
+
+SCREENSAVER_ANIMATION_TIME = 150
class Context:
@@ -33,9 +37,19 @@ class Context:
duration of the program, including references to all device interfaces.
"""
- def __init__(self):
+ def __init__(self, logo=None):
self.display = Display()
- self.input = Input()
+
+ if logo is None:
+ logo = []
+ self.logo = logo
+ for _ in range((self.display.total_lines - len(logo)) // 2):
+ self.logo.insert(0, "")
+ self.logo.append("")
+ self.logo.append("")
+ self.logo.append("")
+
+ self.input = Input(screensaver_fallback=self.screensaver)
self.camera = Camera()
self.light = Light() if "LED_W" in board.config["krux"]["pins"] else None
self.power_manager = None
@@ -53,3 +67,31 @@ def clear(self):
if self.printer is not None:
self.printer.clear()
gc.collect()
+
+ def screensaver(self):
+ """Displays a screensaver until user input"""
+ anim_frame = 0
+ screensaver_time = 0
+
+ fg_color = theme.fg_color
+ bg_color = theme.bg_color
+
+ self.display.clear()
+
+ while True:
+ if screensaver_time + SCREENSAVER_ANIMATION_TIME < time.ticks_ms():
+ screensaver_time = time.ticks_ms()
+
+ # show animation on the screeen
+ if anim_frame < len(self.logo):
+ self.display.draw_line_hcentered_with_fullw_bg(
+ self.logo[anim_frame], anim_frame, fg_color, bg_color
+ )
+
+ anim_frame = anim_frame + 1
+ if anim_frame > len(self.logo) * 1.4:
+ anim_frame = 0
+ bg_color, fg_color = fg_color, bg_color
+
+ if self.input.wait_for_button(block=False) is not None:
+ break
diff --git a/src/krux/display.py b/src/krux/display.py
index 83d81f616..18b4dc23d 100644
--- a/src/krux/display.py
+++ b/src/krux/display.py
@@ -19,12 +19,9 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-import time
import lcd
import board
-from machine import I2C
from .themes import theme
-from .qr import add_qr_frame
DEFAULT_PADDING = 10
FONT_WIDTH, FONT_HEIGHT = board.config["krux"]["display"]["font"]
@@ -32,10 +29,7 @@
QR_DARK_COLOR, QR_LIGHT_COLOR = board.config["krux"]["display"]["qr_colors"]
-MAX_BACKLIGHT = 8
-MIN_BACKLIGHT = 1
-
-FLASH_MSG_TIME = 2000
+DEFAULT_BACKLIGHT = 1
class Display:
@@ -43,13 +37,10 @@ class Display:
def __init__(self):
self.portrait = True
- # self.initialize_lcd()
- self.i2c = None
self.font_width = FONT_WIDTH
self.font_height = FONT_HEIGHT
- self.bottom_line = board.config["lcd"]["width"] // FONT_HEIGHT # total lines
- self.bottom_line -= 1
- self.bottom_line *= FONT_HEIGHT
+ self.total_lines = board.config["lcd"]["width"] // FONT_HEIGHT
+ self.bottom_line = (self.total_lines - 1) * FONT_HEIGHT
if board.config["type"] == "m5stickv":
self.bottom_prompt_line = self.bottom_line - DEFAULT_PADDING
else:
@@ -110,7 +101,7 @@ def initialize_lcd(self):
0x2C,
],
)
- self.initialize_backlight()
+ self.set_backlight(DEFAULT_BACKLIGHT)
else:
invert = (
board.config["type"].startswith("amigo")
@@ -122,21 +113,6 @@ def initialize_lcd(self):
if board.config["type"].startswith("amigo"):
lcd.mirror(True)
- def initialize_backlight(self):
- """Initializes the backlight"""
- if (
- "I2C_SCL" not in board.config["krux"]["pins"]
- or "I2C_SDA" not in board.config["krux"]["pins"]
- ):
- return
- self.i2c = I2C(
- I2C.I2C0,
- freq=400000,
- scl=board.config["krux"]["pins"]["I2C_SCL"],
- sda=board.config["krux"]["pins"]["I2C_SDA"],
- )
- self.set_backlight(MIN_BACKLIGHT)
-
def qr_offset(self):
"""Retuns y offset to subtitle QR codes"""
return self.width() + DEFAULT_PADDING // 2
@@ -165,6 +141,8 @@ def qr_data_width(self):
"""
if self.width() > 300:
return self.width() // 6 # reduce density even more on larger screens
+ if self.width() > 200:
+ return self.width() // 5
return self.width() // 4
def to_landscape(self):
@@ -185,26 +163,27 @@ def to_lines(self, text):
columns = self.usable_width() // self.font_width
else:
columns = self.width() // self.font_width
- words = []
+
+ # Processing the words and maintaining newline characters
+ processed_text = []
for word in text.split(" "):
subwords = word.split("\n")
for i, subword in enumerate(subwords):
if len(subword) > columns:
j = 0
while j < len(subword):
- words.append(subword[j : j + columns])
+ processed_text.append(subword[j : j + columns])
j += columns
else:
- words.append(subword)
+ processed_text.append(subword)
if len(subwords) > 1 and i < len(subwords) - 1:
- # Only add newline to the end of the word if the word
- # is less than the amount of columns. If it's exactly equal,
- # a newline will be implicit.
- if len(words[-1]) < columns:
- words[-1] += "\n"
+ # Ensure proper handling of newline characters at the end of lines
+ if not processed_text[-1].endswith("\n"):
+ processed_text.append("\n")
- num_words = len(words)
+ num_words = len(processed_text)
+ words = processed_text
# calculate cost of all pairs of words
cost_between = [[0 for _ in range(num_words + 1)] for _ in range(num_words + 1)]
@@ -298,16 +277,48 @@ def draw_hcentered_text(
offset_y=DEFAULT_PADDING,
color=theme.fg_color,
bg_color=theme.bg_color,
+ info_box=False,
):
"""Draws text horizontally-centered on the display, at the given offset_y"""
lines = text if isinstance(text, list) else self.to_lines(text)
- for i, line in enumerate(lines):
- offset_x = (self.width() - self.font_width * len(line)) // 2
- offset_x = max(0, offset_x)
- self.draw_string(
- offset_x, offset_y + (i * self.font_height), line, color, bg_color
+ if info_box:
+ bg_color = theme.disabled_color
+ self.fill_rectangle(
+ DEFAULT_PADDING - 3,
+ offset_y - 1,
+ self.usable_width() + 6,
+ (len(lines)) * self.font_height + 2,
+ bg_color,
)
+ for i, line in enumerate(lines):
+ if len(line) > 0:
+ offset_x = self._obtain_hcentered_offset(line)
+ self.draw_string(
+ offset_x, offset_y + (i * self.font_height), line, color, bg_color
+ )
+
+ def _obtain_hcentered_offset(self, line_str):
+ """Return the offset_x to the horizontally-centered line_str"""
+ return max(0, (self.width() - self.font_width * len(line_str)) // 2)
+
+ def draw_line_hcentered_with_fullw_bg(
+ self,
+ line_str,
+ qtd_offset_y,
+ color=theme.fg_color,
+ bg_color=theme.bg_color,
+ ):
+ """Draw a line_str horizontally-centered on the display, at qtd_offset_y times font_height,
+ useful for screensaver"""
+ lcd.fill_rectangle(
+ 0, qtd_offset_y * self.font_height, self.width(), self.font_height, bg_color
+ )
+ offset_x = self._obtain_hcentered_offset(line_str)
+ self.draw_string(
+ offset_x, (qtd_offset_y * self.font_height), line_str, color, bg_color
+ )
+
def draw_centered_text(self, text, color=theme.fg_color, bg_color=theme.bg_color):
"""Draws text horizontally and vertically centered on the display"""
lines = text if isinstance(text, list) else self.to_lines(text)
@@ -315,33 +326,22 @@ def draw_centered_text(self, text, color=theme.fg_color, bg_color=theme.bg_color
offset_y = max(0, (self.height() - lines_height) // 2)
self.draw_hcentered_text(text, offset_y, color, bg_color)
- def flash_text(
- self,
- text,
- color=theme.fg_color,
- bg_color=theme.bg_color,
- duration=FLASH_MSG_TIME,
- ):
- """Flashes text centered on the display for duration ms"""
- self.clear()
- self.draw_centered_text(text, color, bg_color)
- time.sleep_ms(duration)
- self.clear()
-
def draw_qr_code(
self, offset_y, qr_code, dark_color=QR_DARK_COLOR, light_color=QR_LIGHT_COLOR
):
"""Draws a QR code on the screen"""
- _, qr_code = add_qr_frame(qr_code)
- lcd.draw_qr_code(
- offset_y, qr_code, self.width(), dark_color, light_color, theme.bg_color
+ lcd.draw_qr_code_binary(
+ offset_y, qr_code, self.width(), dark_color, light_color, light_color
)
def set_backlight(self, level):
"""Sets the backlight of the display to the given power level, from 0 to 8"""
- if not self.i2c:
- return
- # Ranges from 0 to 8
- level = max(0, min(level, 8))
- val = (level + 7) << 4
- self.i2c.writeto_mem(0x34, 0x91, int(val))
+
+ from .power import power_manager
+
+ power_manager.set_screen_brightness(level)
+
+ def max_lines(self, line_offset=0):
+ """The max lines of text supported by the display"""
+ pad = DEFAULT_PADDING if line_offset else 2 * DEFAULT_PADDING
+ return (self.height() - pad - line_offset) // (2 * self.font_height)
diff --git a/src/krux/encryption.py b/src/krux/encryption.py
index 90857940a..63af9ed28 100644
--- a/src/krux/encryption.py
+++ b/src/krux/encryption.py
@@ -150,9 +150,11 @@ def store_encrypted(self, key, mnemonic_id, mnemonic, sd_card=False, i_vector=No
# load current MNEMONICS_FILE
try:
with SDHandler() as sd:
- mnemonics = json.loads(sd.read(MNEMONICS_FILE))
+ contents = sd.read(MNEMONICS_FILE)
+ orig_len = len(contents)
+ mnemonics = json.loads(contents)
except:
- pass
+ orig_len = 0
# save the new MNEMONICS_FILE
try:
@@ -165,7 +167,11 @@ def store_encrypted(self, key, mnemonic_id, mnemonic, sd_card=False, i_vector=No
"key_iterations"
] = Settings().encryption.pbkdf2_iterations
mnemonics[mnemonic_id]["data"] = encrypted
- sd.write(MNEMONICS_FILE, json.dumps(mnemonics))
+ contents = json.dumps(mnemonics)
+ # pad contents to orig_len to avoid abandoned bytes on sdcard
+ if len(contents) < orig_len:
+ contents += " " * (orig_len - len(contents))
+ sd.write(MNEMONICS_FILE, contents)
except:
return False
else:
@@ -196,7 +202,12 @@ def del_mnemonic(self, mnemonic_id, sd_card=False):
if sd_card:
self.stored_sd.pop(mnemonic_id)
with SDHandler() as sd:
- sd.write(MNEMONICS_FILE, json.dumps(self.stored_sd))
+ orig_len = len(sd.read(MNEMONICS_FILE))
+ contents = json.dumps(self.stored_sd)
+ # pad contents to orig_len to avoid abandoned bytes on sdcard
+ if len(contents) < orig_len:
+ contents += " " * (orig_len - len(contents))
+ sd.write(MNEMONICS_FILE, contents)
else:
self.stored.pop(mnemonic_id)
with open("/flash/" + MNEMONICS_FILE, "w") as f:
diff --git a/src/krux/firmware.py b/src/krux/firmware.py
index 383a4e676..83bef0303 100644
--- a/src/krux/firmware.py
+++ b/src/krux/firmware.py
@@ -32,14 +32,20 @@
from .krux_settings import t
from .wdt import wdt
+FLASH_MSG_TIME = 2000
+
+FLASH_SIZE = 2**24
MAX_FIRMWARE_SIZE = 0x300000
FIRMWARE_SLOT_1 = 0x00080000
-FIRMWARE_SLOT_2 = 0x00280000
+FIRMWARE_SLOT_2 = 0x00280000 # TODO: Move to 0x00390000 - Test all possible cases
+SPIFFS_ADDR = 0xD00000
MAIN_BOOT_CONFIG_SECTOR_ADDRESS = 0x00004000
BACKUP_BOOT_CONFIG_SECTOR_ADDRESS = 0x00005000
+ERASE_BLOCK_SIZE = 0x1000
+
FLASH_IO_WAIT_TIME = 100
@@ -171,6 +177,13 @@ def sha256(firmware_filename, firmware_size=None):
def upgrade():
"""Installs new firmware from SD card"""
+ def flash_text(text):
+ """Flashes text centered on the display for duration ms"""
+ display.clear()
+ display.draw_centered_text(text)
+ time.sleep_ms(FLASH_MSG_TIME)
+ display.clear()
+
firmware_path = ""
try:
firmware_filenames = list(
@@ -204,31 +217,31 @@ def upgrade():
return False
if new_size > MAX_FIRMWARE_SIZE:
- display.flash_text(t("Firmware exceeds max size: %d") % MAX_FIRMWARE_SIZE)
+ flash_text(t("Firmware exceeds max size: %d") % MAX_FIRMWARE_SIZE)
return False
pubkey = None
try:
pubkey = ec.PublicKey.from_string(SIGNER_PUBKEY)
except:
- display.flash_text(t("Invalid public key"))
+ flash_text(t("Invalid public key"))
return False
sig = None
try:
sig = open(firmware_path + ".sig", "rb").read()
except:
- display.flash_text(t("Missing signature file"))
+ flash_text(t("Missing signature file"))
return False
try:
# Parse, serialize, and reparse to ensure signature is compact prior to verification
sig = ec.Signature.parse(ec.Signature.parse(sig).serialize())
if not pubkey.verify(sig, firmware_hash):
- display.flash_text(t("Bad signature"))
+ flash_text(t("Bad signature"))
return False
except:
- display.flash_text(t("Bad signature"))
+ flash_text(t("Bad signature"))
return False
boot_config_sector = flash.read(MAIN_BOOT_CONFIG_SECTOR_ADDRESS, 4096)
@@ -237,7 +250,7 @@ def upgrade():
boot_config_sector = flash.read(BACKUP_BOOT_CONFIG_SECTOR_ADDRESS, 4096)
address, _, entry_index = find_active_firmware(boot_config_sector)
if address is None:
- display.flash_text(t("Invalid bootloader"))
+ flash_text(t("Invalid bootloader"))
return False
# Write new firmware to the opposite slot
@@ -276,5 +289,5 @@ def status_text(text):
4096,
)
- display.flash_text(t("Upgrade complete.\n\nShutting down.."))
+ flash_text(t("Upgrade complete.\n\nShutting down.."))
return True
diff --git a/src/krux/i2c.py b/src/krux/i2c.py
new file mode 100644
index 000000000..1fb7e606b
--- /dev/null
+++ b/src/krux/i2c.py
@@ -0,0 +1,45 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import board
+from machine import I2C
+
+
+class I2CBus:
+ """Shared I2C bus singleton"""
+
+ def __init__(self):
+ self.i2c = None
+ if (
+ "I2C_SCL" not in board.config["krux"]["pins"]
+ or "I2C_SDA" not in board.config["krux"]["pins"]
+ ):
+ return
+ self.i2c = I2C(
+ I2C.I2C0,
+ freq=400000,
+ scl=board.config["krux"]["pins"]["I2C_SCL"],
+ sda=board.config["krux"]["pins"]["I2C_SDA"],
+ )
+
+
+i2c_bus = I2CBus().i2c
diff --git a/src/krux/input.py b/src/krux/input.py
index d9846bb6f..0925fdac2 100644
--- a/src/krux/input.py
+++ b/src/krux/input.py
@@ -21,10 +21,9 @@
# THE SOFTWARE.
import time
import board
-from Maix import GPIO
-from fpioa_manager import fm
from .wdt import wdt
-from .touch import Touch
+from .buttons import PRESSED, RELEASED
+from .krux_settings import Settings
BUTTON_ENTER = 0
BUTTON_PAGE = 1
@@ -38,22 +37,26 @@
QR_ANIM_PERIOD = 300 # milliseconds
LONG_PRESS_PERIOD = 1000 # milliseconds
-PRESSED = 0
-RELEASED = 1
-
BUTTON_WAIT_PRESS_DELAY = 10
+DEBOUNCE = 100
class Input:
"""Input is a singleton interface for interacting with the device's buttons"""
- def __init__(self):
+ def __init__(self, screensaver_fallback=None):
+ self.screensaver_fallback = screensaver_fallback
+ self.screensaver_time = 0
+ self.screensaver_active = False
self.entropy = 0
+ self.debounce_time = 0
+ self.flushed_flag = False
self.enter = None
if "BUTTON_A" in board.config["krux"]["pins"]:
- fm.register(board.config["krux"]["pins"]["BUTTON_A"], fm.fpioa.GPIOHS21)
- self.enter = GPIO(GPIO.GPIOHS21, GPIO.IN, GPIO.PULL_UP)
+ from .buttons import ButtonEnter
+
+ self.enter = ButtonEnter(board.config["krux"]["pins"]["BUTTON_A"])
self.page = None
self.page_prev = None
@@ -64,12 +67,16 @@ def __init__(self):
self.page_prev = EncoderPagePrev()
else:
if "BUTTON_B" in board.config["krux"]["pins"]:
- fm.register(board.config["krux"]["pins"]["BUTTON_B"], fm.fpioa.GPIOHS22)
- self.page = GPIO(GPIO.GPIOHS22, GPIO.IN, GPIO.PULL_UP)
+ from .buttons import ButtonPage
+
+ self.page = ButtonPage(board.config["krux"]["pins"]["BUTTON_B"])
if "BUTTON_C" in board.config["krux"]["pins"]:
- fm.register(board.config["krux"]["pins"]["BUTTON_C"], fm.fpioa.GPIOHS0)
- self.page_prev = GPIO(GPIO.GPIOHS0, GPIO.IN, GPIO.PULL_UP)
+ from .buttons import ButtonPagePrev
+
+ self.page_prev = ButtonPagePrev(
+ board.config["krux"]["pins"]["BUTTON_C"]
+ )
else:
try:
from pmu import PMU_Button
@@ -85,25 +92,29 @@ def __init__(self):
"touch" in board.config["krux"]["display"]
and board.config["krux"]["display"]["touch"]
):
+ from .touch import Touch
+
self.touch = Touch(
- board.config["lcd"]["width"], board.config["lcd"]["height"]
+ board.config["lcd"]["width"],
+ board.config["lcd"]["height"],
+ board.config["krux"]["pins"]["TOUCH_IRQ"],
)
self.buttons_active = False
def enter_value(self):
- """Intermediary method to pull button A state, if available"""
+ """Intermediary method to pull button ENTER state"""
if self.enter is not None:
return self.enter.value()
return RELEASED
def page_value(self):
- """Intermediary method to pull button B state, if available"""
+ """Intermediary method to pull button PAGE state"""
if self.page is not None:
return self.page.value()
return RELEASED
def page_prev_value(self):
- """Intermediary method to pull button C state, if available"""
+ """Intermediary method to pull button PAGE_PREV state"""
if self.page_prev is not None:
return self.page_prev.value()
return RELEASED
@@ -114,6 +125,30 @@ def touch_value(self):
return self.touch.value()
return RELEASED
+ def enter_event(self):
+ """Intermediary method to pull button ENTER event"""
+ if self.enter is not None:
+ return self.enter.event()
+ return False
+
+ def page_event(self):
+ """Intermediary method to pull button PAGE event"""
+ if self.page is not None:
+ return self.page.event()
+ return False
+
+ def page_prev_event(self):
+ """Intermediary method to pull button PAGE_PREV event"""
+ if self.page_prev is not None:
+ return self.page_prev.event()
+ return False
+
+ def touch_event(self):
+ """Intermediary method to pull button TOUCH event"""
+ if self.touch is not None:
+ return self.touch.event()
+ return False
+
def swipe_right_value(self):
"""Intermediary method to pull touch gesture, if touch available"""
if self.touch is not None:
@@ -138,54 +173,74 @@ def swipe_down_value(self):
return self.touch.swipe_down_value()
return RELEASED
- def wait_for_release(self):
- """Loop until all buttons are released (if currently pressed)"""
- while True:
- if self.enter_value() == RELEASED and self.touch_value() == RELEASED:
- if "ENCODER" in board.config["krux"]["pins"]:
- # Encoder is event based, this check may disable event flag unintentionally
- break
- # TODO: Change standard buttons to be event(interrupt) based too
- # So presses during high processing time(camera and animated QR) won't be lost
- if self.page_value() == RELEASED and self.page_prev_value() == RELEASED:
- break
- self.entropy += 1
- wdt.feed()
-
- def wait_for_press(self, block=True, wait_duration=QR_ANIM_PERIOD):
+ def wait_for_press(
+ self, block=True, wait_duration=QR_ANIM_PERIOD, enable_screensaver=False
+ ):
"""Wait for first button press or for wait_duration ms.
Use block to wait indefinitely"""
start_time = time.ticks_ms()
+ self.debounce_time = time.ticks_ms() if not self.flushed_flag else 0
+ while time.ticks_ms() < self.debounce_time + DEBOUNCE:
+ self.flush_events()
+ if not self.flushed_flag or block:
+ # Makes sure events that happened between pages load are cleared.
+ # On animated pages, where block=False, intermediary events must be checked,
+ # so events won't be cleared after flushed_flag is set
+ self.flush_events()
+ self.flushed_flag = not block
+
+ self.screensaver_time = start_time
while True:
- if self.enter_value() == PRESSED:
+ if self.enter_event():
return BUTTON_ENTER
- if self.page_value() == PRESSED:
+ if self.page_event():
return BUTTON_PAGE
- if self.page_prev_value() == PRESSED:
+ if self.page_prev_event():
return BUTTON_PAGE_PREV
- if self.touch_value() == PRESSED:
+ if self.touch_event():
return BUTTON_TOUCH
+
self.entropy += 1
wdt.feed() # here is where krux spends most of its time
+
if not block and time.ticks_ms() > start_time + wait_duration:
return None
+
+ # Check for screensaver
+ if (
+ block
+ and enable_screensaver
+ and not self.screensaver_active
+ and self.screensaver_fallback
+ and self.screensaver_time
+ + (Settings().appearance.screensaver_time * 60000)
+ < time.ticks_ms()
+ ):
+ self.screensaver_active = True
+ self.screensaver_fallback()
+ self.screensaver_active = False
+ self.screensaver_time = time.ticks_ms()
+ return None
+
time.sleep_ms(BUTTON_WAIT_PRESS_DELAY)
- def wait_for_button(self, block=True):
+ def wait_for_button(self, block=True, enable_screensaver=False):
"""Waits for any button to release, optionally blocking if block=True.
- Returns the button that was released, or None if nonblocking.
+ Returns the button that was released, or None if non blocking.
"""
- self.wait_for_release()
- btn = self.wait_for_press(block)
+ if Settings().appearance.screensaver_time == 0:
+ enable_screensaver = False
+ btn = self.wait_for_press(block, enable_screensaver=enable_screensaver)
if btn == BUTTON_ENTER:
# Wait for release
while self.enter_value() == PRESSED:
self.entropy += 1
wdt.feed()
- if self.buttons_active:
- return BUTTON_ENTER
- self.buttons_active = True
+ if not self.buttons_active:
+ self.buttons_active = True
+ btn = None
+
elif btn == BUTTON_PAGE:
start_time = time.ticks_ms()
# Wait for release
@@ -193,10 +248,11 @@ def wait_for_button(self, block=True):
self.entropy += 1
wdt.feed()
if time.ticks_ms() > start_time + LONG_PRESS_PERIOD:
- return SWIPE_LEFT
- if self.buttons_active:
- return BUTTON_PAGE
- self.buttons_active = True
+ btn = SWIPE_LEFT
+ break
+ if not self.buttons_active:
+ self.buttons_active = True
+ btn = None
elif btn == BUTTON_PAGE_PREV:
start_time = time.ticks_ms()
# Wait for release
@@ -204,10 +260,11 @@ def wait_for_button(self, block=True):
self.entropy += 1
wdt.feed()
if time.ticks_ms() > start_time + LONG_PRESS_PERIOD:
- return SWIPE_RIGHT
- if self.buttons_active:
- return BUTTON_PAGE_PREV
- self.buttons_active = True
+ btn = SWIPE_RIGHT
+ break
+ if not self.buttons_active:
+ self.buttons_active = True
+ btn = None
elif btn == BUTTON_TOUCH:
# Wait for release
while self.touch_value() == PRESSED:
@@ -215,13 +272,19 @@ def wait_for_button(self, block=True):
wdt.feed()
self.buttons_active = False
if self.swipe_right_value() == PRESSED:
- return SWIPE_RIGHT
+ btn = SWIPE_RIGHT
if self.swipe_left_value() == PRESSED:
- return SWIPE_LEFT
+ btn = SWIPE_LEFT
if self.swipe_up_value() == PRESSED:
- return SWIPE_UP
+ btn = SWIPE_UP
if self.swipe_down_value() == PRESSED:
- return SWIPE_DOWN
- return BUTTON_TOUCH
+ btn = SWIPE_DOWN
+
+ return btn
- return None
+ def flush_events(self):
+ """Clean eventual event flags unintentionally collected"""
+ self.enter_event()
+ self.page_event()
+ self.page_prev_event()
+ self.touch_event()
diff --git a/src/krux/key.py b/src/krux/key.py
index bb986fd02..39499ff8a 100644
--- a/src/krux/key.py
+++ b/src/krux/key.py
@@ -74,20 +74,37 @@ def account_pubkey_str(self, version=None):
def fingerprint_hex_str(self, pretty=False):
"""Returns the master key fingerprint in hex format"""
- formatted_txt = t("Fingerprint: %s") if pretty else "%s"
+ formatted_txt = t("β %s") if pretty else "%s"
return formatted_txt % hexlify(self.fingerprint).decode("utf-8")
def derivation_str(self, pretty=False):
"""Returns the derivation path for the Hierarchical Deterministic Wallet to
be displayed as string
"""
- formatted_txt = t("Derivation: %s") if pretty else "%s"
+ formatted_txt = t("β³ %s") if pretty else "%s"
return (formatted_txt % self.derivation).replace("h", HARDENED_STR_REPLACE)
def sign(self, message_hash):
"""Signs a message with the extended master private key"""
return self.root.derive(self.derivation).sign(message_hash)
+ def sign_at(self, derivation, message_hash):
+ """Signs a message at an adress derived from master key (code adapted from specterDIY)"""
+ from embit import ec
+ from embit.util import secp256k1
+
+ prv = self.root.derive(derivation).key
+ sig = secp256k1.ecdsa_sign_recoverable(
+ message_hash, prv._secret # pylint: disable=W0212
+ )
+ flag = sig[64]
+ flag = bytes([27 + flag + 4])
+ ec_signature = ec.Signature(sig[:64])
+ ser = flag + secp256k1.ecdsa_signature_serialize_compact(
+ ec_signature._sig # pylint: disable=W0212
+ )
+ return ser
+
@staticmethod
def pick_final_word(entropy, words):
"""Returns a random final word with a valid checksum for the given list of
@@ -113,6 +130,6 @@ def get_default_derivation_str(multisig, network):
"""Return the Krux default derivation path for single-sig or multisig to
be displayd as string
"""
- return Key.get_default_derivation(multisig, network).replace(
+ return "β³ " + Key.get_default_derivation(multisig, network).replace(
"h", HARDENED_STR_REPLACE
)
diff --git a/src/krux/krux_settings.py b/src/krux/krux_settings.py
index 10ea25fa3..f2046eb98 100644
--- a/src/krux/krux_settings.py
+++ b/src/krux/krux_settings.py
@@ -155,8 +155,6 @@ class AdafruitPrinterSettings(SettingsNamespace):
paper_width = NumberSetting(int, "paper_width", 384, [100, 1000])
tx_pin = NumberSetting(int, "tx_pin", DEFAULT_TX_PIN, [0, 10000])
rx_pin = NumberSetting(int, "rx_pin", DEFAULT_RX_PIN, [0, 10000])
- heat_time = NumberSetting(int, "heat_time", 120, [3, 255])
- heat_interval = NumberSetting(int, "heat_interval", 40, [0, 255])
line_delay = NumberSetting(int, "line_delay", 20, [0, 255])
scale = NumberSetting(int, "scale", 75, [25, 100])
@@ -167,8 +165,6 @@ def label(self, attr):
"paper_width": t("Paper Width"),
"tx_pin": t("TX Pin"),
"rx_pin": t("RX Pin"),
- "heat_time": t("Heat Time"),
- "heat_interval": t("Heat Interval"),
"line_delay": t("Line Delay"),
"scale": t("Scale"),
}[attr]
@@ -254,7 +250,7 @@ class EncoderSettings(SettingsNamespace):
"""Encoder debounce settings"""
namespace = "settings.encoder"
- debounce = NumberSetting(int, "debounce", 50, [25, 250])
+ debounce = NumberSetting(int, "debounce", 100, [100, 250])
def label(self, attr):
"""Returns a label for UI when given a setting name or namespace"""
@@ -276,6 +272,38 @@ def label(self, attr):
}[attr]
+class HardwareSettings(SettingsNamespace):
+ """Hardware Related Settings"""
+
+ namespace = "settings.hardware"
+
+ def __init__(self):
+ self.printer = PrinterSettings()
+ if (
+ board.config["type"].startswith("amigo")
+ or board.config["type"] == "yahboom"
+ ):
+ self.touch = TouchSettings()
+ if board.config["type"] == "dock":
+ self.encoder = EncoderSettings()
+
+ def label(self, attr):
+ """Returns a label for UI when given a setting name or namespace"""
+
+ hardware_menu = {
+ "printer": t("Printer"),
+ }
+ if (
+ board.config["type"].startswith("amigo")
+ or board.config["type"] == "yahboom"
+ ):
+ hardware_menu["touchscreen"] = t("Touchscreen")
+ if board.config["type"] == "dock":
+ hardware_menu["encoder"] = t("Encoder")
+
+ return hardware_menu[attr]
+
+
class PersistSettings(SettingsNamespace):
"""Persistent settings"""
@@ -316,21 +344,29 @@ class ThemeSettings(SettingsNamespace):
DARK_THEME = 0
LIGHT_THEME = 1
ORANGE_THEME = 3
+ GREEN_THEME = 4
+ PINK_THEME = 5
DARK_THEME_NAME = "Dark"
LIGHT_THEME_NAME = "Light"
ORANGE_THEME_NAME = "Orange"
+ GREEN_THEME_NAME = "CypherPunk"
+ PINK_THEME_NAME = "CypherPink"
THEME_NAMES = {
DARK_THEME: DARK_THEME_NAME,
LIGHT_THEME: LIGHT_THEME_NAME,
ORANGE_THEME: ORANGE_THEME_NAME,
+ GREEN_THEME: GREEN_THEME_NAME,
+ PINK_THEME: PINK_THEME_NAME,
}
namespace = "settings.appearance"
theme = CategorySetting("theme", DARK_THEME_NAME, list(THEME_NAMES.values()))
+ screensaver_time = NumberSetting(int, "screensaver_time", 5, [0, 30])
def label(self, attr):
"""Returns a label for UI when given a setting name or namespace"""
return {
"theme": t("Theme"),
+ "screensaver_time": t("Screensaver time"),
}[attr]
@@ -341,30 +377,23 @@ class Settings(SettingsNamespace):
def __init__(self):
self.bitcoin = BitcoinSettings()
+ self.hardware = HardwareSettings()
self.i18n = I18nSettings()
self.logging = LoggingSettings()
self.encryption = EncryptionSettings()
- self.printer = PrinterSettings()
self.persist = PersistSettings()
self.appearance = ThemeSettings()
- if board.config["type"].startswith("amigo"):
- self.touch = TouchSettings()
- if board.config["type"] == "dock":
- self.encoder = EncoderSettings()
def label(self, attr):
"""Returns a label for UI when given a setting name or namespace"""
main_menu = {
"bitcoin": t("Bitcoin"),
+ "hardware": t("Hardware"),
"i18n": t("Language"),
"logging": t("Logging"),
"encryption": t("Encryption"),
"persist": t("Persist"),
- "printer": t("Printer"),
- "appearance": t("Theme"),
+ "appearance": t("Appearance"),
}
- if board.config["type"].startswith("amigo"):
- main_menu["touchscreen"] = t("Touchscreen")
- if board.config["type"] == "dock":
- main_menu["encoder"] = t("Encoder")
+
return main_menu[attr]
diff --git a/src/krux/metadata.py b/src/krux/metadata.py
index e2ff900d4..3713e7b38 100644
--- a/src/krux/metadata.py
+++ b/src/krux/metadata.py
@@ -19,5 +19,5 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-VERSION = "23.09.1"
+VERSION = "24.04.beta13"
SIGNER_PUBKEY = "03339e883157e45891e61ca9df4cd3bb895ef32d475b8e793559ea10a36766689b"
diff --git a/src/krux/pages/__init__.py b/src/krux/pages/__init__.py
index 0726dec27..90d7c7681 100644
--- a/src/krux/pages/__init__.py
+++ b/src/krux/pages/__init__.py
@@ -38,17 +38,19 @@
from ..display import DEFAULT_PADDING
from ..qr import to_qr_codes
from ..krux_settings import t, Settings, LoggingSettings, BitcoinSettings
+from ..sd_card import SDHandler
MENU_CONTINUE = 0
MENU_EXIT = 1
MENU_SHUTDOWN = 2
+FLASH_MSG_TIME = 2000
+
ESC_KEY = 1
FIXED_KEYS = 3 # 'More' key only appears when there are multiple keysets
ANTI_GLARE_WAIT_TIME = 500
QR_CODE_STEP_TIME = 100
-CAMERA_INIT_TIME = 1000
SHUTDOWN_WAIT_TIME = 300
TOGGLE_BRIGHTNESS = (BUTTON_PAGE, BUTTON_PAGE_PREV)
@@ -85,6 +87,20 @@ def esc_prompt(self):
return ESC_KEY
return None
+ def flash_text(
+ self,
+ text,
+ color=theme.fg_color,
+ bg_color=theme.bg_color,
+ duration=FLASH_MSG_TIME,
+ ):
+ """Flashes text centered on the display for duration ms"""
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(text, color, bg_color)
+ time.sleep_ms(duration)
+ self.ctx.display.clear()
+ self.ctx.input.flush_events()
+
def capture_from_keypad(
self,
title,
@@ -92,6 +108,7 @@ def capture_from_keypad(
autocomplete_fn=None,
possible_keys_fn=None,
delete_key_fn=None,
+ progress_bar_fn=None,
go_on_change=False,
starting_buffer="",
esc_prompt=True,
@@ -110,7 +127,10 @@ def capture_from_keypad(
self.ctx.display.draw_hcentered_text(title, offset_y)
offset_y += self.ctx.display.font_height * 3 // 2
self.ctx.display.draw_hcentered_text(buffer, offset_y)
- offset_y = pad.keypad_offset()
+
+ # offset_y = pad.keypad_offset() # Dead code?
+ if progress_bar_fn:
+ progress_bar_fn()
possible_keys = pad.keys
if possible_keys_fn is not None:
possible_keys = possible_keys_fn(buffer)
@@ -170,40 +190,35 @@ def capture_qr_code(self):
def callback(part_total, num_parts_captured, new_part):
# Turn on the light as long as the enter button is held down (M5stickV and Amigo)
- if self._time_to_check_input():
- if self.ctx.light:
- if self.ctx.input.enter_value() == PRESSED:
- self.ctx.light.turn_on()
+ if self.ctx.light:
+ if self.ctx.input.enter_value() == PRESSED:
+ self.ctx.light.turn_on()
+ else:
+ self.ctx.light.turn_off()
+ # If board don't have light, ENTER stops the capture
+ elif self.ctx.input.enter_event():
+ return 1
+
+ # Anti-glare mode (M5stickV and Amigo)
+ if self.ctx.input.page_event():
+ if self.ctx.camera.has_antiglare():
+ self._time_frame = time.ticks_ms()
+ self.ctx.display.to_portrait()
+ if not self.ctx.camera.antiglare_enabled:
+ self.ctx.camera.enable_antiglare()
+ self.ctx.display.draw_centered_text(t("Anti-glare enabled"))
else:
- self.ctx.light.turn_off()
- # If board don't have light, ENTER stops the capture
- elif self.ctx.input.enter_value() == PRESSED:
- return 1
-
- # Anti-glare mode (M5stickV and Amigo)
- if self.ctx.input.page_value() == PRESSED:
- if self.ctx.camera.has_antiglare():
- self._time_frame = time.ticks_ms()
- self.ctx.display.to_portrait()
- if not self.ctx.camera.antiglare_enabled:
- self.ctx.camera.enable_antiglare()
- self.ctx.display.draw_centered_text(t("Anti-glare enabled"))
- else:
- self.ctx.camera.disable_antiglare()
- self.ctx.display.draw_centered_text(
- t("Anti-glare disabled")
- )
- time.sleep_ms(ANTI_GLARE_WAIT_TIME)
- self.ctx.display.to_landscape()
- return 0
- return 1
-
- # Exit the capture loop with PAGE_PREV or TOUCH
- if (
- self.ctx.input.page_prev_value() == PRESSED
- or self.ctx.input.touch_value() == PRESSED
- ):
- return 1
+ self.ctx.camera.disable_antiglare()
+ self.ctx.display.draw_centered_text(t("Anti-glare disabled"))
+ time.sleep_ms(ANTI_GLARE_WAIT_TIME)
+ self.ctx.display.to_landscape()
+ self.ctx.input.flush_events()
+ return 0
+ return 1
+
+ # Exit the capture loop with PAGE_PREV or TOUCH
+ if self.ctx.input.page_prev_event() or self.ctx.input.touch_event():
+ return 1
# Indicate progress to the user that a new part was captured
if new_part:
@@ -248,28 +263,18 @@ def callback(part_total, num_parts_captured, new_part):
)
return (code, qr_format)
- def _time_to_check_input(self):
- return time.ticks_ms() > self._time_frame + CAMERA_INIT_TIME
-
def capture_camera_entropy(self):
"Helper to capture camera's entropy as the hash of image buffer"
self._time_frame = time.ticks_ms()
def callback():
- if self._time_to_check_input():
- # Accepted
- if (
- self.ctx.input.enter_value() == PRESSED
- or self.ctx.input.touch_value() == PRESSED
- ):
- return 1
-
- # Exited
- if (
- self.ctx.input.page_value() == PRESSED
- or self.ctx.input.page_prev_value() == PRESSED
- ):
- return 2
+ # Accepted
+ if self.ctx.input.enter_event() or self.ctx.input.touch_event():
+ return 1
+
+ # Exited
+ if self.ctx.input.page_event() or self.ctx.input.page_prev_event():
+ return 2
return 0
self.ctx.display.clear()
@@ -320,26 +325,28 @@ def display_qr_codes(self, data, qr_format, title=""):
)
self.ctx.display.draw_hcentered_text(subtitle, offset_y)
i = (i + 1) % num_parts
- # There are cases we can allow any btn to change the screen
+ self.ctx.input.buttons_active = True
btn = self.ctx.input.wait_for_button(num_parts == 1)
if btn in TOGGLE_BRIGHTNESS:
bright = not bright
elif btn in PROCEED:
+ if self.ctx.input.touch is not None:
+ self.ctx.input.buttons_active = False
done = True
# interval done in input.py using timers
- def display_mnemonic(self, mnemonic):
+ def display_mnemonic(self, mnemonic, suffix=""):
"""Displays the 12 or 24-word list of words to the user"""
words = mnemonic.split(" ")
word_list = [
str(i + 1) + "." + (" " if i + 1 < 10 else " ") + word
for i, word in enumerate(words)
]
+ header = t("BIP39") + " " + suffix
self.ctx.display.clear()
- self.ctx.display.draw_hcentered_text(t("BIP39 Mnemonic"))
+ self.ctx.display.draw_hcentered_text(header)
starting_y_offset = DEFAULT_PADDING // 4 + (
- len(self.ctx.display.to_lines(t("BIP39 Mnemonic")))
- * self.ctx.display.font_height
+ len(self.ctx.display.to_lines(header)) * self.ctx.display.font_height
+ self.ctx.display.font_height
)
for i, word in enumerate(word_list[:12]):
@@ -350,7 +357,7 @@ def display_mnemonic(self, mnemonic):
if board.config["type"] == "m5stickv":
self.ctx.input.wait_for_button()
self.ctx.display.clear()
- self.ctx.display.draw_hcentered_text(t("BIP39 Mnemonic"))
+ self.ctx.display.draw_hcentered_text(header)
for i, word in enumerate(word_list[12:]):
offset_x = DEFAULT_PADDING
offset_y = starting_y_offset + (i * self.ctx.display.font_height)
@@ -365,12 +372,12 @@ def print_qr_prompt(self):
"""Prompts the user to print a QR code in the specified format
if a printer is connected
"""
- if Settings().printer.driver == "none":
+ if not self.has_printer():
return False
self.ctx.display.clear()
if self.prompt(
- t("Print to QR?\n\n%s\n\n") % Settings().printer.driver,
+ t("Print to QR?\n\n%s\n\n") % Settings().hardware.printer.driver,
self.ctx.display.height() // 2,
):
return True
@@ -464,6 +471,37 @@ def prompt(self, text, offset_y=0):
# BUTTON_ENTER
return answer
+ def fit_to_line(self, text, prefix="", fixed_chars=0):
+ """Fits text with prefix plus fixed_chars at the beginning into one line,
+ removing the central content and leaving the ends"""
+
+ add_chars_amount = (
+ self.ctx.display.usable_width() // self.ctx.display.font_width
+ )
+ add_chars_amount -= len(prefix) + fixed_chars + 2
+ add_chars_amount //= 2
+ return (
+ prefix
+ + text[: add_chars_amount + fixed_chars]
+ + ".."
+ + text[-add_chars_amount:]
+ )
+
+ def has_printer(self):
+ """Checks if the device has a printer setup"""
+ return Settings().hardware.printer.driver != "none"
+
+ def has_sd_card(self):
+ """Checks if the device has a SD card inserted"""
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(t("Checking for SD card.."))
+ try:
+ # Check for SD hot-plug
+ with SDHandler():
+ return True
+ except:
+ return False
+
def shutdown(self):
"""Handler for the 'shutdown' menu item"""
if self.prompt(t("Are you sure?"), self.ctx.display.height() // 2):
@@ -528,12 +566,12 @@ class Menu:
and invoke menu item callbacks that return a status
"""
- def __init__(self, ctx, menu):
+ def __init__(self, ctx, menu, offset=0):
self.ctx = ctx
self.menu = menu
+ self.menu_offset = offset
max_viewable = min(
- (self.ctx.display.height() - 2 * DEFAULT_PADDING)
- // (2 * self.ctx.display.font_height),
+ self.ctx.display.max_lines(self.menu_offset),
len(self.menu),
)
self.menu_view = ListView(self.menu, max_viewable)
@@ -549,13 +587,23 @@ def run_loop(self, start_from_index=None):
selected_item_index = start_from_index
while True:
gc.collect()
- self.ctx.display.clear()
+ if self.menu_offset:
+ self.ctx.display.fill_rectangle(
+ 0,
+ self.menu_offset,
+ self.ctx.display.width(),
+ self.ctx.display.height() - self.menu_offset,
+ theme.bg_color,
+ )
+ else:
+ self.ctx.display.clear()
if self.ctx.input.touch is not None:
self._draw_touch_menu(selected_item_index)
else:
self._draw_menu(selected_item_index)
self.draw_status_bar()
+ self.ctx.input.flush_events()
if start_from_submenu:
status = self._clicked_item(selected_item_index)
@@ -563,7 +611,7 @@ def run_loop(self, start_from_index=None):
return (self.menu_view.index(selected_item_index), status)
start_from_submenu = False
else:
- btn = self.ctx.input.wait_for_button()
+ btn = self.ctx.input.wait_for_button(enable_screensaver=True)
if self.ctx.input.touch is not None:
if btn == BUTTON_TOUCH:
selected_item_index = self.ctx.input.touch.current_index()
@@ -594,6 +642,8 @@ def run_loop(self, start_from_index=None):
self.menu_view.move_backward()
def _clicked_item(self, selected_item_index):
+ if self.menu_view[selected_item_index][1] is None:
+ return MENU_CONTINUE
try:
self.ctx.display.clear()
status = self.menu_view[selected_item_index][1]()
@@ -698,10 +748,13 @@ def _draw_touch_menu(self, selected_item_index):
for menu_item in self.menu_view:
offset_y += len(self.ctx.display.to_lines(menu_item[0])) + 1
Page.y_keypad_map.append(offset_y)
- height_multiplier = self.ctx.display.height() - 2 * DEFAULT_PADDING
+ height_multiplier = (
+ self.ctx.display.height() - 2 * DEFAULT_PADDING - self.menu_offset
+ )
height_multiplier //= offset_y
Page.y_keypad_map = [
- n * height_multiplier + DEFAULT_PADDING for n in Page.y_keypad_map
+ n * height_multiplier + DEFAULT_PADDING + self.menu_offset
+ for n in Page.y_keypad_map
]
self.ctx.input.touch.y_regions = Page.y_keypad_map
@@ -711,11 +764,6 @@ def _draw_touch_menu(self, selected_item_index):
self.ctx.display.fill_rectangle(
0, y, self.ctx.display.width(), 1, theme.frame_color
)
- height = Page.y_keypad_map[i + 1] - y
- if selected_item_index == i and self.ctx.input.buttons_active:
- self.ctx.display.fill_rectangle(
- 0, y + 1, self.ctx.display.width(), height - 2, theme.fg_color
- )
# draw centralized strings in regions
for i, menu_item in enumerate(self.menu_view):
@@ -724,30 +772,46 @@ def _draw_touch_menu(self, selected_item_index):
offset_y -= len(menu_item_lines) * self.ctx.display.font_height
offset_y //= 2
offset_y += Page.y_keypad_map[i]
+ fg_color = (
+ theme.fg_color if menu_item[1] is not None else theme.disabled_color
+ )
for j, text in enumerate(menu_item_lines):
if selected_item_index == i and self.ctx.input.buttons_active:
+ self.ctx.display.fill_rectangle(
+ 0,
+ offset_y + 1 - self.ctx.display.font_height // 2,
+ self.ctx.display.width(),
+ (len(menu_item_lines) + 1) * self.ctx.display.font_height,
+ fg_color,
+ )
self.ctx.display.draw_hcentered_text(
text,
offset_y + self.ctx.display.font_height * j,
theme.bg_color,
- theme.fg_color,
+ fg_color,
)
else:
self.ctx.display.draw_hcentered_text(
- text, offset_y + self.ctx.display.font_height * j
+ text, offset_y + self.ctx.display.font_height * j, fg_color
)
def _draw_menu(self, selected_item_index):
- offset_y = len(self.menu_view) * 2
- extra_lines = 0
- for menu_item in self.menu_view:
- extra_lines += len(self.ctx.display.to_lines(menu_item[0])) - 1
- offset_y += extra_lines
- offset_y *= self.ctx.display.font_height
- offset_y = self.ctx.display.height() - offset_y
- offset_y //= 2
- offset_y += self.ctx.display.font_height // 2
+ if self.menu_offset:
+ offset_y = self.menu_offset + self.ctx.display.font_height // 2
+ else:
+ offset_y = len(self.menu_view) * 2
+ extra_lines = 0
+ for menu_item in self.menu_view:
+ extra_lines += len(self.ctx.display.to_lines(menu_item[0])) - 1
+ offset_y += extra_lines
+ offset_y *= self.ctx.display.font_height
+ offset_y = self.ctx.display.height() - offset_y
+ offset_y //= 2
+ offset_y += self.ctx.display.font_height // 2
for i, menu_item in enumerate(self.menu_view):
+ fg_color = (
+ theme.fg_color if menu_item[1] is not None else theme.disabled_color
+ )
menu_item_lines = self.ctx.display.to_lines(menu_item[0])
delta_y = (len(menu_item_lines) + 1) * self.ctx.display.font_height
if selected_item_index == i:
@@ -756,19 +820,18 @@ def _draw_menu(self, selected_item_index):
offset_y + 1 - self.ctx.display.font_height // 2,
self.ctx.display.width(),
delta_y - 2,
- theme.fg_color,
+ fg_color,
)
for j, text in enumerate(menu_item_lines):
self.ctx.display.draw_hcentered_text(
text,
offset_y + self.ctx.display.font_height * j,
theme.bg_color,
- theme.fg_color,
+ fg_color,
)
else:
for j, text in enumerate(menu_item_lines):
self.ctx.display.draw_hcentered_text(
- text,
- offset_y + self.ctx.display.font_height * j,
+ text, offset_y + self.ctx.display.font_height * j, fg_color
)
offset_y += delta_y
diff --git a/src/krux/pages/addresses.py b/src/krux/pages/addresses.py
index 3447d8ed7..f271c0f06 100644
--- a/src/krux/pages/addresses.py
+++ b/src/krux/pages/addresses.py
@@ -21,10 +21,10 @@
# THE SOFTWARE.
import gc
-import board
from ..krux_settings import t
from ..themes import theme
from ..qr import FORMAT_NONE
+from .utils import Utils
from . import (
Page,
Menu,
@@ -32,10 +32,6 @@
MENU_EXIT,
)
-LIST_ADDRESS_QTD = 4 # qtd of address per page
-LIST_ADDRESS_DIGITS = 8 # len on large devices per menu item
-LIST_ADDRESS_DIGITS_SMALL = 4 # len on small devices per menu item
-
SCAN_ADDRESS_LIMIT = 20
@@ -45,12 +41,13 @@ class Addresses(Page):
def __init__(self, ctx):
super().__init__(ctx, None)
self.ctx = ctx
+ self.utils = Utils(self.ctx)
def addresses_menu(self):
"""Handler for the 'address' menu item"""
# only show address for single-sig or multisig with wallet output descriptor loaded
if not self.ctx.wallet.is_loaded() and self.ctx.wallet.is_multisig():
- self.ctx.display.flash_text(
+ self.flash_text(
t("Please load a wallet output descriptor"), theme.error_color
)
return MENU_CONTINUE
@@ -58,10 +55,10 @@ def addresses_menu(self):
submenu = Menu(
self.ctx,
[
- ((t("Scan Address"), self.pre_scan_address)),
+ (t("Scan Address"), self.pre_scan_address),
(t("Receive Addresses"), self.list_address_type),
(t("Change Addresses"), lambda: self.list_address_type(1)),
- (t("Back"), lambda: MENU_EXIT),
+ (t("Back"), lambda: None),
],
)
submenu.run_loop()
@@ -71,52 +68,34 @@ def list_address_type(self, addr_type=0):
"""Handler for the 'receive addresses' or 'change addresses' menu item"""
# only show address for single-sig or multisig with wallet output descriptor loaded
if self.ctx.wallet.is_loaded() or not self.ctx.wallet.is_multisig():
- custom_start_digits = (
- LIST_ADDRESS_DIGITS + 3
- ) # 3 more because of bc1 address
- custom_end_digts = LIST_ADDRESS_DIGITS
- custom_separator = ". "
- if board.config["type"] == "m5stickv":
- custom_start_digits = (
- LIST_ADDRESS_DIGITS_SMALL + 3
- ) # 3 more because of bc1 address
- custom_end_digts = LIST_ADDRESS_DIGITS_SMALL
- custom_separator = " "
- start_digits = custom_start_digits
-
loading_txt = t("Loading receive address %d..")
if addr_type == 1:
loading_txt = t("Loading change address %d..")
+ max_addresses = self.ctx.display.max_lines() - 3
+
num_checked = 0
while True:
items = []
- if num_checked + 1 > LIST_ADDRESS_QTD:
+ if num_checked + 1 > max_addresses:
items.append(
(
- "%d..%d"
- % (num_checked - LIST_ADDRESS_QTD + 1, num_checked),
+ "%d..%d" % (num_checked - max_addresses + 1, num_checked),
lambda: MENU_EXIT,
)
)
for addr in self.ctx.wallet.obtain_addresses(
- num_checked, limit=LIST_ADDRESS_QTD, branch_index=addr_type
+ num_checked, limit=max_addresses, branch_index=addr_type
):
self.ctx.display.clear()
self.ctx.display.draw_centered_text(loading_txt % (num_checked + 1))
- if num_checked + 1 > 99:
- start_digits = custom_start_digits - 1
- pos_str = str(num_checked + 1)
- qr_title = pos_str + ". " + addr
+ pos_str = str(num_checked + 1) + "." + "β" # thin space
+ qr_title = pos_str + addr
items.append(
(
- pos_str
- + custom_separator
- + addr[:start_digits]
- + ".."
- + addr[len(addr) - custom_end_digts :],
+ self.fit_to_line(addr, pos_str, fixed_chars=3),
lambda address=addr, title=qr_title: self.show_address(
address, title
),
@@ -127,7 +106,7 @@ def list_address_type(self, addr_type=0):
items.append(
(
- "%d..%d" % (num_checked + 1, num_checked + LIST_ADDRESS_QTD),
+ "%d..%d" % (num_checked + 1, num_checked + max_addresses),
lambda: MENU_EXIT,
)
)
@@ -147,27 +126,27 @@ def list_address_type(self, addr_type=0):
if index == len(submenu.menu) - 2:
stay_on_this_addr_menu = False
# Prev
- if index == 0 and num_checked > LIST_ADDRESS_QTD:
+ if index == 0 and num_checked > max_addresses:
stay_on_this_addr_menu = False
- num_checked -= 2 * LIST_ADDRESS_QTD
+ num_checked -= 2 * max_addresses
return MENU_CONTINUE
- def show_address(self, addr, title="", qr_format=FORMAT_NONE):
+ def show_address(self, addr, title="", quick_exit=False):
"""Show addr provided as a QRCode"""
- self.display_qr_codes(addr, qr_format, title)
- if self.print_qr_prompt():
- from .print_page import PrintPage
+ from .qr_view import SeedQRView
- print_page = PrintPage(self.ctx)
- print_page.print_qr(addr, qr_format, title)
+ seed_qr_view = SeedQRView(self.ctx, data=addr, title=title)
+ seed_qr_view.display_qr(
+ allow_export=True, transcript_tools=False, quick_exit=quick_exit
+ )
return MENU_CONTINUE
def pre_scan_address(self):
"""Handler for the 'scan address' menu item"""
# only show address for single-sig or multisig with wallet output descriptor loaded
if not self.ctx.wallet.is_loaded() and self.ctx.wallet.is_multisig():
- self.ctx.display.flash_text(
+ self.flash_text(
t("Please load a wallet output descriptor"), theme.error_color
)
return MENU_CONTINUE
@@ -187,7 +166,7 @@ def scan_address(self, addr_type=0):
"""Handler for the 'receive' or 'change' menu item"""
data, qr_format = self.capture_qr_code()
if data is None or qr_format != FORMAT_NONE:
- self.ctx.display.flash_text(t("Failed to load address"), theme.error_color)
+ self.flash_text(t("Failed to load address"), theme.error_color)
return MENU_CONTINUE
addr = None
@@ -196,10 +175,10 @@ def scan_address(self, addr_type=0):
addr = parse_address(data)
except:
- self.ctx.display.flash_text(t("Invalid address"), theme.error_color)
+ self.flash_text(t("Invalid address"), theme.error_color)
return MENU_CONTINUE
- self.show_address(data, title=addr, qr_format=qr_format)
+ self.show_address(data, title=addr, quick_exit=True)
if self.ctx.wallet.is_loaded() or not self.ctx.wallet.is_multisig():
self.ctx.display.clear()
diff --git a/src/krux/pages/capture_entropy.py b/src/krux/pages/capture_entropy.py
new file mode 100644
index 000000000..1a5d6bbdd
--- /dev/null
+++ b/src/krux/pages/capture_entropy.py
@@ -0,0 +1,164 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import math
+from . import Page
+
+POOR_VARIANCE_TH = 10
+INSUFFICIENT_VARIANCE_TH = 5 # %
+INSUFFICIENT_SHANNONS_ENTROPY_TH = 3 # bits per pixel
+SHANNONS_BLOCK_SIZE = 0x4000 # 16384B, result in ~10 blocks for a QVGA img
+
+
+class CameraEntropy(Page):
+ """Class for capturing entropy from a snapshot"""
+
+ def __init__(self, ctx):
+ super().__init__(ctx, None)
+ self.ctx = ctx
+
+ def _callback(self):
+ # Accepted
+ if self.ctx.input.enter_event() or self.ctx.input.touch_event():
+ return 1
+
+ # Exited
+ if self.ctx.input.page_event() or self.ctx.input.page_prev_event():
+ return 2
+ return 0
+
+ def capture(self):
+ """Captures camera's entropy as the hash of image buffer"""
+ import hashlib
+ import board
+ import gc
+ import sensor
+ import lcd
+ from ..wdt import wdt
+ from ..krux_settings import t
+ from ..themes import theme
+ import shannon
+
+ def rms_value(data):
+ if not data:
+ return 0
+ square_sum = sum(x**2 for x in data)
+ mean_square = square_sum / len(data)
+ rms = math.sqrt(mean_square)
+ return int(rms)
+
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(t("TOUCH or ENTER to capture"))
+ self.ctx.display.to_landscape()
+ self.ctx.camera.initialize_sensor()
+ sensor.run(1)
+ self.ctx.display.clear()
+ command = 0
+ y_label_offset = self.ctx.display.bottom_line
+ if board.config["type"].startswith("amigo"):
+ y_label_offset = self.ctx.display.bottom_prompt_line
+ while True:
+ wdt.feed()
+ img = sensor.snapshot()
+ stdev_index = rms_value(
+ [
+ img.get_statistics().l_stdev(),
+ img.get_statistics().a_stdev(),
+ img.get_statistics().b_stdev(),
+ ]
+ )
+ self.ctx.display.to_portrait()
+ self.ctx.display.fill_rectangle(
+ 0,
+ y_label_offset,
+ self.ctx.display.width(),
+ self.ctx.display.font_height,
+ theme.bg_color,
+ )
+ if stdev_index > POOR_VARIANCE_TH:
+ self.ctx.display.draw_hcentered_text(
+ "Good entropy", y_label_offset, theme.go_color
+ )
+ elif stdev_index > INSUFFICIENT_VARIANCE_TH:
+ self.ctx.display.draw_hcentered_text(
+ "Poor entropy", y_label_offset, theme.del_color
+ )
+ else:
+ self.ctx.display.draw_hcentered_text(
+ "Insufficient entropy", y_label_offset, theme.error_color
+ )
+ self.ctx.display.to_landscape()
+ command = self._callback()
+ if command > 0:
+ break
+
+ if board.config["type"] == "m5stickv":
+ img.lens_corr(strength=1.0, zoom=0.56)
+ lcd.display(img, oft=(0, 0), roi=(68, 52, 185, 135))
+ elif board.config["type"].startswith("amigo"):
+ lcd.display(img, oft=(40, 40))
+ else:
+ lcd.display(img, oft=(0, 0), roi=(0, 0, 304, 240))
+
+ self.ctx.display.to_portrait()
+ gc.collect()
+ sensor.run(0)
+
+ # User cancelled
+ if command == 2:
+ self.flash_text(t("Capture cancelled"))
+ return None
+
+ self.ctx.display.draw_centered_text(t("Calculating Shannon's entropy"))
+
+ img_bytes = img.to_bytes()
+ del img
+
+ # Calculate Shannon's entropy:
+ shannon_16b = shannon.entropy_img16b(img_bytes)
+ shannon_16b_total = shannon_16b * 320 * 240
+
+ entropy_msg = "Shannon's entropy:\n"
+ entropy_msg += str(round(shannon_16b, 2)) + " bits/px\n"
+ entropy_msg += "(" + str(int(shannon_16b_total)) + " total)\n\n"
+ entropy_msg += "Pixels deviation index: "
+ entropy_msg += str(stdev_index)
+ self.ctx.display.clear()
+ if (
+ shannon_16b < INSUFFICIENT_SHANNONS_ENTROPY_TH
+ or stdev_index < INSUFFICIENT_VARIANCE_TH
+ ):
+ error_msg = t("Insufficient Entropy!")
+ error_msg += "\n\n"
+ error_msg += entropy_msg
+ self.ctx.display.draw_centered_text(error_msg, theme.error_color)
+ self.ctx.input.wait_for_button()
+ return None
+ self.ctx.display.draw_centered_text(entropy_msg)
+ self.ctx.input.wait_for_button()
+ hasher = hashlib.sha256()
+ image_len = len(img_bytes)
+ hasher_index = 0
+ while hasher_index < image_len:
+ hasher.update(img_bytes[hasher_index : hasher_index + 128])
+ hasher_index += 128
+ return hasher.digest()
diff --git a/src/krux/pages/encryption_ui.py b/src/krux/pages/encryption_ui.py
index eb93ae298..fd2be65cd 100644
--- a/src/krux/pages/encryption_ui.py
+++ b/src/krux/pages/encryption_ui.py
@@ -55,7 +55,7 @@ def encryption_key(self):
)
_, key = submenu.run_loop()
if key in (ESC_KEY, MENU_CONTINUE):
- return None
+ return ""
if key:
self.ctx.display.clear()
@@ -78,10 +78,10 @@ def load_qr_encryption_key(self):
"""Loads and returns a key from a QR code"""
data, _ = self.capture_qr_code()
if data is None:
- self.ctx.display.flash_text(t("Failed to load key"), theme.error_color)
+ self.flash_text(t("Failed to load key"), theme.error_color)
return None
if len(data) > ENCRYPTION_KEY_MAX_LEN:
- self.ctx.display.flash_text(
+ self.flash_text(
t("Maximum length exceeded (%s)") % ENCRYPTION_KEY_MAX_LEN,
theme.error_color,
)
@@ -99,23 +99,19 @@ def __init__(self, ctx):
def encrypt_menu(self):
"""Menu with mnemonic encryption output options"""
- from ..encryption import MnemonicStorage
+ def _sd_store_function():
+ return self.store_mnemonic_on_memory(sd_card=True)
- encrypt_outputs_menu = []
- encrypt_outputs_menu.append(
- (t("Store on Flash"), self.store_mnemonic_on_memory)
- )
- mnemonic_storage = MnemonicStorage()
- if mnemonic_storage.has_sd_card:
- encrypt_outputs_menu.append(
- (
- t("Store on SD Card"),
- lambda: self.store_mnemonic_on_memory(sd_card=True),
- )
- )
- del mnemonic_storage
- encrypt_outputs_menu.append((t("Encrypted QR Code"), self.encrypted_qr_code))
- encrypt_outputs_menu.append((t("Back"), lambda: MENU_EXIT))
+ if self.has_sd_card():
+ sd_store_func = _sd_store_function
+ else:
+ sd_store_func = None
+ encrypt_outputs_menu = [
+ (t("Store on Flash"), self.store_mnemonic_on_memory),
+ (t("Store on SD Card"), sd_store_func),
+ (t("Encrypted QR Code"), self.encrypted_qr_code),
+ (t("Back"), lambda: MENU_EXIT),
+ ]
submenu = Menu(self.ctx, encrypt_outputs_menu)
_, _ = submenu.run_loop()
return MENU_CONTINUE
@@ -127,7 +123,7 @@ def store_mnemonic_on_memory(self, sd_card=False):
key_capture = EncryptionKey(self.ctx)
key = key_capture.encryption_key()
if key is None:
- self.ctx.display.flash_text(t("Mnemonic was not encrypted"))
+ self.flash_text(t("Mnemonic was not encrypted"))
return
version = Settings().encryption.version
@@ -135,7 +131,7 @@ def store_mnemonic_on_memory(self, sd_card=False):
if version == "AES-CBC":
self.ctx.display.clear()
self.ctx.display.draw_centered_text(
- t("Aditional entropy from camera required for AES-CBC mode")
+ t("Additional entropy from camera required for AES-CBC mode")
)
if not self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line):
return
@@ -156,7 +152,7 @@ def store_mnemonic_on_memory(self, sd_card=False):
if mnemonic_id in (None, ESC_KEY):
mnemonic_id = self.ctx.wallet.key.fingerprint_hex_str()
if mnemonic_id in mnemonic_storage.list_mnemonics(sd_card):
- self.ctx.display.flash_text(
+ self.flash_text(
t("ID already exists\n") + t("Encrypted mnemonic was not stored")
)
del mnemonic_storage
@@ -181,7 +177,7 @@ def encrypted_qr_code(self):
key_capture = EncryptionKey(self.ctx)
key = key_capture.encryption_key()
if key is None:
- self.ctx.display.flash_text(t("Mnemonic was not encrypted"))
+ self.flash_text(t("Mnemonic was not encrypted"))
return
version = Settings().encryption.version
i_vector = None
@@ -191,7 +187,7 @@ def encrypted_qr_code(self):
t("Aditional entropy from camera required for AES-CBC mode")
)
if not self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line):
- self.ctx.display.flash_text(t("Mnemonic was not encrypted"))
+ self.flash_text(t("Mnemonic was not encrypted"))
return
i_vector = self.capture_camera_entropy()[:AES_BLOCK_SIZE]
mnemonic_id = None
@@ -222,7 +218,7 @@ def encrypted_qr_code(self):
from .qr_view import SeedQRView
seed_qr_view = SeedQRView(self.ctx, data=qr_data, title=mnemonic_id)
- seed_qr_view.display_seed_qr()
+ seed_qr_view.display_qr(allow_export=True)
class LoadEncryptedMnemonic(Page):
@@ -277,20 +273,21 @@ def _load_encrypted_mnemonic(self, mnemonic_id, sd_card=False):
key_capture = EncryptionKey(self.ctx)
key = key_capture.encryption_key()
- if key is None:
- raise ValueError(t("Failed to decrypt"))
+ if key in (None, "", ESC_KEY):
+ self.flash_text(t("Key was not provided"), theme.error_color)
+ return MENU_CONTINUE
self.ctx.display.clear()
self.ctx.display.draw_centered_text(t("Processing ..."))
- if key in ("", ESC_KEY):
- raise ValueError(t("Failed to decrypt"))
mnemonic_storage = MnemonicStorage()
try:
words = mnemonic_storage.decrypt(key, mnemonic_id, sd_card).split()
except:
- raise ValueError(t("Failed to decrypt"))
+ self.flash_text(t("Failed to decrypt"), theme.error_color)
+ return MENU_CONTINUE
if len(words) not in (12, 24):
- raise ValueError(t("Failed to decrypt"))
+ self.flash_text(t("Failed to decrypt"), theme.error_color)
+ return MENU_CONTINUE
del mnemonic_storage
return words
@@ -300,6 +297,6 @@ def _delete_encrypted_mnemonic(self, mnemonic_id, sd_card=False):
mnemonic_storage = MnemonicStorage()
self.ctx.display.clear()
- if self.prompt(t("Delete %s?" % mnemonic_id), self.ctx.display.height() // 2):
+ if self.prompt(t("Delete %s?") % mnemonic_id, self.ctx.display.height() // 2):
mnemonic_storage.del_mnemonic(mnemonic_id, sd_card)
del mnemonic_storage
diff --git a/src/krux/pages/files_manager.py b/src/krux/pages/files_manager.py
index dd4d9a0a8..d012a5e80 100644
--- a/src/krux/pages/files_manager.py
+++ b/src/krux/pages/files_manager.py
@@ -65,12 +65,19 @@ def select_file(
dir_files = os.listdir(path)
for filename in dir_files:
- # only include files that match extension and directories
+ extension_match = False
+ if isinstance(file_extension, str):
+ # No extension filter or matches
+ extension_match = filename.endswith(file_extension)
+ else:
+ # Check for any matches for tuple / list
+ for ext in file_extension:
+ if filename.endswith(ext):
+ extension_match = True
+ break
+
if (
- # No extension filter
- file_extension == ""
- # Matches filter
- or filename.endswith(file_extension)
+ extension_match
# Is a directory
or SDHandler.dir_exists(path + "/" + filename)
):
diff --git a/src/krux/pages/files_operations.py b/src/krux/pages/files_operations.py
new file mode 100644
index 000000000..050a2d3aa
--- /dev/null
+++ b/src/krux/pages/files_operations.py
@@ -0,0 +1,138 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from . import (
+ Page,
+ ESC_KEY,
+ LETTERS,
+ UPPERCASE_LETTERS,
+)
+from ..krux_settings import t
+from ..sd_card import SDHandler
+
+FILE_SPECIAL = "0123456789()-.[]_~"
+
+
+class SaveFile(Page):
+ """File saver user interface"""
+
+ def __init__(self, ctx):
+ super().__init__(ctx, None)
+
+ def save_file(
+ self,
+ data,
+ empty_name,
+ filename="",
+ file_description="",
+ file_extension="",
+ file_suffix="",
+ save_as_binary=True,
+ ):
+ """File saver handler page"""
+ try:
+ with SDHandler() as sd:
+ # Wait until user defines a filename or select NO on the prompt
+ filename_undefined = True
+ while filename_undefined:
+ self.ctx.display.clear()
+ if self.prompt(
+ file_description + "\n" + t("Save to SD card?") + "\n\n",
+ self.ctx.display.height() // 2,
+ ):
+ filename, filename_undefined = self.set_filename(
+ filename,
+ empty_name,
+ file_suffix,
+ file_extension,
+ )
+
+ # if user defined a filename and it is ok, save!
+ if not filename_undefined:
+ if save_as_binary:
+ sd.write_binary(filename, data)
+ else:
+ sd.write(filename, data)
+ self.flash_text(t("Saved to SD card:\n%s") % filename)
+ else:
+ filename_undefined = False
+ except:
+ self.flash_text(t("SD card not detected."))
+
+ def set_filename(
+ self, curr_filename="", empty_filename="some_file", suffix="", file_extension=""
+ ):
+ """Helper to set the filename based on a suggestion and the user input"""
+ started_filename = curr_filename
+ filename_undefined = True
+
+ # remove the file_extension if exists
+ curr_filename = (
+ curr_filename[: len(curr_filename) - len(file_extension)]
+ if curr_filename.endswith(file_extension)
+ else curr_filename
+ )
+
+ # remove the suffix if exists (because we will add it later)
+ curr_filename = (
+ curr_filename[: len(curr_filename) - len(suffix)]
+ if curr_filename.endswith(suffix)
+ else curr_filename
+ )
+
+ curr_filename = self.capture_from_keypad(
+ t("Filename"),
+ [LETTERS, UPPERCASE_LETTERS, FILE_SPECIAL],
+ starting_buffer=("%s" + suffix) % curr_filename
+ if curr_filename
+ else empty_filename + suffix,
+ )
+
+ # Verify if user defined a filename and it is not just dots
+ if (
+ curr_filename
+ and curr_filename != ESC_KEY
+ and not all(c in "." for c in curr_filename)
+ ):
+ # add the extension ".psbt"
+ curr_filename = (
+ curr_filename
+ if curr_filename.endswith(file_extension)
+ else curr_filename + file_extension
+ )
+ # check and warn for overwrite filename
+ # add the "/sd/" prefix
+ if SDHandler.file_exists("/sd/" + curr_filename):
+ self.ctx.display.clear()
+ if self.prompt(
+ t("Filename %s exists on SD card, overwrite?") % curr_filename
+ + "\n\n",
+ self.ctx.display.height() // 2,
+ ):
+ filename_undefined = False
+ else:
+ filename_undefined = False
+
+ if curr_filename == ESC_KEY:
+ curr_filename = started_filename
+
+ return (curr_filename, filename_undefined)
diff --git a/src/krux/pages/home.py b/src/krux/pages/home.py
index 05ed46fb2..78f844041 100644
--- a/src/krux/pages/home.py
+++ b/src/krux/pages/home.py
@@ -21,34 +21,24 @@
# THE SOFTWARE.
import gc
+from .utils import Utils
from ..themes import theme
from ..display import DEFAULT_PADDING
from ..psbt import PSBTSigner
from ..qr import FORMAT_NONE, FORMAT_PMOFN
-from ..krux_settings import t, Settings, THERMAL_ADAFRUIT_TXT
+from ..krux_settings import t
from . import (
Page,
Menu,
MENU_CONTINUE,
MENU_EXIT,
- ESC_KEY,
- LETTERS,
- UPPERCASE_LETTERS,
)
-from ..sd_card import SDHandler
-
-# to start xpub value without the xpub/zpub/ypub prefix
-WALLET_XPUB_START = 4
-# len of the xpub to show
-WALLET_XPUB_DIGITS = 4
-
-FILE_SPECIAL = "0123456789()-.[]_~"
-
-PSBT_FILE_SUFFIX = "-signed"
-PSBT_FILE_EXTENSION = ".psbt"
-PUBKEY_FILE_EXTENSION = ".pub"
-SIGNATURE_FILE_EXTENSION = ".sig"
-SIGNATURE_FILE_SUFFIX = PSBT_FILE_SUFFIX
+from ..sd_card import (
+ PSBT_FILE_EXTENSION,
+ SIGNED_FILE_SUFFIX,
+ DESCRIPTOR_FILE_EXTENSION,
+ JSON_FILE_EXTENSION,
+)
class Home(Page):
@@ -70,98 +60,14 @@ def __init__(self, ctx):
],
),
)
+ self.utils = Utils(self.ctx)
def mnemonic(self):
"""Handler for the 'mnemonic' menu item"""
- submenu = Menu(
- self.ctx,
- [
- (t("Words"), self.display_mnemonic_words),
- (t("Plaintext QR"), self.display_standard_qr),
- (t("Compact SeedQR"), lambda: self.display_seed_qr(True)),
- (t("SeedQR"), self.display_seed_qr),
- (t("Stackbit 1248"), self.stackbit),
- (t("Tiny Seed"), self.tiny_seed),
- (t("Back"), lambda: MENU_EXIT),
- ],
- )
- submenu.run_loop()
- return MENU_CONTINUE
-
- def display_mnemonic_words(self):
- """Displays only the mnemonic words"""
- self.display_mnemonic(self.ctx.wallet.key.mnemonic)
- self.ctx.input.wait_for_button()
-
- # Avoid printing text on a cnc
- if Settings().printer.driver == THERMAL_ADAFRUIT_TXT:
- self.ctx.display.clear()
- if self.prompt(
- t("Print?\n\n%s\n\n") % Settings().printer.driver,
- self.ctx.display.height() // 2,
- ):
- from .print_page import PrintPage
-
- print_page = PrintPage(self.ctx)
- print_page.print_mnemonic_text()
- return MENU_CONTINUE
-
- def display_standard_qr(self):
- """Displays regular words QR code"""
- title = t("Plaintext QR")
- data = self.ctx.wallet.key.mnemonic
- self.display_qr_codes(data, FORMAT_NONE, title)
- self.print_standard_qr(data, FORMAT_NONE, title)
- return MENU_CONTINUE
-
- def display_seed_qr(self, binary=False):
- """Display Seed QR with with different view modes"""
-
- from .qr_view import SeedQRView
-
- seed_qr_view = SeedQRView(self.ctx, binary)
- return seed_qr_view.display_seed_qr()
+ from .mnemonic_view import MnemonicsView
- def stackbit(self):
- """Displays which numbers 1248 user should punch on 1248 steel card"""
- from .stack_1248 import Stackbit
-
- stackbit = Stackbit(self.ctx)
- word_index = 1
- words = self.ctx.wallet.key.mnemonic.split(" ")
-
- while word_index < len(words):
- y_offset = 2 * self.ctx.display.font_height
- for _ in range(6):
- stackbit.export_1248(word_index, y_offset, words[word_index - 1])
- if self.ctx.display.height() > 240:
- y_offset += 3 * self.ctx.display.font_height
- else:
- y_offset += 5 + 2 * self.ctx.display.font_height
- word_index += 1
- self.ctx.input.wait_for_button()
-
- # removed the hability to go back in favor or the Krux UI patter (always move forward)
- # if self.ctx.input.wait_for_button() == BUTTON_PAGE_PREV:
- # if word_index > 12:
- # word_index -= 12
- # else:
- # word_index = 1
- self.ctx.display.clear()
- return MENU_CONTINUE
-
- def tiny_seed(self):
- """Displays the seed in Tiny Seed format"""
- from .tiny_seed import TinySeed
-
- tiny_seed = TinySeed(self.ctx)
- tiny_seed.export()
-
- # Allow to print on thermal printer only
- if Settings().printer.driver == THERMAL_ADAFRUIT_TXT:
- if self.print_qr_prompt():
- tiny_seed.print_tiny_seed()
- return MENU_CONTINUE
+ mnemonics_viewer = MnemonicsView(self.ctx)
+ return mnemonics_viewer.mnemonic()
def encrypt_mnemonic(self):
"""Handler for Mnemonic > Encrypt Mnemonic menu item"""
@@ -172,40 +78,10 @@ def encrypt_mnemonic(self):
def public_key(self):
"""Handler for the 'xpub' menu item"""
- zpub = "Zpub" if self.ctx.wallet.key.multisig else "zpub"
- for version in [None, self.ctx.wallet.key.network[zpub]]:
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(
- self.ctx.wallet.key.fingerprint_hex_str(True)
- + "\n\n"
- + self.ctx.wallet.key.derivation_str(True)
- + "\n\n"
- + self.ctx.wallet.key.account_pubkey_str(version)
- )
- self.ctx.input.wait_for_button()
+ from .pub_key_view import PubkeyView
- # title receives first 4 chars (ex: XPUB)
- title = self.ctx.wallet.key.account_pubkey_str(version)[
- :WALLET_XPUB_START
- ].upper()
- xpub = self.ctx.wallet.key.key_expression(version)
- self.display_qr_codes(xpub, FORMAT_NONE, title)
- self.print_standard_qr(xpub, FORMAT_NONE, title)
-
- # Try to save the XPUB file on the SD card
- try:
- self._save_file(
- xpub,
- title,
- title,
- title + ":",
- PUBKEY_FILE_EXTENSION,
- save_as_binary=False,
- )
- except OSError:
- pass
-
- return MENU_CONTINUE
+ pubkey_viewer = PubkeyView(self.ctx)
+ return pubkey_viewer.public_key()
def wallet(self):
"""Handler for the 'wallet' menu item"""
@@ -219,17 +95,39 @@ def wallet(self):
else:
self.display_wallet(self.ctx.wallet)
wallet_data, qr_format = self.ctx.wallet.wallet_qr()
- self.print_standard_qr(
- wallet_data, qr_format, t("Wallet output descriptor")
- )
+ title = t("Wallet output descriptor")
+ self.utils.print_standard_qr(wallet_data, qr_format, title)
+
+ # Try to save the Wallet output descriptor on the SD card
+ if self.has_sd_card():
+ from .files_operations import SaveFile
+
+ save_page = SaveFile(self.ctx)
+ save_page.save_file(
+ self.ctx.wallet.descriptor.to_string(),
+ self.ctx.wallet.label,
+ self.ctx.wallet.label,
+ title + ":",
+ DESCRIPTOR_FILE_EXTENSION,
+ save_as_binary=False,
+ )
return MENU_CONTINUE
def _load_wallet(self):
wallet_data, qr_format = self.capture_qr_code()
if wallet_data is None:
- self.ctx.display.flash_text(
- t("Failed to load output descriptor"), theme.error_color
- )
+ # Try to read the wallet output descriptor from a file on the SD card
+ qr_format = FORMAT_NONE
+ try:
+ _, wallet_data = self.utils.load_file(
+ (DESCRIPTOR_FILE_EXTENSION, JSON_FILE_EXTENSION)
+ )
+ except OSError:
+ pass
+
+ if wallet_data is None:
+ # Both the camera and the file on SD card failed!
+ self.flash_text(t("Failed to load output descriptor"), theme.error_color)
return MENU_CONTINUE
try:
@@ -245,7 +143,7 @@ def _load_wallet(self):
"Wallet output descriptor: %s"
% self.ctx.wallet.descriptor.to_string()
)
- self.ctx.display.flash_text(t("Wallet output descriptor loaded!"))
+ self.flash_text(t("Wallet output descriptor loaded!"))
# BlueWallet single sig descriptor without fingerprint
if (
@@ -254,7 +152,8 @@ def _load_wallet(self):
):
self.ctx.display.clear()
self.ctx.display.draw_centered_text(
- t("Warning:\nIncomplete output descriptor"), theme.error_color
+ t("Warning:") + "\n" + t("Incomplete output descriptor"),
+ theme.error_color,
)
self.ctx.input.wait_for_button()
@@ -273,7 +172,7 @@ def addresses_menu(self):
from .addresses import Addresses
adresses = Addresses(self.ctx)
- adresses.addresses_menu()
+ return adresses.addresses_menu()
def sign(self):
"""Handler for the 'sign' menu item"""
@@ -296,10 +195,11 @@ def sign_psbt(self):
# Warns in case multisig wallet descriptor is not loaded
if not self.ctx.wallet.is_loaded() and self.ctx.wallet.is_multisig():
self.ctx.display.draw_centered_text(
- t(
- """Warning:\nWallet output descriptor not found.\n\n
- Some checks cannot be performed."""
- )
+ t("Warning:")
+ + "\n"
+ + t("Wallet output descriptor not found.")
+ + "\n\n"
+ + t("Some checks cannot be performed.")
)
if not self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line):
return MENU_CONTINUE
@@ -312,13 +212,13 @@ def sign_psbt(self):
# Try to read a PSBT from a file on the SD card
qr_format = FORMAT_NONE
try:
- psbt_filename, data = self._load_file(PSBT_FILE_EXTENSION)
+ psbt_filename, data = self.utils.load_file(PSBT_FILE_EXTENSION)
except OSError:
pass
if data is None:
# Both the camera and the file on SD card failed!
- self.ctx.display.flash_text(t("Failed to load PSBT"), theme.error_color)
+ self.flash_text(t("Failed to load PSBT"), theme.error_color)
return MENU_CONTINUE
# PSBT read OK! Will try to sign
@@ -356,7 +256,7 @@ def sign_psbt(self):
title = t("Signed PSBT")
try:
self.display_qr_codes(qr_signed_psbt, qr_format)
- self.print_standard_qr(qr_signed_psbt, qr_format, title, width=45)
+ self.utils.print_standard_qr(qr_signed_psbt, qr_format, title, width=45)
except Exception as e:
self.ctx.log.exception(
"Exception occurred in sign_psbt when trying to show the qr_signed_psbt"
@@ -372,249 +272,26 @@ def sign_psbt(self):
gc.collect()
# Try to save the signed PSBT file on the SD card
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(t("Checking for SD card.."))
- try:
- self._save_file(
+ if self.has_sd_card():
+ from .files_operations import SaveFile
+
+ save_page = SaveFile(self.ctx)
+ save_page.save_file(
serialized_signed_psbt,
"QRCode",
psbt_filename,
title + ":",
PSBT_FILE_EXTENSION,
- PSBT_FILE_SUFFIX,
+ SIGNED_FILE_SUFFIX,
)
- except OSError:
- pass
-
return MENU_CONTINUE
- def _load_file(self, file_ext=""):
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(t("Checking for SD card.."))
- with SDHandler() as sd:
- self.ctx.display.clear()
- if self.prompt(
- t("Load from SD card?") + "\n\n", self.ctx.display.height() // 2
- ):
- from .files_manager import FileManager
-
- file_manager = FileManager(self.ctx)
- filename = file_manager.select_file(file_extension=file_ext)
-
- if filename:
- filename = file_manager.display_file(filename)
-
- if self.prompt(t("Load?"), self.ctx.display.bottom_prompt_line):
- return filename, sd.read_binary(filename)
- return "", None
-
def sign_message(self):
"""Handler for the 'sign message' menu item"""
+ from .sign_message_ui import SignMessage
- import binascii
- import hashlib
- from ..baseconv import base_encode
-
- # Try to read a message from camera
- message_filename = ""
- data, qr_format = self.capture_qr_code()
-
- if data is None:
- # Try to read a message from a file on the SD card
- qr_format = FORMAT_NONE
- try:
- message_filename, data = self._load_file()
- except OSError:
- pass
-
- if data is None:
- self.ctx.display.flash_text(t("Failed to load message"), theme.error_color)
- return MENU_CONTINUE
-
- # message read OK!
- data = data.encode() if isinstance(data, str) else data
-
- message_hash = None
- if len(data) == 32:
- # It's a sha256 hash already
- message_hash = data
- else:
- if len(data) == 64:
- # It may be a hex-encoded sha256 hash
- try:
- message_hash = binascii.unhexlify(data)
- except:
- pass
- if message_hash is None:
- # It's a message, so compute its sha256 hash
- message_hash = hashlib.sha256(data).digest()
-
- # memory management
- del data
- gc.collect()
-
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(
- t("SHA256:\n%s") % binascii.hexlify(message_hash).decode()
- )
- if not self.prompt(t("Sign?"), self.ctx.display.bottom_prompt_line):
- return MENU_CONTINUE
-
- # User confirmed to sign!
- sig = self.ctx.wallet.key.sign(message_hash).serialize()
-
- # Encode sig as base64 string
- encoded_sig = base_encode(sig, 64).decode()
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(t("Signature") + ":\n\n%s" % encoded_sig)
- self.ctx.input.wait_for_button()
-
- # Show the base64 signed message as a QRCode
- title = t("Signed Message")
- self.display_qr_codes(encoded_sig, qr_format, title)
- self.print_standard_qr(encoded_sig, qr_format, title)
-
- # memory management
- del encoded_sig
- gc.collect()
-
- # Show the public key as a QRCode
- pubkey = binascii.hexlify(self.ctx.wallet.key.account.sec()).decode()
- self.ctx.display.clear()
-
- title = t("Hex Public Key")
- self.ctx.display.draw_centered_text(title + ":\n\n%s" % pubkey)
- self.ctx.input.wait_for_button()
-
- # Show the public key in hexadecimal format as a QRCode
- self.display_qr_codes(pubkey, qr_format, title)
- self.print_standard_qr(pubkey, qr_format, title)
-
- # memory management
- gc.collect()
-
- # Try to save the signature file on the SD card
- try:
- self._save_file(
- sig,
- "message",
- message_filename,
- t("Signature") + ":",
- SIGNATURE_FILE_EXTENSION,
- SIGNATURE_FILE_SUFFIX,
- )
- except OSError:
- pass
-
- # Try to save the public key on the SD card
- try:
- self._save_file(
- pubkey, "pubkey", "", title + ":", PUBKEY_FILE_EXTENSION, "", False
- )
- except OSError:
- pass
-
- return MENU_CONTINUE
-
- def _save_file(
- self,
- data,
- empty_name,
- filename="",
- file_description="",
- file_extension="",
- file_suffix="",
- save_as_binary=True,
- ):
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(t("Checking for SD card.."))
- with SDHandler() as sd:
- # Wait until user defines a filename or select NO on the prompt
- filename_undefined = True
- while filename_undefined:
- self.ctx.display.clear()
- if self.prompt(
- file_description + "\n" + t("Save to SD card?") + "\n\n",
- self.ctx.display.height() // 2,
- ):
- filename, filename_undefined = self._set_filename(
- filename,
- empty_name,
- file_suffix,
- file_extension,
- )
-
- # if user defined a filename and it is ok, save!
- if not filename_undefined:
- if save_as_binary:
- sd.write_binary(filename, data)
- else:
- sd.write(filename, data)
- self.ctx.display.clear()
- self.ctx.display.flash_text(
- t("Saved to SD card:\n%s") % filename
- )
- else:
- filename_undefined = False
-
- def _set_filename(
- self, curr_filename="", empty_filename="some_file", suffix="", file_extension=""
- ):
- """Helper to set the filename based on a suggestion and the user input"""
- started_filename = curr_filename
- filename_undefined = True
-
- # remove the file_extension if exists
- curr_filename = (
- curr_filename[: len(curr_filename) - len(file_extension)]
- if curr_filename.endswith(file_extension)
- else curr_filename
- )
-
- # remove the suffix if exists (because we will add it later)
- curr_filename = (
- curr_filename[: len(curr_filename) - len(suffix)]
- if curr_filename.endswith(suffix)
- else curr_filename
- )
-
- curr_filename = self.capture_from_keypad(
- t("Filename"),
- [LETTERS, UPPERCASE_LETTERS, FILE_SPECIAL],
- starting_buffer=("%s" + suffix) % curr_filename
- if curr_filename
- else empty_filename + suffix,
- )
-
- # Verify if user defined a filename and it is not just dots
- if (
- curr_filename
- and curr_filename != ESC_KEY
- and not all(c in "." for c in curr_filename)
- ):
- # add the extension ".psbt"
- curr_filename = (
- curr_filename
- if curr_filename.endswith(file_extension)
- else curr_filename + file_extension
- )
- # check and warn for overwrite filename
- # add the "/sd/" prefix
- if SDHandler.file_exists("/sd/" + curr_filename):
- self.ctx.display.clear()
- if self.prompt(
- t("Filename %s exists on SD card, overwrite?") % curr_filename
- + "\n\n",
- self.ctx.display.height() // 2,
- ):
- filename_undefined = False
- else:
- filename_undefined = False
-
- if curr_filename == ESC_KEY:
- curr_filename = started_filename
-
- return (curr_filename, filename_undefined)
+ message_signer = SignMessage(self.ctx)
+ return message_signer.sign_message()
def display_wallet(self, wallet, include_qr=True):
"""Displays a wallet, including its label and abbreviated xpubs.
@@ -622,35 +299,51 @@ def display_wallet(self, wallet, include_qr=True):
which will contain the same data as was originally loaded, in
the same QR format
"""
- about = wallet.label + "\n"
+ about = [wallet.label]
if wallet.is_multisig():
- xpubs = []
- for i, xpub in enumerate(wallet.policy["cosigners"]):
- xpubs.append(
- str(i + 1)
- + ". "
- + xpub[WALLET_XPUB_START : WALLET_XPUB_START + WALLET_XPUB_DIGITS]
- + ".."
- + xpub[len(xpub) - WALLET_XPUB_DIGITS :]
+ import binascii
+
+ fingerprints = []
+ for i, key in enumerate(wallet.descriptor.keys):
+ fingerprints.append(
+ str(i + 1) + ". " + binascii.hexlify(key.fingerprint).decode()
)
- about += "\n".join(xpubs)
+ about.extend(fingerprints)
else:
+ about.append(wallet.key.fingerprint_hex_str())
xpub = wallet.key.xpub()
- about += (
- xpub[WALLET_XPUB_START : WALLET_XPUB_START + WALLET_XPUB_DIGITS]
- + ".."
- + xpub[len(xpub) - WALLET_XPUB_DIGITS :]
- )
- if include_qr:
+ about.append(self.fit_to_line(xpub))
+
+ if not wallet.is_multisig() and include_qr:
wallet_data, qr_format = wallet.wallet_qr()
self.display_qr_codes(wallet_data, qr_format, title=about)
else:
self.ctx.display.draw_hcentered_text(about, offset_y=DEFAULT_PADDING)
- def print_standard_qr(self, data, qr_format, title="", width=33):
- """Loads printer driver and UI"""
- if self.print_qr_prompt():
- from .print_page import PrintPage
+ # If multisig, show loaded wallet again with all XPUB
+ if wallet.is_multisig():
+ about = [wallet.label]
+ xpubs = []
+ for i, xpub in enumerate(wallet.policy["cosigners"]):
+ xpubs.append(self.fit_to_line(xpub, str(i + 1) + ". "))
+ about.extend(xpubs)
- print_page = PrintPage(self.ctx)
- print_page.print_qr(data, qr_format, title, width)
+ if include_qr:
+ self.ctx.input.wait_for_button()
+ self.ctx.display.clear()
+ self.ctx.display.draw_hcentered_text(about, offset_y=DEFAULT_PADDING)
+ self.ctx.input.wait_for_button()
+
+ # Try to show the wallet output descriptor as a QRCode
+ try:
+ wallet_data, qr_format = wallet.wallet_qr()
+ self.display_qr_codes(wallet_data, qr_format, title=wallet.label)
+ except Exception as e:
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(
+ t("Error:\n%s") % repr(e), theme.error_color
+ )
+ self.ctx.input.wait_for_button()
+ else:
+ self.ctx.input.wait_for_button()
+ self.ctx.display.draw_hcentered_text(about, offset_y=DEFAULT_PADDING)
diff --git a/src/krux/pages/login.py b/src/krux/pages/login.py
index 0d5a86ff6..5e18e215f 100644
--- a/src/krux/pages/login.py
+++ b/src/krux/pages/login.py
@@ -24,6 +24,7 @@
from embit.networks import NETWORKS
from embit.wordlists.bip39 import WORDLIST
from embit import bip39
+from .utils import Utils
from ..themes import theme
from ..krux_settings import Settings
from ..qr import FORMAT_UR
@@ -41,17 +42,10 @@
NUM_SPECIAL_2,
)
-D6_STATES = [str(i + 1) for i in range(6)]
-D20_STATES = [str(i + 1) for i in range(20)]
DIGITS = "0123456789"
DIGITS_HEX = "0123456789ABCDEF"
DIGITS_OCT = "01234567"
-D6_12W_MIN_ROLLS = 50
-D6_24W_MIN_ROLLS = 99
-D20_12W_MIN_ROLLS = 30
-D20_24W_MIN_ROLLS = 60
-
SD_MSG_TIME = 2500
PASSPHRASE_MAX_LEN = 200
@@ -146,8 +140,8 @@ def new_key(self):
self.ctx,
[
(t("Via Camera"), self.new_key_from_snapshot),
- (t("Via D6"), self.new_key_from_d6),
- (t("Via D20"), self.new_key_from_d20),
+ (t("Via D6"), self.new_key_from_dice),
+ (t("Via D20"), lambda: self.new_key_from_dice(True)),
(t("Back"), lambda: MENU_EXIT),
],
)
@@ -156,13 +150,16 @@ def new_key(self):
return MENU_CONTINUE
return status
- def new_key_from_d6(self):
- """Handler for the 'via D6' menu item"""
- return self._new_key_from_die(D6_STATES, D6_12W_MIN_ROLLS, D6_24W_MIN_ROLLS)
+ def new_key_from_dice(self, d_20=False):
+ """Handler for the 'via DX' menu item. Default is D6"""
+ from .new_mnemonic.dice_rolls import DiceEntropy
- def new_key_from_d20(self):
- """Handler for the 'via D20' menu item"""
- return self._new_key_from_die(D20_STATES, D20_12W_MIN_ROLLS, D20_24W_MIN_ROLLS)
+ dice_entropy = DiceEntropy(self.ctx, d_20)
+ captured_entropy = dice_entropy.new_key()
+ if captured_entropy is not None:
+ words = bip39.mnemonic_from_bytes(captured_entropy).split()
+ return self._load_key_from_words(words)
+ return MENU_CONTINUE
def new_key_from_snapshot(self):
"""Use camera's entropy to create a new mnemonic"""
@@ -186,7 +183,10 @@ def new_key_from_snapshot(self):
+ t("(Experimental)")
)
if self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line):
- entropy_bytes = self.capture_camera_entropy()
+ from .capture_entropy import CameraEntropy
+
+ camera_entropy = CameraEntropy(self.ctx)
+ entropy_bytes = camera_entropy.capture()
if entropy_bytes is not None:
import binascii
@@ -201,113 +201,40 @@ def new_key_from_snapshot(self):
return self._load_key_from_words(words)
return MENU_CONTINUE
- def _new_key_from_die(self, roll_states, min_rolls_12w, min_rolls_24w):
- submenu = Menu(
- self.ctx,
- [
- (t("12 words"), lambda: MENU_EXIT),
- (t("24 words"), lambda: MENU_EXIT),
- (t("Back"), lambda: MENU_EXIT),
- ],
- )
- index, _ = submenu.run_loop()
- if index == 2:
- return MENU_CONTINUE
-
- min_rolls = min_rolls_12w if index == 0 else min_rolls_24w
- self.ctx.display.clear()
-
- delete_flag = False
- self.ctx.display.draw_hcentered_text(
- t("Roll dice at least %d times to generate a mnemonic.") % (min_rolls)
- )
- if self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line):
- rolls = []
-
- def delete_roll(buffer):
- # buffer not used here
- nonlocal delete_flag
- delete_flag = True
- return buffer
-
- while True:
- roll = ""
- while True:
- dice_title = t("Rolls: %d\n") % len(rolls)
- entropy = (
- "".join(rolls) if len(roll_states) < 10 else "-".join(rolls)
- )
- if len(entropy) <= 10:
- dice_title += entropy
- else:
- dice_title += "..." + entropy[-10:]
- roll = self.capture_from_keypad(
- dice_title,
- [roll_states],
- delete_key_fn=delete_roll,
- go_on_change=True,
- )
- if roll == ESC_KEY:
- return MENU_CONTINUE
- break
-
- if roll != "":
- rolls.append(roll)
- else:
- # If its not a roll it is Del or Go
- if delete_flag: # Del
- delete_flag = False
- if len(rolls) > 0:
- rolls.pop()
- elif len(rolls) < min_rolls: # Not enough to Go
- self.ctx.display.flash_text(t("Not enough rolls!"))
- else: # Go
- break
-
- entropy = "".join(rolls) if len(roll_states) < 10 else "-".join(rolls)
-
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(t("Rolls:\n\n%s") % entropy)
-
- import hashlib
- import binascii
-
- self.ctx.input.wait_for_button()
- entropy_bytes = entropy.encode()
- entropy_hash = binascii.hexlify(
- hashlib.sha256(entropy_bytes).digest()
- ).decode()
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(
- t("SHA256 of rolls:\n\n%s") % entropy_hash
- )
- self.ctx.input.wait_for_button()
- num_bytes = 16 if min_rolls == min_rolls_12w else 32
- words = bip39.mnemonic_from_bytes(
- hashlib.sha256(entropy_bytes).digest()[:num_bytes]
- ).split()
- return self._load_key_from_words(words)
-
- return MENU_CONTINUE
-
def _load_qr_passphrase(self):
data, _ = self.capture_qr_code()
if data is None:
- self.ctx.display.flash_text(
- t("Failed to load passphrase"), theme.error_color
- )
+ self.flash_text(t("Failed to load passphrase"), theme.error_color)
return MENU_CONTINUE
if len(data) > PASSPHRASE_MAX_LEN:
- self.ctx.display.flash_text(
+ self.flash_text(
t("Maximum length exceeded (%s)") % PASSPHRASE_MAX_LEN,
theme.error_color,
)
return MENU_CONTINUE
return data
- def _load_key_from_words(self, words):
+ def _load_key_from_words(self, words, charset=LETTERS):
mnemonic = " ".join(words)
- self.display_mnemonic(mnemonic)
+
+ if charset != LETTERS:
+ charset_type = {
+ DIGITS: Utils.BASE_DEC,
+ DIGITS_HEX: Utils.BASE_HEX,
+ DIGITS_OCT: Utils.BASE_OCT,
+ }
+ suffix_dict = {
+ DIGITS: Utils.BASE_DEC_SUFFIX,
+ DIGITS_HEX: Utils.BASE_HEX_SUFFIX,
+ DIGITS_OCT: Utils.BASE_OCT_SUFFIX,
+ }
+ numbers_str = Utils.get_mnemonic_numbers(mnemonic, charset_type[charset])
+ self.display_mnemonic(numbers_str, suffix_dict[charset])
+ if not self.prompt(t("Continue?"), self.ctx.display.bottom_prompt_line):
+ return MENU_CONTINUE
+ self.ctx.display.clear()
+
+ self.display_mnemonic(mnemonic, t("Mnemonic"))
if not self.prompt(t("Continue?"), self.ctx.display.bottom_prompt_line):
return MENU_CONTINUE
self.ctx.display.clear()
@@ -413,24 +340,24 @@ def _encrypted_qr_code(self, data):
key_capture = EncryptionKey(self.ctx)
key = key_capture.encryption_key()
- if key is None:
- self.ctx.display.flash_text(t("Mnemonic was not decrypted"))
- return None
+ if key in (None, "", ESC_KEY):
+ self.flash_text(t("Key was not provided"), theme.error_color)
+ return MENU_CONTINUE
self.ctx.display.clear()
self.ctx.display.draw_centered_text(t("Processing ..."))
- if key in ("", ESC_KEY):
- raise ValueError(t("Failed to decrypt"))
word_bytes = encrypted_qr.decrypt(key)
if word_bytes is None:
- raise ValueError(t("Failed to decrypt"))
+ self.flash_text(t("Failed to decrypt"), theme.error_color)
+ return MENU_CONTINUE
return bip39.mnemonic_from_bytes(word_bytes).split()
+ return MENU_CONTINUE # prompt NO
return None
def load_key_from_qr_code(self):
"""Handler for the 'via qr code' menu item"""
data, qr_format = self.capture_qr_code()
if data is None:
- self.ctx.display.flash_text(t("Failed to load mnemonic"), theme.error_color)
+ self.flash_text(t("Failed to load mnemonic"), theme.error_color)
return MENU_CONTINUE
words = []
@@ -447,25 +374,34 @@ def load_key_from_qr_code(self):
pass
if not words:
+ data_bytes = ""
try:
data_bytes = (
data.encode("latin-1") if isinstance(data, str) else data
)
- # CompactSeedQR format
- if len(data_bytes) in (16, 32):
- words = bip39.mnemonic_from_bytes(data_bytes).split()
- # SeedQR format
- elif len(data_bytes) in (48, 96):
- words = [
- WORDLIST[int(data_bytes[i : i + 4])]
- for i in range(0, len(data_bytes), 4)
- ]
except:
- pass
+ try:
+ data_bytes = (
+ data.encode("shift-jis") if isinstance(data, str) else data
+ )
+ except:
+ pass
+
+ if len(data_bytes) in (16, 32):
+ # CompactSeedQR format
+ words = bip39.mnemonic_from_bytes(data_bytes).split()
+ # SeedQR format
+ elif len(data_bytes) in (48, 96):
+ words = [
+ WORDLIST[int(data_bytes[i : i + 4])]
+ for i in range(0, len(data_bytes), 4)
+ ]
if not words:
words = self._encrypted_qr_code(data)
+ if words == MENU_CONTINUE:
+ return MENU_CONTINUE
if not words or (len(words) != 12 and len(words) != 24):
- self.ctx.display.flash_text(t("Invalid mnemonic length"), theme.error_color)
+ self.flash_text(t("Invalid mnemonic length"), theme.error_color)
return MENU_CONTINUE
return self._load_key_from_words(words)
@@ -481,6 +417,12 @@ def _load_key_from_keypad(
self.ctx.display.draw_hcentered_text(title)
if self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line):
while len(words) < 24:
+ if len(words) in (11, 23):
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(
+ t("Leave blank if you'd like Krux to pick a valid final word")
+ )
+ self.ctx.input.wait_for_button()
if len(words) == 12:
self.ctx.display.clear()
if self.prompt(t("Done?"), self.ctx.display.height() // 2):
@@ -524,7 +466,7 @@ def _load_key_from_keypad(
):
words.append(word)
- return self._load_key_from_words(words)
+ return self._load_key_from_words(words, charset)
return MENU_CONTINUE
@@ -773,7 +715,7 @@ def load_key_from_tiny_seed_image(self):
words = tiny_scanner.scanner(w24)
del tiny_scanner
if words is None:
- self.ctx.display.flash_text(t("Failed to load mnemonic"), theme.error_color)
+ self.flash_text(t("Failed to load mnemonic"), theme.error_color)
return MENU_CONTINUE
return self._load_key_from_words(words)
diff --git a/src/krux/pages/mnemonic_view.py b/src/krux/pages/mnemonic_view.py
new file mode 100644
index 000000000..6816c31bc
--- /dev/null
+++ b/src/krux/pages/mnemonic_view.py
@@ -0,0 +1,176 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from .utils import Utils
+from ..qr import FORMAT_NONE
+from ..krux_settings import t, Settings, THERMAL_ADAFRUIT_TXT
+from . import (
+ Page,
+ Menu,
+ MENU_CONTINUE,
+ MENU_EXIT,
+)
+
+
+class MnemonicsView(Page):
+ """UI to show mnemonic in different formats"""
+
+ def __init__(self, ctx):
+ super().__init__(ctx, None)
+ self.ctx = ctx
+ self.utils = Utils(self.ctx)
+
+ def mnemonic(self):
+ """Menu with export mnemonic formats"""
+ submenu = Menu(
+ self.ctx,
+ [
+ (
+ t("Words"),
+ lambda: self.show_mnemonic(
+ self.ctx.wallet.key.mnemonic, t("Mnemonic")
+ ),
+ ),
+ (t("Numbers"), self.display_mnemonic_numbers),
+ (t("Plaintext QR"), self.display_standard_qr),
+ (t("Compact SeedQR"), lambda: self.display_seed_qr(True)),
+ (t("SeedQR"), self.display_seed_qr),
+ (t("Stackbit 1248"), self.stackbit),
+ (t("Tiny Seed"), self.tiny_seed),
+ (t("Back"), lambda: MENU_EXIT),
+ ],
+ )
+ submenu.run_loop()
+ return MENU_CONTINUE
+
+ def show_mnemonic(self, mnemonic, suffix=""):
+ """Displays only the mnemonic words or indexes"""
+ self.display_mnemonic(mnemonic, suffix)
+ self.ctx.input.wait_for_button()
+
+ # Avoid printing text on a cnc
+ if Settings().hardware.printer.driver == THERMAL_ADAFRUIT_TXT:
+ self.ctx.display.clear()
+ if self.prompt(
+ t("Print?\n\n%s\n\n") % Settings().hardware.printer.driver,
+ self.ctx.display.height() // 2,
+ ):
+ from .print_page import PrintPage
+
+ print_page = PrintPage(self.ctx)
+ print_page.print_mnemonic_text(mnemonic, suffix)
+ return MENU_CONTINUE
+
+ def display_mnemonic_numbers(self):
+ """Handler for the 'numbers' menu item"""
+ submenu = Menu(
+ self.ctx,
+ [
+ (
+ t("Decimal"),
+ lambda: self.show_mnemonic(
+ Utils.get_mnemonic_numbers(
+ self.ctx.wallet.key.mnemonic, Utils.BASE_DEC
+ ),
+ Utils.BASE_DEC_SUFFIX,
+ ),
+ ),
+ (
+ t("Hexadecimal"),
+ lambda: self.show_mnemonic(
+ Utils.get_mnemonic_numbers(
+ self.ctx.wallet.key.mnemonic, Utils.BASE_HEX
+ ),
+ Utils.BASE_HEX_SUFFIX,
+ ),
+ ),
+ (
+ t("Octal"),
+ lambda: self.show_mnemonic(
+ Utils.get_mnemonic_numbers(
+ self.ctx.wallet.key.mnemonic, Utils.BASE_OCT
+ ),
+ Utils.BASE_OCT_SUFFIX,
+ ),
+ ),
+ (t("Back"), lambda: MENU_EXIT),
+ ],
+ )
+ submenu.run_loop()
+ return MENU_CONTINUE
+
+ def display_standard_qr(self):
+ """Displays regular words QR code"""
+ title = t("Plaintext QR")
+ data = self.ctx.wallet.key.mnemonic
+ self.display_qr_codes(data, FORMAT_NONE, title)
+ self.utils.print_standard_qr(data, FORMAT_NONE, title)
+ return MENU_CONTINUE
+
+ def display_seed_qr(self, binary=False):
+ """Display Seed QR with with different view modes"""
+
+ from .qr_view import SeedQRView
+
+ seed_qr_view = SeedQRView(self.ctx, binary)
+ return seed_qr_view.display_qr()
+
+ def stackbit(self):
+ """Displays which numbers 1248 user should punch on 1248 steel card"""
+ from .stack_1248 import Stackbit
+
+ stackbit = Stackbit(self.ctx)
+ word_index = 1
+ words = self.ctx.wallet.key.mnemonic.split(" ")
+
+ while word_index < len(words):
+ y_offset = 2 * self.ctx.display.font_height
+ for _ in range(6):
+ stackbit.export_1248(word_index, y_offset, words[word_index - 1])
+ if self.ctx.display.height() > 240:
+ y_offset += 3 * self.ctx.display.font_height
+ else:
+ y_offset += 5 + 2 * self.ctx.display.font_height
+ word_index += 1
+ self.ctx.input.wait_for_button()
+
+ # removed the hability to go back in favor or the Krux UI patter (always move forward)
+ # if self.ctx.input.wait_for_button() == BUTTON_PAGE_PREV:
+ # if word_index > 12:
+ # word_index -= 12
+ # else:
+ # word_index = 1
+ self.ctx.display.clear()
+ return MENU_CONTINUE
+
+ def tiny_seed(self):
+ """Displays the seed in Tiny Seed format"""
+ from .tiny_seed import TinySeed
+
+ tiny_seed = TinySeed(self.ctx)
+ tiny_seed.export()
+
+ # Allow to print on thermal printer only
+ if Settings().hardware.printer.driver == THERMAL_ADAFRUIT_TXT:
+ if self.print_qr_prompt():
+ tiny_seed.print_tiny_seed()
+ return MENU_CONTINUE
diff --git a/src/krux/pages/new_mnemonic/__init__.py b/src/krux/pages/new_mnemonic/__init__.py
new file mode 100644
index 000000000..181abdfd2
--- /dev/null
+++ b/src/krux/pages/new_mnemonic/__init__.py
@@ -0,0 +1,21 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
diff --git a/src/krux/pages/new_mnemonic/dice_rolls.py b/src/krux/pages/new_mnemonic/dice_rolls.py
new file mode 100644
index 000000000..49dc561dd
--- /dev/null
+++ b/src/krux/pages/new_mnemonic/dice_rolls.py
@@ -0,0 +1,292 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from .. import (
+ Page,
+ Menu,
+ MENU_EXIT,
+ ESC_KEY,
+)
+from ...themes import theme
+from ...krux_settings import t
+from ...display import DEFAULT_PADDING
+
+D6_STATES = [str(i + 1) for i in range(6)]
+D20_STATES = [str(i + 1) for i in range(20)]
+
+D6_12W_MIN_ROLLS = 50
+D6_24W_MIN_ROLLS = 99
+D20_12W_MIN_ROLLS = 30
+D20_24W_MIN_ROLLS = 60
+MIN_ENTROPY_12W = 128
+MIN_ENTROPY_24W = 256
+
+BAR_GRAPH_POSITION = 20 # % of screen (top of the graph)
+BAR_GRAPH_SIZE = 60 # % of screen
+
+
+class DiceEntropy(Page):
+ """Capture and analise entropy from dice rolls"""
+
+ def __init__(self, ctx, is_d20=False):
+ super().__init__(ctx, None)
+ self.ctx = ctx
+ self.is_d20 = is_d20
+ self.roll_states = D20_STATES if is_d20 else D6_STATES
+ self.len_states = len(self.roll_states)
+ self.min_rolls = 0
+ self.min_entropy = 0
+ self.rolls = []
+ self.roll_counts = [0] * self.len_states
+
+ def _count_rolls(self):
+ """Recompute rolls count every time entropy is calculated"""
+ self.roll_counts = [0] * self.len_states
+ for roll in self.rolls:
+ self.roll_counts[int(roll) - 1] += 1
+
+ def calculate_entropy(self):
+ """Calculates Shannon's entropy of a given list"""
+ import math
+
+ # Total number of pixels (equal to the number of bytes)
+ total_rolls = len(self.rolls)
+ if not total_rolls:
+ return 0
+ self._count_rolls()
+
+ # Calculate entropy
+ entropy = 0
+ for count in self.roll_counts:
+ probability = count / total_rolls
+ entropy -= probability * (probability and math.log2(probability))
+
+ total_entropy = entropy * total_rolls
+ return int(total_entropy)
+
+ def stats_for_nerds(self):
+ """
+ Displays statistical information and a graphical representation of dice roll outcomes.
+ This method provides a deeper insight into the entropy collection process by showing:
+ 1. Distribution of dice rolls as a bar graph.
+ 2. The calculated Shannon's entropy in bits.
+ It's intended for users interested in the quality and distribution of their entropy source.
+ """
+ self.ctx.display.clear()
+ self.ctx.display.draw_hcentered_text(
+ t("Rolls distribution:"), self.ctx.display.font_height
+ )
+ shannons_entropy = self.calculate_entropy()
+ max_count = max(self.roll_counts)
+
+ scale_factor = (self.ctx.display.height() * BAR_GRAPH_SIZE) // 100
+ scale_factor //= max_count
+ bar_graph = []
+ for count in self.roll_counts:
+ bar_graph.append(int(count * scale_factor))
+ bar_pad = self.ctx.display.width() // (self.len_states + 2)
+ offset_x = bar_pad
+ # Bar graph offset (bottom of the graph)
+ offset_y = BAR_GRAPH_POSITION + BAR_GRAPH_SIZE
+ offset_y *= self.ctx.display.height()
+ offset_y //= 100
+
+ for individual_bar in bar_graph:
+ bar_offset = offset_y - individual_bar
+ self.ctx.display.fill_rectangle(
+ offset_x + 1,
+ bar_offset,
+ bar_pad - 2,
+ individual_bar,
+ theme.highlight_color,
+ )
+ offset_x += bar_pad
+ offset_y += self.ctx.display.font_height
+ self.ctx.display.draw_hcentered_text(
+ t("Shannon's Entropy: ") + str(shannons_entropy) + "bits",
+ offset_y,
+ )
+
+ self.ctx.input.wait_for_button()
+
+ def draw_progress_bar(self):
+ """
+ Draws a progress bar on the display to show the current progress of dice rolls.
+ The progress bar consists of two sections: one indicating the number of rolls
+ made relative to the minimum required, and the other indicating the Shannon's
+ entropy of the rolls relative to the minimum required entropy. It changes color
+ to indicate when the minimum criteria have been met.
+ """
+ offset_y = DEFAULT_PADDING + 2 * self.ctx.display.font_height
+ pb_height = self.ctx.display.font_height - 4
+ if len(self.rolls) > 0: # Only draws if rolls > 0
+ progress = min(self.min_rolls, len(self.rolls))
+ progress *= self.ctx.display.usable_width() - 3
+ progress //= self.min_rolls
+ self.ctx.display.fill_rectangle(
+ DEFAULT_PADDING + 2,
+ offset_y + 2,
+ progress,
+ (pb_height // 2) - 2,
+ theme.fg_color,
+ )
+
+ shannon_entropy = self.calculate_entropy()
+ if shannon_entropy: # Only draws if Shannon's > 0
+ shannon_progress = min(self.min_entropy, shannon_entropy)
+ shannon_progress *= self.ctx.display.usable_width() - 3
+ shannon_progress //= self.min_entropy
+ self.ctx.display.fill_rectangle(
+ DEFAULT_PADDING + 2,
+ offset_y + (pb_height // 2) + 1,
+ shannon_progress,
+ (pb_height // 2) - 2,
+ theme.highlight_color,
+ )
+ if shannon_entropy >= self.min_entropy and len(self.rolls) >= self.min_rolls:
+ outline_color = theme.go_color
+ else:
+ outline_color = theme.no_esc_color
+ self.ctx.display.outline(
+ DEFAULT_PADDING,
+ offset_y,
+ self.ctx.display.usable_width(),
+ pb_height,
+ outline_color,
+ )
+
+ def new_key(self):
+ """Create a new key from dice rolls"""
+ submenu = Menu(
+ self.ctx,
+ [
+ (t("12 words"), lambda: MENU_EXIT),
+ (t("24 words"), lambda: MENU_EXIT),
+ (t("Back"), lambda: MENU_EXIT),
+ ],
+ )
+ index, _ = submenu.run_loop()
+ is_24_words = index == 1
+ if index == 2:
+ return None
+ elif index == 1: # 24 words
+ self.min_entropy = MIN_ENTROPY_24W
+ self.min_rolls = D20_24W_MIN_ROLLS if self.is_d20 else D6_24W_MIN_ROLLS
+ else: # 12 words
+ self.min_entropy = MIN_ENTROPY_12W
+ self.min_rolls = D20_12W_MIN_ROLLS if self.is_d20 else D6_12W_MIN_ROLLS
+ self.ctx.display.clear()
+
+ delete_flag = False
+ self.ctx.display.draw_hcentered_text(
+ t("Roll dice at least %d times to generate a mnemonic.") % (self.min_rolls)
+ )
+ if self.prompt(t("Proceed?"), self.ctx.display.bottom_prompt_line):
+
+ def delete_roll(buffer):
+ # buffer not used here
+ nonlocal delete_flag
+ delete_flag = True
+ return buffer
+
+ while True:
+ roll = ""
+ while True:
+ dice_title = t("Rolls: %d\n") % len(self.rolls)
+ entropy = (
+ "".join(self.rolls)
+ if self.len_states < 10
+ else "-".join(self.rolls)
+ )
+ if len(entropy) <= 10:
+ dice_title += entropy
+ else:
+ dice_title += "..." + entropy[-10:]
+ roll = self.capture_from_keypad(
+ dice_title,
+ [self.roll_states],
+ delete_key_fn=delete_roll,
+ progress_bar_fn=self.draw_progress_bar,
+ go_on_change=True,
+ )
+ if roll == ESC_KEY:
+ return None
+ break
+
+ if roll != "":
+ self.rolls.append(roll)
+ else:
+ # If its not a roll it is Del or Go
+ if delete_flag: # Del
+ delete_flag = False
+ if len(self.rolls) > 0:
+ self.rolls.pop()
+ elif len(self.rolls) < self.min_rolls: # Not enough to Go
+ self.flash_text(t("Not enough rolls!"))
+ elif self.calculate_entropy() < self.min_entropy:
+ self.ctx.display.clear()
+ self.ctx.display.draw_hcentered_text(
+ t("Poor entropy detected. More rolls are recommended")
+ )
+ if self.prompt(
+ t("Proceed anyway?"), self.ctx.display.bottom_prompt_line
+ ):
+ break
+ else: # Go
+ break
+
+ entropy = (
+ "".join(self.rolls) if self.len_states < 10 else "-".join(self.rolls)
+ )
+ self.ctx.display.clear()
+ rolls_str = t("Rolls:\n\n%s") % entropy
+ self.ctx.display.draw_hcentered_text(rolls_str, info_box=True)
+
+ submenu = Menu(
+ self.ctx,
+ [
+ (t("Stats for Nerds"), lambda: MENU_EXIT),
+ (t("Generate Words"), lambda: MENU_EXIT),
+ ],
+ offset=(len(self.ctx.display.to_lines(rolls_str)) + 1)
+ * self.ctx.display.font_height,
+ )
+ index, _ = submenu.run_loop()
+ if index == 0:
+ self.stats_for_nerds()
+
+ import hashlib
+ import binascii
+
+ entropy_bytes = entropy.encode()
+ entropy_hash = binascii.hexlify(
+ hashlib.sha256(entropy_bytes).digest()
+ ).decode()
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(
+ t("SHA256 of rolls:\n\n%s") % entropy_hash
+ )
+ self.ctx.input.wait_for_button()
+ num_bytes = 32 if is_24_words else 16
+ return hashlib.sha256(entropy_bytes).digest()[:num_bytes]
+
+ return None
diff --git a/src/krux/pages/print_page.py b/src/krux/pages/print_page.py
index 7e3bbace2..17f939501 100644
--- a/src/krux/pages/print_page.py
+++ b/src/krux/pages/print_page.py
@@ -31,7 +31,6 @@ class PrintPage(Page):
"""Printing user interface"""
def __init__(self, ctx):
- # Returns True if printer successfully created
super().__init__(ctx, None)
self.ctx = ctx
self.ctx.display.clear()
@@ -40,7 +39,7 @@ def __init__(self, ctx):
def _send_qr_to_printer(self, qr_code, i=0, count=1):
self.ctx.display.clear()
- if Settings().printer.driver == "cnc/file":
+ if Settings().hardware.printer.driver == "cnc/file":
self.ctx.display.draw_centered_text(t("Exporting to SD card.."))
else:
self.ctx.display.draw_centered_text(t("Printing\n%d / %d") % (i + 1, count))
@@ -52,7 +51,7 @@ def print_qr(self, data, qr_format=FORMAT_NONE, title="", width=33, is_qr=False)
if a printer is connected
"""
if self.printer is None:
- self.ctx.display.flash_text(t("Printer Driver not set!"), theme.error_color)
+ self.flash_text(t("Printer Driver not set!"), theme.error_color)
return
self.ctx.display.clear()
if title:
@@ -67,14 +66,14 @@ def print_qr(self, data, qr_format=FORMAT_NONE, title="", width=33, is_qr=False)
self._send_qr_to_printer(qr_code, i, count)
i += 1
- def print_mnemonic_text(self):
+ def print_mnemonic_text(self, mnemonic, suffix=""):
"""Prints Mnemonics words as text"""
self.ctx.display.clear()
self.ctx.display.draw_hcentered_text(
t("Printing ..."), self.ctx.display.height() // 2
)
- self.printer.print_string(t("Mnemonic") + "\n\n")
- words = self.ctx.wallet.key.mnemonic.split(" ")
+ self.printer.print_string(t("BIP39") + " " + suffix + "\n\n")
+ words = mnemonic.split(" ")
lines = len(words) // 3
for i in range(lines):
index = i + 1
@@ -88,4 +87,4 @@ def print_mnemonic_text(self):
index += lines
string += str(index) + ":" + words[index - 1] + "\n"
self.printer.print_string(string)
- self.printer.feed(3)
+ self.printer.feed(4)
diff --git a/src/krux/pages/pub_key_view.py b/src/krux/pages/pub_key_view.py
new file mode 100644
index 000000000..9fae2be4a
--- /dev/null
+++ b/src/krux/pages/pub_key_view.py
@@ -0,0 +1,122 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ..krux_settings import t
+from . import (
+ Page,
+ Menu,
+ MENU_CONTINUE,
+ MENU_EXIT,
+)
+
+from ..sd_card import PUBKEY_FILE_EXTENSION
+
+# to start xpub value without the xpub/zpub/ypub prefix
+WALLET_XPUB_START = 4
+
+
+class PubkeyView(Page):
+ """UI to show and export extended public key"""
+
+ def __init__(self, ctx):
+ super().__init__(ctx, None)
+ self.ctx = ctx
+
+ def public_key(self):
+ """Handler for the 'xpub' menu item"""
+
+ def _save_xpub_to_sd(version):
+ from .files_operations import SaveFile
+
+ save_page = SaveFile(self.ctx)
+ xpub = self.ctx.wallet.key.key_expression(version)
+ title = self.ctx.wallet.key.account_pubkey_str(version)[
+ :WALLET_XPUB_START
+ ].upper()
+ save_page.save_file(
+ xpub,
+ title,
+ title,
+ title + ":",
+ PUBKEY_FILE_EXTENSION,
+ save_as_binary=False,
+ )
+
+ def _pub_key_text(version):
+ def _save_sd_pubk_function():
+ return _save_xpub_to_sd(version)
+
+ if self.has_sd_card():
+ save_sd_pubk_func = _save_sd_pubk_function
+ else:
+ save_sd_pubk_func = None
+
+ pub_text_menu_items = [
+ (t("Save to SD card"), save_sd_pubk_func),
+ (t("Back"), lambda: MENU_EXIT),
+ ]
+ full_pub_key = self.ctx.wallet.key.account_pubkey_str(version)
+ menu_offset = 5 + len(self.ctx.display.to_lines(full_pub_key))
+ menu_offset *= self.ctx.display.font_height
+ pub_key_menu = Menu(self.ctx, pub_text_menu_items, offset=menu_offset)
+ self.ctx.display.clear()
+ self.ctx.display.draw_hcentered_text(
+ self.ctx.wallet.key.fingerprint_hex_str(pretty=True)
+ + "\n\n"
+ + self.ctx.wallet.key.derivation_str(pretty=True)
+ + "\n\n"
+ + full_pub_key,
+ offset_y=self.ctx.display.font_height,
+ info_box=True,
+ )
+ pub_key_menu.run_loop()
+
+ def _pub_key_qr(version):
+ title = self.ctx.wallet.key.account_pubkey_str(version)[
+ :WALLET_XPUB_START
+ ].upper()
+ xpub = self.ctx.wallet.key.key_expression(version)
+ from .qr_view import SeedQRView
+
+ seed_qr_view = SeedQRView(self.ctx, data=xpub, title=title)
+ seed_qr_view.display_qr(allow_export=True, transcript_tools=False)
+
+ zpub = "Zpub" if self.ctx.wallet.key.multisig else "zpub"
+ pub_key_menu_items = []
+ for version in [None, self.ctx.wallet.key.network[zpub]]:
+ title = self.ctx.wallet.key.account_pubkey_str(version)[
+ :WALLET_XPUB_START
+ ].upper()
+ pub_key_menu_items.append(
+ (title + " - " + t("Text"), lambda ver=version: _pub_key_text(ver))
+ )
+ pub_key_menu_items.append(
+ (title + " - " + t("QR Code"), lambda ver=version: _pub_key_qr(ver))
+ )
+ pub_key_menu_items.append((t("Back"), lambda: MENU_EXIT))
+ pub_key_menu = Menu(self.ctx, pub_key_menu_items)
+ while True:
+ _, status = pub_key_menu.run_loop()
+ if status == MENU_EXIT:
+ break
+
+ return MENU_CONTINUE
diff --git a/src/krux/pages/qr_view.py b/src/krux/pages/qr_view.py
index b8e107d7a..d92992d71 100644
--- a/src/krux/pages/qr_view.py
+++ b/src/krux/pages/qr_view.py
@@ -22,12 +22,12 @@
import qrcode
from embit.wordlists.bip39 import WORDLIST
-from . import Page
+from . import Page, Menu, MENU_CONTINUE, MENU_EXIT
+from ..sd_card import SDHandler
from ..themes import theme, WHITE, BLACK
from ..krux_settings import t
-from ..qr import get_size, add_qr_frame
+from ..qr import get_size
from ..display import DEFAULT_PADDING
-from . import MENU_CONTINUE
from ..input import (
BUTTON_ENTER,
BUTTON_PAGE,
@@ -54,7 +54,7 @@ def __init__(self, ctx, binary=False, data=None, title=None):
self.ctx = ctx
self.binary = binary
if data:
- self.code = qrcode.encode_to_string(data)
+ self.code = qrcode.encode(data)
self.title = title
else:
if self.binary:
@@ -67,17 +67,18 @@ def __init__(self, ctx, binary=False, data=None, title=None):
self.region_size = 7 if self.qr_size == 21 else 5
self.columns = (self.qr_size + self.region_size - 1) // self.region_size
self.lr_index = 0
+ self.bright = theme.bg_color == WHITE
def _seed_qr(self):
words = self.ctx.wallet.key.mnemonic.split(" ")
numbers = ""
for word in words:
numbers += str("%04d" % WORDLIST.index(word))
- return qrcode.encode_to_string(numbers)
+ return qrcode.encode(numbers)
def _binary_seed_qr(self):
binary_seed = self._to_compact_seed_qr(self.ctx.wallet.key.mnemonic)
- return qrcode.encode_to_string(binary_seed)
+ return qrcode.encode(binary_seed)
def _to_compact_seed_qr(self, mnemonic):
mnemonic = mnemonic.split(" ")
@@ -91,38 +92,38 @@ def _to_compact_seed_qr(self, mnemonic):
def highlight_qr_region(self, code, region=(0, 0, 0, 0), zoom=False):
"""Draws in white a highlighted region of the QR code"""
reg_x, reg_y, reg_width, reg_height = region
- size, code = add_qr_frame(code)
max_width = self.ctx.display.width()
if zoom:
max_width -= DEFAULT_PADDING
- if size == 23: # 21 + 2(frame)
+ if self.qr_size == 21:
qr_size = 7
else:
qr_size = 5
offset_x = 0
offset_y = 0
+ scale = max_width // qr_size
else:
- qr_size = size
- offset_x = reg_x + 1
- offset_y = reg_y + 1
-
- scale = max_width // qr_size
+ qr_size = self.qr_size
+ offset_x = reg_x
+ offset_y = reg_y
+ scale = max_width // (qr_size + 2)
qr_width = qr_size * scale
offset = (self.ctx.display.width() - qr_width) // 2
for y in range(reg_height): # vertical blocks loop
for x in range(reg_width): # horizontal blocks loop
- y_index = reg_y + y + 1
- x_index = reg_x + x + 1
- xy_index = y_index * (size + 1)
+ y_index = reg_y + y
+ x_index = reg_x + x
+ xy_index = y_index * self.qr_size
xy_index += x_index
- if y_index < size and x_index < size:
- if code[xy_index] == "0":
+ if y_index < self.qr_size and x_index < self.qr_size:
+ bit_value = code[xy_index >> 3] & (1 << (xy_index % 8))
+ if bit_value:
self.ctx.display.fill_rectangle(
offset + (offset_x + x) * scale,
offset + (offset_y + y) * scale,
scale,
scale,
- WHITE,
+ BLACK,
)
else:
self.ctx.display.fill_rectangle(
@@ -130,7 +131,7 @@ def highlight_qr_region(self, code, region=(0, 0, 0, 0), zoom=False):
offset + (offset_y + y) * scale,
scale,
scale,
- BLACK,
+ WHITE,
)
def _region_legend(self, row, column):
@@ -153,7 +154,7 @@ def draw_grided_qr(self, mode):
grid_pad = self.ctx.display.width() // (self.qr_size + 2)
grid_offset += grid_pad
if mode == STANDARD_MODE:
- if theme.bg_color == WHITE:
+ if self.bright:
self.ctx.display.draw_qr_code(0, self.code, light_color=WHITE)
else:
self.ctx.display.draw_qr_code(0, self.code)
@@ -291,36 +292,166 @@ def draw_grided_qr(self, mode):
theme.highlight_color,
)
- def display_seed_qr(self):
- """Disables touch and displays compact SeedQR code with grid to help
- drawing"""
+ def add_frame(self, binary_image, size):
+ """Adds a 1 block frame to QR codes"""
+ new_size = size + 2
+ # Create a new bytearray to store the framed image
+ framed_image = bytearray(b"\x00" * ((new_size * new_size + 7) >> 3))
+ # Copy the original image into the center of the framed image
+ for y in range(0, size):
+ for x in range(0, size):
+ original_index = y * size + x
+ original_bit = (
+ binary_image[original_index >> 3] >> (original_index % 8)
+ ) & 1
+ if original_bit:
+ framed_index = (y + 1) * new_size + x + 1
+ framed_image[framed_index >> 3] |= 1 << (framed_index % 8)
+
+ return framed_image, new_size
+
+ def save_pbm_image(self, file_name):
+ """Saves QR code image as compact B&W bitmap format file"""
+ from ..sd_card import PBM_IMAGE_EXTENSION
+
+ code, size = self.add_frame(self.code, self.qr_size)
+ pbm_data = bytearray()
+ pbm_data.extend(("P4\n{0} {0}\n".format(size)).encode())
+ for row in range(size):
+ byte = 0
+ for col in range(size):
+ bit_index = row * size + col
+ if code[bit_index >> 3] & (1 << (bit_index % 8)):
+ byte |= 1 << (7 - (col % 8))
+
+ # If we filled a byte or reached the end of the row, append it
+ if col % 8 == 7 or col == size - 1:
+ pbm_data.append(byte)
+ byte = 0
+
+ file_name += PBM_IMAGE_EXTENSION
+ with SDHandler() as sd:
+ sd.write_binary(file_name, pbm_data)
+ self.flash_text(t("Saved to SD card:\n%s") % file_name)
+
+ def save_bmp_image(self, file_name, resolution):
+ """Save QR code image as .bmp file"""
+ from ..sd_card import BMP_IMAGE_EXTENSION
+
+ # TODO: Try Compression?
+ import image
+ import lcd
+
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(t("Saving ..."))
+ code, size = self.add_frame(self.code, self.qr_size)
+ raw_image = image.Image(size=(size, size))
+ for y_index in range(0, size):
+ for x_index in range(0, size):
+ index = y_index * size + x_index
+ bit_value = (code[index >> 3] >> (index % 8)) & 1
+ if bit_value:
+ raw_image.set_pixel((x_index, y_index), lcd.BLACK)
+ else:
+ raw_image.set_pixel((x_index, y_index), lcd.WHITE)
+ bmp_img = image.Image(size=(resolution, resolution), copy_to_fb=True)
+ scale = resolution // size
+ bmp_img.draw_image(
+ raw_image,
+ 0,
+ 0,
+ x_scale=scale,
+ y_scale=scale,
+ )
+ file_name += BMP_IMAGE_EXTENSION
+ bmp_img.save("/sd/" + file_name)
+ self.flash_text(t("Saved to SD card:\n%s") % file_name)
+
+ def save_qr_image_menu(self):
+ """Options to save QR codes as images on SD card"""
+ from .files_operations import SaveFile
+
+ file_saver = SaveFile(self.ctx)
+ suggested_file_name = self.title.replace(" ", "_")
+ suggested_file_name = suggested_file_name.replace(
+ "β", ""
+ ) # Replaces thin spaces too
+ if len(suggested_file_name) > 10:
+ # Crop file name
+ suggested_file_name = suggested_file_name[:10]
+ file_name, filename_undefined = file_saver.set_filename(
+ suggested_file_name,
+ )
+ if filename_undefined:
+ return
+ size = self.qr_size + 2
+ bmp_resolutions = []
+ resolution = size
+ for _ in range(4):
+ resolution *= 2
+ if resolution <= 480:
+ bmp_resolutions.append(resolution)
+ self.ctx.display.clear()
+ self.ctx.display.draw_hcentered_text(
+ t("Res. - Format"), self.ctx.display.font_height, info_box=True
+ )
+ qr_menu = []
+ qr_menu.append(
+ ("%dx%d - PBM" % (size, size), lambda: self.save_pbm_image(file_name))
+ )
+ for bmp_resolution in bmp_resolutions:
+ qr_menu.append(
+ (
+ "%dx%d - BMP" % (bmp_resolution, bmp_resolution),
+ lambda res=bmp_resolution: self.save_bmp_image(file_name, res),
+ )
+ )
+ submenu = Menu(self.ctx, qr_menu, offset=2 * self.ctx.display.font_height)
+ submenu.run_loop()
+ # return MENU_EXIT # Uncomment to exit QR Viewer after saving
+
+ def print_qr(self):
+ "Printer handler"
+ from .utils import Utils
+
+ utils = Utils(self.ctx)
+ utils.print_standard_qr(self.code, title=self.title, is_qr=True)
+ # return MENU_EXIT # Uncomment to exit QR Viewer after printing
+
+ def display_qr(self, allow_export=False, transcript_tools=True, quick_exit=False):
+ """Displays QR codes in multiple modes"""
+
if self.title:
label = self.title
else:
label = ""
- label += "\n" + t("Swipe to change mode")
+ if transcript_tools:
+ label += "\n" + t("Swipe to change mode")
mode = 0
while True:
button = None
while button not in (SWIPE_DOWN, SWIPE_UP):
+
+ def toggle_brightness():
+ self.bright = not self.bright
+
self.draw_grided_qr(mode)
if self.ctx.input.touch is not None:
self.ctx.display.draw_hcentered_text(
label,
self.ctx.display.qr_offset() + self.ctx.display.font_height,
)
- # # Avoid the need of double click
- # self.ctx.input.buttons_active = True
button = self.ctx.input.wait_for_button()
- if button in (BUTTON_PAGE, SWIPE_LEFT): # page, swipe
- mode += 1
- mode %= 5
- self.lr_index = 0
- elif button in (BUTTON_PAGE_PREV, SWIPE_RIGHT): # page, swipe
- mode -= 1
- mode %= 5
- self.lr_index = 0
- elif button in (BUTTON_ENTER, BUTTON_TOUCH):
+ if transcript_tools:
+ if button in (BUTTON_PAGE, SWIPE_LEFT): # page, swipe
+ mode += 1
+ mode %= 5
+ self.lr_index = 0
+ elif button in (BUTTON_PAGE_PREV, SWIPE_RIGHT): # page, swipe
+ mode -= 1
+ mode %= 5
+ self.lr_index = 0
+ if button in (BUTTON_ENTER, BUTTON_TOUCH):
if mode in (LINE_MODE, REGION_MODE, ZOOMED_R_MODE):
self.lr_index += 1
else:
@@ -330,15 +461,21 @@ def display_seed_qr(self):
self.lr_index %= self.qr_size
elif mode in (REGION_MODE, ZOOMED_R_MODE):
self.lr_index %= self.columns * self.columns
- self.ctx.display.clear()
- if self.prompt(t("Are you sure?"), self.ctx.display.height() // 2):
- break
- if not self.print_qr_prompt():
- return MENU_CONTINUE
-
- from .print_page import PrintPage
-
- print_page = PrintPage(self.ctx)
- print_page.print_qr(self.code, title=self.title, is_qr=True)
-
- return MENU_CONTINUE
+ if quick_exit:
+ return MENU_CONTINUE
+ if self.has_sd_card() and allow_export:
+ sd_func = self.save_qr_image_menu
+ else:
+ sd_func = None
+ printer_func = self.print_qr if self.has_printer() else None
+ qr_menu = [
+ (t("Return to QR Viewer"), lambda: None),
+ (t("Toggle Brightness"), toggle_brightness),
+ (t("Save QR Image to SD Card"), sd_func),
+ (t("Print to QR"), printer_func),
+ (t("Back to Menu"), lambda: MENU_EXIT),
+ ]
+ submenu = Menu(self.ctx, qr_menu)
+ _, status = submenu.run_loop()
+ if status == MENU_EXIT:
+ return MENU_CONTINUE
diff --git a/src/krux/pages/settings_page.py b/src/krux/pages/settings_page.py
index 364660f22..9db6a78be 100644
--- a/src/krux/pages/settings_page.py
+++ b/src/krux/pages/settings_page.py
@@ -25,9 +25,10 @@
from ..settings import (
CategorySetting,
NumberSetting,
+ store,
SD_PATH,
FLASH_PATH,
- Store,
+ SETTINGS_FILENAME,
)
from ..krux_settings import (
Settings,
@@ -40,10 +41,10 @@
from ..input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, BUTTON_TOUCH
from ..sd_card import SDHandler
from ..encryption import QR_CODE_ITER_MULTIPLE
-from ..display import FLASH_MSG_TIME
from . import (
Page,
Menu,
+ FLASH_MSG_TIME,
MENU_CONTINUE,
MENU_EXIT,
ESC_KEY,
@@ -77,16 +78,12 @@ def settings(self):
"""Handler for the settings"""
location = Settings().persist.location
if location == SD_PATH:
- self.ctx.display.clear()
- self.ctx.display.draw_centered_text(t("Checking for SD card.."))
- try:
- # Check for SD hot-plug
- with SDHandler():
- self._display_centered_text(
- t("Your changes will be kept on the SD card."),
- duration=SD_MSG_TIME,
- )
- except OSError:
+ if self.has_sd_card():
+ self._display_centered_text(
+ t("Your changes will be kept on the SD card."),
+ duration=SD_MSG_TIME,
+ )
+ else:
self._display_centered_text(
t("SD card not detected.")
+ "\n\n"
@@ -162,6 +159,66 @@ def _touch_to_physical(self, index):
return BUTTON_ENTER
return BUTTON_PAGE
+ def restore_settings(self):
+ """Restore default settings by deleting the settings files"""
+ self.ctx.display.clear()
+ if self.prompt(
+ t("Restore factory settings and reboot?"), self.ctx.display.height() // 2
+ ):
+ self.ctx.display.clear()
+ try:
+ # Delete settings from SD
+ with SDHandler() as sd:
+ sd.delete(SETTINGS_FILENAME)
+ except:
+ pass
+ try:
+ # Delete settings from flash
+ os.remove("/%s/%s" % (FLASH_PATH, SETTINGS_FILENAME))
+ except:
+ pass
+ self.ctx.power_manager.reboot()
+
+ def erase_spiffs(self):
+ """Erase all SPIFFS, removing all saved configs and mnemonics"""
+
+ import flash
+ from ..firmware import FLASH_SIZE, SPIFFS_ADDR, ERASE_BLOCK_SIZE
+
+ empty_buf = b"\xff" * ERASE_BLOCK_SIZE
+ for address in range(SPIFFS_ADDR, FLASH_SIZE, ERASE_BLOCK_SIZE):
+ if flash.read(address, ERASE_BLOCK_SIZE) == empty_buf:
+ continue
+ flash.erase(address, ERASE_BLOCK_SIZE)
+
+ def wipe_device(self):
+ """Fully formats SPIFFS memory"""
+ self.ctx.display.clear()
+ if self.prompt(
+ t(
+ "Permanently remove all stored encrypted mnemonics and settings from flash?"
+ ),
+ self.ctx.display.height() // 2,
+ ):
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(t("Wiping Device.."))
+ self.erase_spiffs()
+ # Reboot so default settings take place and SPIFFS is formatted.
+ self.ctx.power_manager.reboot()
+
+ def restore_menu(self):
+ """Option to restore settings and wipe device"""
+ submenu = Menu(
+ self.ctx,
+ [
+ (t("Restore Default Settings"), self.restore_settings),
+ (t("Wipe Device"), self.wipe_device),
+ (t("Back"), lambda: MENU_EXIT),
+ ],
+ )
+ submenu.run_loop()
+ return MENU_CONTINUE
+
def _settings_exit_check(self):
"""Handler for the 'Back' on settings screen"""
@@ -173,14 +230,29 @@ def _settings_exit_check(self):
try:
# Check for SD hot-plug
with SDHandler():
- Store.save_settings()
+ if store.save_settings():
+ self._display_centered_text(
+ t("Changes persisted to SD card!"),
+ duration=SD_MSG_TIME,
+ )
+ except OSError:
+ self._display_centered_text(
+ t("SD card not detected.")
+ + "\n\n"
+ + t("Changes will last until shutdown."),
+ duration=SD_MSG_TIME,
+ )
+ else:
+ self.ctx.display.clear()
+ try:
+ if store.save_settings():
self._display_centered_text(
- t("Changes persisted to SD card!"),
+ t("Changes persisted to Flash!"),
duration=SD_MSG_TIME,
)
- except OSError:
+ except:
self._display_centered_text(
- t("SD card not detected.")
+ t("Unexpected error saving to Flash.")
+ "\n\n"
+ t("Changes will last until shutdown."),
duration=SD_MSG_TIME,
@@ -218,6 +290,7 @@ def handler():
# Case for "Back" on the main Settings
if settings_namespace.namespace == Settings.namespace:
+ items.append((t("Factory Settings"), self.restore_menu))
items.append((t("Back"), self._settings_exit_check))
else:
items.append((t("Back"), lambda: MENU_EXIT))
@@ -253,14 +326,16 @@ def _touch_threshold_exit_check(self):
# Update touch detection threshold
if self.ctx.input.touch is not None:
- self.ctx.input.touch.touch_driver.threshold(Settings().touch.threshold)
+ self.ctx.input.touch.touch_driver.threshold(
+ Settings().hardware.touch.threshold
+ )
def _encoder_threshold_exit_check(self):
"""Handler for the 'Back' on encoder settings screen"""
from ..rotary import encoder
# Update rotary encoder debounce time
- encoder.debounce = Settings().encoder.debounce
+ encoder.debounce = Settings().hardware.encoder.debounce
def category_setting(self, settings_namespace, setting):
"""Handler for viewing and editing a CategorySetting"""
@@ -304,13 +379,13 @@ def category_setting(self, settings_namespace, setting):
if self.prompt(
t("Change theme and reboot?"), self.ctx.display.height() // 2
):
+ self._settings_exit_check()
self.ctx.display.clear()
self.ctx.power_manager.reboot()
else:
# Restore previous theme
setting.__set__(settings_namespace, starting_category)
theme.update()
- Store.save_settings()
return MENU_CONTINUE
@@ -339,14 +414,14 @@ def number_setting(self, settings_namespace, setting):
setting.attr == "pbkdf2_iterations"
and (new_value % QR_CODE_ITER_MULTIPLE) != 0
):
- self.ctx.display.flash_text(
+ self.flash_text(
t("Value must be multiple of %s") % QR_CODE_ITER_MULTIPLE,
theme.error_color,
)
else:
setting.__set__(settings_namespace, new_value)
else:
- self.ctx.display.flash_text(
+ self.flash_text(
t("Value %s out of range: [%s, %s]")
% (new_value, setting.value_range[0], setting.value_range[1]),
theme.error_color,
diff --git a/src/krux/pages/sign_message_ui.py b/src/krux/pages/sign_message_ui.py
new file mode 100644
index 000000000..249a3cc9f
--- /dev/null
+++ b/src/krux/pages/sign_message_ui.py
@@ -0,0 +1,223 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import board
+import gc
+from embit import bip32, compact
+import hashlib
+import binascii
+from . import Page, MENU_CONTINUE
+from ..themes import theme
+from ..baseconv import base_encode
+from ..krux_settings import t
+from ..qr import FORMAT_NONE
+from ..sd_card import (
+ SIGNATURE_FILE_EXTENSION,
+ SIGNED_FILE_SUFFIX,
+ PUBKEY_FILE_EXTENSION,
+)
+from .utils import Utils
+
+
+class SignMessage(Page):
+ """Message Signing user interface"""
+
+ def __init__(self, ctx):
+ super().__init__(ctx, None)
+ self.utils = Utils(self.ctx)
+
+ def sign_at_address(self, data, qr_format):
+ """Message signed at a derived Bitcoin address - Sparrow/Specter"""
+
+ if data.startswith(b"signmessage"):
+ data_blocks = data.split(b" ")
+ if len(data_blocks) >= 3:
+ derivation = data_blocks[1].decode()
+ message = b" ".join(data_blocks[2:])
+ message = message.split(b":")
+ if len(message) >= 2 and message[0] == b"ascii":
+ message = b" ".join(message[1:])
+ derivation = bip32.parse_path(derivation)
+ self.ctx.display.clear()
+ address = self.ctx.wallet.descriptor.derive(
+ derivation[4], branch_index=0
+ ).address(network=self.ctx.wallet.key.network)
+ short_address = self.fit_to_line(
+ address, str(derivation[4]) + ". ", fixed_chars=3
+ )
+ # Amount of lines to subtract for free room for message
+ subtract_lines = 6 if board.config["type"] == "m5stickv" else 10
+
+ message_to_display = self.ctx.display.to_lines(message.decode())
+ if (
+ len(message_to_display)
+ > self.ctx.display.total_lines - subtract_lines
+ ):
+ message_cut = (
+ self.ctx.display.total_lines - subtract_lines
+ ) // 2
+ message_to_display = (
+ message_to_display[:message_cut]
+ + ["\n...\n"]
+ + message_to_display[-message_cut:]
+ )
+ message_to_display = "".join(message_to_display)
+ else:
+ message_to_display = message.decode()
+
+ self.ctx.display.draw_hcentered_text(
+ t("Message:")
+ + "\n"
+ + message_to_display
+ + "\n\n"
+ + "Address:"
+ + "\n"
+ + short_address
+ )
+ if not self.prompt(t("Sign?"), self.ctx.display.bottom_prompt_line):
+ return True
+ message_hash = hashlib.sha256(
+ hashlib.sha256(
+ b"\x18Bitcoin Signed Message:\n"
+ + compact.to_bytes(len(message))
+ + message
+ ).digest()
+ ).digest()
+ sig = self.ctx.wallet.key.sign_at(derivation, message_hash)
+
+ # Encode sig as base64 string
+ encoded_sig = base_encode(sig, 64).strip().decode()
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(
+ t("Signature") + ":\n\n%s" % encoded_sig
+ )
+ self.ctx.input.wait_for_button()
+ title = t("Signed Message")
+ self.display_qr_codes(encoded_sig, qr_format, title)
+ self.utils.print_standard_qr(encoded_sig, qr_format, title)
+ return True
+ return False
+
+ def sign_message(self):
+ """Sign message user interface"""
+
+ # Try to read a message from camera
+ message_filename = ""
+ data, qr_format = self.capture_qr_code()
+
+ if data is None:
+ # Try to read a message from a file on the SD card
+ qr_format = FORMAT_NONE
+ try:
+ message_filename, data = self.utils.load_file()
+ except OSError:
+ pass
+
+ if data is None:
+ self.flash_text(t("Failed to load message"), theme.error_color)
+ return MENU_CONTINUE
+
+ # message read OK!
+ data = data.encode() if isinstance(data, str) else data
+
+ if self.sign_at_address(data, qr_format):
+ return MENU_CONTINUE
+
+ message_hash = None
+ if len(data) == 32:
+ # It's a sha256 hash already
+ message_hash = data
+ else:
+ if len(data) == 64:
+ # It may be a hex-encoded sha256 hash
+ try:
+ message_hash = binascii.unhexlify(data)
+ except:
+ pass
+ if message_hash is None:
+ # It's a message, so compute its sha256 hash
+ message_hash = hashlib.sha256(data).digest()
+
+ # memory management
+ del data
+ gc.collect()
+
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(
+ t("SHA256:\n%s") % binascii.hexlify(message_hash).decode()
+ )
+ if not self.prompt(t("Sign?"), self.ctx.display.bottom_prompt_line):
+ return MENU_CONTINUE
+
+ # User confirmed to sign!
+ sig = self.ctx.wallet.key.sign(message_hash).serialize()
+
+ # Encode sig as base64 string
+ encoded_sig = base_encode(sig, 64).decode()
+ self.ctx.display.clear()
+ self.ctx.display.draw_centered_text(t("Signature") + ":\n\n%s" % encoded_sig)
+ self.ctx.input.wait_for_button()
+
+ # Show the base64 signed message as a QRCode
+ title = t("Signed Message")
+ self.display_qr_codes(encoded_sig, qr_format, title)
+ self.utils.print_standard_qr(encoded_sig, qr_format, title)
+
+ # memory management
+ del encoded_sig
+ gc.collect()
+
+ # Show the public key as a QRCode
+ pubkey = binascii.hexlify(self.ctx.wallet.key.account.sec()).decode()
+ self.ctx.display.clear()
+
+ title = t("Hex Public Key")
+ self.ctx.display.draw_centered_text(title + ":\n\n%s" % pubkey)
+ self.ctx.input.wait_for_button()
+
+ # Show the public key in hexadecimal format as a QRCode
+ self.display_qr_codes(pubkey, qr_format, title)
+ self.utils.print_standard_qr(pubkey, qr_format, title)
+
+ # memory management
+ gc.collect()
+
+ # Try to save the signature file on the SD card
+ if self.has_sd_card():
+ from .files_operations import SaveFile
+
+ save_page = SaveFile(self.ctx)
+ save_page.save_file(
+ sig,
+ "message",
+ message_filename,
+ t("Signature") + ":",
+ SIGNATURE_FILE_EXTENSION,
+ SIGNED_FILE_SUFFIX,
+ )
+
+ # Try to save the public key on the SD card
+ save_page.save_file(
+ pubkey, "pubkey", "", title + ":", PUBKEY_FILE_EXTENSION, "", False
+ )
+
+ return MENU_CONTINUE
diff --git a/src/krux/pages/tiny_seed.py b/src/krux/pages/tiny_seed.py
index 778dc4adb..03204f654 100644
--- a/src/krux/pages/tiny_seed.py
+++ b/src/krux/pages/tiny_seed.py
@@ -5,13 +5,13 @@
import sensor
import time
from embit.wordlists.bip39 import WORDLIST
-from . import Page
+from . import Page, FLASH_MSG_TIME
from ..themes import theme
from ..wdt import wdt
from ..krux_settings import t
-from ..display import DEFAULT_PADDING, FLASH_MSG_TIME
+from ..display import DEFAULT_PADDING
from ..camera import OV7740_ID, OV2640_ID, OV5642_ID
-from ..input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, BUTTON_TOUCH, PRESSED
+from ..input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV, BUTTON_TOUCH
# Tiny Seed last bit index positions according to checksums
TS_LAST_BIT_NO_CS = 143
@@ -240,7 +240,7 @@ def print_tiny_seed(self):
wdt.feed()
grid_x_offset = border_x + 16 # 4,1mm*8px
grid_y_offset = border_y + 25 # 6,2mm*8px
- self.printer.feed(3)
+ self.printer.feed(4)
self.ctx.display.clear()
def _draw_index(self, index):
@@ -843,7 +843,7 @@ def _detect_and_draw_punches(self, img, gradient_corners):
page_seed_numbers[word_index], bit
)
index += 1
- print(page_seed_numbers)
+ # print(page_seed_numbers)
return page_seed_numbers
def _set_camera_sensitivity(self):
@@ -880,21 +880,14 @@ def _exit_camera(self):
self.ctx.display.clear()
def _check_buttons(self, w24, page):
- if time.ticks_ms() > self.time_frame + 1000:
- enter_or_touch = (
- self.ctx.input.enter_value() == PRESSED
- or self.ctx.input.touch_value() == PRESSED
- )
- if w24:
- if page == 0 and enter_or_touch:
- self.capturing = True
- elif enter_or_touch:
- return True
- if (
- self.ctx.input.page_value() == PRESSED
- or self.ctx.input.page_prev_value() == PRESSED
- ):
- return True
+ enter_or_touch = self.ctx.input.enter_event() or self.ctx.input.touch_event()
+ if w24:
+ if page == 0 and enter_or_touch:
+ self.capturing = True
+ elif enter_or_touch:
+ return True
+ if self.ctx.input.page_event() or self.ctx.input.page_prev_event():
+ return True
return False
def _process_12w_scan(self, page_seed_numbers):
@@ -913,8 +906,7 @@ def _process_12w_scan(self, page_seed_numbers):
if words: # If words confirmed
return words
# Else esc command was given, turn camera on again and reset words
- self.ctx.display.clear()
- self.ctx.display.flash_text(
+ self.flash_text(
t("Scanning words 1-12 again") + "\n\n" + t("Wait for the capture")
)
self._run_camera()
@@ -934,15 +926,13 @@ def _process_24w_pg0_scan(self, page_seed_numbers):
words = self.tiny_seed.enter_tiny_seed(True, page_seed_numbers, True)
self.capturing = False
if words is not None: # Fisrt 12 words confirmed, moving to 13-24
- self.ctx.display.clear()
- self.ctx.display.flash_text(
+ self.flash_text(
t("Scanning words 13-24") + "\n\n" + t("Wait for the capture")
)
self._run_camera()
return words
# Esc command was given
- self.ctx.display.clear()
- self.ctx.display.flash_text(
+ self.flash_text(
t("Scanning words 1-12 again") + "\n\n" + t("TOUCH or ENTER to capture")
)
self._run_camera() # Run camera and rotate screen after message was given
@@ -971,6 +961,7 @@ def scanner(self, w24=False):
if precamera_ticks + FLASH_MSG_TIME > postcamera_ticks:
time.sleep_ms(precamera_ticks + FLASH_MSG_TIME - postcamera_ticks)
del message, precamera_ticks, postcamera_ticks
+ self.ctx.input.flush_events()
# # Debug FPS 1/4
# clock = time.clock()
# fps = 0
@@ -983,7 +974,7 @@ def scanner(self, w24=False):
rect = self._detect_tiny_seed(img)
if rect:
gradient_corners = self._gradient_corners(rect, img)
- print(gradient_corners)
+ # print(gradient_corners)
# map_regions
self._map_punches_region(rect, page)
page_seed_numbers = self._detect_and_draw_punches(img, gradient_corners)
diff --git a/src/krux/pages/tools.py b/src/krux/pages/tools.py
index 3a12cd820..667830f8f 100644
--- a/src/krux/pages/tools.py
+++ b/src/krux/pages/tools.py
@@ -97,7 +97,7 @@ def sd_check(self):
select_file_handler=file_manager.show_file_details
)
except OSError:
- self.ctx.display.flash_text(t("SD card not detected"), theme.error_color)
+ self.flash_text(t("SD card not detected"), theme.error_color)
return MENU_CONTINUE
@@ -138,5 +138,5 @@ def create_qr(self):
title = t("Custom QR Code")
seed_qr_view = SeedQRView(self.ctx, data=text, title=title)
- return seed_qr_view.display_seed_qr()
+ return seed_qr_view.display_qr(allow_export=True)
return MENU_CONTINUE
diff --git a/src/krux/pages/utils.py b/src/krux/pages/utils.py
new file mode 100644
index 000000000..5a89bd647
--- /dev/null
+++ b/src/krux/pages/utils.py
@@ -0,0 +1,88 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from . import Page
+from ..krux_settings import t
+from ..sd_card import SDHandler
+from embit.wordlists.bip39 import WORDLIST
+
+
+class Utils(Page):
+ """Methods as subpages, shared by other pages"""
+
+ BASE_DEC = 10
+ BASE_HEX = 16
+ BASE_OCT = 8
+
+ BASE_DEC_SUFFIX = "DEC"
+ BASE_HEX_SUFFIX = "HEX"
+ BASE_OCT_SUFFIX = "OCT"
+
+ def __init__(self, ctx):
+ super().__init__(ctx, None)
+
+ def print_standard_qr(self, data, qr_format=None, title="", width=33, is_qr=False):
+ """Loads printer driver and UI"""
+ # Only loads printer related modules if needed
+ if self.print_qr_prompt():
+ from .print_page import PrintPage
+
+ print_page = PrintPage(self.ctx)
+ print_page.print_qr(data, qr_format, title, width, is_qr)
+
+ def load_file(self, file_ext=""):
+ """Load a file from SD card"""
+ if self.has_sd_card():
+ with SDHandler() as sd:
+ self.ctx.display.clear()
+ if self.prompt(
+ t("Load from SD card?") + "\n\n", self.ctx.display.height() // 2
+ ):
+ from .files_manager import FileManager
+
+ file_manager = FileManager(self.ctx)
+ filename = file_manager.select_file(file_extension=file_ext)
+
+ if filename:
+ filename = file_manager.display_file(filename)
+
+ if self.prompt(t("Load?"), self.ctx.display.bottom_prompt_line):
+ return filename, sd.read_binary(filename)
+ return "", None
+
+ @staticmethod
+ def get_mnemonic_numbers(words, base=BASE_DEC):
+ """Returns the mnemonic as indexes in decimal, hexadecimal, or octal"""
+ word_numbers = []
+ for word in words.split(" "):
+ word_numbers.append(WORDLIST.index(word) + 1)
+
+ if base == Utils.BASE_HEX:
+ for i, number in enumerate(word_numbers):
+ word_numbers[i] = hex(number)[2:].upper()
+
+ if base == Utils.BASE_OCT:
+ for i, number in enumerate(word_numbers):
+ word_numbers[i] = oct(number)[2:]
+
+ numbers_str = [str(value) for value in word_numbers]
+ return " ".join(numbers_str)
diff --git a/src/krux/power.py b/src/krux/power.py
index 59cbb93f9..5f4c80523 100644
--- a/src/krux/power.py
+++ b/src/krux/power.py
@@ -22,6 +22,7 @@
import machine
import sys
import board
+from .i2c import i2c_bus
# https://github.com/m5stack/M5StickC/blob/0527606d9e56c956ab17b278c25e3d07d7664f5e/src/AXP192.cpp#L20
MAX_BATTERY_MV = 4200
@@ -34,38 +35,27 @@ class PowerManager:
def __init__(self):
self.pmu = None
- if board.config["type"].startswith("amigo"):
- try:
- from pmu import axp173
-
- self.pmu = axp173()
- self.pmu.enableADCs(True)
- self.pmu.enablePMICSleepMode(False)
- # Amigo already have a dedicated reset button
- # Will only enable button checking when in sleep mode
- except:
- pass
- else:
- try:
- from pmu import axp192
+ try:
+ from pmu import PMUController
- self.pmu = axp192()
- self.pmu.enableADCs(True)
- self.pmu.enablePMICSleepMode(True)
- except:
- pass
+ self.pmu = PMUController(i2c_bus)
+ self.pmu.enable_adcs(True)
+ if board.config["type"] == "m5stickv":
+ self.pmu.enable_pek_button_monitor()
+ except Exception as e:
+ print(e)
def has_battery(self):
"""Returns if the device has a battery"""
try:
- assert int(self.pmu.getVbatVoltage()) > 0
+ assert int(self.pmu.get_battery_voltage()) > 0
except:
return False
return True
def battery_charge_remaining(self):
"""Returns the state of charge of the device's battery"""
- mv = int(self.pmu.getVbatVoltage())
+ mv = int(self.pmu.get_battery_voltage())
if board.config["type"].startswith("amigo"):
charge = max(0, (mv - 3394.102415024943) / 416.73204356)
elif board.config["type"] == "m5stickv":
@@ -76,21 +66,25 @@ def battery_charge_remaining(self):
def charging(self):
"""Returns true if device has power delivered through USB"""
- return self.pmu.getUSBVoltage() > 4200
+ return self.pmu.get_usb_voltage() > 4200
+
+ def set_screen_brightness(self, value):
+ """Sets the screen brightness by modifying the backlight voltage"""
+ # Accpeted values range from 0 to 8
+ # pmu register allow values from 0 to 15, but values below 7 result in no backlight
+ value += 7
+ self.pmu.set_screen_brightness(value)
def shutdown(self):
"""Shuts down the device"""
if self.pmu is not None:
- # Enable button checking before shutdown
- self.pmu.enablePMICSleepMode(True)
- self.pmu.setEnterSleepMode()
+ self.pmu.enable_adcs(False)
+ self.pmu.enter_sleep_mode()
machine.reset()
sys.exit()
def reboot(self):
"""Reboots the device"""
- if self.pmu is not None:
- self.pmu.enablePMICSleepMode(False)
machine.reset()
sys.exit()
diff --git a/src/krux/printers/__init__.py b/src/krux/printers/__init__.py
index 4d61c9812..5270fbe09 100644
--- a/src/krux/printers/__init__.py
+++ b/src/krux/printers/__init__.py
@@ -58,7 +58,7 @@ def print_string(self, text):
def create_printer():
"""Instantiates a new printer dynamically based on the default in Settings"""
- module, cls = PrinterSettings.PRINTERS[Settings().printer.driver]
+ module, cls = PrinterSettings.PRINTERS[Settings().hardware.printer.driver]
if not cls:
return None
return getattr(
diff --git a/src/krux/printers/cnc.py b/src/krux/printers/cnc.py
index cb460d31a..a584b7546 100644
--- a/src/krux/printers/cnc.py
+++ b/src/krux/printers/cnc.py
@@ -38,15 +38,15 @@ class GCodeGenerator(Printer):
"""
def __init__(self):
- self.unit = Settings().printer.cnc.unit
- self.flute_diameter = Settings().printer.cnc.flute_diameter
- self.plunge_rate = Settings().printer.cnc.plunge_rate
- self.feed_rate = Settings().printer.cnc.feed_rate
- self.cut_depth = Settings().printer.cnc.cut_depth
- self.pass_depth = Settings().printer.cnc.depth_per_pass
- self.part_size = Settings().printer.cnc.part_size
- self.border_padding = Settings().printer.cnc.border_padding
- self.invert = Settings().printer.cnc.invert
+ self.unit = Settings().hardware.printer.cnc.unit
+ self.flute_diameter = Settings().hardware.printer.cnc.flute_diameter
+ self.plunge_rate = Settings().hardware.printer.cnc.plunge_rate
+ self.feed_rate = Settings().hardware.printer.cnc.feed_rate
+ self.cut_depth = Settings().hardware.printer.cnc.cut_depth
+ self.pass_depth = Settings().hardware.printer.cnc.depth_per_pass
+ self.part_size = Settings().hardware.printer.cnc.part_size
+ self.border_padding = Settings().hardware.printer.cnc.border_padding
+ self.invert = Settings().hardware.printer.cnc.invert
if self.plunge_rate > self.feed_rate / 2:
raise ValueError("plunge rate must be less than half of feed rate")
@@ -103,7 +103,7 @@ def print_qr_code(self, qr_code):
def cut_cell(self, x, y, cell_size, plunge_depth):
"""Hollows out the specified cell using a cutting method defined in settings"""
- if Settings().printer.cnc.cut_method == "spiral":
+ if Settings().hardware.printer.cnc.cut_method == "spiral":
self.spiral_cut_cell(x, y, cell_size, plunge_depth)
else:
self.row_cut_cell(x, y, cell_size, plunge_depth)
@@ -281,10 +281,10 @@ def clear(self):
# def __init__(self):
# super().__init__()
-# fm.register(Settings().printer.cnc.grbl.tx_pin, fm.fpioa.UART2_TX, force=False)
-# fm.register(Settings().printer.cnc.grbl.rx_pin, fm.fpioa.UART2_RX, force=False)
-# self.uart_conn = UART(UART.UART2, Settings().printer.cnc.grbl.baudrate)
-# self.byte_time = 11.0 / float(Settings().printer.cnc.grbl.baudrate)
+# fm.register(Settings().hardware.printer.cnc.grbl.tx_pin, fm.fpioa.UART2_TX, force=False)
+# fm.register(Settings().hardware.printer.cnc.grbl.rx_pin, fm.fpioa.UART2_RX, force=False)
+# self.uart_conn = UART(UART.UART2, Settings().hardware.printer.cnc.grbl.baudrate)
+# self.byte_time = 11.0 / float(Settings().hardware.printer.cnc.grbl.baudrate)
# res = self.uart_conn.readline()
# if res is None or not res.decode().lower().startswith("grbl"):
# raise ValueError("not connected")
diff --git a/src/krux/printers/thermal.py b/src/krux/printers/thermal.py
index 74a012633..96edd7173 100644
--- a/src/krux/printers/thermal.py
+++ b/src/krux/printers/thermal.py
@@ -56,80 +56,28 @@ class AdafruitPrinter(Printer):
def __init__(self):
fm.register(
- Settings().printer.thermal.adafruit.tx_pin, fm.fpioa.UART2_TX, force=False
+ Settings().hardware.printer.thermal.adafruit.tx_pin,
+ fm.fpioa.UART2_TX,
+ force=False,
)
fm.register(
- Settings().printer.thermal.adafruit.rx_pin, fm.fpioa.UART2_RX, force=False
+ Settings().hardware.printer.thermal.adafruit.rx_pin,
+ fm.fpioa.UART2_RX,
+ force=False,
)
- self.uart_conn = UART(UART.UART2, Settings().printer.thermal.adafruit.baudrate)
+ self.uart_conn = UART(
+ UART.UART2, Settings().hardware.printer.thermal.adafruit.baudrate
+ )
self.character_height = 24
self.byte_time = 1 # miliseconds
- self.dot_print_time = Settings().printer.thermal.adafruit.line_delay
+ self.dot_print_time = Settings().hardware.printer.thermal.adafruit.line_delay
self.dot_feed_time = 2 # miliseconds
- self.setup()
-
if not self.has_paper():
raise ValueError("missing paper")
- def setup(self):
- """Sets up the connection to the printer and sets default settings"""
- # The printer can't start receiving data immediately
- # upon power up -- it needs a moment to cold boot
- # and initialize. Allow at least 1/2 sec of uptime
- # before printer can receive data.
- time.sleep_ms(INITIALIZE_WAIT_TIME)
-
- # Wake up the printer to get ready for printing
- self.write_bytes(255)
- self.write_bytes(27, 118, 0) # Sleep off (important!)
-
- # Reset the printer
- self.write_bytes(27, 64) # Esc @ = init command
- # Configure tab stops on recent printers
- self.write_bytes(27, 68) # Set tab stops
- self.write_bytes(4, 8, 12, 16) # every 4 columns,
- self.write_bytes(20, 24, 28, 0) # 0 is end-of-list.
-
- # Description of print settings from p. 23 of manual:
- # ESC 7 n1 n2 n3 Setting Control Parameter Command
- # Decimal: 27 55 n1 n2 n3
- # max heating dots, heating time, heating interval
- # n1 = 0-255 Max heat dots, Unit (8dots), Default: 7 (64 dots)
- # n2 = 3-255 Heating time, Unit (10us), Default: 80 (800us)
- # n3 = 0-255 Heating interval, Unit (10us), Default: 2 (20us)
- # The more max heating dots, the more peak current
- # will cost when printing, the faster printing speed.
- # The max heating dots is 8*(n1+1). The more heating
- # time, the more density, but the slower printing
- # speed. If heating time is too short, blank page
- # may occur. The more heating interval, the more
- # clear, but the slower printing speed.
- self.write_bytes(
- 27, # Esc
- 55, # 7 (print settings)
- 11, # Heat dots
- Settings().printer.thermal.adafruit.heat_time,
- Settings().printer.thermal.adafruit.heat_interval,
- )
-
- # Description of print density from p. 23 of manual:
- # DC2 # n Set printing density
- # Decimal: 18 35 n
- # D4..D0 of n is used to set the printing density.
- # Density is 50% + 5% * n(D4-D0) printing density.
- # D7..D5 of n is used to set the printing break time.
- # Break time is n(D7-D5)*250us.
- # (Unsure of default values -- not documented)
- print_density = 10 # 100%
- print_break_time = 2 # 500 uS
-
- self.write_bytes(
- 18, 35, (print_break_time << 5) | print_density # DC2 # Print density
- )
-
def write_bytes(self, *args):
"""Writes bytes to the printer at a stable speed"""
for arg in args:
@@ -143,9 +91,11 @@ def write_bytes(self, *args):
def feed(self, x=1):
"""Feeds paper through the machine x times"""
- self.write_bytes(27, 100, x)
- # Wait for the paper to feed
- time.sleep_ms(self.dot_feed_time * self.character_height)
+ while x > 0:
+ x -= 1
+ self.write_bytes(10)
+ # Wait for the paper to feed
+ time.sleep_ms(self.dot_feed_time * self.character_height)
def has_paper(self):
"""Returns a boolean indicating if the printer has paper or not"""
@@ -181,33 +131,40 @@ def clear(self):
def print_qr_code(self, qr_code):
"""Prints a QR code, scaling it up as large as possible"""
- size = 0
- while qr_code[size] != "\n":
- size += 1
+ from ..qr import get_size
- scale = Settings().printer.thermal.adafruit.paper_width // size
- scale *= Settings().printer.thermal.adafruit.scale
- scale //= 200 # 100*2 because printer will scale 2X later to save data
+ size = get_size(qr_code)
+
+ scale = Settings().hardware.printer.thermal.adafruit.paper_width // size
+ scale *= Settings().hardware.printer.thermal.adafruit.scale # Scale in %
+ scale //= 200 # 100% * 2 because printer will scale 2X later to save data
# Being at full size sometimes makes prints more faded (can't apply too much heat?)
line_bytes_size = (size * scale + 7) // 8 # amount of bytes per line
- self.set_bitmap_mode(line_bytes_size, scale * size, 3)
- for y in range(size):
- # Scale the line (width) by scaling factor
- line = 0
- for char in qr_code[y * (size + 1) : y * (size + 1) + size]:
- bit = int(char)
- for _ in range(scale):
- line <<= 1
- line |= bit
- line_bytes = line.to_bytes(line_bytes_size, "big")
- # Print height * scale lines out to scale by
-
+ self.set_bitmap_mode(line_bytes_size, size * scale, 3)
+ for row in range(size):
+ byte = 0
+ line_bytes = bytearray()
+ for col in range(size):
+ bit_index = row * size + col
+ bit = qr_code[bit_index >> 3] & (1 << (bit_index % 8))
+ for i in range(scale):
+ byte <<= 1
+ if bit:
+ byte |= 1
+ end_line = col == size - 1 and i == scale - 1
+ shift_index = (col * scale + i) % 8
+ # If we filled a byte or reached the end of the row, append it
+ if shift_index == 7 or end_line:
+ if end_line:
+ # Shift pending bits if on last byte of row
+ byte <<= 7 - shift_index
+ line_bytes.append(byte)
+ byte = 0
for _ in range(scale):
- # command += line_bytes
self.uart_conn.write(line_bytes)
time.sleep_ms(self.dot_print_time)
- self.feed(3)
+ self.feed(4)
def set_bitmap_mode(self, width, height, scale_mode=1):
"""Set image format to be printed"""
diff --git a/src/krux/qr.py b/src/krux/qr.py
index 9ad82e515..87c0cd0fb 100644
--- a/src/krux/qr.py
+++ b/src/krux/qr.py
@@ -23,7 +23,6 @@
import io
import math
import qrcode
-from ur.ur_encoder import UREncoder
from ur.ur_decoder import URDecoder
from ur.ur import UR
@@ -31,6 +30,43 @@
FORMAT_PMOFN = 1
FORMAT_UR = 2
+PMOFN_PREFIX_LENGTH_1D = 6
+PMOFN_PREFIX_LENGTH_2D = 8
+UR_GENERIC_PREFIX_LENGTH = 22
+
+# CBOR_PREFIX = 6 bytes for tags, 1 for index, 1 for max_index, 2 for message len, 4 for checksum
+# Check UR's fountain_encoder.py file, on Part.cbor() function for more details
+UR_CBOR_PREFIX_LEN = 14
+UR_BYTEWORDS_CRC_LEN = 4 # 32 bits CRC used on Bytewords encoding
+
+UR_MIN_FRAGMENT_LENGTH = 10
+
+# List of capacities, based on versions
+# Version 1(index 0)=21x21px = 17 bytes, version 2=25x25px = 32 bytes ...
+# Limited to version 20
+QR_CAPACITY = [
+ 17,
+ 32,
+ 53,
+ 78,
+ 106,
+ 134,
+ 154,
+ 192,
+ 230,
+ 271,
+ 321,
+ 367,
+ 425,
+ 458,
+ 520,
+ 586,
+ 644,
+ 718,
+ 792,
+ 858,
+]
+
class QRPartParser:
"""Responsible for parsing either a singular or animated series of QR codes
@@ -50,7 +86,15 @@ def parsed_count(self):
if self.decoder.fountain_decoder.expected_part_indexes is None:
return 1 if self.decoder.result is not None else 0
completion_pct = self.decoder.estimated_percent_complete()
- return math.ceil(completion_pct * self.total_count())
+ return math.ceil(completion_pct * self.total_count() / 2) + len(
+ self.decoder.fountain_decoder.received_part_indexes
+ )
+ return len(self.parts)
+
+ def processed_parts_count(self):
+ """Returns quantity of processed QR code parts"""
+ if self.format == FORMAT_UR:
+ return self.decoder.fountain_decoder.processed_parts_count
return len(self.parts)
def total_count(self):
@@ -59,7 +103,7 @@ def total_count(self):
# Single-part URs have no expected part indexes
if self.decoder.fountain_decoder.expected_part_indexes is None:
return 1
- return self.decoder.expected_part_count()
+ return self.decoder.expected_part_count() * 2
return self.total
def parse(self, data):
@@ -93,6 +137,9 @@ def result(self):
return UR(self.decoder.result.type, bytearray(self.decoder.result.cbor))
code_buffer = io.StringIO("")
for _, part in sorted(self.parts.items()):
+ if isinstance(part, bytes):
+ # Encoded data won't write on StringIO
+ return part
code_buffer.write(part)
code = code_buffer.getvalue()
code_buffer.close()
@@ -104,83 +151,86 @@ def to_qr_codes(data, max_width, qr_format):
the max_width constraint
"""
if qr_format == FORMAT_NONE:
- code = qrcode.encode_to_string(data)
+ code = qrcode.encode(data)
yield (code, 1)
else:
- num_parts = find_min_num_parts(data, max_width, qr_format)
- while math.ceil(data_len(data) / num_parts) > 128:
- num_parts += 1
- part_size = math.ceil(data_len(data) / num_parts)
-
+ num_parts, part_size = find_min_num_parts(data, max_width, qr_format)
if qr_format == FORMAT_PMOFN:
- for i in range(num_parts):
- part_number = "p%dof%d " % (i + 1, num_parts)
+ part_index = 0
+ while True:
+ part_number = "p%dof%d " % (part_index + 1, num_parts)
part = None
- if i == num_parts - 1:
- part = part_number + data[i * part_size :]
+ if part_index == num_parts - 1:
+ part = part_number + data[part_index * part_size :]
+ part_index = 0
else:
- part = part_number + data[i * part_size : i * part_size + part_size]
- code = qrcode.encode_to_string(part)
+ part = (
+ part_number
+ + data[part_index * part_size : (part_index + 1) * part_size]
+ )
+ part_index += 1
+ code = qrcode.encode(part)
yield (code, num_parts)
elif qr_format == FORMAT_UR:
+ from ur.ur_encoder import UREncoder
+
encoder = UREncoder(data, part_size, 0)
while True:
part = encoder.next_part()
- code = qrcode.encode_to_string(part)
+ code = qrcode.encode(part)
yield (code, encoder.fountain_encoder.seq_len())
-def add_qr_frame(qr_code):
- """Add a 1 block white border around the code before displaying"""
- qr_code = qr_code.strip()
- lines = qr_code.split("\n")
- size = len(lines)
- size += 2
- new_lines = ["0" * size]
- for line in lines:
- new_lines.append("0" + line + "0")
- new_lines.append("0" * size)
- return size, "\n".join(new_lines)
-
-
def get_size(qr_code):
"""Returns the size of the qr code as the number of chars until the first newline"""
- size = 0
- while qr_code[size] != "\n":
- size += 1
- return size
+ size = math.sqrt(len(qr_code) * 8)
+ return int(size)
-def data_len(data):
- """Returns the length of the payload data, accounting for the UR type"""
- if isinstance(data, UR):
- return len(data.cbor)
- return len(data)
+def max_qr_bytes(max_width):
+ """Calculates the maximum length, in bytes, a QR code of a given size can store"""
+ # Given qr_size = 17 + 4 * version + 2 * frame_size
+ max_width -= 2 # Subtract frame width
+ qr_version = (max_width - 17) // 4
+ try:
+ return QR_CAPACITY[qr_version - 1]
+ except:
+ # Limited to version 20
+ return QR_CAPACITY[-1]
def find_min_num_parts(data, max_width, qr_format):
"""Finds the minimum number of QR parts necessary to encode the data in
the specified format within the max_width constraint
"""
- num_parts = 1
- part_size = math.ceil(data_len(data) / num_parts)
- while True:
- part = ""
- if qr_format == FORMAT_PMOFN:
- part_number = "p1of%d " % num_parts
- part = part_number + data[0:part_size]
- elif qr_format == FORMAT_UR:
- encoder = UREncoder(data, part_size, 0)
- part = encoder.next_part()
- # The worst-case number of bytes needed to store one QR Code, up to and including
- # version 40. This value equals 3918, which is just under 4 kilobytes.
- if len(part) < 3918:
- code = qrcode.encode_to_string(part)
- if get_size(code) <= max_width:
- break
- num_parts += 1
- part_size = math.ceil(data_len(data) / num_parts)
- return num_parts
+ qr_capacity = max_qr_bytes(max_width)
+ if qr_format == FORMAT_PMOFN:
+ data_length = len(data)
+ part_size = qr_capacity - PMOFN_PREFIX_LENGTH_1D
+ # where prefix = "pXofY " where Y < 9
+ num_parts = (data_length + part_size - 1) // part_size
+ if num_parts > 9: # Prefix has 2 digits numbers, so re-calculate
+ part_size = qr_capacity - PMOFN_PREFIX_LENGTH_2D
+ # where prefix = "pXXofYY " where max YY = 99
+ num_parts = (data_length + part_size - 1) // part_size
+ part_size = (data_length + num_parts - 1) // num_parts
+ elif qr_format == FORMAT_UR:
+ qr_capacity -= (
+ # This is an approximation, UR index grows indefinitely
+ UR_GENERIC_PREFIX_LENGTH # index: ~ "ur:crypto-psbt/xxx-xx/"
+ )
+ # UR will add a bunch of info (some duplicated) on the body of each QR
+ # Info's lenght is multiplied by 2 in Bytewords.encode step
+ qr_capacity -= (UR_CBOR_PREFIX_LEN + UR_BYTEWORDS_CRC_LEN) * 2
+ data_length = len(data.cbor)
+ data_length *= 2 # UR will Bytewords.encode, which multiply bytes length by 2
+ num_parts = (data_length + qr_capacity - 1) // qr_capacity
+ # For UR, part size will be the input for "max_fragment_len"
+ part_size = len(data.cbor) // num_parts
+ part_size = max(part_size, UR_MIN_FRAGMENT_LENGTH)
+ else:
+ raise ValueError("Invalid format type")
+ return num_parts, part_size
def parse_pmofn_qr_part(data):
diff --git a/src/krux/rotary.py b/src/krux/rotary.py
index e75fb6a01..5e0ca3ed4 100644
--- a/src/krux/rotary.py
+++ b/src/krux/rotary.py
@@ -25,7 +25,7 @@
from fpioa_manager import fm
import time
from .krux_settings import Settings
-from .logging import logger as log
+from .buttons import Button
RIGHT = 1
LEFT = 0
@@ -55,9 +55,7 @@ def __init__(self):
self.value = 0
self.time_frame = 0
- self.debounce = Settings().encoder.debounce
-
- log.info("Encoder Initiated Pins: %d and %d" % (pins[0], pins[1]))
+ self.debounce = Settings().hardware.encoder.debounce
def process(self, new_state):
"""Sets new encoder state after position is changed"""
@@ -103,29 +101,23 @@ def _left():
encoder = RotaryEncoder() # Singleton
-class EncoderPage:
+class EncoderPage(Button):
"""Encoder class that mimics Krux Page GPIO Button behavior"""
- def __init__(self):
- pass
-
- def value(self):
- """Returns encoder status while mimics Krux GPIO Buttons behavior"""
+ def event(self):
+ """Returns encoder events while mimics Krux GPIO Buttons behavior"""
if encoder.value > 0:
- encoder.value -= 1
- return 0
- return 1
+ encoder.value = 0
+ return True
+ return False
-class EncoderPagePrev:
+class EncoderPagePrev(Button):
"""Encoder class that mimics Krux Page_prev GPIO Button behavior"""
- def __init__(self):
- pass
-
- def value(self):
- """Returns encoder status while mimics Krux GPIO Buttons behavior"""
+ def event(self):
+ """Returns encoder events while mimics Krux GPIO Buttons behavior"""
if encoder.value < 0:
- encoder.value += 1
- return 0
- return 1
+ encoder.value = 0
+ return True
+ return False
diff --git a/src/krux/sd_card.py b/src/krux/sd_card.py
index d877e2617..5cbb33d62 100644
--- a/src/krux/sd_card.py
+++ b/src/krux/sd_card.py
@@ -23,6 +23,15 @@
from machine import SDCard
from .settings import SD_PATH
+SIGNED_FILE_SUFFIX = "-signed"
+PSBT_FILE_EXTENSION = ".psbt"
+DESCRIPTOR_FILE_EXTENSION = ".txt"
+JSON_FILE_EXTENSION = ".json"
+SIGNATURE_FILE_EXTENSION = ".sig"
+PUBKEY_FILE_EXTENSION = ".pub"
+BMP_IMAGE_EXTENSION = ".bmp"
+PBM_IMAGE_EXTENSION = ".pbm"
+
class SDHandler:
"""A simple handler to work with files on SDCard"""
diff --git a/src/krux/settings.py b/src/krux/settings.py
index fe7f49200..801225b46 100644
--- a/src/krux/settings.py
+++ b/src/krux/settings.py
@@ -75,7 +75,12 @@ def __get__(self, obj, objtype=None):
return store.get(obj.namespace, self.attr, self.default_value)
def __set__(self, obj, value):
- store.set(obj.namespace, self.attr, value)
+ if self.attr == "location":
+ store.update_file_location(value)
+ if value == self.default_value:
+ store.delete(obj.namespace, self.attr) # do not store defaults
+ else:
+ store.set(obj.namespace, self.attr, value) # do store custom settings
class CategorySetting(Setting):
@@ -101,6 +106,7 @@ class Store:
def __init__(self):
self.settings = {}
self.file_location = "/" + FLASH_PATH + "/"
+ self.dirty = False
# Check for the correct settings persist location
try:
@@ -134,49 +140,84 @@ def __init__(self):
)
def get(self, namespace, setting_name, default_value):
- """Loads a setting under the given namespace, returning the default value if not set"""
- s = self.settings
+ """Returns a setting value under the given namespace, or default value if not set"""
+ s = json.loads(
+ json.dumps(self.settings)
+ ) # deepcopy to avoid building out namespaces
for level in namespace.split("."):
s[level] = s.get(level, {})
s = s[level]
if setting_name not in s:
- self.set(namespace, setting_name, default_value)
+ return default_value
return s[setting_name]
def set(self, namespace, setting_name, setting_value):
- """Stores a setting value under the given namespace. We don't use SDHandler
- here because set is called too many times every time the user changes a setting
- and SDHandler remount causes a small delay
+ """Stores a setting value under the given namespace if new/changed.
+ Does NOT automatically save settings to flash or sd!
"""
s = self.settings
for level in namespace.split("."):
s[level] = s.get(level, {})
s = s[level]
old_value = s.get(setting_name, None)
- s[setting_name] = setting_value
+ if old_value != setting_value:
+ s[setting_name] = setting_value
+ self.dirty = True
- # if is a change in settings persist location, delete file from old location,
- # and later it will save on the new location
- if setting_name == "location" and old_value:
- # update the file location
- self.file_location = "/" + setting_value + "/"
+ def delete(self, namespace, setting_name):
+ """Deletes dict storage in self.settings for namespace.setting_name,
+ also deletes storage for setting_name's parent namespace nodes, if empty.
+ """
+ s = self.settings
+ levels = []
+ for level in namespace.split("."):
+ s[level] = s.get(level, {})
+ levels.append([s, level])
+ s = s[level]
+ if setting_name in s:
+ del s[setting_name]
+ self.dirty = True
+ for s, level in reversed(levels):
+ if not s[level]:
+ del s[level]
+ self.dirty = True
+
+ def update_file_location(self, location):
+ """Assumes settings.persist.location will be changed to location:
+ tries to delete current persistent settings file
+ then updates file_location attribute
+ """
+ if "/" + location + "/" != self.file_location:
try:
- # remove old SETTINGS_FILENAME
- os.remove("/" + old_value + "/" + SETTINGS_FILENAME)
+ if os.stat(self.file_location + SETTINGS_FILENAME):
+ os.remove(self.file_location + SETTINGS_FILENAME)
except:
pass
+ self.file_location = "/" + location + "/"
- Store.save_settings()
-
- @staticmethod
- def save_settings():
+ def save_settings(self):
"""Helper to persist SETTINGS_FILENAME where user selected"""
- try:
- # save the new SETTINGS_FILENAME
- with open(store.file_location + SETTINGS_FILENAME, "w") as f:
- f.write(json.dumps(store.settings))
- except:
- pass
+ persisted = False
+
+ if self.dirty:
+ settings_filename = self.file_location + SETTINGS_FILENAME
+ new_contents = json.dumps(self.settings)
+ try:
+ with open(settings_filename, "r") as f:
+ old_contents = f.read()
+ except:
+ old_contents = None
+
+ if new_contents != old_contents:
+ try:
+ with open(settings_filename, "w") as f:
+ f.write(new_contents)
+ persisted = True
+ except:
+ pass
+ self.dirty = False
+
+ return persisted
# Initialize singleton
diff --git a/src/krux/themes.py b/src/krux/themes.py
index b098946d6..c2688de2d 100644
--- a/src/krux/themes.py
+++ b/src/krux/themes.py
@@ -24,41 +24,35 @@
DEFAULT_THEME = ThemeSettings.DARK_THEME_NAME
-# Colors: Ditching firmware colors
-# To create new colors from RGB values use firmware/scripts/krux_colors.py script
+# To create new colors from RGB values use firmware/scripts/rgbconv.py script
BLACK = 0x0000
WHITE = 0xFFFF
LIGHTBLACK = 0x0842
DARKGREY = 0xEF7B
-LIGHTGREY = 0x18C6
+LIGHTGREY = 0x38C6
+LIGHT_GREEN = 0xEC9F
GREEN = 0xE007
DARKGREEN = 0x8005
RED = 0x00F8
+LIGHT_PINK = 0xDFFC
+PINK = 0x1FF8
+PURPLE = 0x0F78
ORANGE = 0x20FD
-DARKORANGE = 0x40C3
-YELLOW = 0xE0FF
+DARKORANGE = 0xA0CA
+YELLOW = 0x85F6
BLUE = 0xF800
LIGHTBLUE = 0xBD0E
CYAN = 0xFF07
-MAGENTA = 0x1FF8
-
-# define NAVY 0x0F00
-# define DARKGREEN 0xE003
-# define DARKCYAN 0xEF03
-# define MAROON 0x0078
-# define PURPLE 0x0F78
-# define OLIVE 0xE07B
-# define RED 0x00F8
-# define GREENYELLOW 0xE5AF
-# define PINK 0x1FF8
+MAGENTA = 0x1FF8 # Remove this color after logger is removed
+
THEMES = {
ThemeSettings.DARK_THEME_NAME: {
"background": BLACK,
"foreground": WHITE,
"frame": DARKGREY,
- "disabled": LIGHTBLACK,
+ "disabled": DARKGREY,
"go": GREEN,
"esc_no": RED,
"del": YELLOW,
@@ -88,7 +82,31 @@
"del": YELLOW,
"toggle": CYAN,
"error": RED,
- "highlight": ORANGE,
+ "highlight": YELLOW,
+ },
+ ThemeSettings.PINK_THEME_NAME: {
+ "background": BLACK,
+ "foreground": LIGHT_PINK,
+ "frame": PURPLE,
+ "disabled": DARKGREY,
+ "go": PINK,
+ "esc_no": RED,
+ "del": YELLOW,
+ "toggle": CYAN,
+ "error": RED,
+ "highlight": PINK,
+ },
+ ThemeSettings.GREEN_THEME_NAME: {
+ "background": BLACK,
+ "foreground": LIGHT_GREEN,
+ "frame": DARKGREEN,
+ "disabled": DARKGREY,
+ "go": GREEN,
+ "esc_no": RED,
+ "del": YELLOW,
+ "toggle": CYAN,
+ "error": RED,
+ "highlight": GREEN,
},
}
diff --git a/src/krux/touch.py b/src/krux/touch.py
index 1ddffe761..a594e2177 100644
--- a/src/krux/touch.py
+++ b/src/krux/touch.py
@@ -22,10 +22,15 @@
# pylint: disable=R0902
import time
-from .touchscreens.ft6x36 import FT6X36
+
+from .touchscreens.ft6x36 import touch_control
from .logging import logger as log
from .krux_settings import Settings
+IDLE = 0
+PRESSED = 1
+RELEASED = 2
+
SWIPE_THRESHOLD = 50
SWIPE_RIGHT = 1
SWIPE_LEFT = 2
@@ -33,30 +38,27 @@
SWIPE_DOWN = 4
TOUCH_S_PERIOD = 20 # Touch sample period - Min = 10
-TOUCH_DEBOUNCE = 200 # Time to wait before sampling touch again after a release
class Touch:
"""Touch is a singleton API to interact with touchscreen driver"""
- idle, press, release = 0, 1, 2
-
- def __init__(self, width, height):
+ def __init__(self, width, height, irq_pin=None):
"""Touch API init - width and height are in Landscape mode
For Krux width = max_y, height = max_x
"""
self.sample_time = 0
- self.debounce = 0
self.y_regions = []
self.x_regions = []
self.index = 0
self.press_point = []
self.release_point = (0, 0)
self.gesture = None
- self.state = Touch.idle
+ self.state = IDLE
self.width, self.height = width, height
- self.touch_driver = FT6X36()
- self.touch_driver.threshold(Settings().touch.threshold)
+ self.touch_driver = touch_control
+ self.touch_driver.activate_irq(irq_pin)
+ self.touch_driver.threshold(Settings().hardware.touch.threshold)
def clear_regions(self):
"""Remove previously stored buttons map"""
@@ -75,6 +77,18 @@ def add_x_delimiter(self, region):
raise ValueError("Touch region added outside display area")
self.x_regions.append(region)
+ def valid_position(self, data):
+ """Checks if touch position is within buttons area"""
+ if self.x_regions and data[0] < self.x_regions[0]:
+ return False
+ if self.x_regions and data[0] > self.x_regions[-1]:
+ return False
+ if self.y_regions and data[1] < self.y_regions[0]:
+ return False
+ if self.y_regions and data[1] > self.y_regions[-1]:
+ return False
+ return True
+
def _extract_index(self, data):
"""Gets an index from touched points, x and y delimiters"""
index = 0
@@ -82,35 +96,26 @@ def _extract_index(self, data):
for region in self.y_regions:
if data[1] > region:
index += 1
- if index == 0 or index >= len(self.y_regions): # outside y areas
- self.state = self.release
- else:
- index -= 1
- if self.x_regions: # if 2D array
- index *= len(self.x_regions) - 1
- x_index = 0
- for x_region in self.x_regions:
- if data[0] > x_region:
- x_index += 1
- if x_index == 0 or x_index >= len(
- self.x_regions
- ): # outside x areas
- self.state = self.release
- else:
- x_index -= 1
- index += x_index
- else:
- index = 0
- return index
+ index -= 1
+ if self.x_regions: # if 2D array
+ index *= len(self.x_regions) - 1
+ x_index = 0
+ for x_region in self.x_regions:
+ if data[0] > x_region:
+ x_index += 1
+ x_index -= 1
+ index += x_index
+ return index
+ return 0
def _store_points(self, data):
"""Store pressed points and calculare an average pressed point"""
- if self.state == self.idle:
- self.state = self.press
+ if self.state == IDLE:
+ self.state = PRESSED
self.press_point = [data]
self.index = self._extract_index(self.press_point[0])
# Calculare an average (max. 10 samples) pressed point to increase precision
- elif self.state == self.press and len(self.press_point) < 10:
+ elif self.state == PRESSED and len(self.press_point) < 10:
self.press_point.append(data)
len_press = len(self.press_point)
x = 0
@@ -125,20 +130,15 @@ def _store_points(self, data):
def current_state(self):
"""Returns the touchscreen state"""
- current_time = time.ticks_ms()
- if (
- current_time > self.sample_time + TOUCH_S_PERIOD
- and current_time > self.debounce + TOUCH_DEBOUNCE
- ):
- self.sample_time = time.ticks_ms()
- data = self.touch_driver.current_point()
- if isinstance(data, tuple):
- self._store_points(data)
- elif data is None: # gets release then return to idle.
- if self.state == self.release:
- self.state = self.idle
- self.debounce = time.ticks_ms()
- elif self.state == self.press:
+ self.sample_time = time.ticks_ms()
+ data = self.touch_driver.current_point()
+ if isinstance(data, tuple):
+ self._store_points(data)
+ elif data is None: # gets release then return to idle.
+ if self.state == RELEASED: # On touch release
+ self.state = IDLE
+ elif self.state == PRESSED:
+ if self.release_point is not None:
lateral_lenght = self.release_point[0] - self.press_point[0][0]
if lateral_lenght > SWIPE_THRESHOLD:
self.gesture = SWIPE_RIGHT
@@ -156,14 +156,27 @@ def current_state(self):
and -vertical_lenght > lateral_lenght
):
self.gesture = SWIPE_UP
- self.state = self.release
- else:
- log.warn("Touch error: " + str(data))
+ self.state = RELEASED
+ else:
+ log.warn("Touch error: " + str(data))
return self.state
+ def event(self):
+ """Checks if a touch happened and stores the point"""
+ current_time = time.ticks_ms()
+ if current_time > self.sample_time + TOUCH_S_PERIOD:
+ if self.touch_driver.event():
+ # Resets touch and gets irq point
+ self.state = IDLE
+ if isinstance(self.touch_driver.irq_point, tuple):
+ if self.valid_position(self.touch_driver.irq_point):
+ self._store_points(self.touch_driver.irq_point)
+ return True
+ return False
+
def value(self):
"""Wraps touch states to behave like a regular button"""
- return 0 if self.current_state() == self.press else 1
+ return 1 if self.current_state() == IDLE else 0
def swipe_right_value(self):
"""Returns detected gestures and clean respective variable"""
diff --git a/src/krux/touchscreens/ft6x36.py b/src/krux/touchscreens/ft6x36.py
index 323387403..3b10c715f 100644
--- a/src/krux/touchscreens/ft6x36.py
+++ b/src/krux/touchscreens/ft6x36.py
@@ -24,9 +24,10 @@
# FT6x36 specs:
# Max sample rate: 100 samples per second
+from Maix import GPIO
+from fpioa_manager import fm
from . import Touchscreen
-import board
-from machine import I2C
+from ..i2c import i2c_bus
FT_DEVICE_MODE = 0x00
GEST_ID = 0x01
@@ -40,33 +41,45 @@
TOUCH_THRESHOLD = 22 # Default 22
+def __handler__(pin_num=None):
+ # pylint: disable=unused-argument
+ """GPIO interrupt handler"""
+ touch_control.trigger_event()
+
+
class FT6X36(Touchscreen):
"""FT6X36 is a minimal wrapper around a I2C connection to setup and get
data from FT6X36 touchscreen IC, part of Sipeed's Maix Amigo device"""
def __init__(self):
+ self.touch_irq_pin = None
+ self.event_flag = False
+ self.irq_point = None
self.addr = FT6X36_ADDR
- self.i2c = I2C(
- I2C.I2C0,
- freq=100000,
- scl=board.config["krux"]["pins"]["I2C_SCL"],
- sda=board.config["krux"]["pins"]["I2C_SDA"],
- )
"""Setup registers"""
# Device mode
self.write_reg(FT_DEVICE_MODE, 0)
# Threshold for touch detection
self.write_reg(FT_ID_G_THGROUP, TOUCH_THRESHOLD)
- # Mode = 0 = polling mode
+ # Mode = 0 = polling mode | Mode = 1 = trigger mode
self.write_reg(FT_ID_G_MODE, 0)
+ def activate_irq(self, irq_pin):
+ """Register IRQ pin IO and its IRQ handler"""
+ fm.register(irq_pin, fm.fpioa.GPIOHS1)
+ self.touch_irq_pin = GPIO(GPIO.GPIOHS1, GPIO.IN, GPIO.PULL_UP)
+ self.touch_irq_pin.irq(__handler__, GPIO.IRQ_FALLING)
+
def write_reg(self, reg_addr, buf):
"""Writes buffer content to a register address"""
- self.i2c.writeto_mem(self.addr, reg_addr, buf, mem_size=8)
+ if i2c_bus is not None:
+ i2c_bus.writeto_mem(self.addr, reg_addr, buf, mem_size=8)
def read_reg(self, reg_addr, buf_len):
"""Reads from a register address"""
- return self.i2c.readfrom_mem(self.addr, reg_addr, buf_len, mem_size=8)
+ if i2c_bus is not None:
+ return i2c_bus.readfrom_mem(self.addr, reg_addr, buf_len, mem_size=8)
+ return None
def current_point(self):
"""If touch is pressed, returns x and y points"""
@@ -83,6 +96,20 @@ def current_point(self):
return e # debug
return None
+ def trigger_event(self):
+ """Called by IRQ handler to set event flag and capture touch point"""
+ self.event_flag = True
+ self.irq_point = self.current_point()
+
+ def event(self):
+ """Returns event status and clears its flag"""
+ flag = self.event_flag
+ self.event_flag = False # Always clean event flag
+ return flag
+
def threshold(self, value):
"""Sets touch sensitivity threshold"""
self.write_reg(FT_ID_G_THGROUP, value)
+
+
+touch_control = FT6X36()
diff --git a/src/krux/translations.py b/src/krux/translations.py
index 266e1614d..16ef4e2c5 100644
--- a/src/krux/translations.py
+++ b/src/krux/translations.py
@@ -42,7 +42,6 @@
88746165: "Anti reflecterend uitgeschakeld",
1521033296: "Anti reflecterend ingeschakeld",
1056821534: "Weet je het zeker?",
- 3247612282: "BIP-39 geheugensteun",
3455872521: "Terug",
2541860807: "Backup van de bootloader...\n\n%d%%",
2256777600: "Ongeldige handtekening",
@@ -72,6 +71,7 @@
597912140: "Snijmethode",
2504034831: "Decimaal",
2751113454: "Ontsleutelen?",
+ 3510912550: "Verwijderen %s?",
1016609898: "Bestand verwijderen?",
1364509700: "Geheugensteun verwijderen",
4102535566: "Diepte per pas",
@@ -117,11 +117,10 @@
4120536442: "GRBL",
299338213: "Eigen ID gebruiken voor geheugensteun? Anders vingerafdruk gebruiken",
602716148: "Ga",
- 831562513: "Warmte interval",
- 2300171403: "Warmte tijd",
3580020863: "Hex publieke sleutel",
2691246967: "Hexadecimaal",
2736309107: "ID bestaat al\n",
+ 1706005805: "Incomplete portemonnee descriptor",
631342955: "Invoer (%d): ",
2585599782: "Ongeldig adres",
2874529150: "Ongeldige bootloader",
@@ -222,6 +221,7 @@
3672006076: "PSBT ondertekend",
2281377987: "Enkele sleutel",
4221794628: "Grootte: ",
+ 2344747135: "Sommige controles kunnen niet worden uitgevoerd.",
2309020186: "Uitgaven (%d): ",
3355862324: "Stackbit 1248",
3303592908: "Opslaan op apparaat",
@@ -258,7 +258,7 @@
4232654916: "Portemonnee descriptor",
2587172867: "Portemonnee descriptor geladen!",
2499782468: "Portemonnee descriptor niet gevonden.",
- 1831109430: "Waarschuwing:\nIncomplete portemonnee descriptor",
+ 2671738224: "Waarschuwing:",
797660533: "Woord %d",
3742424146: "Woord nummers",
2965123464: "Woorden",
@@ -287,7 +287,6 @@
88746165: "Chα»ng lΓ³a bα» vΓ΄ hiα»u hΓ³a",
1521033296: "ΔΓ£ bαΊt chα»ng lΓ³a",
1056821534: "BαΊ‘n cΓ³ chαΊ―c khΓ΄ng?",
- 3247612282: "MΓ£ mnemonic dαΊ‘ng chuαΊ©n BIP39",
3455872521: "Trα» lαΊ‘i",
2541860807: "Sao lΖ°u bα» tαΊ£i khα»i Δα»ng..\n\n%d%%",
2256777600: "Chα»― kΓ½ xαΊ₯u",
@@ -317,6 +316,7 @@
597912140: "PhΖ°Ζ‘ng phΓ‘p cαΊ―t",
2504034831: "Sα» thαΊp phΓ’n",
2751113454: "PhαΊ£n Δα»i?",
+ 3510912550: "XΓ³a %s?",
1016609898: "XΓ³a tΓ i liα»u?",
1364509700: "XΓ³a ghi nhα»",
4102535566: "Δα» sΓ’u mα»i lαΊ§n vượt qua",
@@ -362,11 +362,10 @@
4120536442: "GRBL",
299338213: "Cung cαΊ₯p cho Mnemonic nΓ y mα»t ID tΓΉy chα»nh?NαΊΏu khΓ΄ng thΓ¬ dαΊ₯u vΓ’n tay hiα»n tαΊ‘i sαΊ½ Δược sα» dα»₯ng",
602716148: "ΔαΊΏn",
- 831562513: "KhoαΊ£ng thα»i gian nhiα»t",
- 2300171403: "Thα»i gian nhiα»t",
3580020863: "KhΓ³a cΓ΄ng khai Hex",
2691246967: "ThαΊp lα»₯c phΓ’n",
2736309107: "Id ΔΓ£ tα»n tαΊ‘i\n",
+ 1706005805: "Bα» mΓ΄ tαΊ£ ΔαΊ§u ra chΖ°a hoΓ n chα»nh",
631342955: "ΔαΊ§u vΓ o (%d): ",
2585599782: "Δα»a chα» khΓ΄ng hợp lα»",
2874529150: "Bα» tαΊ£i khα»i Δα»ng khΓ΄ng hợp lα»",
@@ -467,6 +466,7 @@
3672006076: "ΔΓ£ kΓ½ PSBT",
2281377987: "KhΓ³a ΔΖ‘n",
4221794628: "Dung lượng: ",
+ 2344747135: "Mα»t sα» kiα»m tra khΓ΄ng thα» Δược thα»±c hiα»n.",
2309020186: "Chi tiΓͺu (%d): ",
3355862324: "Stackbit 1248",
3303592908: "LΖ°u trα»― trΓͺn flash",
@@ -503,7 +503,7 @@
4232654916: "VΓ ΔαΊ§u ra mΓ΄ tαΊ£",
2587172867: "ΔΓ£ tαΊ£i bα» mΓ΄ tαΊ£ ΔαΊ§u ra của vΓ!",
2499782468: "KhΓ΄ng tΓ¬m thαΊ₯y bα» mΓ΄ tαΊ£ ΔαΊ§u ra vΓ.",
- 1831109430: "CαΊ£nh bΓ‘o:\nBα» mΓ΄ tαΊ£ ΔαΊ§u ra chΖ°a hoΓ n chα»nh",
+ 2671738224: "CαΊ£nh bΓ‘o:",
797660533: "KΓ tα»± %d",
3742424146: "Tα»« sα»",
2965123464: "Tα»« ngα»―",
@@ -532,7 +532,6 @@
88746165: "Anti-Γ©blouissement dΓ©sactivΓ©",
1521033296: "Anti-Γ©blouissement activΓ©",
1056821534: "Es-tu sΓ»r?",
- 3247612282: "BIP39 MnΓ©monique",
3455872521: "Retour",
2541860807: "Sauvegarde du chargeur de dΓ©marrage..\n\n%d%%",
2256777600: "Mauvaise signature",
@@ -562,6 +561,7 @@
597912140: "MΓ©thode de coupe",
2504034831: "DΓ©cimal",
2751113454: "DΓ©crypter?",
+ 3510912550: "Supprimer %s?",
1016609898: "Supprimer le fichier?",
1364509700: "Supprimer mnΓ©monique",
4102535566: "Profondeur par passage",
@@ -607,11 +607,10 @@
4120536442: "GRBL",
299338213: "Donnez Γ ce mnΓ©monique un identifiant personnalisΓ©?Sinon l'empreinte actuelle sera utilisΓ©e",
602716148: "Go",
- 831562513: "Intervalle de chauffe",
- 2300171403: "Temps de chauffe",
3580020863: "ClΓ© public hexadΓ©cimal",
2691246967: "HexadΓ©cimal",
2736309107: "Id existe dΓ©jΓ \n",
+ 1706005805: "Descripteur de sortie incomplet",
631342955: "EntrΓ©es (%d) : ",
2585599782: "Adresse invalide",
2874529150: "Chargeur de dΓ©marrage invalide",
@@ -712,6 +711,7 @@
3672006076: "PSBT signΓ©",
2281377987: "ClΓ© unique",
4221794628: "CapacitΓ©: ",
+ 2344747135: "Certains chΓ¨ques ne peuvent pas Γͺtre effectuΓ©s.",
2309020186: "DΓ©pense (%d) : ",
3355862324: "Stackbit 1248",
3303592908: "Stocker sur flash",
@@ -738,7 +738,7 @@
2402455261: "Utilisez l'entropie de la camΓ©ra pour crΓ©er un nouveau mnΓ©monique",
236075140: "UtilisΓ©: ",
4003084591: "Valeur %s hors de portΓ©e: [%s, %s]",
- 989428076: "La valeur doit Γͺtre multiple de %s",
+ 989428076: "La valeur doit Γͺtre un multiple de %s",
4191058607: "Par camΓ©ra",
1254681955: "Via D20",
525309547: "Via D6",
@@ -748,7 +748,7 @@
4232654916: "Descripteur de sortie du portefeuille",
2587172867: "Descripteur de sortie du portefeuille chargΓ©!",
2499782468: "Descripteur de sortie du portefeuille introuvable.",
- 1831109430: "Attention:\nDescripteur de sortie incomplet",
+ 2671738224: "Avertissement:",
797660533: "Mot %d",
3742424146: "NumΓ©ros de mots",
2965123464: "Mots",
@@ -777,7 +777,6 @@
88746165: "Antirreflexo desativado",
1521033296: "Antirreflexo ativado",
1056821534: "Tem certeza?",
- 3247612282: "MnemΓ΄nico BIP39",
3455872521: "Voltar",
2541860807: "Backing up bootloader..\n\n%d%%",
2256777600: "Assinatura InvΓ‘lida",
@@ -807,6 +806,7 @@
597912140: "MΓ©todo de Corte",
2504034831: "Decimal",
2751113454: "Descriptografar?",
+ 3510912550: "Excluir %s?",
1016609898: "Excluir Arquivo?",
1364509700: "Excluir MnemΓ΄nico",
4102535566: "Profundidade da Passagem",
@@ -852,11 +852,10 @@
4120536442: "GRBL",
299338213: "DΓͺ a este mnemΓ΄nico um ID personalizado? Caso contrΓ‘rio, a impressΓ£o digital atual serΓ‘ usada",
602716148: "Ir",
- 831562513: "Intervalo de Aquecimento",
- 2300171403: "Tempo de Aquecimento",
3580020863: "Chave pΓΊblica hexadecimal",
2691246967: "Hexadecimal",
2736309107: "Id jΓ‘ existe\n",
+ 1706005805: "Descritor incompleto",
631342955: "Entradas (%d): ",
2585599782: "Endereço invÑlido",
2874529150: "Bootloader invΓ‘lido",
@@ -957,6 +956,7 @@
3672006076: "PSBT Assinada",
2281377987: "Single-sig",
4221794628: "Total: ",
+ 2344747135: "Algumas verificaçáes não podem ser realizadas.",
2309020186: "Gastos (%d): ",
3355862324: "Stackbit 1248",
3303592908: "Armazene na Flash",
@@ -993,7 +993,7 @@
4232654916: "Descritor da carteira",
2587172867: "Descritor de saΓda da carteira carregado!",
2499782468: "O descritor de saΓda da carteira nΓ£o foi encontrado.",
- 1831109430: "Atenção:\nDescritor de saΓda incompleto",
+ 2671738224: "Aviso:",
797660533: "Palavra %d",
3742424146: "NΓΊmeros das Palavras",
2965123464: "Palavras",
@@ -1001,6 +1001,494 @@
771968845: "Suas alteraçáes serão mantidas no armazenamento flash do dispositivo.",
2569054451: "Suas alteraçáes serão mantidas no cartão SD.",
},
+ "ru-RU": {
+ 1185266064: "%d ΠΈΠ· %d ΠΌΡΠ»ΡΡΠΈΠΏΠΎΠ΄ΠΏΠΈΡΡ",
+ 2004520398: "%d. Π‘Π΄Π°ΡΠ°: \n\n%s\n\n",
+ 3862364126: "%d. ΠΠ΅ΡΠ΅Π²ΠΎΠ΄ ΡΠ°ΠΌΠΎΠΌΡ ΡΠ΅Π±Π΅: \n\n%s\n\n",
+ 3264377309: "%d. Π Π°ΡΡ
ΠΎΠ΄: \n\n%s\n\n",
+ 2399232215: "%s\n\nΠ²Π°Π»ΠΈΠ΄Π½ΡΠΉ Π°Π΄ΡΠ΅Ρ ΡΠ΄Π°ΡΠΈ!",
+ 3921290840: "%s\n\nΠ²Π°Π»ΠΈΠ΄Π½ΡΠΉ Π°Π΄ΡΠ΅Ρ ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ!",
+ 1808355833: "%s\n\nΠΠ ΠΠΠΠΠΠ Π² ΠΏΠ΅ΡΠ²ΡΡ
%d Π°Π΄ΡΠ΅ΡΠ°Ρ
ΡΠ΄Π°ΡΠΈ",
+ 1306127065: "%s\n\nΠΠ ΠΠΠΠΠΠ Π² ΠΏΠ΅ΡΠ²ΡΡ
%d Π°Π΄ΡΠ΅ΡΠ°Ρ
ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ",
+ 3348584292: "(ΠΠΊΡΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½ΡΠ°Π»ΡΠ½ΡΠΉ)",
+ 2739590230: "12 ΡΠ»ΠΎΠ²",
+ 1310058127: "24 ΡΠ»ΠΎΠ²Π°",
+ 2743272264: "ABC",
+ 1949634023: "Π ΠΡΠΎΠ³ΡΠ°ΠΌΠΌΠ΅",
+ 1517128857: "Adafruit",
+ 3270727197: "ΠΠ΄ΡΠ΅Ρ",
+ 2574498267: "ΠΠ»Ρ ΡΠ΅ΠΆΠΈΠΌΠ° AES-CBC ΡΡΠ΅Π±ΡΠ΅ΡΡΡ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡΠ΅Π»ΡΠ½Π°Ρ ΡΠ½ΡΡΠΎΠΏΠΈΡ ΠΎΡ ΠΊΠ°ΠΌΠ΅ΡΡ",
+ 283202181: "ΠΡΠ°Π²ΠΈΠ»ΡΠ½ΠΎ ΡΠΎΠ²ΠΌΠ΅ΡΡΠΈΡΠ΅ ΠΊΠ°ΠΌΠ΅ΡΡ ΠΈ ΠΠΈΠ½ΠΈ Π‘ΠΈΠ΄-ΡΡΠ°Π·Ρ.",
+ 88746165: "ΠΠ½ΡΠΈΠ±Π»ΠΈΠΊ ΠΎΡΠΊΠ»ΡΡΠ΅Π½",
+ 1521033296: "ΠΠ½ΡΠΈΠ±Π»ΠΈΠΊ Π²ΠΊΠ»ΡΡΠ΅Π½",
+ 1056821534: "ΠΡ ΡΠ²Π΅ΡΠ΅Π½Ρ?",
+ 3455872521: "ΠΠ°Π·Π°Π΄",
+ 2541860807: "Π Π΅Π·Π΅ΡΠ²Π½ΠΎΠ΅ ΠΊΠΎΠΏΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ Π·Π°Π³ΡΡΠ·ΡΠΈΠΊΠ°..\n\n%d%%",
+ 2256777600: "ΠΠ»ΠΎΡ
Π°Ρ ΠΏΠΎΠ΄ΠΏΠΈΡΡ",
+ 3937333362: "Π‘ΠΊΠΎΡΠΎΡΡΡ ΠΠ΅ΡΠ΅Π΄Π°ΡΠΈ ΠΠ°Π½Π½ΡΡ
",
+ 427617266: "ΠΠΈΡΠΊΠΎΠΈΠ½",
+ 928727036: "ΠΠ°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ Π³ΡΠ°Π½ΠΈΡ",
+ 213030954: "CNC",
+ 1207696150: "Π‘Π΄Π°ΡΠ°",
+ 3126552510: "ΠΠ΄ΡΠ΅Ρ Π‘Π΄Π°ΡΠΈ",
+ 1583186953: "Π‘ΠΌΠ΅Π½ΠΈΡΡ ΡΠ΅ΠΌΡ ΠΈ ΠΏΠ΅ΡΠ΅Π·Π°Π³ΡΡΠ·ΠΈΡΡ?",
+ 2697733395: "ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ ΡΠΎΡ
ΡΠ°Π½Π΅Π½Ρ Π½Π° SD ΠΊΠ°ΡΡΠ΅!",
+ 388908871: "ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ Π±ΡΠ΄ΡΡ Ρ
ΡΠ°Π½ΠΈΡΡΡΡ Π΄ΠΎ Π²ΡΠΊΠ»ΡΡΠ΅Π½ΠΈΡ.",
+ 3442025874: "ΠΡΠΎΠ²Π΅ΡΠΈΡΡ SD ΠΠ°ΡΡΡ",
+ 3119547911: "ΠΡΠΎΠ²Π΅ΡΠΈΡΡ, ΡΡΠΎ Π°Π΄ΡΠ΅Ρ ΠΏΡΠΈΠ½Π°Π΄Π»Π΅ΠΆΠΈΡ ΡΡΠΎΠΌΡ ΠΊΠΎΡΠ΅Π»ΡΠΊΡ?",
+ 2856261511: "ΠΡΠΎΠ²Π΅ΡΠ΅Π½ΠΎ %d Π°Π΄ΡΠ΅ΡΠΎΠ² ΡΠ΄Π°ΡΠΈ Π±Π΅Π· ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠΉ.",
+ 2788541416: "ΠΡΠΎΠ²Π΅ΡΠ΅Π½ΠΎ %d Π°Π΄ΡΠ΅ΡΠΎΠ² ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ Π±Π΅Π· ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠΉ.",
+ 2446472910: "ΠΡΠΎΠ²Π΅ΡΡΠ΅ΠΌ Π°Π΄ΡΠ΅Ρ ΡΠ΄Π°ΡΠΈ %d Π½Π° ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠ΅..",
+ 2470115694: "ΠΡΠΎΠ²Π΅ΡΠΊΠ° SD ΠΊΠ°ΡΡΡ..",
+ 3655273987: "ΠΡΠΎΠ²Π΅ΡΡΠ΅ΠΌ Π°Π΄ΡΠ΅Ρ ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ %d Π½Π° ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠ΅..",
+ 2407028014: "ΠΠΎΠΌΠΏΠ°ΠΊΡΠ½ΡΠΉ SeedQR",
+ 4041895036: "ΠΡΠΎΠ΄ΠΎΠ»ΠΆΠΈΡΡ?",
+ 4094072796: "Π‘ΠΎΠ·Π΄Π°ΡΡ QR ΠΠΎΠ΄",
+ 167798282: "Π‘ΠΎΠ·Π΄Π°ΡΡ QR ΠΊΠΎΠ΄ ΠΈΠ· ΡΠ΅ΠΊΡΡΠ°?",
+ 2767642191: "Π‘ΠΎΠ·Π΄Π°Π½ΠΎ",
+ 3513215254: "ΠΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»ΡΡΠΊΠΈΠΉ QR ΠΠΎΠ΄",
+ 124617190: "ΠΠ»ΡΠ±ΠΈΠ½Π° Π Π΅Π·ΠΊΠΈ",
+ 597912140: "ΠΠ΅ΡΠΎΠ΄ Π Π΅Π·ΠΊΠΈ",
+ 2504034831: "ΠΠ΅ΡΡΡΠΈΡΠ½ΡΠΉ",
+ 2751113454: "Π Π°ΡΡΠΈΡΡΠΎΠ²Π°ΡΡ?",
+ 3510912550: "Π£Π΄Π°Π»ΠΈΡΡ %s?",
+ 1016609898: "Π£Π΄Π°Π»ΠΈΡΡ Π€Π°ΠΉΠ»?",
+ 1364509700: "Π£Π΄Π°Π»ΠΈΡΡ ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ 4102535566: "ΠΠ»ΡΠ±ΠΈΠ½Π° ΠΠ° ΠΡΠΎΡ
ΠΎΠ΄",
+ 2791699253: "ΠΡΠΎΠΈΠ·Π²ΠΎΠ΄Π½ΡΠΉ ΠΏΡΡΡ: %s",
+ 1230133196: "Π€Π»ΡΡ ΠΏΠ°ΠΌΡΡΡ ΡΡΡΡΠΎΠΉΡΡΠ²Π° Π½Π΅ ΠΎΠ±Π½Π°ΡΡΠΆΠ΅Π½Π°.",
+ 3836852788: "ΠΠΎΡΠΎΠ²ΠΎ?",
+ 382368239: "ΠΡΠ°ΠΉΠ²Π΅Ρ",
+ 3978947916: "ΠΠΎΠ΄Π΅Ρ",
+ 4090746898: "ΠΠ΅Π±Π°ΡΠ½Ρ ΠΠΎΠ΄Π΅ΡΠ°",
+ 374684711: "ΠΠ°ΡΠΈΡΡΠΎΠ²Π°ΡΡ ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ 1244124409: "ΠΠ°ΡΠΈΡΡΠΎΠ²Π°Π½Π½ΡΠΉ QR ΠΠΎΠ΄",
+ 2968548114: "ΠΠ°ΡΠΈΡΡΠΎΠ²Π°Π½Π½Π°Ρ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π½Π΅ Π±ΡΠ»Π° ΡΠΎΡ
ΡΠ°Π½Π΅Π½Π°",
+ 3315319371: "ΠΠ°ΡΠΈΡΡΠΎΠ²Π°Π½Π½Π°Ρ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π±ΡΠ»Π° ΡΠΎΡ
ΡΠ°Π½Π΅Π½Π° Ρ ID: ",
+ 350279787: "Π¨ΠΈΡΡΠΎΠ²Π°Π½ΠΈΠ΅",
+ 2601598799: "ΠΠ΅ΡΠΎΠ΄ ΡΠΈΡΡΠΎΠ²Π°Π½ΠΈΡ",
+ 3504179008: "ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ ΡΠ»ΠΎΠ²ΠΎ Π²Π°ΡΠ΅ΠΉ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ ΡΠΈΡΠ»Π° ΠΎΡ 1 Π΄ΠΎ 2048.",
+ 1100685007: "ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ ΡΠ»ΠΎΠ²ΠΎ Π²Π°ΡΠ΅ΠΉ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ ΡΠ΅ΡΡΠ½Π°Π΄ΡΠ°ΡΠ΅ΡΠΈΡΠ½ΠΎΠ³ΠΎ ΡΠΈΡΠ»Π° ΠΎΡ 1 Π΄ΠΎ 800.",
+ 4090266642: "ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ ΡΠ»ΠΎΠ²ΠΎ Π²Π°ΡΠ΅ΠΉ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ BIP-39 Π² Π²ΠΈΠ΄Π΅ Π²ΠΎΡΡΠΌΠ΅ΡΠΈΡΠ½ΠΎΠ³ΠΎ ΡΠΈΡΠ»Π° ΠΎΡ 1 Π΄ΠΎ 4000.",
+ 2780625730: "ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ ΡΠ»ΠΎΠ²ΠΎ Π²Π°ΡΠ΅ΠΉ BIP-39 ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ.",
+ 784361051: "ΠΡΠΈΠ±ΠΊΠ°:\n%s",
+ 1505332462: "ΠΡΠΉΡΠΈ",
+ 3838465623: "ΠΡΡΠ»Π΅Π΄ΠΎΠ²Π°ΡΡ ΡΠ°ΠΉΠ»Ρ?",
+ 4170881190: "ΠΠΊΡΠΏΠΎΡΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ Π½Π° SD ΠΊΠ°ΡΡΡ..",
+ 1711312434: "Π Π°ΡΡΠΈΡΠ΅Π½Π½ΡΠΉ ΠΡΠ±Π»ΠΈΡΠ½ΡΠΉ ΠΠ»ΡΡ",
+ 383371114: "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΡΠ°ΡΡΠΈΡΡΠΎΠ²Π°ΡΡ",
+ 3048830188: "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ PSBT",
+ 4192663412: "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ Π°Π΄ΡΠ΅Ρ",
+ 1996021743: "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ ΠΊΠ»ΡΡ",
+ 1108715658: "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅",
+ 1081425878: "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ 928667220: "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ Π²ΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ",
+ 1620572516: "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π·Π°Π³ΡΡΠ·ΠΈΡΡ ΡΡΠ°Π·Ρ-ΠΏΠ°ΡΠΎΠ»Ρ",
+ 2946146830: "ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΡΠΎΡ
ΡΠ°Π½ΠΈΡΡ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ 1303554751: "ΠΠΎΠΌΠΈΡΡΠΈΡ: ",
+ 104500973: "Π‘ΠΊΠΎΡΠΎΡΡΡ ΠΏΠΎΠ΄Π°ΡΠΈ",
+ 3313339187: "ΠΠΌΡ ΡΠ°ΠΉΠ»Π°",
+ 1982637349: "Π€Π°ΠΉΠ» %s ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ Π½Π° SD ΠΊΠ°ΡΡΠ΅, ΠΏΠ΅ΡΠ΅Π·Π°ΠΏΠΈΡΠ°ΡΡ?",
+ 3737729752: "Π€ΠΈΠ½Π³Π΅ΡΠΏΡΠΈΠ½Ρ: %s",
+ 2542772894: "ΠΡΠΎΡΠΈΠ²ΠΊΠ° ΠΏΡΠ΅Π²ΡΡΠ°Π΅Ρ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠΉ ΡΠ°Π·ΠΌΠ΅Ρ: %d",
+ 1406590538: "Flute ΠΠΈΠ°ΠΌΠ΅ΡΡ",
+ 3086093110: "ΠΠ΅ΡΠΏΠ»Π°ΡΠ½ΠΎ: ",
+ 1893243331: "ΠΠ· ΠΠ°ΠΌΡΡΠΈ",
+ 4120536442: "GRBL",
+ 299338213: "ΠΠ°Π·Π½Π°ΡΠΈΡΡ ΡΡΠΎΠΉ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ ΠΊΠ°ΡΡΠΎΠΌΠ½ΡΠΉ ID? Π ΠΈΠ½ΠΎΠΌ ΡΠ»ΡΡΠ°Π΅ Π±ΡΠ΄Π΅Ρ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ ΡΠ΅ΠΊΡΡΠΈΠΉ ΡΠΈΠ½Π³Π΅ΡΠΏΡΠΈΠ½Ρ",
+ 602716148: "OK",
+ 3580020863: "Π¨Π΅ΡΡΠ½Π°Π΄ΡΠ°ΡΠ΅ΡΠΈΡΠ½ΡΠΉ ΠΡΠ±Π»ΠΈΡΠ½ΡΠΉ ΠΠ»ΡΡ",
+ 2691246967: "Π¨Π΅ΡΡΠ½Π°Π΄ΡΠ°ΡΠ΅ΡΠΈΡΠ½ΡΠΉ",
+ 2736309107: "ID ΡΠΆΠ΅ ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ\n",
+ 1706005805: "ΠΠ΅ΠΏΠΎΠ»Π½ΡΠΉ Π²ΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ",
+ 631342955: "ΠΡ
ΠΎΠ΄Ρ (%d): ",
+ 2585599782: "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ Π°Π΄ΡΠ΅Ρ",
+ 2874529150: "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ Π·Π°Π³ΡΡΠ·ΡΠΈΠΊ",
+ 4093416954: "ΠΠ΅Π²Π΅ΡΠ½Π°Ρ Π΄Π»ΠΈΠ½Π° ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ",
+ 1422874211: "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΠΏΡΠ±Π»ΠΈΡΠ½ΡΠΉ ΠΊΠ»ΡΡ",
+ 2443867979: "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΠΊΠΎΡΠ΅Π»Π΅ΠΊ:\n%s",
+ 4122897393: "ΠΠ½Π²Π΅ΡΡΠΈΡΠΎΠ²Π°ΡΡ",
+ 3000888649: "ΠΠ»ΡΡ",
+ 2686333978: "ΠΠ»ΡΡ: ",
+ 4123798664: "Krux\n\n\nΠΠ΅ΡΡΠΈΡ\n%s",
+ 3835918229: "Π’Π΅ΡΡΠΎΠ²ΡΠΉ QR ΠΡΠΈΠ½ΡΠ΅ΡΠ° Krux",
+ 766317539: "Π―Π·ΡΠΊ",
+ 972436696: "ΠΠ°Π΄Π΅ΡΠΆΠΊΠ° ΠΠΈΠ½ΠΈΠΈ",
+ 3596093890: "ΠΠΈΠ½ΠΈΡ: ",
+ 2820726296: "ΠΠ°Π³ΡΡΠ·ΠΈΡΡ ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ 879727077: "ΠΠ°Π³ΡΡΠ·ΠΈΡΡ Ρ SD ΠΊΠ°ΡΡΡ?",
+ 669106195: "ΠΠ°Π³ΡΡΠ·ΠΈΡΡ ΠΎΠ΄Π½Ρ?",
+ 3330705289: "ΠΠ°Π³ΡΡΠ·ΠΈΡΡ?",
+ 2596531078: "ΠΠ°Π³ΡΡΠ·ΠΊΠ° ΠΠ°ΠΌΠ΅ΡΡ..",
+ 596389387: "ΠΠ°Π³ΡΡΠ·ΠΊΠ° Π°Π΄ΡΠ΅ΡΠ° ΡΠ΄Π°ΡΠΈ %d..",
+ 336702608: "ΠΠ°Π³ΡΡΠ·ΠΊΠ° ΠΏΡΠΈΠ½ΡΠ΅ΡΠ°..",
+ 2538883522: "ΠΠ°Π³ΡΡΠ·ΠΊΠ° Π°Π΄ΡΠ΅ΡΠ° ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ %d..",
+ 3159494909: "ΠΠ°Π³ΡΡΠ·ΠΊΠ°..",
+ 1177338798: "ΠΠΎΠΊΠ°Π»Ρ",
+ 2817059741: "Π Π°ΡΠΏΠΎΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅",
+ 63976957: "Π£ΡΠΎΠ²Π΅Π½Ρ Π»ΠΎΠ³ΠΈΡΠΎΠ²Π°Π½ΠΈΡ",
+ 86530918: "ΠΠΎΠ³ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅",
+ 2917810189: "ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½Π°Ρ Π΄Π»ΠΈΠ½Π° ΠΏΡΠ΅Π²ΡΡΠ΅Π½Π° (%s)",
+ 2030045667: "Π‘ΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅",
+ 3928301843: "ΠΡΡΡΡΡΡΠ²ΡΠ΅Ρ ΡΠ°ΠΉΠ» ΠΏΠΎΠ΄ΠΏΠΈΡΠΈ",
+ 1948316555: "ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ°",
+ 2123991188: "ID ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ",
+ 3911073154: "ID ΠΏΠ°ΠΌΡΡΠΈ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠΈ",
+ 570639842: "ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π½Π΅ Π±ΡΠ»Π° ΡΠ°ΡΡΠΈΡΡΠΎΠ²Π°Π½Π°",
+ 1746030071: "ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ° Π½Π΅ Π±ΡΠ»Π° Π·Π°ΡΠΈΡΡΠΎΠ²Π°Π½Π°",
+ 1458925155: "ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΎ: ",
+ 1845376098: "ΠΡΠ»ΡΡΠΈΠΏΠΎΠ΄ΠΏΠΈΡΡ",
+ 2939797024: "Π‘Π΅ΡΡ",
+ 73574491: "ΠΠΎΠ²Π°Ρ ΠΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΠ°",
+ 2792272353: "ΠΠ±Π½Π°ΡΡΠΆΠ΅Π½Π° Π½ΠΎΠ²Π°Ρ ΠΏΡΠΎΡΠΈΠ²ΠΊΠ°.\n\nSHA256:\n%s\n\n\n\nΠ£ΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ?",
+ 4063104189: "ΠΠ΅Ρ",
+ 3927838899: "ΠΠ΅Π· BIP39 ΡΡΠ°Π·Ρ-ΠΏΠ°ΡΠΎΠ»Ρ",
+ 4092516657: "ΠΠ΅Π΄ΠΎΡΡΠ°ΡΠΎΡΠ½ΠΎ Π±ΡΠΎΡΠΊΠΎΠ²!",
+ 1577637745: "ΠΠΎΡΡΠΌΠ΅ΡΠΈΡΠ½ΡΠΉ",
+ 3312581301: "PBKDF2 ΠΡΠ΅ΡΠ°ΡΠΈΠΈ",
+ 721090621: "PSBT",
+ 995862913: "ΠΠ°ΠΊΡΠ°ΡΡΡΠ΅ ΠΏΠ΅ΡΡΠΎΡΠΈΡΠΎΠ²Π°Π½Π½ΡΠ΅ ΡΠΎΡΠΊΠΈ ΡΠ΅ΡΠ½ΡΠΌ ΡΠ²Π΅ΡΠΎΠΌ, ΡΡΠΎΠ±Ρ ΠΈΡ
ΠΌΠΎΠΆΠ½ΠΎ Π±ΡΠ»ΠΎ ΠΎΠ±Π½Π°ΡΡΠΆΠΈΡΡ.",
+ 2987800462: "Π¨ΠΈΡΠΈΠ½Π° ΠΡΠΌΠ°Π³ΠΈ",
+ 3050763890: "Π§Π°ΡΡΡ\n%d / %d",
+ 3559456868: "Π Π°Π·ΠΌΠ΅Ρ Π§Π°ΡΡΠΈ",
+ 4249903283: "Π€ΡΠ°Π·Π°-ΠΏΠ°ΡΠΎΠ»Ρ",
+ 3712257341: "Π€ΡΠ°Π·Π°-ΠΏΠ°ΡΠΎΠ»Ρ: ",
+ 140802882: "ΠΠΎΡΡΠΎΡΠ½Π½Π°Ρ ΠΠ°ΠΌΡΡΡ",
+ 1703779997: "QR ΠΎΡΠΊΡΡΡΡΠΌ ΡΠ΅ΠΊΡΡΠΎΠΌ",
+ 3561756278: "ΠΠΎΠΆΠ°Π»ΡΠΉΡΡΠ° Π·Π°Π³ΡΡΠ·ΠΈΡΠ΅ Π²ΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ ΠΊΠΎΡΠ΅Π»ΡΠΊΠ°",
+ 784609464: "Π‘ΠΊΠΎΡΠΎΡΡΡ ΠΠΎΠ³ΡΡΠΆΠ΅Π½ΠΈΡ",
+ 3037062877: "ΠΠ°ΠΏΠ΅ΡΠ°ΡΠ°ΡΡ Π’Π΅ΡΡΠΎΠ²ΡΠΉ QR",
+ 4278257699: "ΠΠ°ΠΏΠ΅ΡΠ°ΡΠ°ΡΡ Π² Π²ΠΈΠ΄Π΅ QR?\n\n%s\n\n",
+ 516488026: "ΠΠ΅ΡΠ°ΡΠ°ΡΡ?\n\n%s\n\n",
+ 1123106929: "ΠΡΠΈΠ½ΡΠ΅Ρ",
+ 3903571079: "ΠΡΠ°ΠΉΠ²Π΅Ρ ΠΡΠΈΠ½ΡΠ΅ΡΠ° Π½Π΅ ΡΡΡΠ°Π½ΠΎΠ²Π»Π΅Π½!",
+ 2609799302: "ΠΠ΄Π΅Ρ ΠΏΠ΅ΡΠ°ΡΡ\n%d / %d",
+ 844861889: "ΠΠ΄Π΅Ρ ΠΏΠ΅ΡΠ°ΡΡ ...",
+ 2580599003: "ΠΡΠΎΠ΄ΠΎΠ»ΠΆΠΈΡΡ?",
+ 556126964: "ΠΠ±ΡΠ°Π±ΠΎΡΠΊΠ° ...",
+ 1848310591: "QR ΠΠΎΠ΄",
+ 710709610: "RX ΠΠΈΠ½",
+ 2697857197: "ΠΠΎΠ»ΡΡΠΈΡΡ",
+ 1746677167: "ΠΠ΄ΡΠ΅Ρ ΠΠΎΠ»ΡΡΠ΅Π½ΠΈΡ",
+ 364354944: "Π Π΅Π³ΠΈΠΎΠ½: ",
+ 1662254634: "ΠΡΠΎΡΠΌΠΎΡΡΠΈΡΠ΅ ΠΎΡΡΠΊΠ°Π½ΠΈΡΠΎΠ²Π°Π½Π½ΡΠ΅ Π΄Π°Π½Π½ΡΠ΅, ΠΎΡΡΠ΅Π΄Π°ΠΊΡΠΈΡΡΠΉΡΠ΅ ΠΏΡΠΈ Π½Π΅ΠΎΠ±Ρ
ΠΎΠ΄ΠΈΠΌΠΎΡΡΠΈ",
+ 770350922: "ΠΡΠΎΡΡΡΠ΅ ΠΊΡΠ±ΠΈΠΊ Π½Π΅ ΠΌΠ΅Π½Π΅Π΅ %d ΡΠ°Π·, ΡΡΠΎΠ±Ρ ΡΠ³Π΅Π½Π΅ΡΠΈΡΠΎΠ²Π°ΡΡ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ.",
+ 856795528: "ΠΡΠΎΡΠΊΠΈ:\n\n%s",
+ 255086803: "ΠΡΠΎΡΠΊΠΈ: %d\n",
+ 3976793317: "SD ΠΊΠ°ΡΡΠ°",
+ 2827687530: "SD ΠΊΠ°ΡΡΠ° Π½Π΅ ΠΎΠ±Π½Π°ΡΡΠΆΠ΅Π½Π°",
+ 2736513298: "SD ΠΊΠ°ΡΡΠ° Π½Π΅ ΠΎΠ±Π½Π°ΡΡΠΆΠ΅Π½Π°.",
+ 3593785196: "SHA256 Π±ΡΠΎΡΠΊΠΎΠ²:\n\n%s",
+ 1143278725: "SHA256 ΡΠ½ΡΠΏΡΠΎΡΠ°:\n\n%s",
+ 3338679392: "SHA256:\n%s",
+ 3531742595: "Π‘ΠΎΡ
ΡΠ°Π½ΠΈΡΡ Π½Π° SD ΠΊΠ°ΡΡΡ?",
+ 810036588: "Π‘ΠΎΡ
ΡΠ°Π½Π΅Π½ΠΎ Π½Π° SD ΠΊΠ°ΡΡΡ:\n%s",
+ 763824768: "Π¨ΠΊΠ°Π»Π°",
+ 4117455079: "ΠΡΡΠΊΠ°Π½ΠΈΡΠΎΠ²Π°ΡΡ ΠΠ΄ΡΠ΅Ρ",
+ 3219991109: "ΠΡΡΠΊΠ°Π½ΠΈΡΠΎΠ²Π°ΡΡ BIP39 ΡΡΠ°Π·Ρ-ΠΏΠ°ΡΠΎΠ»Ρ",
+ 2537207336: "ΠΡΡΠΊΠ°Π½ΠΈΡΠΎΠ²Π°ΡΡ ΠΠ»ΡΡ QR ΠΊΠΎΠ΄",
+ 4006316572: "Π‘ΠΊΠ°Π½ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΡΠ»ΠΎΠ² 1-12 ΡΠ½ΠΎΠ²Π°",
+ 2736506158: "Π‘ΠΊΠ°Π½ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΡΠ»ΠΎΠ² 13-24",
+ 266935239: "SeedQR",
+ 1698829144: "Π’ΡΠ°Π½ΡΡΠ΅Ρ ΡΠ°ΠΌΠΎΠΌΡ ΡΠ΅Π±Π΅ ΠΈΠ»ΠΈ Π‘Π΄Π°ΡΠ° (%d): ",
+ 473154195: "ΠΠ°ΡΡΡΠΎΠΉΠΊΠΈ",
+ 1825881236: "ΠΡΠΊΠ»ΡΡΠΈΡΡ",
+ 2120776272: "ΠΡΠΊΠ»ΡΡΠ΅Π½ΠΈΠ΅..",
+ 1061961408: "ΠΠΎΠ΄ΠΏΠΈΡΠ°ΡΡ",
+ 4282338366: "ΠΠΎΠ΄ΠΏΠΈΡΠ°ΡΡ?",
+ 746161122: "ΠΠΎΠ΄ΠΏΠΈΡΡ",
+ 1988416729: "ΠΠΎΠ΄ΠΏΠΈΡΠ°Π½Π½ΠΎΠ΅ Π‘ΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅",
+ 3672006076: "ΠΠΎΠ΄ΠΏΠΈΡΠ°Π½Π½ΠΎΠ΅ PSBT",
+ 2281377987: "ΠΠ΄Π½Π° ΠΏΠΎΠ΄ΠΏΠΈΡΡ",
+ 4221794628: "Π Π°Π·ΠΌΠ΅Ρ: ",
+ 2344747135: "ΠΠ΅ΠΊΠΎΡΠΎΡΡΠ΅ ΠΏΡΠΎΠ²Π΅ΡΠΊΠΈ Π½Π΅ ΠΌΠΎΠ³ΡΡ Π±ΡΡΡ Π²ΡΠΏΠΎΠ»Π½Π΅Π½Ρ.",
+ 2309020186: "Π Π°ΡΡ
ΠΎΠ΄ (%d): ",
+ 3355862324: "Π‘ΡΡΠΊΠ±ΠΈΡ 1248",
+ 3303592908: "Π‘ΠΎΡ
ΡΠ°Π½ΠΈΡΡ Π½Π° Π€Π»ΡΡ ΠΠ°ΠΌΡΡΡ",
+ 720041451: "Π‘ΠΎΡ
ΡΠ°Π½ΠΈΡΡ Π½Π° SD ΠΠ°ΡΡΡ",
+ 3514476519: "Π‘Π²Π°ΠΉΠΏΠ½ΠΈΡΠ΅, ΡΡΠΎΠ±Ρ ΡΠΌΠ΅Π½ΠΈΡΡ ΡΠ΅ΠΆΠΈΠΌ",
+ 1898550184: "ΠΠ ΠΠΠΠ‘ΠΠΠ’ΠΠ‘Π¬ ΠΈΠ»ΠΈ Π½Π°ΠΆΠΌΠΈΡΠ΅ ΠΠΠΠ, ΡΡΠΎΠ±Ρ Π·Π°Ρ
Π²Π°ΡΠΈΡΡ",
+ 4228215415: "TX ΠΠΈΠ½",
+ 2612594937: "Π’Π΅ΠΊΡΡ",
+ 1454688268: "Π’Π΅ΠΌΠ°",
+ 1180180513: "Π’Π΅ΡΠΌΠ°Π»ΡΠ½ΡΠΉ",
+ 4119292117: "ΠΠΈΠ½ΠΈ Π‘ΠΈΠ΄-ΡΡΠ°Π·Π°",
+ 1732872974: "ΠΠΈΠ½ΠΈ Π‘ΠΈΠ΄-ΡΡΠ°Π·Π° (ΠΠΈΡΡ)",
+ 725348723: "ΠΠ½ΡΡΡΡΠΌΠ΅Π½ΡΡ",
+ 3684696112: "ΠΡΠΈΠΊΠΎΡΠ½ΠΈΡΠ΅ΡΡ ΠΡΠ°Π½ΠΈΡΡ",
+ 2978718564: "Π’Π°ΡΡΠΊΡΠΈΠ½",
+ 2732611775: "ΠΠΎΠΏΡΠΎΠ±ΠΎΠ²Π°ΡΡ Π΅ΡΡ?",
+ 1487826746: "ΠΠ²Π΅Π΄ΠΈΡΠ΅ BIP39 ΡΡΠ°Π·Ρ-ΠΏΠ°ΡΠΎΠ»Ρ",
+ 2061556020: "ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΠ»ΡΡ",
+ 2089395053: "ΠΠ½ΠΈΡ",
+ 2845607430: "ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ Π·Π°Π³ΡΡΠ·ΡΠΈΠΊΠ°..\n\n%d%%",
+ 4164597446: "ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ Π·Π°Π²Π΅ΡΡΠ΅Π½ΠΎ.\n\nΠΡΠΊΠ»ΡΡΠ΅Π½ΠΈΠ΅..",
+ 2736001501: "ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΏΡΠΎΡΠΈΠ²ΠΊΠΈ..\n\n%d%%",
+ 2674953168: "ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΡ ΡΠ΅ΡΠ½ΡΡ ΡΠΎΠ½ΠΎΠ²ΡΡ ΠΏΠΎΠ²Π΅ΡΡ
Π½ΠΎΡΡΡ.",
+ 2402455261: "ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΡ ΡΠ½ΡΡΠΎΠΏΠΈΡ ΠΊΠ°ΠΌΠ΅ΡΡ, ΡΡΠΎΠ±Ρ ΡΠΎΠ·Π΄Π°ΡΡ Π½ΠΎΠ²ΡΡ ΠΌΠ½Π΅ΠΌΠΎΠ½ΠΈΠΊΡ",
+ 236075140: "ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΎ: ",
+ 4003084591: "ΠΠ½Π°ΡΠ΅Π½ΠΈΠ΅ %s Π²Π½Π΅ Π΄ΠΈΠ°ΠΏΠΎΠ·ΠΎΠ½Π°: [%s, %s]",
+ 4191058607: "Π‘ ΠΠΎΠΌΠΎΡΡΡ ΠΠ°ΠΌΠ΅ΡΡ",
+ 1254681955: "Π‘ ΠΠΎΠΌΠΎΡΡΡ D20",
+ 525309547: "Π‘ ΠΠΎΠΌΠΎΡΡΡ D6",
+ 590330112: "Π‘ ΠΠΎΠΌΠΎΡΡΡ Π ΡΡΠ½ΠΎΠ³ΠΎ ΠΠ²ΠΎΠ΄Π°",
+ 2504354847: "ΠΠΎΠΆΠ΄ΠΈΡΠ΅ΡΡ ΠΠ°Ρ
Π²Π°ΡΠ°",
+ 2297028319: "ΠΠ΅ΡΠΊΡΠΈΠΏΡΠΎΡ ΠΠΎΡΠ΅Π»ΡΠΊΠ°",
+ 4232654916: "ΠΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ ΠΊΠΎΡΠ΅Π»ΡΠΊΠ°",
+ 2587172867: "ΠΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ ΠΊΠΎΡΠ΅Π»ΡΠΊΠ° Π·Π°Π³ΡΡΠΆΠ΅Π½!",
+ 2499782468: "ΠΡΡ
ΠΎΠ΄Π½ΠΎΠΉ Π΄Π΅ΡΠΊΡΠΈΠΏΡΠΎΡ ΠΊΠΎΡΠ΅Π»ΡΠΊΠ° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½.",
+ 2671738224: "ΠΡΠ΅Π΄ΡΠΏΡΠ΅ΠΆΠ΄Π΅Π½ΠΈΠ΅:",
+ 797660533: "Π‘Π»ΠΎΠ²ΠΎ %d",
+ 3742424146: "Π§ΠΈΡΠ»Π° Π‘Π»ΠΎΠ²",
+ 2965123464: "Π‘Π»ΠΎΠ²Π°",
+ 1303016265: "ΠΠ°",
+ 771968845: "ΠΠ°ΡΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ Π±ΡΠ΄ΡΡ ΡΠΎΡ
ΡΠ°Π½Π΅Π½Ρ Π½Π° ΡΠ»ΡΡ ΠΏΠ°ΠΌΡΡΠΈ ΡΡΡΡΠΎΠΉΡΡΠ²Π°.",
+ 2569054451: "ΠΠ°ΡΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ Π±ΡΠ΄ΡΡ ΡΠΎΡ
ΡΠ°Π½Π΅Π½Ρ Π½Π° SD ΠΊΠ°ΡΡΠ΅.",
+ },
+ "pl-PL": {
+ 1185266064: "%d z %d multisig",
+ 2004520398: "%D.Zmiana:\n\n%s\n\n",
+ 3862364126: "%D.Samo-transfer:\n\n%s\n\n",
+ 3264377309: "%D.WydaΔ:\n\n%s\n\n",
+ 2399232215: "%s\n\nis prawidΕowy adres zmiany!",
+ 3921290840: "%s\n\nis waΕΌny adres odbierania!",
+ 1808355833: "%s\n\nwas nie znaleziono w pierwszych adresach zmiany",
+ 1306127065: "%s\n\nwas nie znaleziono w pierwszych %D Otrzymuj adresy",
+ 3348584292: "(Eksperymentalny)",
+ 2739590230: "12 sΕΓ³w",
+ 1310058127: "24 sΕowa",
+ 2743272264: "ABC",
+ 1949634023: "O",
+ 1517128857: "Adafruit",
+ 3270727197: "Adres",
+ 2574498267: "Dodatkowa entropia z kamery wymaganej dla trybu AES-CBC",
+ 283202181: "WΕaΕciwie wyrΓ³wnaj kamerΔ i maleΕkie nasiona.",
+ 88746165: "NiepeΕnosprawne przeciwblokowane",
+ 1521033296: "WΕΔ
czona anty-zabawa",
+ 1056821534: "JesteΕ pewny?",
+ 3455872521: "Z powrotem",
+ 2541860807: "Kopie zapasowe bootloader ..\n\n%d %%",
+ 2256777600: "zΕy podpis",
+ 3937333362: "Baudrate",
+ 427617266: "Bitcoin",
+ 928727036: "WyΕciΓ³Εka graniczna",
+ 213030954: "CNC",
+ 1207696150: "Zmiana",
+ 3126552510: "ZmieΕ adresy",
+ 1583186953: "ZmieniΔ motyw i ponownie uruchomiΔ?",
+ 2697733395: "Zmiany utrzymywaΕy siΔ na karcie SD!",
+ 388908871: "Zmiany bΔdΔ
trwaΔ do zamkniΔcia.",
+ 3442025874: "SprawdΕΊ kartΔ SD",
+ 3119547911: "SprawdΕΊ, czy adres naleΕΌy do tego portfela?",
+ 2856261511: "Sprawdzone %d adresy zmiany bez dopasowaΕ.",
+ 2788541416: "Sprawdzone %D Otrzymuj adresy bez zapaΕek.",
+ 2446472910: "Sprawdzanie Zmieniania Adres %D dla dopasowania.",
+ 2470115694: "Sprawdzanie karty SD ..",
+ 3655273987: "Sprawdzanie adresu Otrzymaj %D dla meczu.",
+ 2407028014: "Kompaktowy seedqr",
+ 4041895036: "KontynuowaΔ?",
+ 4094072796: "UtwΓ³rz kod QR",
+ 167798282: "UtwΓ³rz kod QR z tekstu?",
+ 2767642191: "Utworzony:",
+ 3513215254: "Niestandardowy kod QR",
+ 124617190: "Wytnij gΕΔbokoΕΔ",
+ 597912140: "Metoda ciΔcia",
+ 2504034831: "DziesiΔtny",
+ 2751113454: "OdszyfrowaΔ?",
+ 3510912550: "UsuΕ %s?",
+ 1016609898: "UsunΔ
Δ plik?",
+ 1364509700: "UsuΕ mnemonic",
+ 4102535566: "GΕΔbokoΕΔ na przepustkΔ",
+ 2791699253: "Pochodzenie: %s",
+ 1230133196: "Nie wykryto pamiΔci flash urzΔ
dzenia.",
+ 3836852788: "Zrobione?",
+ 382368239: "Kierowca",
+ 3978947916: "Enkoder",
+ 4090746898: "Encoder Debunet",
+ 374684711: "Szyfrowanie mnemoniki",
+ 1244124409: "Zaszyfrowany kod QR",
+ 2968548114: "Szyfrowana mnemonika nie byΕa przechowywana",
+ 3315319371: "Szyfrowana mnemonika byΕa przechowywana z ID:",
+ 350279787: "Szyfrowanie",
+ 2601598799: "Tryb szyfrowania",
+ 3504179008: "WprowadΕΊ kaΕΌde sΕowo swojego mnemonika BIP-39 jako liczbΔ od 1 do 2048 r.",
+ 1100685007: "WprowadΕΊ kaΕΌde sΕowo swojego mnemonika BIP-39 jako liczbΔ w heksadecimal od 1 do 800.",
+ 4090266642: "WprowadΕΊ kaΕΌde sΕowo swojego mnemonika BIP-39 jako liczbΔ w wysokoΕci od 1 do 4000.",
+ 2780625730: "WprowadΕΊ kaΕΌde sΕowo swojego mnemonika BIP-39.",
+ 784361051: "BΕΔ
d:\n%s",
+ 1505332462: "wyjΕcie",
+ 3838465623: "EksplorowaΔ pliki?",
+ 4170881190: "Eksportowanie do karty SD ..",
+ 1711312434: "Rozszerzony klucz publiczny",
+ 383371114: "Nie udaΕo siΔ odszyfrowaΔ",
+ 3048830188: "Nie udaΕo siΔ zaΕadowaΔ PSBT",
+ 4192663412: "Nie udaΕo siΔ zaΕadowaΔ adresu",
+ 1996021743: "Nie udaΕo siΔ zaΕadowaΔ klucza",
+ 1108715658: "Nie udaΕo siΔ zaΕadowaΔ wiadomoΕci",
+ 1081425878: "Nie udaΕo siΔ zaΕadowaΔ mnemonika",
+ 928667220: "Nie udaΕo siΔ zaΕadowaΔ deskryptora wyjΕciowego",
+ 1620572516: "Nie udaΕo siΔ zaΕadowaΔ pseudonim",
+ 2946146830: "Nie udaΕo siΔ przechowywaΔ mnemonika",
+ 1303554751: "OpΕata:",
+ 104500973: "SzybkoΕΔ pasz",
+ 3313339187: "Nazwa pliku",
+ 1982637349: "Nazwa pliku %s istnieje na karcie SD, zastΔ
p?",
+ 3737729752: "Odcisk palca: %s",
+ 2542772894: "Oprogramowanie ukΕadowe przekracza maksymalny rozmiar: %D",
+ 1406590538: "Εrednica fletu",
+ 3086093110: "BezpΕatny:",
+ 1893243331: "Z przechowywania",
+ 4120536442: "Grbl",
+ 299338213: "Podaj temu mnemonicznemu identyfikatorowi?W przeciwnym razie zostanie uΕΌyty obecny odcisk palca",
+ 602716148: "IΕΔ",
+ 3580020863: "Hex Key Public",
+ 2691246967: "Szesnastkowy",
+ 2736309107: "ID juΕΌ istnieje\n",
+ 1706005805: "inComplete Descriptor",
+ 631342955: "WejΕcia (%d):",
+ 2585599782: "BΕΔdny adres",
+ 2874529150: "NieprawidΕowy bootloader",
+ 4093416954: "NieprawidΕowa dΕugoΕΔ mnemoniczna",
+ 1422874211: "NieprawidΕowy klucz publiczny",
+ 2443867979: "NieprawidΕowy portfel:\n%s",
+ 4122897393: "OdwracaΔ",
+ 3000888649: "Klucz",
+ 2686333978: "Klucz:",
+ 4123798664: "Krux\n\n\neversion\n%s",
+ 3835918229: "Test drukarki Krux QR",
+ 766317539: "JΔzyk",
+ 972436696: "OpΓ³ΕΊnienie linii",
+ 3596093890: "Linia:",
+ 2820726296: "Εaduj mnemoniczne",
+ 879727077: "ZaΕaduj z karty SD?",
+ 669106195: "ZaΕaduj jeden?",
+ 3330705289: "ObciΔ
ΕΌenie?",
+ 2596531078: "Εadowanie aparatu ..",
+ 596389387: "Εadowanie ZMIANY Adres %D ..",
+ 336702608: "Εadowanie drukarki ..",
+ 2538883522: "Εadowanie adresu odbierania %d ..",
+ 3159494909: "Εadowanie..",
+ 1177338798: "Widownia",
+ 2817059741: "Lokalizacja",
+ 63976957: "Poziom dziennika",
+ 86530918: "Logowanie",
+ 2917810189: "Maksymalna dΕugoΕΔ przekroczona (%s)",
+ 2030045667: "WiadomoΕΔ",
+ 3928301843: "BrakujΔ
cy plik podpisu",
+ 1948316555: "Mnemoniczny",
+ 2123991188: "Mnemoniczne id",
+ 3911073154: "Mnemoniczny identyfikator przechowywania",
+ 570639842: "Mnemonik nie zostaΕ odszyfrowany",
+ 1746030071: "Mnemoniczny nie byΕ szyfrowany",
+ 1458925155: "Zmodyfikowany:",
+ 1845376098: "Multisig",
+ 2939797024: "SieΔ",
+ 73574491: "Nowy Mnemonic",
+ 2792272353: "Wykryto nowe oprogramowanie.\n\nsha256:\n%s\n\n\n\ninstall?",
+ 4063104189: "NIE",
+ 3927838899: "Brak frazy BIP39",
+ 4092516657: "Za maΕo rolki!",
+ 1577637745: "Octal",
+ 3312581301: "PBKDF2 ITER.",
+ 721090621: "PSBT",
+ 995862913: "Parzyj kropki czarne, aby moΕΌna je byΕo wykryΔ.",
+ 2987800462: "SzerokoΕΔ papieru",
+ 3050763890: "CzΔΕΔ\n %d / %d",
+ 3559456868: "Rozmiar czΔΕci",
+ 4249903283: "Fraza",
+ 3712257341: "FRASSE:",
+ 140802882: "TrwaΔ",
+ 1703779997: "PlainText QR",
+ 3561756278: "ZaΕaduj deskryptor wyjΕciowy portfela",
+ 784609464: "SzybkoΕΔ spadku",
+ 3037062877: "Test wydrukuj QR",
+ 4278257699: "Drukuj do qr?\n\n%s\n\n",
+ 516488026: "WydrukowaΔ?\n\n%s\n\n",
+ 1123106929: "Drukarka",
+ 3903571079: "Sterownik drukarki nie jest ustawiony!",
+ 2609799302: "Drukowanie\n %d / %d",
+ 844861889: "Drukowanie ...",
+ 2580599003: "PrzystΔpowaΔ?",
+ 556126964: "Przetwarzanie ...",
+ 1848310591: "Kod QR",
+ 710709610: "Pin Rx",
+ 2697857197: "OdbieraΔ",
+ 1746677167: "OdbieraΔ adresy",
+ 364354944: "Region:",
+ 1662254634: "Przejrzyj zeskanowane dane, w razie potrzeby edytuj",
+ 770350922: "RzuΔ kostkΔ co najmniej %d, aby wygenerowaΔ mnemoniczny.",
+ 856795528: "Rolls:\n\n%s",
+ 255086803: "Rolls: %d\n",
+ 3976793317: "karta SD",
+ 2827687530: "Karta SD nie zostaΕa wykryta",
+ 2736513298: "Karta SD nie zostaΕa wykryta.",
+ 3593785196: "SHA256 Rolls:\n\n%s",
+ 1143278725: "SHA256 migawki:\n\n%s",
+ 3338679392: "SHA256:\n%s",
+ 3531742595: "Zapisz na karcie SD?",
+ 810036588: "Zapisano na karcie SD:\n%s",
+ 763824768: "Skala",
+ 4117455079: "Adres skanowania",
+ 3219991109: "Scan BIP39 Passhraz",
+ 2537207336: "Skanuj kod QR",
+ 4006316572: "Znowu skanowanie sΕΓ³w 1-12",
+ 2736506158: "Skanowanie sΕΓ³w 13-24",
+ 266935239: "Seedqr",
+ 1698829144: "Samo-transfer lub zmiana (%d):",
+ 473154195: "Ustawienia",
+ 1825881236: "ZamkniΔcie",
+ 2120776272: "WyΕΔ
czanie..",
+ 1061961408: "PodpisaΔ",
+ 4282338366: "PodpisaΔ?",
+ 746161122: "Podpis",
+ 1988416729: "Podpisana wiadomoΕΔ",
+ 3672006076: "Podpisano PSBT",
+ 2281377987: "Pojedyncze Sig",
+ 4221794628: "Rozmiar:",
+ 2344747135: "Nie moΕΌna wykonaΔ niektΓ³rych kontroli.",
+ 2309020186: "WydaΔ (%d):",
+ 3355862324: "Stackbit 1248",
+ 3303592908: "Przechowuj na Flash",
+ 720041451: "Przechowuj na karcie SD",
+ 3514476519: "PrzesuΕ tryb zmiany",
+ 1898550184: "Dotknij lub wejdΕΊ do przechwytywania",
+ 4228215415: "Pin TX",
+ 2612594937: "Tekst",
+ 1454688268: "Temat",
+ 1180180513: "Termiczny",
+ 4119292117: "MaΕe ziarno",
+ 1732872974: "MaΕe nasiona (bity)",
+ 725348723: "NarzΔdzia",
+ 3684696112: "PrΓ³g dotykowy",
+ 2978718564: "Ekran dotykowy",
+ 2732611775: "PrΓ³buj bardziej?",
+ 1487826746: "PassΓ³wka typu BIP39",
+ 2061556020: "Klucz typu",
+ 2089395053: "Jednostka",
+ 2845607430: "Aktualizacja bootloader ..\n\n%d %%",
+ 4164597446: "Uaktualnienie zakoΕczone.\n\nshutting w dΓ³Ε ..",
+ 2736001501: "Aktualizacja oprogramowania ukΕadowego ..\n\n%d %%",
+ 2674953168: "UΕΌyj czarnej powierzchni tΕa.",
+ 2402455261: "UΕΌyj entropii aparatu, aby stworzyΔ nowy mnemonik",
+ 236075140: "UΕΌywany:",
+ 4003084591: "WartoΕΔ %s z zakresu: [ %s, %s]",
+ 4191058607: "Za poΕrednictwem aparatu",
+ 1254681955: "Via D20",
+ 525309547: "Przez D6",
+ 590330112: "Poprzez rΔczne wejΕcie",
+ 2504354847: "Poczekaj na schwytanie",
+ 2297028319: "Deskryptor portfela",
+ 4232654916: "Deskryptor wyjΕciowy portfela",
+ 2587172867: "ZaΕadowany deskryptor wyjΕciowy portfela!",
+ 2499782468: "Nie znaleziono deskryptora wyjΕciowego portfela.",
+ 2671738224: "OstrzeΕΌenie:",
+ 797660533: "SΕowo %d",
+ 3742424146: "Numery sΕΓ³w",
+ 2965123464: "SΕowa",
+ 1303016265: "Tak",
+ 771968845: "Twoje zmiany bΔdΔ
przechowywane w pamiΔci flash urzΔ
dzenia.",
+ 2569054451: "Twoje zmiany bΔdΔ
przechowywane na karcie SD.",
+ },
"es-MX": {
1185266064: "%d de %d multisig",
2004520398: "%d. Cambio: \n\n%s\n\n",
@@ -1022,7 +1510,6 @@
88746165: "Antideslumbrante desactivado",
1521033296: "Antideslumbrante habilitado",
1056821534: "ΒΏEstas seguro?",
- 3247612282: "BIP39 MnemΓ³nico",
3455872521: "AtrΓ‘s",
2541860807: "Copia de seguridad del cargador de arranque..\n\n%d%%",
2256777600: "Mala asignatura",
@@ -1052,6 +1539,7 @@
597912140: "MΓ©todo de corte",
2504034831: "Decimal",
2751113454: "Descifrar?",
+ 3510912550: "Eliminar %s?",
1016609898: "ΒΏBorrar archivo?",
1364509700: "Eliminar mnemΓ³nico",
4102535566: "Profundidad por pasada",
@@ -1097,11 +1585,10 @@
4120536442: "GRBL",
299338213: "ΒΏDarle a este mnemΓ³nico una identificaciΓ³n personalizada?De lo contrario se utilizarΓ‘ la huella digital actual",
602716148: "Ir",
- 831562513: "Intervalo de calor",
- 2300171403: "Tiempo de calor",
3580020863: "Clave pΓΊblica hexadecimal",
2691246967: "Hexadecimal",
2736309107: "ID ya existe\n",
+ 1706005805: "Descriptor incompleto",
631342955: "Entradas (%d): ",
2585599782: "DirecciΓ³n invΓ‘lida",
2874529150: "Cargador de arranque invΓ‘lido",
@@ -1202,6 +1689,7 @@
3672006076: "PSBT firmado",
2281377987: "Single-sig",
4221794628: "Espacio: ",
+ 2344747135: "No se pueden realizar algunos cheques.",
2309020186: "Gastos (%d): ",
3355862324: "Stackbit 1248",
3303592908: "Almacenar en flash",
@@ -1238,7 +1726,7 @@
4232654916: "Descriptor de salida de billetera",
2587172867: "Β‘Se ha cargado el descriptor de salida de la cartera!",
2499782468: "No se encontrΓ³ el descriptor de salida de la cartera.",
- 1831109430: "Advertencia:\nDescriptor de salida incompleto",
+ 2671738224: "Advertencia:",
797660533: "Palabra %d",
3742424146: "NΓΊmeros de palabra",
2965123464: "Palabras",
@@ -1267,7 +1755,6 @@
88746165: "Blendschutz deaktiviert",
1521033296: "Blendschutz aktiviert",
1056821534: "Bist Du sicher?",
- 3247612282: "BIP39 Mnemonic",
3455872521: "ZurΓΌck",
2541860807: "Bootloader wird gesichert..\n\n%d%%",
2256777600: "UngΓΌltige Signatur",
@@ -1297,6 +1784,7 @@
597912140: "Cut-Methode",
2504034831: "Dezimal",
2751113454: "EntschlΓΌsseln?",
+ 3510912550: "LΓΆschen %s?",
1016609898: "Datei lΓΆschen?",
1364509700: "Mnemonic lΓΆschen",
4102535566: "Tiefe pro Durchgang",
@@ -1342,11 +1830,10 @@
4120536442: "GRBL",
299338213: "Dieser Mnemonic eine benutzerdefinierte ID zuteilen? Andernfalls wird der aktuelle Fingerabdruck verwendet",
602716148: "Go",
- 831562513: "WΓ€rmeintervall",
- 2300171403: "Hitzezeit",
3580020863: "Hex ΓΆffentlicher SchlΓΌssel",
2691246967: "Hexadezimal",
2736309107: "ID existiert bereits\n",
+ 1706005805: "UnvollstΓ€ndiger Ausgabedeskriptor",
631342955: "Input (%d): ",
2585599782: "UngΓΌltige Adresse",
2874529150: "UngΓΌltiger Bootloader",
@@ -1447,6 +1934,7 @@
3672006076: "Signierte PSBT",
2281377987: "Single-Sig",
4221794628: "GrΓΆΓe: ",
+ 2344747135: "Einige Schecks kΓΆnnen nicht durchgefΓΌhrt werden.",
2309020186: "Ausgabe (%d): ",
3355862324: "Stackbit 1248",
3303592908: "Auf Flash speichern",
@@ -1483,7 +1971,7 @@
4232654916: "Wallet Ausgabedeskriptor",
2587172867: "Wallet Ausgabedeskriptor geladen!",
2499782468: "Wallet Ausgabedeskriptor nicht gefunden.",
- 1831109430: "Warnung:\nUnvollstΓ€ndiger Ausgabedeskriptor",
+ 2671738224: "Warnung:",
797660533: "Wort %d",
3742424146: "Wortnummern",
2965123464: "WΓΆrter",
diff --git a/src/krux/wallet.py b/src/krux/wallet.py
index 1d9e71c2e..9dfe310de 100644
--- a/src/krux/wallet.py
+++ b/src/krux/wallet.py
@@ -43,7 +43,7 @@ def __init__(self, key):
self.policy = None
if not self.key.multisig:
self.descriptor = Descriptor.from_string(
- "wpkh(%s/{0,1}/*)" % self.key.key_expression()
+ "wpkh(%s/<0;1>/*)" % self.key.key_expression()
)
self.label = t("Single-sig")
self.policy = {"type": self.descriptor.scriptpubkey_type()}
@@ -206,7 +206,7 @@ def parse_wallet(wallet_data, network):
)
else:
# Single-sig
- descriptor = Descriptor.from_string("wpkh(%s/{0,1}/*)" % keys[0])
+ descriptor = Descriptor.from_string("wpkh(%s/<0;1>/*)" % keys[0])
label = (
key_vals[key_vals.index("Name") + 1]
if key_vals.index("Name") >= 0
diff --git a/tests/conftest.py b/tests/conftest.py
index e94a35f34..bbb9ed37f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4,6 +4,7 @@
board_dock,
board_m5stickv,
encode_to_string,
+ encode,
statvfs,
)
@@ -26,7 +27,7 @@ def mp_modules(mocker, monkeypatch):
monkeypatch.setitem(
sys.modules,
"qrcode",
- mocker.MagicMock(encode_to_string=encode_to_string),
+ mocker.MagicMock(encode_to_string=encode_to_string, encode=encode),
)
monkeypatch.setitem(sys.modules, "secp256k1", mocker.MagicMock(wraps=secp256k1))
monkeypatch.setitem(sys.modules, "urandom", random)
diff --git a/tests/pages/new_mnemonic/__init__.py b/tests/pages/new_mnemonic/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/pages/new_mnemonic/test_dice_rolls.py b/tests/pages/new_mnemonic/test_dice_rolls.py
new file mode 100644
index 000000000..399d49ebd
--- /dev/null
+++ b/tests/pages/new_mnemonic/test_dice_rolls.py
@@ -0,0 +1,272 @@
+from ..test_login import create_ctx
+from embit import bip39
+
+
+
+def test_new_12w_from_d6(m5stickv, mocker):
+ from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D6_12W_MIN_ROLLS
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ BTN_SEQUENCE = (
+ # 1 press to proceed to 12 words
+ [BUTTON_ENTER]
+ +
+ # 1 press to proceed msg
+ [BUTTON_ENTER]
+ +
+ # 1 presses per roll
+ [BUTTON_ENTER for _ in range(D6_12W_MIN_ROLLS)]
+ +
+ # 1 press prev and 1 press on btn Go
+ [BUTTON_PAGE_PREV, BUTTON_ENTER]
+ + [
+ BUTTON_ENTER, # proceed with poor entropy,
+ BUTTON_PAGE, # move to Generate Words, while seeing rolls
+ BUTTON_ENTER, # generate words
+ BUTTON_ENTER, # 1 press to confirm SHA
+ ]
+ )
+ MNEMONIC = "diet glad hat rural panther lawsuit act drop gallery urge where fit"
+
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ dice_entropy = DiceEntropy(ctx)
+ entropy = dice_entropy.new_key()
+ words = bip39.mnemonic_from_bytes(entropy)
+
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+ assert words == MNEMONIC
+
+
+def test_new_24w_from_d6(m5stickv, mocker):
+ from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D6_24W_MIN_ROLLS
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ BTN_SEQUENCE = (
+ # 1 press change to 24 words and 1 press to proceed
+ [BUTTON_PAGE, BUTTON_ENTER]
+ +
+ # 1 press to proceed msg
+ [BUTTON_ENTER]
+ +
+ # 1 presses per roll
+ [BUTTON_ENTER for _ in range(D6_24W_MIN_ROLLS)]
+ +
+ # 1 press prev and 1 press on btn Go
+ [BUTTON_PAGE_PREV, BUTTON_ENTER]
+ + [
+ BUTTON_ENTER, # proceed with poor entropy,
+ BUTTON_PAGE, # move to Generate Words, while seeing rolls
+ BUTTON_ENTER, # generate words
+ BUTTON_ENTER, # 1 press to confirm SHA
+ ]
+ )
+ MNEMONIC = "wheel erase puppy pistol chapter accuse carpet drop quote final attend near scrap satisfy limit style crunch person south inspire lunch meadow enact tattoo"
+
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ dice_entropy = DiceEntropy(ctx)
+ entropy = dice_entropy.new_key()
+ words = bip39.mnemonic_from_bytes(entropy)
+
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+ assert words == MNEMONIC
+
+
+
+def test_new_12w_from_d6_on_amigo_device(amigo_tft, mocker):
+ from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D6_12W_MIN_ROLLS
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ BTN_SEQUENCE = (
+ # 1 press to proceed to 12 words
+ [BUTTON_ENTER]
+ +
+ # 1 press to proceed msg
+ [BUTTON_ENTER]
+ +
+ # 1 presses per roll
+ [BUTTON_ENTER for _ in range(D6_12W_MIN_ROLLS)]
+ +
+ # 1 press prev and 1 press on btn Go
+ [BUTTON_PAGE_PREV, BUTTON_ENTER]
+ + [
+ BUTTON_ENTER, # proceed with poor entropy,
+ BUTTON_PAGE, # move to Generate Words, while seeing rolls
+ BUTTON_ENTER, # generate words
+ BUTTON_ENTER, # 1 press to confirm SHA
+ ]
+ )
+ MNEMONIC = "diet glad hat rural panther lawsuit act drop gallery urge where fit"
+
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ dice_entropy = DiceEntropy(ctx)
+ entropy = dice_entropy.new_key()
+ words = bip39.mnemonic_from_bytes(entropy)
+
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+ assert words == MNEMONIC
+
+
+def test_new_24w_from_d6_on_amigo_device(amigo_tft, mocker):
+ from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D6_24W_MIN_ROLLS
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ BTN_SEQUENCE = (
+ # 1 press change to 24 words and 1 press to proceed
+ [BUTTON_PAGE, BUTTON_ENTER]
+ +
+ # 1 press to proceed msg
+ [BUTTON_ENTER]
+ +
+ # 1 presses per roll
+ [BUTTON_ENTER for _ in range(D6_24W_MIN_ROLLS)]
+ +
+ # 1 press prev and 1 press on btn Go
+ [BUTTON_PAGE_PREV, BUTTON_ENTER]
+ + [
+ BUTTON_ENTER, # proceed with poor entropy,
+ BUTTON_PAGE, # move to Generate Words, while seeing rolls
+ BUTTON_ENTER, # generate words
+ BUTTON_ENTER, # 1 press to confirm SHA
+ ]
+ )
+ MNEMONIC = "wheel erase puppy pistol chapter accuse carpet drop quote final attend near scrap satisfy limit style crunch person south inspire lunch meadow enact tattoo"
+
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ dice_entropy = DiceEntropy(ctx, )
+ entropy = dice_entropy.new_key()
+ words = bip39.mnemonic_from_bytes(entropy)
+
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+ assert words == MNEMONIC
+
+
+def test_cancel_new_12w_from_d6_on_amigo_device(amigo_tft, mocker):
+ "Will test the Esc button on the roll screen"
+ from krux.pages.new_mnemonic.dice_rolls import DiceEntropy
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE_PREV
+
+ BTN_SEQUENCE = (
+ # 1 press to proceed to 12 words
+ [BUTTON_ENTER]
+ +
+ # 1 press to proceed msg
+ [BUTTON_ENTER]
+ +
+ # 2 press prev and 1 press on btn Esc
+ [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER]
+ +
+ # 1 press to proceed confirm exit msg
+ [BUTTON_ENTER]
+ )
+
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ dice_entropy = DiceEntropy(ctx, is_d20=True)
+ dice_entropy.new_key()
+
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+
+def test_new_12w_from_d20(m5stickv, mocker):
+ from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D20_12W_MIN_ROLLS
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ BTN_SEQUENCE = (
+ # 1 press to proceed to 12 words
+ [BUTTON_ENTER]
+ +
+ # 1 press to proceed msg
+ [BUTTON_ENTER]
+ +
+ # 1 presses per roll
+ [BUTTON_ENTER for _ in range(D20_12W_MIN_ROLLS)]
+ +
+ # 1 press prev and 1 press on btn Go
+ [BUTTON_PAGE_PREV, BUTTON_ENTER]
+ + [
+ BUTTON_ENTER, # proceed with poor entropy,
+ BUTTON_PAGE, # move to Generate Words, while seeing rolls
+ BUTTON_ENTER, # generate words
+ BUTTON_ENTER, # 1 press to confirm SHA
+ ]
+ )
+ MNEMONIC = (
+ "erupt remain ride bleak year cabin orange sure ghost gospel husband oppose"
+ )
+
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ dice_entropy = DiceEntropy(ctx, is_d20=True)
+ entropy = dice_entropy.new_key()
+ words = bip39.mnemonic_from_bytes(entropy)
+
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+ assert words == MNEMONIC
+
+
+def test_new_24w_from_d20(m5stickv, mocker):
+ from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D20_24W_MIN_ROLLS
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ BTN_SEQUENCE = (
+ # 1 press change to 24 words and 1 press to proceed
+ [BUTTON_PAGE, BUTTON_ENTER]
+ +
+ # 1 press to proceed msg
+ [BUTTON_ENTER]
+ +
+ # 1 presses per roll
+ [BUTTON_ENTER for _ in range(D20_24W_MIN_ROLLS)]
+ +
+ # 1 press prev and 1 press on btn Go
+ [BUTTON_PAGE_PREV, BUTTON_ENTER]
+ + [
+ BUTTON_ENTER, # proceed with poor entropy,
+ BUTTON_PAGE, # move to Generate Words, while seeing rolls
+ BUTTON_ENTER, # generate words
+ BUTTON_ENTER, # 1 press to confirm SHA
+ ]
+ )
+ MNEMONIC = "fun island vivid slide cable pyramid device tuition only essence thought gain silk jealous eternal anger response virus couple faculty ozone test key vocal"
+
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ dice_entropy = DiceEntropy(ctx, is_d20=True)
+ entropy = dice_entropy.new_key()
+ words = bip39.mnemonic_from_bytes(entropy)
+
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
+ assert words == MNEMONIC
+
+def test_cancel_new_12w_from_d20(m5stickv, mocker):
+ "Will test the Deletion button and the minimum roll on the roll screen"
+ from krux.pages.new_mnemonic.dice_rolls import DiceEntropy, D20_12W_MIN_ROLLS
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+
+ BTN_SEQUENCE = (
+ # 1 press to proceed to 12 words
+ [BUTTON_ENTER]
+ +
+ # 1 press to proceed msg
+ [BUTTON_ENTER]
+ +
+ # 1 presses per roll
+ [BUTTON_ENTER for _ in range(D20_12W_MIN_ROLLS)]
+ +
+ # 3 press prev and 1 press on btn < (delete last roll)
+ [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER]
+ +
+ # 1 press prev and 1 press on btn Go
+ [BUTTON_PAGE_PREV, BUTTON_ENTER]
+ +
+ # 1 press for msg not enough rolls!
+ [BUTTON_ENTER]
+ +
+ # 2 press prev and 1 press on btn Esc
+ [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER]
+ +
+ # 1 press to proceed confirm exit msg
+ [BUTTON_ENTER]
+ )
+
+ ctx = create_ctx(mocker, BTN_SEQUENCE)
+ dice_entropy = DiceEntropy(ctx, is_d20=True)
+ dice_entropy.new_key()
+
+ assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
\ No newline at end of file
diff --git a/tests/pages/test_addresses.py b/tests/pages/test_addresses.py
index 134fa90a7..d1880fa60 100644
--- a/tests/pages/test_addresses.py
+++ b/tests/pages/test_addresses.py
@@ -9,45 +9,41 @@ def test_scan_address(mocker, m5stickv, tdata):
from krux.qr import FORMAT_PMOFN, FORMAT_NONE
cases = [
- # Single-sig, loaded, owned address, No print prompt, search successful
+ # Single-sig, loaded, owned address, search successful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"bc1qrhjqrz2d9tdym3p2r9m2vwzn2sn2yl6k5m357y",
- None,
True,
[BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
),
- # Single-sig, not loaded, owned address, No print prompt, search successful
+ # Single-sig, not loaded, owned address, search successful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
False,
"bc1qrhjqrz2d9tdym3p2r9m2vwzn2sn2yl6k5m357y",
- None,
True,
[BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
),
- # Single-sig, loaded, owned address, Print, search successful
+ # Single-sig, loaded, owned address, search successful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"bc1qrhjqrz2d9tdym3p2r9m2vwzn2sn2yl6k5m357y",
- MockPrinter(),
True,
- [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
+ [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
),
- # Single-sig, loaded, owned address, Decline to print, search successful
+ # Single-sig, loaded, owned address, search successful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"bc1qrhjqrz2d9tdym3p2r9m2vwzn2sn2yl6k5m357y",
- MockPrinter(),
True,
- [BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER, BUTTON_ENTER],
+ [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
),
# Multisig, loaded, owned address, No print prompt, search successful
(
@@ -55,167 +51,150 @@ def test_scan_address(mocker, m5stickv, tdata):
tdata.SPECTER_MULTISIG_WALLET_DATA,
True,
"bc1q6y95p2qkcmsr7kp5zpnt04qx5l2slq73d9um62ka3s5nr83mlcfsywsn65",
- None,
True,
[BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
),
- # Multisig, not loaded, owned address, No print prompt, can't search
+ # Multisig, not loaded, owned address, can't search
(
tdata.MULTISIG_12_WORD_KEY,
tdata.SPECTER_MULTISIG_WALLET_DATA,
False,
"bc1q6y95p2qkcmsr7kp5zpnt04qx5l2slq73d9um62ka3s5nr83mlcfsywsn65",
- None,
True,
[BUTTON_ENTER],
),
- # Multisig, loaded, owned address, Print, search successful
+ # Multisig, loaded, owned address, search successful
(
tdata.MULTISIG_12_WORD_KEY,
tdata.SPECTER_MULTISIG_WALLET_DATA,
True,
"bc1q6y95p2qkcmsr7kp5zpnt04qx5l2slq73d9um62ka3s5nr83mlcfsywsn65",
- MockPrinter(),
True,
- [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
+ [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
),
- # Multisig, loaded, owned address, Decline to print, search successful
+ # Multisig, loaded, owned address, search successful
(
tdata.MULTISIG_12_WORD_KEY,
tdata.SPECTER_MULTISIG_WALLET_DATA,
True,
"bc1q6y95p2qkcmsr7kp5zpnt04qx5l2slq73d9um62ka3s5nr83mlcfsywsn65",
- MockPrinter(),
True,
- [BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER, BUTTON_ENTER],
+ [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
),
- # Single-sig, loaded, unowned address, No print prompt, search unsuccessful
+ # Single-sig, loaded, unowned address, search unsuccessful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"bc1q6y95p2qkcmsr7kp5zpnt04qx5l2slq73d9um62ka3s5nr83mlcfsywsn65",
- None,
True,
[BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
),
- # Multisig, loaded, unowned address, No print prompt, search unsuccessful
+ # Multisig, loaded, unowned address, search unsuccessful
(
tdata.MULTISIG_12_WORD_KEY,
tdata.SPECTER_MULTISIG_WALLET_DATA,
True,
"bc1qrhjqrz2d9tdym3p2r9m2vwzn2sn2yl6k5m357y",
- None,
True,
[BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
),
- # Single-sig, loaded, unowned m/44 address, No print prompt, skip search
+ # Single-sig, loaded, unowned m/44 address, skip search
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"14ihRbmxbgZ6JN9HdDDo6u6nGradHDy4GJ",
- None,
True,
[BUTTON_ENTER, BUTTON_PAGE],
),
- # Single-sig, loaded, unowned m/44 address, No print prompt, search unsuccessful
+ # Single-sig, loaded, unowned m/44 address, search unsuccessful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"14ihRbmxbgZ6JN9HdDDo6u6nGradHDy4GJ",
- None,
True,
[BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
),
- # Single-sig, loaded, unowned m/44 address, No print prompt, 2x search unsuccessful
+ # Single-sig, loaded, unowned m/44 address, 2x search unsuccessful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"14ihRbmxbgZ6JN9HdDDo6u6nGradHDy4GJ",
- None,
True,
[BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
),
- # Single-sig, loaded, unowned m/48/0/0/2 address, No print prompt, search unsuccessful
+ # Single-sig, loaded, unowned m/48/0/0/2 address, search unsuccessful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"1BRwWQ3GHabCV5DP6MfnCpr6dF6GBAwQ7k",
- None,
True,
[BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
),
- # Single-sig, loaded, unowned m/84 address, No print prompt, search unsuccessful
+ # Single-sig, loaded, unowned m/84 address, search unsuccessful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"bc1qx2zuday8d6j4ufh4df6e9ttd06lnfmn2cuz0vn",
- None,
True,
[BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
),
- # Single-sig, loaded, unowned m/49 address, No print prompt, search unsuccessful
+ # Single-sig, loaded, unowned m/49 address, search unsuccessful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"32iCX1pY1iztdgM5qzurGLPMu5xhNfAUtg",
- None,
True,
[BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
),
- # Single-sig, loaded, unowned m/0 address, No print prompt, search unsuccessful
+ # Single-sig, loaded, unowned m/0 address, search unsuccessful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"3KLoUhwLihgC5aPQPFHakWUtJ4QoBkT7Aw",
- None,
True,
[BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
),
- # Single-sig, loaded, unowned m/0 address, Print, search unsuccessful
+ # Single-sig, loaded, unowned m/0 address, search unsuccessful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"3KLoUhwLihgC5aPQPFHakWUtJ4QoBkT7Aw",
- MockPrinter(),
True,
- [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
+ [BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
),
- # Single-sig, loaded, unowned m/0 address, Decline to print, search unsuccessful
+ # Single-sig, loaded, unowned m/0 address, search unsuccessful
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"3KLoUhwLihgC5aPQPFHakWUtJ4QoBkT7Aw",
- MockPrinter(),
True,
- [BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
+ [BUTTON_ENTER, BUTTON_ENTER, BUTTON_PAGE, BUTTON_ENTER],
),
- # Single-sig, loaded, fail to capture QR of address, No print prompt, can't search
+ # Single-sig, loaded, fail to capture QR of address, can't search
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
None,
- None,
False,
[],
),
- # Single-sig, loaded, invalid address, No print prompt, can't search
+ # Single-sig, loaded, invalid address, can't search
(
tdata.SINGLESIG_12_WORD_KEY,
tdata.SPECTER_SINGLESIG_WALLET_DATA,
True,
"invalidaddress",
- None,
False,
[],
),
@@ -225,28 +204,29 @@ def test_scan_address(mocker, m5stickv, tdata):
if case[2]:
wallet.load(case[1], FORMAT_PMOFN)
- ctx = create_ctx(mocker, case[6], wallet, case[4])
+ ctx = create_ctx(mocker, case[5], wallet, None)
addresses_ui = Addresses(ctx)
mocker.patch.object(
addresses_ui, "capture_qr_code", new=lambda: (case[3], FORMAT_NONE)
)
+
mocker.patch.object(
addresses_ui,
- "display_qr_codes",
- new=lambda data, qr_format, title=None, allow_any_btn=True: ctx.input.wait_for_button(),
+ "show_address",
+ new=lambda addr, title="", quick_exit=False: ctx.input.wait_for_button(),
)
+ mocker.spy(addresses_ui, "show_address")
mocker.spy(addresses_ui, "capture_qr_code")
- mocker.spy(addresses_ui, "display_qr_codes")
addresses_ui.scan_address()
addresses_ui.capture_qr_code.assert_called_once()
- if case[5]:
- addresses_ui.display_qr_codes.assert_called_once()
+ if case[4]:
+ addresses_ui.show_address.assert_called_once()
else:
- addresses_ui.display_qr_codes.assert_not_called()
+ addresses_ui.show_address.assert_not_called()
- assert ctx.input.wait_for_button.call_count == len(case[6])
+ assert ctx.input.wait_for_button.call_count == len(case[5])
def test_list_receive_addresses(mocker, m5stickv, tdata):
@@ -265,11 +245,14 @@ def test_list_receive_addresses(mocker, m5stickv, tdata):
None,
[
BUTTON_ENTER, # Show address nΒΊ1
+ BUTTON_ENTER, # QR Menu
+ BUTTON_PAGE_PREV, # Go to "Back to Menu"
BUTTON_ENTER, # Leave
BUTTON_PAGE_PREV, # Go to "Back"
BUTTON_ENTER, # Leave
],
),
+ # TODO: Add cases for multisig and thermal printing
]
for case in cases:
wallet = Wallet(case[0])
@@ -278,14 +261,9 @@ def test_list_receive_addresses(mocker, m5stickv, tdata):
ctx = create_ctx(mocker, case[5], wallet, case[4])
addresses_ui = Addresses(ctx)
- mocker.patch.object(
- addresses_ui,
- "display_qr_codes",
- new=lambda data, qr_format=FORMAT_NONE, title=None, allow_any_btn=True: ctx.input.wait_for_button(),
- )
- mocker.spy(addresses_ui, "display_qr_codes")
+ mocker.spy(addresses_ui, "show_address")
addresses_ui.list_address_type()
- addresses_ui.display_qr_codes.assert_called_once()
+ addresses_ui.show_address.assert_called_once()
assert ctx.input.wait_for_button.call_count == len(case[5])
diff --git a/tests/pages/test_encryption_ui.py b/tests/pages/test_encryption_ui.py
index ffeb8d87f..fd6473e47 100644
--- a/tests/pages/test_encryption_ui.py
+++ b/tests/pages/test_encryption_ui.py
@@ -72,6 +72,7 @@ def test_load_key_from_qr_code(m5stickv, mocker):
from krux.pages.encryption_ui import EncryptionKey, ENCRYPTION_KEY_MAX_LEN
from krux.input import BUTTON_ENTER, BUTTON_PAGE
+ print("case 1: load_key_from_qr_code")
BTN_SEQUENCE = (
[BUTTON_PAGE] # move to QR code key
+ [BUTTON_ENTER] # choose QR code key
@@ -87,6 +88,7 @@ def test_load_key_from_qr_code(m5stickv, mocker):
key = key_generator.encryption_key()
assert key == "qr key"
+ print("case 2: load_key_from_qr_code")
# Repeat with too much characters >ENCRYPTION_KEY_MAX_LEN
BTN_SEQUENCE = [BUTTON_PAGE] + [ # move to QR code key
BUTTON_ENTER
@@ -146,7 +148,7 @@ def test_encrypt_to_qrcode_ecb_ui(m5stickv, mocker):
from embit.networks import NETWORKS
BTN_SEQUENCE = (
- [BUTTON_PAGE] # Move to store on Encrypted QR
+ [BUTTON_PAGE] * 2 # Move to store on Encrypted QR
+ [BUTTON_ENTER] # Confirm Encrypted QR
+ [BUTTON_PAGE] # add custom ID - No
+ [BUTTON_ENTER] # Confirm and leave
@@ -181,7 +183,7 @@ def test_encrypt_to_qrcode_cbc_ui(m5stickv, mocker):
from embit.networks import NETWORKS
BTN_SEQUENCE = (
- [BUTTON_PAGE] # Move to store on Encrypted QR
+ [BUTTON_PAGE] * 2 # Move to store on Encrypted QR
+ [BUTTON_ENTER] # Confirm Encrypted QR
+ [BUTTON_ENTER] # Confirm add CBC cam entropy
+ [BUTTON_PAGE] # add custom ID - No
diff --git a/tests/pages/test_home.py b/tests/pages/test_home.py
index 4ba34ae9d..caf796cd3 100644
--- a/tests/pages/test_home.py
+++ b/tests/pages/test_home.py
@@ -87,14 +87,15 @@ def create_ctx(mocker, btn_seq, wallet=None, printer=None, touch_seq=None):
ctx = mock_context(mocker)
ctx.power_manager.battery_charge_remaining.return_value = 1
ctx.input.wait_for_button = mocker.MagicMock(side_effect=btn_seq)
+ ctx.display.max_lines = mocker.MagicMock(return_value=7)
ctx.wallet = wallet
ctx.printer = printer
if printer is None:
- Settings().printer.driver = "none"
+ Settings().hardware.printer.driver = "none"
else:
mocker.patch("krux.printers.create_printer", new=mocker.MagicMock())
- Settings().printer.driver = THERMAL_ADAFRUIT_TXT
+ Settings().hardware.printer.driver = THERMAL_ADAFRUIT_TXT
if touch_seq:
ctx.input.touch = mocker.MagicMock(
@@ -104,7 +105,7 @@ def create_ctx(mocker, btn_seq, wallet=None, printer=None, touch_seq=None):
def test_mnemonic_words(mocker, m5stickv, tdata):
- from krux.pages.home import Home
+ from krux.pages.mnemonic_view import MnemonicsView
from krux.wallet import Wallet
from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
from krux.qr import FORMAT_NONE
@@ -152,17 +153,19 @@ def test_mnemonic_words(mocker, m5stickv, tdata):
print(num)
num = num + 1
ctx = create_ctx(mocker, case[2], case[0], case[1])
- home = Home(ctx)
+ mnemonics = MnemonicsView(ctx)
- mocker.spy(home, "display_mnemonic")
- home.mnemonic()
+ mocker.spy(mnemonics, "display_mnemonic")
+ mnemonics.mnemonic()
- home.display_mnemonic.assert_called_with(ctx.wallet.key.mnemonic)
+ mnemonics.display_mnemonic.assert_called_with(
+ ctx.wallet.key.mnemonic, "Mnemonic"
+ )
assert ctx.input.wait_for_button.call_count == len(case[2])
def test_mnemonic_standard_qr(mocker, m5stickv, tdata):
- from krux.pages.home import Home
+ from krux.pages.mnemonic_view import MnemonicsView
from krux.wallet import Wallet
from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
from krux.qr import FORMAT_NONE
@@ -174,10 +177,12 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata):
None, # printer
[
BUTTON_PAGE,
- BUTTON_ENTER,
+ BUTTON_PAGE, # select Plaintext QR
+ BUTTON_ENTER, # click
BUTTON_ENTER,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
@@ -186,10 +191,12 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata):
None, # printer
[
BUTTON_PAGE,
- BUTTON_ENTER,
+ BUTTON_PAGE, # select Plaintext QR
+ BUTTON_ENTER, # click
BUTTON_ENTER,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
@@ -199,11 +206,13 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata):
MockPrinter(),
[
BUTTON_PAGE,
- BUTTON_ENTER,
+ BUTTON_PAGE, # select Plaintext QR
+ BUTTON_ENTER, # click
BUTTON_ENTER,
BUTTON_ENTER,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
@@ -212,11 +221,13 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata):
MockPrinter(),
[
BUTTON_PAGE,
- BUTTON_ENTER,
+ BUTTON_PAGE, # select Plaintext QR
+ BUTTON_ENTER, # click
BUTTON_ENTER,
BUTTON_ENTER,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
@@ -226,11 +237,13 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata):
MockPrinter(),
[
BUTTON_PAGE,
- BUTTON_ENTER,
+ BUTTON_PAGE, # select Plaintext QR
+ BUTTON_ENTER, # click
BUTTON_ENTER,
BUTTON_PAGE,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
@@ -239,11 +252,13 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata):
MockPrinter(),
[
BUTTON_PAGE,
- BUTTON_ENTER,
+ BUTTON_PAGE, # select Plaintext QR
+ BUTTON_ENTER, # click
BUTTON_ENTER,
BUTTON_PAGE,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
@@ -253,124 +268,165 @@ def test_mnemonic_standard_qr(mocker, m5stickv, tdata):
print(num)
num = num + 1
ctx = create_ctx(mocker, case[2], case[0], case[1])
- home = Home(ctx)
+ mnemonics = MnemonicsView(ctx)
- mocker.spy(home, "display_qr_codes")
- mocker.spy(home, "print_standard_qr")
- home.mnemonic()
+ mocker.spy(mnemonics, "display_qr_codes")
+ mocker.spy(mnemonics.utils, "print_standard_qr")
+ mnemonics.mnemonic()
title = "Plaintext QR"
- home.display_qr_codes.assert_called_with(
+ mnemonics.display_qr_codes.assert_called_with(
ctx.wallet.key.mnemonic, FORMAT_NONE, title
)
if case[1] is not None:
- home.print_standard_qr.assert_called_with(
+ mnemonics.utils.print_standard_qr.assert_called_with(
ctx.wallet.key.mnemonic, FORMAT_NONE, title
)
assert ctx.input.wait_for_button.call_count == len(case[2])
def test_mnemonic_compact_qr(mocker, m5stickv, tdata):
- from krux.pages.home import Home
+ from krux.pages.mnemonic_view import MnemonicsView
from krux.wallet import Wallet
from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
from krux.qr import FORMAT_NONE
cases = [
- # No print prompt
+ # 0 - 12W
(
Wallet(tdata.SINGLESIG_12_WORD_KEY),
None,
[
BUTTON_PAGE,
BUTTON_PAGE,
+ BUTTON_PAGE, # Select Compact SeedQR
BUTTON_ENTER, # Open Compact SeedQR
- BUTTON_ENTER, # Leave
- BUTTON_ENTER, # Are you sure? yes
+ BUTTON_ENTER, # Open QR Menu
+ BUTTON_PAGE_PREV, # Move to leave QR Viewer
+ BUTTON_ENTER, # Leave QR Viewer
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
+ # 1 - 24W
(
Wallet(tdata.SINGLESIG_24_WORD_KEY),
None,
[
BUTTON_PAGE,
BUTTON_PAGE,
+ BUTTON_PAGE, # Select Compact SeedQR
BUTTON_ENTER, # Open Compact SeedQR
- BUTTON_ENTER, # Leave
- BUTTON_ENTER, # Are you sure? yes
+ BUTTON_ENTER, # Open QR Menu
+ BUTTON_PAGE_PREV, # Move to leave QR Viewer
+ BUTTON_ENTER, # Leave QR Viewer
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
- # Print
+ # 2 - 12W Print
(
Wallet(tdata.SINGLESIG_12_WORD_KEY),
MockPrinter(),
[
BUTTON_PAGE,
BUTTON_PAGE,
+ BUTTON_PAGE, # select Compact SeedQR
BUTTON_ENTER, # Open Compact SeedQR
+ BUTTON_ENTER, # Open QR Menu
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE, # Move to Print
+ BUTTON_ENTER, # Print
+ BUTTON_ENTER, # Print confirm
BUTTON_ENTER, # Leave
- BUTTON_ENTER, # Are you sure? yes
- BUTTON_ENTER, # say yes to print prompt
+ BUTTON_PAGE_PREV, # Move to leave QR Viewer
+ BUTTON_ENTER, # Leave QR Viewer
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
+ # 3 - 24W Print
(
Wallet(tdata.SINGLESIG_24_WORD_KEY),
MockPrinter(),
[
BUTTON_PAGE,
BUTTON_PAGE,
+ BUTTON_PAGE, # select Compact SeedQR
BUTTON_ENTER, # Open Compact SeedQR
+ BUTTON_ENTER, # Open QR Menu
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE, # Move to Print
+ BUTTON_ENTER, # Print
+ BUTTON_ENTER, # Print confirm
BUTTON_ENTER, # Leave
- BUTTON_ENTER, # Are you sure? yes
- BUTTON_ENTER, # say yes to print prompt
+ BUTTON_PAGE_PREV, # Move to leave QR Viewer
+ BUTTON_ENTER, # Leave QR Viewer
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
- # Decline to print
+ # 4 - 12W Print, Decline to print
(
Wallet(tdata.SINGLESIG_12_WORD_KEY),
MockPrinter(),
[
BUTTON_PAGE,
BUTTON_PAGE,
+ BUTTON_PAGE, # select Compact SeedQR
BUTTON_ENTER, # Open Compact SeedQR
+ BUTTON_ENTER, # Open QR Menu
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE, # Move to Print
+ BUTTON_ENTER, # Print
+ BUTTON_PAGE, # Print decline
BUTTON_ENTER, # Leave
- BUTTON_ENTER, # Are you sure? yes
- BUTTON_PAGE, # say no to print prompt
+ BUTTON_PAGE_PREV, # Move to leave QR Viewer
+ BUTTON_ENTER, # Leave QR Viewer
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
+ # 5 - 24W Print, Decline to print
(
Wallet(tdata.SINGLESIG_24_WORD_KEY),
MockPrinter(),
[
BUTTON_PAGE,
BUTTON_PAGE,
+ BUTTON_PAGE, # select Compact SeedQR
BUTTON_ENTER, # Open Compact SeedQR
+ BUTTON_ENTER, # Open QR Menu
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE, # Move to Print
+ BUTTON_ENTER, # Print
+ BUTTON_PAGE, # Print decline
BUTTON_ENTER, # Leave
- BUTTON_ENTER, # Are you sure? yes
- BUTTON_PAGE, # say no to print prompt
+ BUTTON_PAGE_PREV, # Move to leave QR Viewer
+ BUTTON_ENTER, # Leave QR Viewer
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
),
@@ -380,22 +436,24 @@ def test_mnemonic_compact_qr(mocker, m5stickv, tdata):
print(num)
num = num + 1
ctx = create_ctx(mocker, case[2], case[0], case[1])
- home = Home(ctx)
+ mnemonics = MnemonicsView(ctx)
- mocker.spy(home, "display_seed_qr")
- home.mnemonic()
+ mocker.spy(mnemonics, "display_seed_qr")
+ mnemonics.mnemonic()
- home.display_seed_qr.assert_called_once()
+ mnemonics.display_seed_qr.assert_called_once()
assert ctx.input.wait_for_button.call_count == len(case[2])
def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata):
- from krux.pages.home import Home
+ from krux.pages.mnemonic_view import MnemonicsView
from krux.wallet import Wallet
from krux.input import BUTTON_TOUCH, BUTTON_PAGE_PREV, BUTTON_ENTER
from krux.qr import FORMAT_NONE
+ position = [2, 0]
+
cases = [
# No print prompt
(
@@ -406,9 +464,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata):
BUTTON_TOUCH,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
- [1, 0],
+ position,
),
(
Wallet(tdata.SINGLESIG_24_WORD_KEY),
@@ -418,9 +477,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata):
BUTTON_TOUCH,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
- [1, 0],
+ position,
),
# Print
(
@@ -432,9 +492,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata):
BUTTON_TOUCH,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
- [1, 0, 0],
+ position + [0],
),
(
Wallet(tdata.SINGLESIG_24_WORD_KEY),
@@ -445,9 +506,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata):
BUTTON_TOUCH,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
- [1, 0, 0],
+ position + [0],
),
# Decline to print
(
@@ -459,9 +521,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata):
BUTTON_TOUCH,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
- [1, 0, 1],
+ position + [1],
),
(
Wallet(tdata.SINGLESIG_24_WORD_KEY),
@@ -472,9 +535,10 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata):
BUTTON_TOUCH,
BUTTON_PAGE_PREV, # change to btn Back
BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
BUTTON_ENTER, # click on back to return to home init screen
],
- [1, 0, 1],
+ position + [1],
),
]
num = 0
@@ -482,19 +546,19 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata):
print(num)
num = num + 1
ctx = create_ctx(mocker, case[2], case[0], case[1], touch_seq=case[3])
- home = Home(ctx)
+ mnemonics = MnemonicsView(ctx)
- mocker.spy(home, "display_qr_codes")
- mocker.spy(home, "print_standard_qr")
+ mocker.spy(mnemonics, "display_qr_codes")
+ mocker.spy(mnemonics.utils, "print_standard_qr")
- home.mnemonic()
+ mnemonics.mnemonic()
title = "Plaintext QR"
- home.display_qr_codes.assert_called_with(
+ mnemonics.display_qr_codes.assert_called_with(
ctx.wallet.key.mnemonic, FORMAT_NONE, title
)
if case[1] is not None:
- home.print_standard_qr.assert_called_with(
+ mnemonics.utils.print_standard_qr.assert_called_with(
ctx.wallet.key.mnemonic, FORMAT_NONE, title
)
@@ -502,107 +566,119 @@ def test_mnemonic_st_qr_touch(mocker, amigo_tft, tdata):
def test_public_key(mocker, m5stickv, tdata):
- from krux.pages.home import Home
+ from krux.pages.pub_key_view import PubkeyView
from krux.wallet import Wallet
- from krux.input import BUTTON_ENTER, BUTTON_PAGE
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
from krux.qr import FORMAT_NONE
cases = [
- # No print prompt
+ # Case parameters: [Wallet, Printer, Button Sequence, Show XPUB, Show ZPUB]
+ # 0 - Singlesig - Show all text and QR codes
(
Wallet(tdata.SINGLESIG_12_WORD_KEY),
None,
- [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
- ),
- (
- Wallet(tdata.MULTISIG_12_WORD_KEY),
- None,
- [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
- ),
- # Print
- (
- Wallet(tdata.SINGLESIG_12_WORD_KEY),
- MockPrinter(),
[
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
+ BUTTON_ENTER, # Enter XPUB - Text
+ BUTTON_PAGE, # move to Back
+ BUTTON_ENTER, # Press Back
+ BUTTON_PAGE, # move to XPUB - QR Code
+ BUTTON_ENTER, # Enter XPUB - QR Code
+ BUTTON_ENTER, # Enter QR Menu
+ BUTTON_PAGE_PREV, # move to Back to Menu
+ BUTTON_ENTER, # Press Back to Menu
+ BUTTON_PAGE, # move to XPUB - QR Code
+ BUTTON_PAGE, # move to ZPUB - Text
+ BUTTON_PAGE, # move to ZPUB - QR Code
+ BUTTON_ENTER, # Enter ZPUB - QR Code
+ BUTTON_ENTER, # Enter QR Menu
+ BUTTON_PAGE_PREV, # move to Back to Menu
+ BUTTON_ENTER, # Press Back to Menu
+ BUTTON_PAGE_PREV, # Move Back
+ BUTTON_ENTER, # Press Back to leave
],
+ True,
+ True,
),
+ # 1 - Multisig - Show all text and QR codes
(
Wallet(tdata.MULTISIG_12_WORD_KEY),
- MockPrinter(),
- [
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- ],
- ),
- # Decline to print
- (
- Wallet(tdata.SINGLESIG_12_WORD_KEY),
- MockPrinter(),
+ None,
[
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_PAGE,
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_PAGE,
- ],
- ),
- (
- Wallet(tdata.MULTISIG_12_WORD_KEY),
- MockPrinter(),
- [
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_PAGE,
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_PAGE,
+ BUTTON_ENTER, # Enter XPUB - Text
+ BUTTON_PAGE, # move to Back
+ BUTTON_ENTER, # Press Back
+ BUTTON_PAGE, # move to XPUB - QR Code
+ BUTTON_ENTER, # Enter XPUB - QR Code
+ BUTTON_ENTER, # Enter QR Menu
+ BUTTON_PAGE_PREV, # move to Back to Menu
+ BUTTON_ENTER, # Press Back to Menu
+ BUTTON_PAGE, # move to XPUB - QR Code
+ BUTTON_PAGE, # move to ZPUB - Text
+ BUTTON_PAGE, # move to ZPUB - QR Code
+ BUTTON_ENTER, # Enter ZPUB - QR Code
+ BUTTON_ENTER, # Enter QR Menu
+ BUTTON_PAGE_PREV, # move to Back to Menu
+ BUTTON_ENTER, # Press Back to Menu
+ BUTTON_PAGE_PREV, # Move Back
+ BUTTON_ENTER, # Press Back to leave
],
+ True,
+ True,
),
+ # TODO: Create cases were not all text and QR codes are shown
]
+ num = 0
+
for case in cases:
+ print(num)
+ num += 1
+ mock_seed_qr_view = mocker.patch(
+ "krux.pages.qr_view.SeedQRView"
+ ) # Mock SeedQRView
ctx = create_ctx(mocker, case[2], case[0], case[1])
- home = Home(ctx)
+ pub_key_viewer = PubkeyView(ctx)
- mocker.spy(home, "display_qr_codes")
- mocker.spy(home, "print_standard_qr")
-
- home.public_key()
+ pub_key_viewer.public_key()
version = "Zpub" if ctx.wallet.key.multisig else "zpub"
- display_qr_calls = [
- mocker.call(
- ctx.wallet.key.key_expression(None),
- FORMAT_NONE,
- "XPUB",
- ),
- mocker.call(
- ctx.wallet.key.key_expression(ctx.wallet.key.network[version]),
- FORMAT_NONE,
- "ZPUB",
- ),
- ]
- print_qr_calls = [
- mocker.call(ctx.wallet.key.key_expression(None), FORMAT_NONE, "XPUB"),
- mocker.call(
- ctx.wallet.key.key_expression(ctx.wallet.key.network[version]),
- FORMAT_NONE,
- "ZPUB",
- ),
- ]
- home.display_qr_codes.assert_has_calls(display_qr_calls)
- if case[1] is not None:
- home.print_standard_qr.assert_has_calls(print_qr_calls)
+ qr_view_calls = []
+ print_qr_calls = []
+
+ if case[3]: # Show XPUB
+ qr_view_calls.append(
+ mocker.call(
+ ctx,
+ data=ctx.wallet.key.key_expression(None),
+ title="XPUB",
+ ),
+ )
+ print_qr_calls.append(
+ mocker.call(
+ ctx.wallet.key.key_expression(None),
+ FORMAT_NONE,
+ "XPUB",
+ ),
+ )
+ if case[4]: # Show ZPUB
+ qr_view_calls.append(
+ mocker.call(
+ ctx,
+ data=ctx.wallet.key.key_expression(ctx.wallet.key.network[version]),
+ title="ZPUB",
+ ),
+ )
+ print_qr_calls.append(
+ mocker.call(
+ ctx.wallet.key.key_expression(ctx.wallet.key.network[version]),
+ FORMAT_NONE,
+ "ZPUB",
+ ),
+ )
+
+ # Assert SeedQRView was initialized with the correct parameters
+ mock_seed_qr_view.assert_has_calls(qr_view_calls, any_order=True)
+
+ # TODO: Assert XPUB and ZPUB text was displayed
assert ctx.input.wait_for_button.call_count == len(case[2])
@@ -614,7 +690,7 @@ def test_wallet(mocker, m5stickv, tdata):
from krux.qr import FORMAT_PMOFN
cases = [
- # Don't load
+ # 0 Don't load
(
False,
tdata.SINGLESIG_12_WORD_KEY,
@@ -622,7 +698,7 @@ def test_wallet(mocker, m5stickv, tdata):
None,
[BUTTON_PAGE],
),
- # Load, good data, accept
+ # 1 Load, good data, accept
(
False,
tdata.SINGLESIG_12_WORD_KEY,
@@ -630,7 +706,7 @@ def test_wallet(mocker, m5stickv, tdata):
None,
[BUTTON_ENTER, BUTTON_ENTER],
),
- # Load, good data, decline
+ # 2 Load, good data, decline
(
False,
tdata.SINGLESIG_12_WORD_KEY,
@@ -638,11 +714,11 @@ def test_wallet(mocker, m5stickv, tdata):
None,
[BUTTON_ENTER, BUTTON_PAGE],
),
- # Load, bad capture
+ # 3 Load, bad capture
(False, tdata.SINGLESIG_12_WORD_KEY, None, None, [BUTTON_ENTER]),
- # Load, bad wallet data
+ # 4 Load, bad wallet data
(False, tdata.SINGLESIG_12_WORD_KEY, "{}", None, [BUTTON_ENTER, BUTTON_ENTER]),
- # No print prompt
+ # 5 No print prompt
(
True,
tdata.SINGLESIG_12_WORD_KEY,
@@ -650,7 +726,7 @@ def test_wallet(mocker, m5stickv, tdata):
None,
[BUTTON_ENTER],
),
- # Print
+ # 6 Print
(
True,
tdata.SINGLESIG_12_WORD_KEY,
@@ -658,7 +734,7 @@ def test_wallet(mocker, m5stickv, tdata):
MockPrinter(),
[BUTTON_ENTER, BUTTON_ENTER],
),
- # Decline to print
+ # 7 Decline to print
(
True,
tdata.SINGLESIG_12_WORD_KEY,
@@ -666,16 +742,20 @@ def test_wallet(mocker, m5stickv, tdata):
MockPrinter(),
[BUTTON_ENTER, BUTTON_PAGE],
),
- # Multisig wallet, no print prompt
+ # 8 Multisig wallet, no print prompt
(
True,
tdata.MULTISIG_12_WORD_KEY,
tdata.SPECTER_MULTISIG_WALLET_DATA,
None,
- [BUTTON_ENTER],
+ [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER],
),
]
+
+ num = 0
for case in cases:
+ print("case: %d" % num)
+ num = num + 1
wallet = Wallet(case[1])
if case[0]:
wallet.load(case[2], FORMAT_PMOFN)
@@ -690,7 +770,7 @@ def test_wallet(mocker, m5stickv, tdata):
"display_qr_codes",
new=lambda data, qr_format, title=None: ctx.input.wait_for_button(),
)
- mocker.spy(home, "print_standard_qr")
+ mocker.spy(home.utils, "print_standard_qr")
mocker.spy(home, "capture_qr_code")
mocker.spy(home, "display_wallet")
@@ -698,7 +778,7 @@ def test_wallet(mocker, m5stickv, tdata):
if case[0]:
home.display_wallet.assert_called_once()
- home.print_standard_qr.assert_called_once()
+ home.utils.print_standard_qr.assert_called_once()
else:
if case[4][0] == BUTTON_ENTER:
home.capture_qr_code.assert_called_once()
@@ -993,7 +1073,7 @@ def test_sign_psbt(mocker, m5stickv, tdata):
)
mocker.spy(home, "capture_qr_code")
mocker.spy(home, "display_qr_codes")
- mocker.spy(home, "print_standard_qr")
+ mocker.spy(home.utils, "print_standard_qr")
# case SD available
if case[10] is not None:
mocker.patch("os.listdir", new=mocker.MagicMock(return_value=["test.psbt"]))
@@ -1020,7 +1100,7 @@ def test_sign_psbt(mocker, m5stickv, tdata):
home.capture_qr_code.assert_called_once()
if case[5]: # signed!
home.display_qr_codes.assert_called_once()
- home.print_standard_qr.assert_called_once()
+ home.utils.print_standard_qr.assert_called_once()
else:
home.display_qr_codes.assert_not_called()
else:
@@ -1031,7 +1111,7 @@ def test_sign_psbt(mocker, m5stickv, tdata):
def test_sign_message(mocker, m5stickv, tdata):
import binascii
- from krux.pages.home import Home
+ from krux.pages.sign_message_ui import SignMessage
from krux.wallet import Wallet
from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
from krux.qr import FORMAT_NONE
@@ -1165,14 +1245,14 @@ def test_sign_message(mocker, m5stickv, tdata):
wallet = Wallet(tdata.SINGLESIG_SIGNING_KEY)
ctx = create_ctx(mocker, case[3], wallet, case[2])
- home = Home(ctx)
+ home = SignMessage(ctx)
mocker.patch.object(home, "capture_qr_code", new=lambda: (case[0], case[1]))
mocker.patch.object(
home,
"display_qr_codes",
new=lambda data, qr_format, title=None: ctx.input.wait_for_button(),
)
- mocker.spy(home, "print_standard_qr")
+ mocker.spy(home.utils, "print_standard_qr")
mocker.spy(home, "capture_qr_code")
mocker.spy(home, "display_qr_codes")
if case[6] is not None:
@@ -1199,7 +1279,7 @@ def test_sign_message(mocker, m5stickv, tdata):
mocker.call(case[5], case[1], "Hex Public Key"),
]
)
- home.print_standard_qr.assert_has_calls(
+ home.utils.print_standard_qr.assert_has_calls(
[
mocker.call(case[4], case[1], "Signed Message"),
mocker.call(case[5], case[1], "Hex Public Key"),
@@ -1207,6 +1287,6 @@ def test_sign_message(mocker, m5stickv, tdata):
)
else:
home.display_qr_codes.assert_not_called()
- home.print_standard_qr.assert_not_called()
+ home.utils.print_standard_qr.assert_not_called()
assert ctx.input.wait_for_button.call_count == len(case[3])
diff --git a/tests/pages/test_login.py b/tests/pages/test_login.py
index a28571f7f..2cd3ab536 100644
--- a/tests/pages/test_login.py
+++ b/tests/pages/test_login.py
@@ -1,13 +1,7 @@
import sys
+from unittest import mock
import pytest
-from Crypto.Cipher import AES
from ..shared_mocks import mock_context
-from unittest import mock
-
-if "ucryptolib" not in sys.modules:
- sys.modules["ucryptolib"] = mock.MagicMock(
- aes=AES.new, MODE_ECB=AES.MODE_ECB, MODE_CBC=AES.MODE_CBC
- )
@pytest.fixture
@@ -207,89 +201,15 @@ def test_qr_passphrase_fail(m5stickv, mocker):
assert test_passphrase == MENU_CONTINUE
-
-################### New words from dice tests
-
-
-def test_new_12w_from_d6(m5stickv, mocker, mocker_printer):
- from krux.pages.login import Login, D6_12W_MIN_ROLLS
- from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
-
- BTN_SEQUENCE = (
- # 1 press to proceed to 12 words
- [BUTTON_ENTER]
- +
- # 1 press to proceed msg
- [BUTTON_ENTER]
- +
- # 1 presses per roll
- [BUTTON_ENTER for _ in range(D6_12W_MIN_ROLLS)]
- +
- # 1 press prev and 1 press on btn Go
- [BUTTON_PAGE_PREV, BUTTON_ENTER]
- + [
- BUTTON_ENTER, # 1 press to confirm roll string,
- BUTTON_ENTER, # 1 press to confirm SHA
- BUTTON_ENTER, # 1 press to continue loading key
- BUTTON_PAGE, # 1 press to move to Scan passphrase
- BUTTON_PAGE, # 1 press to move to No passphrase
- BUTTON_ENTER, # 1 press to skip passphrase
- BUTTON_ENTER, # 1 press to confirm fingerprint
- BUTTON_ENTER, # 1 press to select single-sig
- ]
- )
- MNEMONIC = "diet glad hat rural panther lawsuit act drop gallery urge where fit"
-
- ctx = create_ctx(mocker, BTN_SEQUENCE)
- login = Login(ctx)
- login.new_key_from_d6()
-
- assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
- assert ctx.wallet.key.mnemonic == MNEMONIC
-
-
-def test_new_24w_from_d6(m5stickv, mocker):
- from krux.pages.login import Login, D6_24W_MIN_ROLLS
- from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
-
- BTN_SEQUENCE = (
- # 1 press change to 24 words and 1 press to proceed
- [BUTTON_PAGE, BUTTON_ENTER]
- +
- # 1 press to proceed msg
- [BUTTON_ENTER]
- +
- # 1 presses per roll
- [BUTTON_ENTER for _ in range(D6_24W_MIN_ROLLS)]
- +
- # 1 press prev and 1 press on btn Go
- [BUTTON_PAGE_PREV, BUTTON_ENTER]
- + [
- BUTTON_ENTER, # 1 press to confirm roll string,
- BUTTON_ENTER, # 1 press to confirm SHA
- BUTTON_ENTER, # 1 press to see the next 12 words (24 total)
- BUTTON_ENTER, # 1 press to continue loading key
- BUTTON_PAGE, # 1 press to move to Scan passphrase
- BUTTON_PAGE, # 1 press to move to No passphrase
- BUTTON_ENTER, # 1 press to skip passphrase
- BUTTON_ENTER, # 1 press to confirm fingerprint
- BUTTON_ENTER, # 1 press to select single-sig
- ]
- )
- MNEMONIC = "wheel erase puppy pistol chapter accuse carpet drop quote final attend near scrap satisfy limit style crunch person south inspire lunch meadow enact tattoo"
-
- ctx = create_ctx(mocker, BTN_SEQUENCE)
- login = Login(ctx)
- login.new_key_from_d6()
-
- assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
- assert ctx.wallet.key.mnemonic == MNEMONIC
-
-
def test_new_12w_from_snapshot(m5stickv, mocker):
- from ..shared_mocks import IMAGE_TO_HASH
from krux.pages.login import Login
- from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE_PREV
+
+ # mocks a result of a hashed image
+ mock_capture_entropy = mocker.patch(
+ "krux.pages.capture_entropy.CameraEntropy.capture",
+ return_value = b'\x01' * 32
+ )
BTN_SEQUENCE = (
# 1 press to proceed to 12 words
@@ -317,7 +237,7 @@ def test_new_12w_from_snapshot(m5stickv, mocker):
[BUTTON_ENTER]
)
MNEMONIC = (
- "credit knee panther note mule luggage attitude era must junior party general"
+ "absurd amount doctor acoustic avoid letter advice cage absurd amount doctor adjust"
)
ctx = create_ctx(mocker, BTN_SEQUENCE)
login = Login(ctx)
@@ -326,222 +246,6 @@ def test_new_12w_from_snapshot(m5stickv, mocker):
assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
assert ctx.wallet.key.mnemonic == MNEMONIC
-
-def test_new_12w_from_d6_on_amigo_device(amigo_tft, mocker, mocker_printer):
- from krux.pages.login import Login, D6_12W_MIN_ROLLS
- from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
-
- BTN_SEQUENCE = (
- # 1 press to proceed to 12 words
- [BUTTON_ENTER]
- +
- # 1 press to proceed msg
- [BUTTON_ENTER]
- +
- # 1 presses per roll
- [BUTTON_ENTER for _ in range(D6_12W_MIN_ROLLS)]
- +
- # 1 press prev and 1 press on btn Go
- [BUTTON_PAGE_PREV, BUTTON_ENTER]
- + [
- BUTTON_ENTER, # 1 press to confirm roll string,
- BUTTON_ENTER, # 1 press to confirm SHA
- BUTTON_ENTER, # 1 press to continue loading key
- BUTTON_PAGE, # 1 press to move to Scan passphrase
- BUTTON_PAGE, # 1 press to move to No passphrase
- BUTTON_ENTER, # 1 press to skip passphrase
- BUTTON_ENTER, # 1 press to confirm fingerprint
- BUTTON_ENTER, # 1 press to select single-sig
- ]
- )
- MNEMONIC = "diet glad hat rural panther lawsuit act drop gallery urge where fit"
-
- ctx = create_ctx(mocker, BTN_SEQUENCE)
- login = Login(ctx)
- login.new_key_from_d6()
-
- assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
- assert ctx.wallet.key.mnemonic == MNEMONIC
-
-
-def test_new_24w_from_d6_on_amigo_device(amigo_tft, mocker, mocker_printer):
- from krux.pages.login import Login, D6_24W_MIN_ROLLS
- from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
-
- BTN_SEQUENCE = (
- # 1 press change to 24 words and 1 press to proceed
- [BUTTON_PAGE, BUTTON_ENTER]
- +
- # 1 press to proceed msg
- [BUTTON_ENTER]
- +
- # 1 presses per roll
- [BUTTON_ENTER for _ in range(D6_24W_MIN_ROLLS)]
- +
- # 1 press prev and 1 press on btn Go
- [BUTTON_PAGE_PREV, BUTTON_ENTER]
- + [
- BUTTON_ENTER, # 1 press to confirm roll string,
- BUTTON_ENTER, # 1 press to confirm SHA
- BUTTON_ENTER, # 1 press to continue loading key
- BUTTON_PAGE, # 1 press to move to Scan passphrase
- BUTTON_PAGE, # 1 press to move to No passphrase
- BUTTON_ENTER, # 1 press to skip passphrase
- BUTTON_ENTER, # 1 press to confirm fingerprint
- BUTTON_ENTER, # 1 press to select single-sig
- ]
- )
- MNEMONIC = "wheel erase puppy pistol chapter accuse carpet drop quote final attend near scrap satisfy limit style crunch person south inspire lunch meadow enact tattoo"
-
- ctx = create_ctx(mocker, BTN_SEQUENCE)
- login = Login(ctx)
- login.new_key_from_d6()
-
- assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
- assert ctx.wallet.key.mnemonic == MNEMONIC
-
-
-def test_cancel_new_12w_from_d6_on_amigo_device(amigo_tft, mocker, mocker_printer):
- "Will test the Esc button on the roll screen"
- from krux.pages.login import Login, D6_12W_MIN_ROLLS
- from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
-
- BTN_SEQUENCE = (
- # 1 press to proceed to 12 words
- [BUTTON_ENTER]
- +
- # 1 press to proceed msg
- [BUTTON_ENTER]
- +
- # 2 press prev and 1 press on btn Esc
- [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER]
- +
- # 1 press to proceed confirm exit msg
- [BUTTON_ENTER]
- )
-
- ctx = create_ctx(mocker, BTN_SEQUENCE)
- login = Login(ctx)
- login.new_key_from_d6()
-
- assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
-
-
-def test_new_12w_from_d20(m5stickv, mocker, mocker_printer):
- from krux.pages.login import Login, D20_12W_MIN_ROLLS
- from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
-
- BTN_SEQUENCE = (
- # 1 press to proceed to 12 words
- [BUTTON_ENTER]
- +
- # 1 press to proceed msg
- [BUTTON_ENTER]
- +
- # 1 presses per roll
- [BUTTON_ENTER for _ in range(D20_12W_MIN_ROLLS)]
- +
- # 1 press prev and 1 press on btn Go
- [BUTTON_PAGE_PREV, BUTTON_ENTER]
- + [
- BUTTON_ENTER, # 1 press to confirm roll string,
- BUTTON_ENTER, # 1 press to confirm SHA
- BUTTON_ENTER, # 1 press to continue loading key
- BUTTON_PAGE, # 1 press to move to Scan passphrase
- BUTTON_PAGE, # 1 press to move to No passphrase
- BUTTON_ENTER, # 1 press to skip passphrase
- BUTTON_ENTER, # 1 press to confirm fingerprint
- BUTTON_ENTER, # 1 press to select single-sig
- ]
- )
- MNEMONIC = (
- "erupt remain ride bleak year cabin orange sure ghost gospel husband oppose"
- )
-
- ctx = create_ctx(mocker, BTN_SEQUENCE)
- login = Login(ctx)
- login.new_key_from_d20()
-
- assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
- assert ctx.wallet.key.mnemonic == MNEMONIC
-
-
-def test_new_24w_from_d20(m5stickv, mocker, mocker_printer):
- from krux.pages.login import Login, D20_24W_MIN_ROLLS
- from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
-
- BTN_SEQUENCE = (
- # 1 press change to 24 words and 1 press to proceed
- [BUTTON_PAGE, BUTTON_ENTER]
- +
- # 1 press to proceed msg
- [BUTTON_ENTER]
- +
- # 1 presses per roll
- [BUTTON_ENTER for _ in range(D20_24W_MIN_ROLLS)]
- +
- # 1 press prev and 1 press on btn Go
- [BUTTON_PAGE_PREV, BUTTON_ENTER]
- + [
- BUTTON_ENTER, # 1 press to confirm roll string,
- BUTTON_ENTER, # 1 press to confirm SHA
- BUTTON_ENTER, # 1 press to see the next 12 words (24 total)
- BUTTON_ENTER, # 1 press to continue loading key
- BUTTON_PAGE, # 1 press to move to Scan passphrase
- BUTTON_PAGE, # 1 press to move to No passphrase
- BUTTON_ENTER, # 1 press to skip passphrase
- BUTTON_ENTER, # 1 press to confirm fingerprint
- BUTTON_ENTER, # 1 press to select single-sig
- ]
- )
- MNEMONIC = "fun island vivid slide cable pyramid device tuition only essence thought gain silk jealous eternal anger response virus couple faculty ozone test key vocal"
-
- ctx = create_ctx(mocker, BTN_SEQUENCE)
- login = Login(ctx)
- login.new_key_from_d20()
-
- assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
- assert ctx.wallet.key.mnemonic == MNEMONIC
-
-
-def test_cancel_new_12w_from_d20(m5stickv, mocker, mocker_printer):
- "Will test the Deletion button and the minimum roll on the roll screen"
- from krux.pages.login import Login, D20_12W_MIN_ROLLS
- from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
-
- BTN_SEQUENCE = (
- # 1 press to proceed to 12 words
- [BUTTON_ENTER]
- +
- # 1 press to proceed msg
- [BUTTON_ENTER]
- +
- # 1 presses per roll
- [BUTTON_ENTER for _ in range(D20_12W_MIN_ROLLS)]
- +
- # 3 press prev and 1 press on btn < (delete last roll)
- [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER]
- +
- # 1 press prev and 1 press on btn Go
- [BUTTON_PAGE_PREV, BUTTON_ENTER]
- +
- # 1 press for msg not enough rolls!
- [BUTTON_ENTER]
- +
- # 2 press prev and 1 press on btn Esc
- [BUTTON_PAGE_PREV, BUTTON_PAGE_PREV, BUTTON_ENTER]
- +
- # 1 press to proceed confirm exit msg
- [BUTTON_ENTER]
- )
-
- ctx = create_ctx(mocker, BTN_SEQUENCE)
- login = Login(ctx)
- login.new_key_from_d20()
-
- assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
-
-
########## load words from qrcode tests
@@ -863,6 +567,7 @@ def test_load_key_from_text(m5stickv, mocker, mocker_printer):
[BUTTON_ENTER]
)
* 11
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+ (
# N
[BUTTON_PAGE for _ in range(13)]
@@ -907,6 +612,7 @@ def test_load_key_from_text(m5stickv, mocker, mocker_printer):
[BUTTON_ENTER]
)
* 11
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+
# Go + Confirm word
[BUTTON_PAGE for _ in range(28)]
@@ -961,6 +667,7 @@ def test_load_key_from_text_on_amigo_tft_with_touch(amigo_tft, mocker, mocker_pr
[BUTTON_ENTER]
)
* 11
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+ (
# N
[BUTTON_TOUCH] # index 13 -> "n"
@@ -1008,6 +715,7 @@ def test_load_key_from_text_on_amigo_tft_with_touch(amigo_tft, mocker, mocker_pr
[BUTTON_ENTER]
)
* 11
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+
# Move to Go, press Go, confirm word
[BUTTON_PAGE_PREV] + [BUTTON_ENTER] + [BUTTON_ENTER] +
@@ -1065,6 +773,7 @@ def test_load_key_from_digits(m5stickv, mocker, mocker_printer):
] # 1 press to select and 1 press to confirm
)
* 11 # repeat selection of word=2 (ability) eleven times
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+ (
# 1
[BUTTON_ENTER]
@@ -1088,6 +797,7 @@ def test_load_key_from_digits(m5stickv, mocker, mocker_printer):
)
+ [
BUTTON_ENTER, # Done?
+ BUTTON_ENTER, # 12 numbers confirm
BUTTON_ENTER, # 12 word confirm
BUTTON_PAGE, # 1 press to move to Scan passphrase
BUTTON_PAGE, # 1 press to move to No passphrase
@@ -1108,6 +818,7 @@ def test_load_key_from_digits(m5stickv, mocker, mocker_printer):
+ [BUTTON_ENTER, BUTTON_ENTER]
)
* 11
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+
# Go + Confirm
[BUTTON_PAGE for _ in range(11)]
@@ -1115,6 +826,7 @@ def test_load_key_from_digits(m5stickv, mocker, mocker_printer):
+ [BUTTON_ENTER]
+ [
BUTTON_ENTER, # Done?
+ BUTTON_ENTER, # 12 numbers confirm
BUTTON_ENTER, # 12 word confirm
BUTTON_PAGE, # 1 press to move to Scan passphrase
BUTTON_PAGE, # 1 press to move to No passphrase
@@ -1127,7 +839,7 @@ def test_load_key_from_digits(m5stickv, mocker, mocker_printer):
]
num = 0
for case in cases:
- print(num)
+ print("case:", num)
num = num + 1
ctx = create_ctx(mocker, case[0])
login = Login(ctx)
@@ -1155,6 +867,7 @@ def test_load_12w_from_hexadecimal(m5stickv, mocker, mocker_printer):
+ [BUTTON_ENTER] # 1 press to confirm word=FF(255 decimal) cabin
)
* 11 # repeat selection of word=FF(255, cabin) eleven times
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+ (
[BUTTON_ENTER] # 1 press to number 1
+ [BUTTON_ENTER] # 1 press to number 1
@@ -1164,6 +877,7 @@ def test_load_12w_from_hexadecimal(m5stickv, mocker, mocker_printer):
)
+ [
BUTTON_ENTER, # Done?
+ BUTTON_ENTER, # 12 numbers confirm
BUTTON_ENTER, # 12 word confirm
BUTTON_PAGE, # 1 press to move to Scan passphrase
BUTTON_PAGE, # 1 press to move to No passphrase
@@ -1224,6 +938,7 @@ def test_possible_letters_from_hexadecimal(m5stickv, mocker, mocker_printer):
+ [BUTTON_ENTER] # 1 press to confirm word=80(128 decimal) avocado
)
* 11 # repeat selection of word=80(128, avocado) eleven times
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+ (
[BUTTON_PAGE_PREV] # 1 press change to btn Go
+ [BUTTON_ENTER] # 1 press to select Go
@@ -1231,6 +946,7 @@ def test_possible_letters_from_hexadecimal(m5stickv, mocker, mocker_printer):
)
+ [
BUTTON_ENTER, # Done?
+ BUTTON_ENTER, # 12 numbers confirm
BUTTON_ENTER, # 12 word confirm
BUTTON_PAGE, # 1 press to move to Scan passphrase
BUTTON_PAGE, # 1 press to move to No passphrase
@@ -1264,6 +980,7 @@ def test_load_12w_from_octal(m5stickv, mocker, mocker_printer):
+ [BUTTON_ENTER] # 1 press to confirm word=777(511 decimal) divert
)
* 11 # repeat selection of word=777(511, divert) eleven times
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+ (
[BUTTON_ENTER] # 1 press to number 1
+ [BUTTON_PAGE for _ in range(4)] # 4 press change to number 5
@@ -1276,6 +993,7 @@ def test_load_12w_from_octal(m5stickv, mocker, mocker_printer):
)
+ [
BUTTON_ENTER, # Done?
+ BUTTON_ENTER, # 12 numbers confirm
BUTTON_ENTER, # 12 word confirm
BUTTON_PAGE, # 1 press to move to Scan passphrase
BUTTON_PAGE, # 1 press to move to No passphrase
@@ -1329,6 +1047,7 @@ def test_possible_letters_from_octal(m5stickv, mocker, mocker_printer):
+ [BUTTON_ENTER] # 1 press to confirm word=400(256 decimal) cable
)
* 11 # repeat selection of word=400(256, cable) eleven times
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+ (
[BUTTON_PAGE_PREV] # 1 press change to btn Go
+ [BUTTON_ENTER] # 1 press to select Go
@@ -1336,6 +1055,7 @@ def test_possible_letters_from_octal(m5stickv, mocker, mocker_printer):
)
+ [
BUTTON_ENTER, # Done?
+ BUTTON_ENTER, # 12 numbers confirm
BUTTON_ENTER, # 12 word confirm
BUTTON_PAGE, # 1 press to move to Scan passphrase
BUTTON_PAGE, # 1 press to move to No passphrase
@@ -1393,6 +1113,7 @@ def test_no_passphrase_on_amigo(mocker, amigo_tft):
[BUTTON_ENTER]
)
* 11
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+
# Move to Go, press Go, confirm word
[BUTTON_PAGE_PREV]
@@ -1446,6 +1167,7 @@ def test_passphrase(amigo_tft, mocker, mocker_printer):
[BUTTON_ENTER]
)
* 11
+ + [BUTTON_ENTER] # Pick valid checksum final word message
+
# Move to Go, press Go, confirm word
[BUTTON_PAGE_PREV]
@@ -1612,333 +1334,8 @@ def test_load_12w_from_1248(m5stickv, mocker, mocker_printer):
assert ctx.wallet.key.mnemonic == MNEMONIC
-# import unittest
-# tc = unittest.TestCase()
-# tc.assertEqual(Settings().i18n.locale, 'b')
-
-
-def test_settings_m5stickv(m5stickv, mocker, mocker_printer):
- import krux
-
- from krux.pages.login import Login
- from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
- from krux.krux_settings import Settings, CategorySetting, NumberSetting
- from krux.translations import translation_table
- from krux.themes import WHITE, RED, GREEN, ORANGE, MAGENTA
-
- tlist = list(translation_table)
- index_pt = tlist.index("pt-BR")
- index_next = (index_pt + 1) % (len(tlist))
- text_pt = translation_table[tlist[index_pt]][1177338798] + "\n" + tlist[index_pt]
- text_next = (
- translation_table[tlist[index_next]][1177338798] + "\n" + tlist[index_next]
- )
-
- cases = [
- ( # 0
- (
- # Bitcoin
- BUTTON_ENTER,
- # Change network
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Leave Settings
- BUTTON_PAGE_PREV,
- BUTTON_ENTER,
- ),
- [
- mocker.call("Network\nmain", ORANGE),
- mocker.call("Network\ntest", GREEN),
- ],
- lambda: Settings().bitcoin.network == "test",
- CategorySetting,
- ),
- ( # 1
- (
- # Printer
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Thermal
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Change Baudrate
- BUTTON_ENTER,
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Back to Thermal
- BUTTON_PAGE_PREV,
- BUTTON_ENTER,
- # Back to Printer
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Leave Settings
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- ),
- [
- mocker.call("Baudrate\n9600", WHITE),
- mocker.call("Baudrate\n19200", WHITE),
- ],
- lambda: Settings().printer.thermal.adafruit.baudrate == 19200,
- CategorySetting,
- ),
- ( # 2
- (
- # Language
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Change Locale
- BUTTON_PAGE,
- BUTTON_ENTER,
- ),
- [
- mocker.call(text_pt, WHITE),
- mocker.call(text_next, WHITE),
- ],
- lambda: Settings().i18n.locale == tlist[index_next],
- CategorySetting,
- ),
- ( # 3
- (
- # Logging
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Change log level
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Leave Settings
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- ),
- [
- mocker.call("Log Level\nNONE", WHITE),
- mocker.call("Log Level\nERROR", RED),
- mocker.call("Log Level\nWARN", ORANGE),
- mocker.call("Log Level\nINFO", GREEN),
- mocker.call("Log Level\nDEBUG", MAGENTA),
- ],
- lambda: Settings().logging.level == "DEBUG",
- CategorySetting,
- ),
- ( # 4
- (
- # Printer
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Thermal
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Paper Width
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Change width
- # Remove digit
- BUTTON_PAGE_PREV,
- BUTTON_PAGE_PREV,
- BUTTON_PAGE_PREV,
- BUTTON_ENTER,
- # Add 9
- BUTTON_PAGE_PREV,
- BUTTON_ENTER,
- # Go
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Back to Thermal
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Back to Printer
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- # Leave Settings
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_ENTER,
- ),
- [
- mocker.call("Paper Width", 10),
- ],
- lambda: Settings().printer.thermal.adafruit.paper_width == 389,
- NumberSetting,
- ),
- ]
- case_num = 0
- for case in cases:
- print("test_settings cases[" + str(case_num) + "]")
- case_num = case_num + 1
-
- ctx = create_ctx(mocker, case[0])
- login = Login(ctx)
-
- Settings().i18n.locale = "pt-BR"
- login.settings()
-
- assert ctx.input.wait_for_button.call_count == len(case[0])
-
- assert case[2]()
-
-
-def test_settings_on_amigo_tft(amigo_tft, mocker, mocker_printer):
- import krux
- from krux.pages.login import Login
- from krux.input import BUTTON_TOUCH
- from krux.krux_settings import Settings, CategorySetting, NumberSetting
- from krux.translations import translation_table
- from krux.themes import WHITE, RED, GREEN, ORANGE, MAGENTA
-
- tlist = list(translation_table)
- index_pt = tlist.index("pt-BR")
- index_next = (index_pt + 1) % (len(tlist))
- text_pt = translation_table[tlist[index_pt]][1177338798] + "\n" + tlist[index_pt]
- text_next = (
- translation_table[tlist[index_next]][1177338798] + "\n" + tlist[index_next]
- )
-
- PREV_INDEX = 0
- GO_INDEX = 1
- NEXT_INDEX = 2
-
- LOCALE_INDEX = 2
- LOGGING_INDEX = 3
- PRINTER_INDEX = 5
- LEAVE_INDEX = 8
-
- cases = [
- (
- (
- # Bitcoin
- 0,
- # Change network
- NEXT_INDEX,
- GO_INDEX,
- # Leave Settings
- LEAVE_INDEX,
- ),
- [
- mocker.call("Network\nmain", ORANGE),
- mocker.call("Network\ntest", GREEN),
- ],
- lambda: Settings().bitcoin.network == "test",
- CategorySetting,
- ),
- (
- (
- # Printer
- PRINTER_INDEX,
- # Thermal
- 1,
- # Change Baudrate
- 0,
- NEXT_INDEX,
- GO_INDEX,
- # Back to Thermal
- 8,
- # Back to Printer
- 3,
- # Leave Settings
- LEAVE_INDEX,
- ),
- [
- mocker.call("Baudrate\n9600", WHITE),
- mocker.call("Baudrate\n19200", WHITE),
- ],
- lambda: Settings().printer.thermal.adafruit.baudrate == 19200,
- CategorySetting,
- ),
- (
- (
- # Language
- LOCALE_INDEX,
- # Change Locale
- NEXT_INDEX,
- GO_INDEX,
- ),
- [
- mocker.call(text_pt, WHITE),
- mocker.call(text_next, WHITE),
- ],
- lambda: Settings().i18n.locale == tlist[index_next],
- CategorySetting,
- ),
- (
- (
- # Logging
- LOGGING_INDEX,
- # Change log level
- NEXT_INDEX,
- NEXT_INDEX,
- NEXT_INDEX,
- NEXT_INDEX,
- GO_INDEX,
- # Leave Settings
- LEAVE_INDEX,
- ),
- [
- mocker.call("Log Level\nNONE", WHITE),
- mocker.call("Log Level\nERROR", RED),
- mocker.call("Log Level\nWARN", ORANGE),
- mocker.call("Log Level\nINFO", GREEN),
- mocker.call("Log Level\nDEBUG", MAGENTA),
- ],
- lambda: Settings().logging.level == "DEBUG",
- CategorySetting,
- ),
- ]
- case_num = 0
- for case in cases:
- print("test_settings_on_amigo_tft cases[" + str(case_num) + "]")
- case_num = case_num + 1
-
- ctx = mock_context(mocker)
- ctx.power_manager.battery_charge_remaining.return_value = 1
- ctx.input.wait_for_button = mocker.MagicMock(return_value=BUTTON_TOUCH)
- ctx.input.touch = mocker.MagicMock(
- current_index=mocker.MagicMock(side_effect=case[0])
- )
-
- mocker.patch.object(ctx.input.touch, "x_regions", (0, 100, 200, 300))
- mocker.patch.object(ctx.input.touch, "y_regions", (100, 200))
-
- login = Login(ctx)
-
- Settings().i18n.locale = "pt-BR"
- login.settings()
-
- assert ctx.input.wait_for_button.call_count == len(case[0])
-
- assert case[2]()
-
-
def test_about(mocker, m5stickv):
import krux
-
from krux.pages.login import Login
from krux.metadata import VERSION
from krux.input import BUTTON_ENTER
diff --git a/tests/pages/test_page.py b/tests/pages/test_page.py
index f9160cace..eb26e6884 100644
--- a/tests/pages/test_page.py
+++ b/tests/pages/test_page.py
@@ -37,6 +37,19 @@ def test_init(mocker, m5stickv, mock_page_cls):
assert isinstance(page, Page)
+def test_flash_text(mocker, m5stickv, mock_page_cls):
+ import krux
+
+ ctx = mock_context(mocker)
+ mocker.patch("time.ticks_ms", new=lambda: 0)
+ page = mock_page_cls(ctx)
+ page.flash_text("Hello world", krux.display.lcd.WHITE, 0, 1000)
+
+ assert ctx.display.clear.call_count == 2
+ ctx.display.draw_centered_text.assert_called_once()
+ krux.pages.time.sleep_ms.assert_called_with(1000)
+
+
def test_capture_qr_code(mocker, m5stickv, mock_page_cls):
mocker.patch(
"krux.camera.sensor.snapshot", new=snapshot_generator(outcome=SNAP_SUCCESS)
@@ -50,6 +63,7 @@ def test_capture_qr_code(mocker, m5stickv, mock_page_cls):
mocker.patch("time.ticks_ms", new=lambda: 0)
page = mock_page_cls(ctx)
+ ctx.input.flush_events()
qr_code, qr_format = page.capture_qr_code()
assert qr_code == "12345678910"
@@ -60,7 +74,7 @@ def test_capture_qr_code(mocker, m5stickv, mock_page_cls):
ctx.display.draw_centered_text.assert_has_calls([mocker.call("Loading Camera..")])
-def test_camera_antiglare_light(mocker, m5stickv, mock_page_cls):
+def test_camera_antiglare(mocker, m5stickv, mock_page_cls):
from krux.camera import OV7740_ID
from krux.input import PRESSED, RELEASED
@@ -75,12 +89,12 @@ def test_camera_antiglare_light(mocker, m5stickv, mock_page_cls):
from krux.camera import Camera
ctx = mock_context(mocker)
- ENTER_SEQ = [RELEASED] + [PRESSED] + [PRESSED] + [RELEASED]
- PAGE_PREV_SEQ = [RELEASED] + [RELEASED] + [RELEASED] + [PRESSED]
+ ENTER_SEQ = [False] + [True] + [True] + [False]
+ PAGE_PREV_SEQ = [False] + [False] + [False] + [True]
mocker.patch("time.ticks_ms", time_mocker.tick)
- ctx.input.enter_value = mocker.MagicMock(side_effect=ENTER_SEQ)
- ctx.input.page_value = mocker.MagicMock(side_effect=ENTER_SEQ)
- ctx.input.page_prev_value = mocker.MagicMock(side_effect=PAGE_PREV_SEQ)
+ ctx.input.enter_event = mocker.MagicMock(side_effect=ENTER_SEQ)
+ ctx.input.page_event = mocker.MagicMock(side_effect=ENTER_SEQ)
+ ctx.input.page_prev_event = mocker.MagicMock(side_effect=PAGE_PREV_SEQ)
ctx.camera = Camera()
ctx.camera.cam_id = OV7740_ID
mocker.spy(ctx.camera, "disable_antiglare")
diff --git a/tests/pages/test_qr_view.py b/tests/pages/test_qr_view.py
index 0114c167d..1c67a8551 100644
--- a/tests/pages/test_qr_view.py
+++ b/tests/pages/test_qr_view.py
@@ -3,7 +3,7 @@
def test_load_qr_view(amigo_tft, mocker):
from krux.pages.qr_view import SeedQRView
- from krux.input import BUTTON_ENTER, SWIPE_LEFT, SWIPE_RIGHT
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE_PREV, SWIPE_LEFT, SWIPE_RIGHT
BTN_SEQUENCE = [
SWIPE_LEFT, # lines mode
@@ -15,7 +15,7 @@ def test_load_qr_view(amigo_tft, mocker):
SWIPE_LEFT, # lines mode again
SWIPE_RIGHT, # back to standard mode
BUTTON_ENTER, # leave
- BUTTON_ENTER, # confirm
+ BUTTON_PAGE_PREV, # move to Back to Main Menu
BUTTON_ENTER, # confirm
]
@@ -23,13 +23,13 @@ def test_load_qr_view(amigo_tft, mocker):
ctx.input.wait_for_button = mocker.MagicMock(side_effect=BTN_SEQUENCE)
data = "test code"
seed_qr_view = SeedQRView(ctx, data=data, title="Test QR Code")
- seed_qr_view.display_seed_qr()
+ seed_qr_view.display_qr()
assert ctx.display.draw_qr_code.call_count == 8
def test_loop_through_regions(amigo_tft, mocker):
from krux.pages.qr_view import SeedQRView
- from krux.input import BUTTON_ENTER, SWIPE_LEFT, SWIPE_RIGHT
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE_PREV, SWIPE_LEFT, SWIPE_RIGHT
from ..test_encryption import CBC_ENCRYPTED_QR
BTN_SEQUENCE = (
@@ -46,7 +46,7 @@ def test_loop_through_regions(amigo_tft, mocker):
SWIPE_LEFT, # lines mode again
SWIPE_RIGHT, # back to standard mode
BUTTON_ENTER, # leave
- BUTTON_ENTER, # confirm
+ BUTTON_PAGE_PREV, # move to Back to Main Menu
BUTTON_ENTER, # confirm
]
)
@@ -55,5 +55,5 @@ def test_loop_through_regions(amigo_tft, mocker):
ctx.input.wait_for_button = mocker.MagicMock(side_effect=BTN_SEQUENCE)
data = CBC_ENCRYPTED_QR # Will produce an QR code with 48 regions, max=G7
seed_qr_view = SeedQRView(ctx, data=data, title="Test QR Code")
- seed_qr_view.display_seed_qr()
+ seed_qr_view.display_qr()
assert ctx.display.draw_qr_code.call_count == 57
diff --git a/tests/pages/test_settings_page.py b/tests/pages/test_settings_page.py
new file mode 100644
index 000000000..9622deaf5
--- /dev/null
+++ b/tests/pages/test_settings_page.py
@@ -0,0 +1,391 @@
+import pytest
+from ..shared_mocks import mock_context
+import sys
+
+
+@pytest.fixture
+def mocker_printer(mocker):
+ mocker.patch("krux.printers.thermal.AdafruitPrinter", new=mocker.MagicMock())
+
+
+@pytest.fixture
+def mocker_ucryptolib(mocker):
+ sys.modules["ucryptolib"] = mocker.MagicMock()
+
+
+def create_ctx(mocker, btn_seq, touch_seq=None):
+ """Helper to create mocked context obj"""
+
+ ctx = mock_context(mocker)
+ ctx.input.wait_for_button = mocker.MagicMock(side_effect=btn_seq)
+
+ if touch_seq:
+ ctx.input.touch = mocker.MagicMock(
+ current_index=mocker.MagicMock(side_effect=touch_seq)
+ )
+ return ctx
+
+
+################### Test menus
+
+
+def test_settings_m5stickv(m5stickv, mocker, mocker_printer, mocker_ucryptolib):
+ import krux
+ from krux.pages.settings_page import SettingsPage
+ from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
+ from krux.krux_settings import Settings, CategorySetting, NumberSetting
+ from krux.translations import translation_table
+ from krux.themes import WHITE, RED, GREEN, ORANGE, MAGENTA
+
+ tlist = list(translation_table)
+ index_pt = tlist.index("pt-BR")
+ index_next = (index_pt + 1) % (len(tlist))
+ text_pt = translation_table[tlist[index_pt]][1177338798] + "\n" + tlist[index_pt]
+ text_next = (
+ translation_table[tlist[index_next]][1177338798] + "\n" + tlist[index_next]
+ )
+
+ cases = [
+ ( # 0
+ (
+ # Bitcoin
+ BUTTON_ENTER,
+ # Change network
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Leave Settings
+ BUTTON_PAGE_PREV,
+ BUTTON_ENTER,
+ ),
+ [
+ mocker.call("Network\nmain", ORANGE),
+ mocker.call("Network\ntest", GREEN),
+ ],
+ lambda: Settings().bitcoin.network == "test",
+ CategorySetting,
+ ),
+ ( # 1
+ (
+ # Hardware
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # TODO: Identify it's printer settings
+ # Thermal
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Change Baudrate
+ BUTTON_ENTER,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Back to Thermal
+ BUTTON_PAGE_PREV,
+ BUTTON_ENTER,
+ # Back to Printer
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Leave Settings
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ ),
+ [
+ mocker.call("Baudrate\n9600", WHITE),
+ mocker.call("Baudrate\n19200", WHITE),
+ ],
+ lambda: Settings().hardware.printer.thermal.adafruit.baudrate == 19200,
+ CategorySetting,
+ ),
+ ( # 2
+ (
+ # Language
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Change Locale
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ ),
+ [
+ mocker.call(text_pt, WHITE),
+ mocker.call(text_next, WHITE),
+ ],
+ lambda: Settings().i18n.locale == tlist[index_next],
+ CategorySetting,
+ ),
+ ( # 3
+ (
+ # Logging
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Change log level
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Leave Settings
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ ),
+ [
+ mocker.call("Log Level\nNONE", WHITE),
+ mocker.call("Log Level\nERROR", RED),
+ mocker.call("Log Level\nWARN", ORANGE),
+ mocker.call("Log Level\nINFO", GREEN),
+ mocker.call("Log Level\nDEBUG", MAGENTA),
+ ],
+ lambda: Settings().logging.level == "DEBUG",
+ CategorySetting,
+ ),
+ ( # 4
+ (
+ # Printer
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Thermal
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Paper Width
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Change width
+ # Remove digit
+ BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
+ BUTTON_PAGE_PREV,
+ BUTTON_ENTER,
+ # Add 9
+ BUTTON_PAGE_PREV,
+ BUTTON_ENTER,
+ # Go
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Back to Thermal
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Back to Printer
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ # Leave Settings
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_PAGE,
+ BUTTON_ENTER,
+ ),
+ [
+ mocker.call("Paper Width", 10),
+ ],
+ lambda: Settings().hardware.printer.thermal.adafruit.paper_width == 389,
+ NumberSetting,
+ ),
+ ]
+ case_num = 0
+ for case in cases:
+ print("test_settings cases[" + str(case_num) + "]")
+ case_num = case_num + 1
+
+ ctx = create_ctx(mocker, case[0])
+ settings_page = SettingsPage(ctx)
+
+ Settings().i18n.locale = "pt-BR"
+ settings_page.settings()
+
+ assert ctx.input.wait_for_button.call_count == len(case[0])
+
+ assert case[2]()
+
+
+def test_settings_on_amigo_tft(amigo_tft, mocker, mocker_printer):
+ import krux
+ from krux.pages.settings_page import SettingsPage
+ from krux.input import BUTTON_TOUCH
+ from krux.krux_settings import Settings, CategorySetting, NumberSetting
+ from krux.translations import translation_table
+ from krux.themes import WHITE, RED, GREEN, ORANGE, MAGENTA
+
+ tlist = list(translation_table)
+ index_pt = tlist.index("pt-BR")
+ index_next = (index_pt + 1) % (len(tlist))
+ text_pt = translation_table[tlist[index_pt]][1177338798] + "\n" + tlist[index_pt]
+ text_next = (
+ translation_table[tlist[index_next]][1177338798] + "\n" + tlist[index_next]
+ )
+
+ PREV_INDEX = 0
+ GO_INDEX = 1
+ NEXT_INDEX = 2
+
+ HARDWARE_INDEX = 2
+ LOCALE_INDEX = 3
+ LOGGING_INDEX = 4
+ PRINTER_INDEX = 0
+ LEAVE_INDEX = 8
+
+ cases = [
+ (
+ # Case 0
+ (
+ # Bitcoin
+ 0,
+ # Change network
+ NEXT_INDEX,
+ GO_INDEX,
+ # Leave Settings
+ LEAVE_INDEX,
+ ),
+ [
+ mocker.call("Network\nmain", ORANGE),
+ mocker.call("Network\ntest", GREEN),
+ ],
+ lambda: Settings().bitcoin.network == "test",
+ CategorySetting,
+ ),
+ (
+ # Case 1
+ (
+ # Hardware
+ HARDWARE_INDEX,
+ # Printer
+ PRINTER_INDEX,
+ # Thermal
+ 1,
+ # Change Baudrate
+ 0,
+ NEXT_INDEX,
+ GO_INDEX,
+ # Back from Thermal
+ 6,
+ # Back from Printer
+ 3,
+ # Back from Hardware
+ 2,
+ # Leave Settings
+ LEAVE_INDEX,
+ ),
+ [
+ mocker.call("Baudrate\n9600", WHITE),
+ mocker.call("Baudrate\n19200", WHITE),
+ ],
+ lambda: Settings().hardware.printer.thermal.adafruit.baudrate == 19200,
+ CategorySetting,
+ ),
+ (
+ # Case 2
+ (
+ # Language
+ LOCALE_INDEX,
+ # Change Locale
+ NEXT_INDEX,
+ GO_INDEX,
+ ),
+ [
+ mocker.call(text_pt, WHITE),
+ mocker.call(text_next, WHITE),
+ ],
+ lambda: Settings().i18n.locale == tlist[index_next],
+ CategorySetting,
+ ),
+ (
+ # Case 3
+ (
+ # Logging
+ LOGGING_INDEX,
+ # Change log level
+ NEXT_INDEX,
+ NEXT_INDEX,
+ NEXT_INDEX,
+ NEXT_INDEX,
+ GO_INDEX,
+ # Leave Settings
+ LEAVE_INDEX,
+ ),
+ [
+ mocker.call("Log Level\nNONE", WHITE),
+ mocker.call("Log Level\nERROR", RED),
+ mocker.call("Log Level\nWARN", ORANGE),
+ mocker.call("Log Level\nINFO", GREEN),
+ mocker.call("Log Level\nDEBUG", MAGENTA),
+ ],
+ lambda: Settings().logging.level == "DEBUG",
+ CategorySetting,
+ ),
+ ]
+ case_num = 0
+ for case in cases:
+ print("test_settings_on_amigo_tft cases[" + str(case_num) + "]")
+ case_num = case_num + 1
+
+ ctx = mock_context(mocker)
+ ctx.power_manager.battery_charge_remaining.return_value = 1
+ ctx.input.wait_for_button = mocker.MagicMock(return_value=BUTTON_TOUCH)
+ ctx.input.touch = mocker.MagicMock(
+ current_index=mocker.MagicMock(side_effect=case[0])
+ )
+
+ mocker.patch.object(ctx.input.touch, "x_regions", (0, 100, 200, 300))
+ mocker.patch.object(ctx.input.touch, "y_regions", (100, 200))
+
+ settings_page = SettingsPage(ctx)
+
+ Settings().i18n.locale = "pt-BR"
+ settings_page.settings()
+
+ assert ctx.input.wait_for_button.call_count == len(case[0])
+
+ assert case[2]()
+
+
+def test_encryption_pbkdf2_setting(m5stickv, mocker, mocker_ucryptolib):
+ from krux.pages.settings_page import SettingsPage
+ from krux.krux_settings import Settings, EncryptionSettings
+ from krux.settings import NumberSetting
+
+ ctx = mock_context(mocker)
+ settings_page = SettingsPage(ctx)
+
+ enc_setting = EncryptionSettings()
+
+ # pbkdf2_iterations has default value
+ assert Settings().encryption.pbkdf2_iterations == 100000
+
+ # try to change the value
+ settings_page.capture_from_keypad = mocker.MagicMock(return_value=100001)
+ settings_page.number_setting(
+ EncryptionSettings(), EncryptionSettings.pbkdf2_iterations
+ )
+
+ # continue with default value because it must be multiple of 10000
+ assert Settings().encryption.pbkdf2_iterations == 100000
+
+ # try to change the value to a multiple of 10000
+ settings_page.capture_from_keypad = mocker.MagicMock(return_value=110000)
+ settings_page.number_setting(
+ EncryptionSettings(), EncryptionSettings.pbkdf2_iterations
+ )
+
+ # value changed!
+ assert Settings().encryption.pbkdf2_iterations == 110000
diff --git a/tests/pages/test_stackbit.py b/tests/pages/test_stackbit.py
index 03c51bd34..718bd0e92 100644
--- a/tests/pages/test_stackbit.py
+++ b/tests/pages/test_stackbit.py
@@ -2,7 +2,7 @@
def test_export_mnemonic_stackbit(mocker, m5stickv, tdata):
- from krux.pages.home import Home
+ from krux.pages.mnemonic_view import MnemonicsView
from krux.wallet import Wallet
from krux.input import BUTTON_ENTER, BUTTON_PAGE
@@ -14,6 +14,7 @@ def test_export_mnemonic_stackbit(mocker, m5stickv, tdata):
BUTTON_PAGE,
BUTTON_PAGE,
BUTTON_PAGE,
+ BUTTON_PAGE,
BUTTON_ENTER, # Open Stackbit
BUTTON_ENTER, # PG2
BUTTON_ENTER, # PG3
@@ -25,15 +26,15 @@ def test_export_mnemonic_stackbit(mocker, m5stickv, tdata):
],
]
ctx = create_ctx(mocker, case[2], case[0], case[1])
- home = Home(ctx)
- mocker.spy(home, "stackbit")
- home.mnemonic()
- home.stackbit.assert_called_once()
+ mnemonics = MnemonicsView(ctx)
+ mocker.spy(mnemonics, "stackbit")
+ mnemonics.mnemonic()
+ mnemonics.stackbit.assert_called_once()
assert ctx.input.wait_for_button.call_count == len(case[2])
def test_export_mnemonic_stackbit_amigo(mocker, amigo_tft, tdata):
- from krux.pages.home import Home
+ from krux.pages.mnemonic_view import MnemonicsView
from krux.wallet import Wallet
from krux.input import BUTTON_ENTER, BUTTON_PAGE
@@ -45,6 +46,7 @@ def test_export_mnemonic_stackbit_amigo(mocker, amigo_tft, tdata):
BUTTON_PAGE,
BUTTON_PAGE,
BUTTON_PAGE,
+ BUTTON_PAGE,
BUTTON_ENTER, # Open Stackbit
BUTTON_ENTER, # PG2
BUTTON_ENTER, # PG3
@@ -56,10 +58,10 @@ def test_export_mnemonic_stackbit_amigo(mocker, amigo_tft, tdata):
],
]
ctx = create_ctx(mocker, case[2], case[0], case[1])
- home = Home(ctx)
- mocker.spy(home, "stackbit")
- home.mnemonic()
- home.stackbit.assert_called_once()
+ mnemonics = MnemonicsView(ctx)
+ mocker.spy(mnemonics, "stackbit")
+ mnemonics.mnemonic()
+ mnemonics.stackbit.assert_called_once()
assert ctx.input.wait_for_button.call_count == len(case[2])
diff --git a/tests/pages/test_tiny_seed.py b/tests/pages/test_tiny_seed.py
index 60d9f226a..463daac09 100644
--- a/tests/pages/test_tiny_seed.py
+++ b/tests/pages/test_tiny_seed.py
@@ -3,7 +3,7 @@
def test_export_mnemonic_tiny_seed_menu(mocker, m5stickv, tdata):
- from krux.pages.home import Home
+ from krux.pages.mnemonic_view import MnemonicsView
from krux.wallet import Wallet
from krux.input import BUTTON_ENTER, BUTTON_PAGE
@@ -18,6 +18,7 @@ def test_export_mnemonic_tiny_seed_menu(mocker, m5stickv, tdata):
BUTTON_PAGE,
BUTTON_PAGE,
BUTTON_PAGE,
+ BUTTON_PAGE,
BUTTON_ENTER, # Open TinySeed
BUTTON_ENTER, # go to page 2
BUTTON_ENTER, # Leave
@@ -27,10 +28,10 @@ def test_export_mnemonic_tiny_seed_menu(mocker, m5stickv, tdata):
],
]
ctx = create_ctx(mocker, case[2], case[0], case[1])
- home = Home(ctx)
- mocker.spy(home, "tiny_seed")
- home.mnemonic()
- home.tiny_seed.assert_called_once()
+ mnemonics = MnemonicsView(ctx)
+ mocker.spy(mnemonics, "tiny_seed")
+ mnemonics.mnemonic()
+ mnemonics.tiny_seed.assert_called_once()
assert ctx.input.wait_for_button.call_count == len(case[2])
@@ -276,10 +277,10 @@ def test_scan_tiny_seed_24w(m5stickv, mocker):
# Seed will be returned as its word index
import time
from krux.pages.tiny_seed import TinyScanner
- from krux.input import BUTTON_ENTER, PRESSED, RELEASED
+ from krux.input import BUTTON_ENTER
BTN_SEQUENCE = [BUTTON_ENTER] + [BUTTON_ENTER] # Intro # Check OK
- ENTER_SEQ = [RELEASED] + [PRESSED] + [RELEASED] * 3
+ ENTER_SEQ = [False] + [True] + [False] * 3
TIME_STAMPS = (0, 1, 1000, 2000, 3000, 4000, 5000)
TINYSEED_RECTANGLE = (10, 10, 100, 100)
TEST_WORDS_NUMBERS_1_12 = [
@@ -314,7 +315,9 @@ def test_scan_tiny_seed_24w(m5stickv, mocker):
TEST_24_WORDS = "market glass laugh warm cream either robot end blood awful escape fan palm waste surge kick display shoe remove achieve shoulder siren loop gate"
mocker.patch.object(time, "ticks_ms", mocker.MagicMock(side_effect=TIME_STAMPS))
ctx = create_ctx(mocker, BTN_SEQUENCE)
- ctx.input.enter_value = mocker.MagicMock(side_effect=ENTER_SEQ)
+ mocker.patch.object(ctx.input, "page_event", new=lambda: False)
+ mocker.patch.object(ctx.input, "page_prev_event", new=lambda: False)
+ ctx.input.enter_event = mocker.MagicMock(side_effect=ENTER_SEQ)
tiny_seed = TinyScanner(ctx)
mocker.patch.object(
tiny_seed, "_detect_tiny_seed", new=lambda image: TINYSEED_RECTANGLE
@@ -335,7 +338,7 @@ def test_scan_tiny_seed_24w_amigo(amigo_tft, mocker):
from krux.input import BUTTON_ENTER, PRESSED, RELEASED
BTN_SEQUENCE = [BUTTON_ENTER] + [BUTTON_ENTER] # Intro # Check OK
- ENTER_SEQ = [RELEASED] + [PRESSED] + [RELEASED] * 3
+ ENTER_SEQ = [False] + [True] + [False] * 3
TIME_STAMPS = (0, 1, 1000, 2000, 3000, 4000, 5000)
TINYSEED_RECTANGLE = (10, 10, 100, 100)
TEST_WORDS_NUMBERS_1_12 = [
@@ -370,7 +373,10 @@ def test_scan_tiny_seed_24w_amigo(amigo_tft, mocker):
TEST_24_WORDS = "market glass laugh warm cream either robot end blood awful escape fan palm waste surge kick display shoe remove achieve shoulder siren loop gate"
mocker.patch.object(time, "ticks_ms", mocker.MagicMock(side_effect=TIME_STAMPS))
ctx = create_ctx(mocker, BTN_SEQUENCE)
- ctx.input.enter_value = mocker.MagicMock(side_effect=ENTER_SEQ)
+ mocker.patch.object(ctx.input, "page_event", new=lambda: False)
+ mocker.patch.object(ctx.input, "page_prev_event", new=lambda: False)
+ mocker.patch.object(ctx.input, "touch_event", new=lambda: False)
+ ctx.input.enter_event = mocker.MagicMock(side_effect=ENTER_SEQ)
tiny_seed = TinyScanner(ctx)
mocker.patch.object(
tiny_seed, "_detect_tiny_seed", new=lambda image: TINYSEED_RECTANGLE
diff --git a/tests/pages/test_tools.py b/tests/pages/test_tools.py
index cd187ec34..34abfa514 100644
--- a/tests/pages/test_tools.py
+++ b/tests/pages/test_tools.py
@@ -89,9 +89,9 @@ def test_sd_check_no_sd(m5stickv, mocker):
ctx = mock_context(mocker)
ctx.input.wait_for_button = mocker.MagicMock(side_effect=BTN_SEQUENCE)
tool = Tools(ctx)
+ tool.flash_text = mocker.MagicMock()
tool.sd_check()
-
- ctx.display.flash_text.assert_has_calls([mocker.call("SD card not detected", ANY)])
+ tool.flash_text.assert_has_calls([mocker.call("SD card not detected", ANY)])
def test_sd_check(m5stickv, mocker, mock_file_operations):
@@ -136,4 +136,5 @@ def test_delete_mnemonic_from_sd(m5stickv, mocker, mock_file_operations):
tool.del_stored_mnemonic()
# First mnemonic in the list (ECB) will be deleted
# Assert only CBC remains
- m().write.assert_called_once_with(CBC_ONLY_JSON)
+ padding_size = len(SEEDS_JSON) - len(CBC_ONLY_JSON)
+ m().write.assert_called_once_with(CBC_ONLY_JSON + " " * padding_size)
\ No newline at end of file
diff --git a/tests/printers/test_cnc.py b/tests/printers/test_cnc.py
index 4d4f73e2e..eb48dc49e 100644
--- a/tests/printers/test_cnc.py
+++ b/tests/printers/test_cnc.py
@@ -81,8 +81,8 @@ def test_print_qr_code_with_row_cutmethod(mocker, m5stickv, mocker_sd_card):
)
)
- Settings().printer.cnc.cut_method = "row"
- Settings().printer.cnc.invert = False
+ Settings().hardware.printer.cnc.cut_method = "row"
+ Settings().hardware.printer.cnc.invert = False
p = FilePrinter()
@@ -112,8 +112,8 @@ def test_print_qr_code_with_spiral_cutmethod(mocker, m5stickv, mocker_sd_card):
)
)
- Settings().printer.cnc.cut_method = "spiral"
- Settings().printer.cnc.invert = False
+ Settings().hardware.printer.cnc.cut_method = "spiral"
+ Settings().hardware.printer.cnc.invert = False
p = FilePrinter()
@@ -143,8 +143,8 @@ def test_print_qr_code_inverted(mocker, m5stickv, mocker_sd_card):
)
)
- Settings().printer.cnc.cut_method = "spiral"
- Settings().printer.cnc.invert = True
+ Settings().hardware.printer.cnc.cut_method = "spiral"
+ Settings().hardware.printer.cnc.invert = True
p = FilePrinter()
diff --git a/tests/shared_mocks.py b/tests/shared_mocks.py
index 60b75fada..8eb78e42e 100644
--- a/tests/shared_mocks.py
+++ b/tests/shared_mocks.py
@@ -32,6 +32,24 @@ def encode_to_string(data):
return new_code_str
+def encode(data):
+ # Uses string encoded qr as it already cleaned up the frames
+ # PyQRcode also doesn't offer any binary output
+
+ frame_less_qr = encode_to_string(data)
+ size = 0
+ while frame_less_qr[size] != "\n":
+ size += 1
+ binary_qr = bytearray(b"\x00" * ((size * size + 7) // 8))
+ for y in range(size):
+ for x in range(size):
+ bit_index = y * size + x
+ bit_string_index = y * (size + 1) + x
+ if frame_less_qr[bit_string_index] == "1":
+ binary_qr[bit_index >> 3] |= 1 << (bit_index % 8)
+ return binary_qr
+
+
def get_mock_open(files: dict[str, str]):
def open_mock(filename, *args, **kwargs):
for expected_filename, content in files.items():
@@ -99,6 +117,9 @@ def total_count(self):
def parsed_count(self):
return len(self.parts)
+ def processed_parts_count(self):
+ return self.parsed_count()
+
def parse(self, part):
if part not in self.parts:
self.parts.append(part)
@@ -285,6 +306,7 @@ def board_amigo_tft():
"BUTTON_A": 16,
"BUTTON_B": 20,
"BUTTON_C": 23,
+ "TOUCH_IRQ": 33,
"LED_W": 32,
"I2C_SDA": 27,
"I2C_SCL": 24,
@@ -333,24 +355,38 @@ def mock_context(mocker):
if board.config["type"] == "m5stickv":
return mocker.MagicMock(
- input=mocker.MagicMock(touch=None),
+ input=mocker.MagicMock(
+ touch=None,
+ enter_event=mocker.MagicMock(return_value=False),
+ page_event=mocker.MagicMock(return_value=False),
+ page_prev_event=mocker.MagicMock(return_value=False),
+ touch_event=mocker.MagicMock(return_value=False),
+ ),
display=mocker.MagicMock(
font_width=8,
font_height=14,
width=mocker.MagicMock(return_value=135),
height=mocker.MagicMock(return_value=240),
to_lines=mocker.MagicMock(return_value=[""]),
+ max_lines=mocker.MagicMock(return_value=7),
),
)
elif board.config["type"] == "dock":
return mocker.MagicMock(
- input=mocker.MagicMock(touch=None),
+ input=mocker.MagicMock(
+ touch=None,
+ enter_event=mocker.MagicMock(return_value=False),
+ page_event=mocker.MagicMock(return_value=False),
+ page_prev_event=mocker.MagicMock(return_value=False),
+ touch_event=mocker.MagicMock(return_value=False),
+ ),
display=mocker.MagicMock(
font_width=8,
font_height=16,
width=mocker.MagicMock(return_value=240),
height=mocker.MagicMock(return_value=320),
to_lines=mocker.MagicMock(return_value=[""]),
+ max_lines=mocker.MagicMock(return_value=9),
),
)
elif board.config["type"].startswith("amigo"):
@@ -361,5 +397,6 @@ def mock_context(mocker):
width=mocker.MagicMock(return_value=320),
height=mocker.MagicMock(return_value=480),
to_lines=mocker.MagicMock(return_value=[""]),
+ max_lines=mocker.MagicMock(return_value=9),
),
)
diff --git a/tests/test_context.py b/tests/test_context.py
index b58ad19ea..dfbe16c41 100644
--- a/tests/test_context.py
+++ b/tests/test_context.py
@@ -4,9 +4,9 @@
def mock_modules(mocker):
mocker.patch("krux.context.logger", new=mocker.MagicMock())
mocker.patch("krux.context.Display", new=mocker.MagicMock())
- mocker.patch("krux.context.Input", new=mocker.MagicMock())
mocker.patch("krux.context.Camera", new=mocker.MagicMock())
mocker.patch("krux.context.Light", new=mocker.MagicMock())
+ mocker.patch("krux.context.Input", new=mocker.MagicMock())
def test_init(mocker, m5stickv):
@@ -51,3 +51,58 @@ def test_clear_clears_printer(mocker, m5stickv):
assert c.wallet is None
c.printer.clear.assert_called()
+
+
+def test_screensaver(mocker, m5stickv):
+ """Test whether the screensaver is animating and changing color over time"""
+ mock_modules(mocker)
+ from krux.context import Context, SCREENSAVER_ANIMATION_TIME
+ from krux.themes import theme
+ from krux.input import BUTTON_ENTER
+ import time
+
+ logo = """
+ ββ
+ ββ
+ ββ
+ ββββββ
+ ββ
+ ββ ββ
+ ββ ββ
+ ββββ
+ ββ ββ
+ ββ ββ
+ ββ ββ
+"""[
+ 1:-1
+ ].split(
+ "\n"
+ )
+ c = Context(logo)
+
+ # a sequence of events to simulate users waiting and after some time press BUTTON_ENTER
+ btn_seq = []
+ time_seq = []
+ tmp = SCREENSAVER_ANIMATION_TIME + 1
+ for _ in range(28):
+ time_seq.append(tmp)
+ time_seq.append(tmp)
+ tmp += SCREENSAVER_ANIMATION_TIME + 1
+ btn_seq.append(None)
+
+ time_seq.append(tmp)
+ time_seq.append(tmp)
+ btn_seq.append(BUTTON_ENTER)
+
+ c.input.wait_for_button = mocker.MagicMock(side_effect=btn_seq)
+ time.ticks_ms = mocker.MagicMock(side_effect=time_seq)
+
+ c.screensaver()
+
+ c.display.draw_line_hcentered_with_fullw_bg.assert_any_call(
+ logo[10], 10, theme.fg_color, theme.bg_color
+ )
+ c.display.draw_line_hcentered_with_fullw_bg.assert_any_call(
+ logo[5], 5, theme.bg_color, theme.fg_color
+ )
+ c.input.wait_for_button.assert_called()
diff --git a/tests/test_display.py b/tests/test_display.py
index e054aace3..058af6c31 100644
--- a/tests/test_display.py
+++ b/tests/test_display.py
@@ -1,78 +1,6 @@
-TEST_QR = """
-111111100111000100010011001111111
-100000101001100100101111001000001
-101110101001100001000001001011101
-101110100010101000110011101011101
-101110101101001110000101001011101
-100000101010100111011110101000001
-111111101010101010101010101111111
-000000001111101101101111000000000
-110100110101111010010110101110110
-001101000100111011101100101000001
-010101101010100000011000111110101
-001101010101010100111100010011001
-010110110010110011001100001000000
-011100011010100010010110101010001
-100110100001101011101101000110110
-000100001001011001110100010011011
-001011111100100011011110001010001
-111100011111010010100001110000000
-000111111110011000000010000110111
-101101000111100111001100101010001
-100010111100110010110100110100011
-011001011000110011001010111000011
-110111100110111010111000111000101
-011000000100011110011100001111001
-100101110101110011011111111110000
-000000001000101000011111100010001
-111111101111110010000010101010110
-100000100101110111011100100010011
-101110100010000101010110111110000
-101110101111101010100100001111111
-101110100100001000000010110110011
-100000101001000011110101100111000
-111111101000011010001101101011010
-""".strip()
-
-TEST_QR_WITH_BORDER = """
-00000000000000000000000000000000000
-01111111001110001000100110011111110
-01000001010011001001011110010000010
-01011101010011000010000010010111010
-01011101000101010001100111010111010
-01011101011010011100001010010111010
-01000001010101001110111101010000010
-01111111010101010101010101011111110
-00000000011111011011011110000000000
-01101001101011110100101101011101100
-00011010001001110111011001010000010
-00101011010101000000110001111101010
-00011010101010101001111000100110010
-00101101100101100110011000010000000
-00111000110101000100101101010100010
-01001101000011010111011010001101100
-00001000010010110011101000100110110
-00010111111001000110111100010100010
-01111000111110100101000011100000000
-00001111111100110000000100001101110
-01011010001111001110011001010100010
-01000101111001100101101001101000110
-00110010110001100110010101110000110
-01101111001101110101110001110001010
-00110000001000111100111000011110010
-01001011101011100110111111111100000
-00000000010001010000111111000100010
-01111111011111100100000101010101100
-01000001001011101110111001000100110
-01011101000100001010101101111100000
-01011101011111010101001000011111110
-01011101001000010000000101101100110
-01000001010010000111101011001110000
-01111111010000110100011011010110100
-00000000000000000000000000000000000
-""".strip()
-
-DEFAULT_BG_COLOR = 0x0000 # Black
+TEST_QR = bytearray(
+ b"\x7fn\xfd\x830\x08v9\xd6\xedj\xa0\xdbUU7\xc8\xa0\xe0_U\x7f\x00i\x00\xe3\xd61P\x08\xf5Q\xef^\xfe`\xe8\xc1\x7f\xdex\x936Y\x91\xb8\xeb\xd29c\xd5\xd4\x7f\x00\n#\xfe\xcd\xd7\rJ\x8e\xd9\xe5\xf8\xb9K\xe6x\x17\xb9\xca\xa0\x9a\x9a\x7f\xbb\x1b\x01"
+)
def test_init(mocker, m5stickv):
@@ -182,6 +110,9 @@ def test_to_lines(mocker, m5stickv):
cases = [
(135, "Two Words", ["Two Words"]),
+ (135, "Two Words", ["Two Words"]),
+ (135, "Two Words", ["Two Words"]),
+ (135, "Two Words", ["Two Words"]),
(135, "Two\nWords", ["Two", "Words"]),
(135, "Two\n\nWords", ["Two", "", "Words"]),
(135, "Two\n\n\nWords", ["Two", "", "", "Words"]),
@@ -238,6 +169,7 @@ def test_to_lines(mocker, m5stickv):
"52aad7bd7dcce121",
"",
"",
+ "",
"Install?",
],
),
@@ -307,6 +239,50 @@ def test_to_lines(mocker, m5stickv):
assert lines == case[2]
+def test_to_lines_exact_match_amigo(mocker, amigo_tft):
+ from krux.display import Display
+
+ cases = [
+ (320, "01234 0123456789012345678", ["01234 0123456789012345678"]),
+ (320, "0123456789 01234567890 01234", ["0123456789", "01234567890 01234"]),
+ (320, "01234567890123456789012345", ["0123456789012345678901234", "5"]),
+ (
+ 320,
+ "01234 0123456789012345678\n01234 0123456789012345678",
+ ["01234 0123456789012345678", "01234 0123456789012345678"],
+ ),
+ (
+ 320,
+ "01 34 0123456789012345678\n01234 0123456789012345678",
+ ["01 34 0123456789012345678", "01234 0123456789012345678"],
+ ),
+ (
+ 320,
+ "01 01 01 01 01 01 01 01 0\n01 01 01 01 01 01 01 0123",
+ ["01 01 01 01 01 01 01 01 0", "01 01 01 01 01 01 01 0123"],
+ ),
+ (
+ 320,
+ "0 0 0 0 0 0 0 0 0 0 0 0 0\n01 01 01 01 01 01 01 0123",
+ ["0 0 0 0 0 0 0 0 0 0 0 0 0", "01 01 01 01 01 01 01 0123"],
+ ),
+ (
+ 320,
+ "01 345 0123456789012345678\n01234 0123456789012345678",
+ ["01 345", "0123456789012345678", "01234 0123456789012345678"],
+ ),
+ ]
+ for case in cases:
+ mocker.patch(
+ "krux.display.lcd",
+ new=mocker.MagicMock(width=mocker.MagicMock(return_value=case[0])),
+ )
+ d = Display()
+ lines = d.to_lines(case[1])
+ print(lines)
+ assert lines == case[2]
+
+
def test_outline(mocker, m5stickv):
mocker.patch("krux.display.lcd", new=mocker.MagicMock())
import krux
@@ -403,40 +379,44 @@ def test_draw_hcentered_text(mocker, m5stickv):
)
-def test_draw_centered_text(mocker, m5stickv):
+def test_draw_line_hcentered_with_fullw_bg(mocker, m5stickv):
mocker.patch("krux.display.lcd", new=mocker.MagicMock())
import krux
from krux.display import Display
d = Display()
mocker.patch.object(d, "width", new=lambda: 135)
- mocker.patch.object(d, "height", new=lambda: 240)
- mocker.spy(d, "draw_hcentered_text")
+ mocker.spy(d, "draw_string")
- d.draw_centered_text("Hello world", krux.display.lcd.WHITE, 0)
+ d.draw_line_hcentered_with_fullw_bg(
+ "Hello world", 10, krux.display.lcd.WHITE, krux.display.lcd.BLACK
+ )
- d.draw_hcentered_text.assert_called_with(
- "Hello world", 113, krux.display.lcd.WHITE, 0
+ d.draw_string.assert_called_with(
+ 23,
+ d.font_height * 10,
+ "Hello world",
+ krux.display.lcd.WHITE,
+ krux.display.lcd.BLACK,
)
+ krux.display.lcd.fill_rectangle.assert_called()
-def test_flash_text(mocker, m5stickv):
+def test_draw_centered_text(mocker, m5stickv):
mocker.patch("krux.display.lcd", new=mocker.MagicMock())
- mocker.patch("krux.display.time", new=mocker.MagicMock())
import krux
from krux.display import Display
d = Display()
mocker.patch.object(d, "width", new=lambda: 135)
mocker.patch.object(d, "height", new=lambda: 240)
- mocker.spy(d, "draw_centered_text")
- mocker.spy(d, "clear")
+ mocker.spy(d, "draw_hcentered_text")
- d.flash_text("Hello world", krux.display.lcd.WHITE, 0, 1000)
+ d.draw_centered_text("Hello world", krux.display.lcd.WHITE, 0)
- assert d.clear.call_count == 2
- d.draw_centered_text.assert_called_once()
- krux.display.time.sleep_ms.assert_called_with(1000)
+ d.draw_hcentered_text.assert_called_with(
+ "Hello world", 113, krux.display.lcd.WHITE, 0
+ )
def test_draw_qr_code(mocker, m5stickv):
@@ -449,6 +429,6 @@ def test_draw_qr_code(mocker, m5stickv):
d.draw_qr_code(0, TEST_QR)
- krux.display.lcd.draw_qr_code.assert_called_with(
- 0, TEST_QR_WITH_BORDER, 135, QR_DARK_COLOR, QR_LIGHT_COLOR, DEFAULT_BG_COLOR
+ krux.display.lcd.draw_qr_code_binary.assert_called_with(
+ 0, TEST_QR, 135, QR_DARK_COLOR, QR_LIGHT_COLOR, QR_LIGHT_COLOR
)
diff --git a/tests/test_encryption.py b/tests/test_encryption.py
index e1d540478..52c7c249c 100644
--- a/tests/test_encryption.py
+++ b/tests/test_encryption.py
@@ -61,6 +61,9 @@ def mock_file_operations(mocker):
mocker.patch("builtins.open", mocker.mock_open(read_data=SEEDS_JSON))
+# -------------------------
+
+
def test_ecb_encryption(m5stickv):
from krux.encryption import AESCipher
@@ -117,7 +120,7 @@ def test_load_decrypt_cbc(m5stickv, mock_file_operations):
assert words_sd == CBC_WORDS
-def test_encrypt_ecb_flash(mocker):
+def test_encrypt_ecb_flash(m5stickv, mocker):
from krux.krux_settings import Settings
from krux.encryption import MnemonicStorage
@@ -129,7 +132,7 @@ def test_encrypt_ecb_flash(mocker):
m().write.assert_called_once_with(ECB_ONLY_JSON)
-def test_encrypt_cbc_flash(mocker):
+def test_encrypt_cbc_flash(m5stickv, mocker):
from krux.krux_settings import Settings
from krux.encryption import MnemonicStorage
@@ -143,7 +146,7 @@ def test_encrypt_cbc_flash(mocker):
m().write.assert_called_once_with(CBC_ONLY_JSON)
-def test_encrypt_ecb_sd(mocker, mock_file_operations):
+def test_encrypt_ecb_sd(m5stickv, mocker, mock_file_operations):
from krux.krux_settings import Settings
from krux.encryption import MnemonicStorage
@@ -155,7 +158,7 @@ def test_encrypt_ecb_sd(mocker, mock_file_operations):
m().write.assert_called_once_with(ECB_ONLY_JSON)
-def test_encrypt_cbc_sd(mocker, mock_file_operations):
+def test_encrypt_cbc_sd(m5stickv, mocker, mock_file_operations):
from krux.krux_settings import Settings
from krux.encryption import MnemonicStorage
@@ -169,7 +172,7 @@ def test_encrypt_cbc_sd(mocker, mock_file_operations):
m().write.assert_called_once_with(CBC_ONLY_JSON)
-def test_delet_from_flash(mocker):
+def test_delete_from_flash(m5stickv, mocker):
from krux.encryption import MnemonicStorage
# Loads a file with 2 mnemonics, one with ID="ecbID", other with ID="cbcID"
@@ -180,7 +183,7 @@ def test_delet_from_flash(mocker):
m().write.assert_called_once_with(CBC_ONLY_JSON)
-def test_delet_from_sd(mocker, mock_file_operations):
+def test_delete_from_sd(m5stickv, mocker, mock_file_operations):
from krux.encryption import MnemonicStorage
# Loads a file with 2 mnemonics, one with ID="ecbID", other with ID="cbcID"
@@ -188,10 +191,12 @@ def test_delet_from_sd(mocker, mock_file_operations):
with patch("krux.sd_card.open", new=mocker.mock_open(read_data=SEEDS_JSON)) as m:
storage = MnemonicStorage()
storage.del_mnemonic("ecbID", sd_card=True)
- m().write.assert_called_once_with(CBC_ONLY_JSON)
+ # Calculate padding size
+ padding_size = len(SEEDS_JSON) - len(CBC_ONLY_JSON)
+ m().write.assert_called_once_with(CBC_ONLY_JSON + " " * padding_size)
-def test_create_ecb_encrypted_qr_code():
+def test_create_ecb_encrypted_qr_code(m5stickv):
from krux.encryption import EncryptedQRCode
from krux.krux_settings import Settings
@@ -201,7 +206,7 @@ def test_create_ecb_encrypted_qr_code():
assert qr_data == ECB_ENCRYPTED_QR
-def test_create_cbc_encrypted_qr_code():
+def test_create_cbc_encrypted_qr_code(m5stickv):
from krux.encryption import EncryptedQRCode
from krux.krux_settings import Settings
@@ -212,7 +217,7 @@ def test_create_cbc_encrypted_qr_code():
assert qr_data == CBC_ENCRYPTED_QR
-def test_decode_ecb_encrypted_qr_code():
+def test_decode_ecb_encrypted_qr_code(m5stickv):
from krux.encryption import EncryptedQRCode
from embit import bip39
@@ -224,7 +229,7 @@ def test_decode_ecb_encrypted_qr_code():
assert words == TEST_WORDS
-def test_decode_cbc_encrypted_qr_code():
+def test_decode_cbc_encrypted_qr_code(m5stickv):
from krux.encryption import EncryptedQRCode
from embit import bip39
@@ -237,14 +242,24 @@ def test_decode_cbc_encrypted_qr_code():
assert words == TEST_WORDS
-def create_ctx(mocker, btn_seq, touch_seq=None):
- """Helper to create mocked context obj"""
- ctx = mock_context(mocker)
- ctx.power_manager.battery_charge_remaining.return_value = 1
- ctx.input.wait_for_button = mocker.MagicMock(side_effect=btn_seq)
+def test_customize_pbkdf2_iterations_create_and_decode(m5stickv):
+ from krux.encryption import EncryptedQRCode
+ from krux.krux_settings import Settings
+ from embit import bip39
- if touch_seq:
- ctx.input.touch = mocker.MagicMock(
- current_index=mocker.MagicMock(side_effect=touch_seq)
- )
- return ctx
+ print("case Encode: customize_pbkdf2_iterations")
+ Settings().encryption.version = "AES-ECB"
+ Settings().encryption.pbkdf2_iterations = 99999
+ encrypted_qr = EncryptedQRCode()
+ qr_data = encrypted_qr.create(TEST_KEY, TEST_MNEMONIC_ID, TEST_WORDS)
+ print(qr_data)
+ print(ECB_ENCRYPTED_QR)
+
+ print("case Decode: customize_pbkdf2_iterations")
+ public_data = encrypted_qr.public_data(qr_data)
+ assert public_data == (
+ "Encrypted QR Code:\nID: test ID\nVersion: AES-ECB\nKey iter.: 90000"
+ )
+ word_bytes = encrypted_qr.decrypt(TEST_KEY)
+ words = bip39.mnemonic_from_bytes(word_bytes)
+ assert words == TEST_WORDS
diff --git a/tests/test_input.py b/tests/test_input.py
index e0487d51f..fc3812119 100644
--- a/tests/test_input.py
+++ b/tests/test_input.py
@@ -7,18 +7,22 @@ def reset_input_states(mocker, input):
if input.enter:
mocker.patch.object(input, "enter_value", new=lambda: RELEASED)
+ mocker.patch.object(input, "enter_event", new=lambda: False)
if input.page:
mocker.patch.object(input, "page_value", new=lambda: RELEASED)
+ mocker.patch.object(input, "page_event", new=lambda: False)
if input.page_prev:
mocker.patch.object(input, "page_prev_value", new=lambda: RELEASED)
+ mocker.patch.object(input, "page_prev_event", new=lambda: False)
if input.touch:
mocker.patch.object(input.touch.touch_driver, "current_point", new=lambda: None)
+ mocker.patch.object(input.touch, "event", new=lambda: False)
return input
def test_init(mocker, m5stickv):
- mocker.patch("krux.input.fm.register", new=mocker.MagicMock())
- mocker.patch("krux.input.GPIO", new=mocker.MagicMock())
+ mocker.patch("krux.buttons.fm.register", new=mocker.MagicMock())
+ mocker.patch("krux.buttons.GPIO", new=mocker.MagicMock())
import krux
from krux.input import Input
import board
@@ -26,37 +30,37 @@ def test_init(mocker, m5stickv):
input = Input()
assert isinstance(input, Input)
- krux.input.fm.register.assert_has_calls(
+ krux.buttons.fm.register.assert_has_calls(
[
mocker.call(board.config["krux"]["pins"]["BUTTON_A"], mocker.ANY),
mocker.call(board.config["krux"]["pins"]["BUTTON_B"], mocker.ANY),
]
)
assert (
- krux.input.fm.register.call_args_list[0].args[1]._extract_mock_name()
+ krux.buttons.fm.register.call_args_list[0].args[1]._extract_mock_name()
== "mock.fm.fpioa.GPIOHS21"
)
assert (
- krux.input.fm.register.call_args_list[1].args[1]._extract_mock_name()
+ krux.buttons.fm.register.call_args_list[1].args[1]._extract_mock_name()
== "mock.fm.fpioa.GPIOHS22"
)
assert input.enter is not None
assert input.page is not None
- assert krux.input.GPIO.call_count == 2
+ assert krux.buttons.GPIO.call_count == 2
assert (
- krux.input.GPIO.call_args_list[0].args[0]._extract_mock_name()
+ krux.buttons.GPIO.call_args_list[0].args[0]._extract_mock_name()
== "mock.GPIOHS21"
)
assert (
- krux.input.GPIO.call_args_list[1].args[0]._extract_mock_name()
+ krux.buttons.GPIO.call_args_list[1].args[0]._extract_mock_name()
== "mock.GPIOHS22"
)
def test_init_amigo_tft(mocker, amigo_tft):
- mocker.patch("krux.input.fm.register", new=mocker.MagicMock())
- mocker.patch("krux.input.GPIO", new=mocker.MagicMock())
+ mocker.patch("krux.buttons.fm.register", new=mocker.MagicMock())
+ mocker.patch("krux.buttons.GPIO", new=mocker.MagicMock())
import krux
from krux.input import Input
import board
@@ -64,7 +68,7 @@ def test_init_amigo_tft(mocker, amigo_tft):
input = Input()
assert isinstance(input, Input)
- krux.input.fm.register.assert_has_calls(
+ krux.buttons.fm.register.assert_has_calls(
[
mocker.call(board.config["krux"]["pins"]["BUTTON_A"], mocker.ANY),
mocker.call(board.config["krux"]["pins"]["BUTTON_B"], mocker.ANY),
@@ -72,38 +76,39 @@ def test_init_amigo_tft(mocker, amigo_tft):
]
)
assert (
- krux.input.fm.register.call_args_list[0].args[1]._extract_mock_name()
+ krux.buttons.fm.register.call_args_list[0].args[1]._extract_mock_name()
== "mock.fm.fpioa.GPIOHS21"
)
assert (
- krux.input.fm.register.call_args_list[1].args[1]._extract_mock_name()
+ krux.buttons.fm.register.call_args_list[1].args[1]._extract_mock_name()
== "mock.fm.fpioa.GPIOHS22"
)
assert (
- krux.input.fm.register.call_args_list[2].args[1]._extract_mock_name()
+ krux.buttons.fm.register.call_args_list[2].args[1]._extract_mock_name()
== "mock.fm.fpioa.GPIOHS0"
)
assert input.enter is not None
assert input.page is not None
assert input.page_prev is not None
- assert krux.input.GPIO.call_count == 3
+ assert krux.buttons.GPIO.call_count == 3
assert (
- krux.input.GPIO.call_args_list[0].args[0]._extract_mock_name()
+ krux.buttons.GPIO.call_args_list[0].args[0]._extract_mock_name()
== "mock.GPIOHS21"
)
assert (
- krux.input.GPIO.call_args_list[1].args[0]._extract_mock_name()
+ krux.buttons.GPIO.call_args_list[1].args[0]._extract_mock_name()
== "mock.GPIOHS22"
)
assert (
- krux.input.GPIO.call_args_list[2].args[0]._extract_mock_name() == "mock.GPIOHS0"
+ krux.buttons.GPIO.call_args_list[2].args[0]._extract_mock_name()
+ == "mock.GPIOHS0"
)
def test_init_dock(mocker, dock):
- mocker.patch("krux.input.fm.register", new=mocker.MagicMock())
- mocker.patch("krux.input.GPIO", new=mocker.MagicMock())
+ mocker.patch("krux.buttons.fm.register", new=mocker.MagicMock())
+ mocker.patch("krux.buttons.GPIO", new=mocker.MagicMock())
import krux
from krux.input import Input
import board
@@ -111,7 +116,7 @@ def test_init_dock(mocker, dock):
input = Input()
assert isinstance(input, Input)
- krux.input.fm.register.assert_has_calls(
+ krux.buttons.fm.register.assert_has_calls(
[
mocker.call(board.config["krux"]["pins"]["BUTTON_A"], mocker.ANY),
]
@@ -123,7 +128,7 @@ def test_init_dock(mocker, dock):
]
)
assert (
- krux.input.fm.register.call_args_list[0].args[1]._extract_mock_name()
+ krux.buttons.fm.register.call_args_list[0].args[1]._extract_mock_name()
== "mock.fm.fpioa.GPIOHS21"
)
assert (
@@ -138,10 +143,10 @@ def test_init_dock(mocker, dock):
assert input.page is not None
assert input.page_prev is not None
- assert krux.input.GPIO.call_count == 1
+ assert krux.buttons.GPIO.call_count == 1
assert krux.rotary.GPIO.call_count == 2
assert (
- krux.input.GPIO.call_args_list[0].args[0]._extract_mock_name()
+ krux.buttons.GPIO.call_args_list[0].args[0]._extract_mock_name()
== "mock.GPIOHS21"
)
assert (
@@ -225,19 +230,21 @@ def test_wait_for_release(mocker, m5stickv):
input = Input()
input = reset_input_states(mocker, input)
+ mocker.patch.object(input, "enter_event", new=lambda: True)
mocker.patch.object(input, "enter_value", new=lambda: PRESSED)
def release():
- mocker.patch.object(time, "ticks_ms", new=lambda: 0)
+ mocker.patch.object(time, "ticks_ms", new=lambda: 1000)
+ time.sleep(0.1)
+ mocker.patch.object(time, "ticks_ms", new=lambda: 2000)
time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 100)
mocker.patch.object(input, "enter_value", new=lambda: RELEASED)
assert input.entropy == 0
t = threading.Thread(target=release)
t.start()
- input.wait_for_release()
+ input.wait_for_button()
t.join()
assert input.entropy > 0
@@ -252,13 +259,14 @@ def test_wait_for_button_blocks_until_enter_released(mocker, m5stickv):
input = Input()
input = reset_input_states(mocker, input)
+ mocker.patch.object(input, "enter_event", new=lambda: True)
+ mocker.patch.object(input, "enter_value", new=lambda: PRESSED)
+
def release():
- mocker.patch.object(time, "ticks_ms", new=lambda: 0)
+ mocker.patch.object(time, "ticks_ms", new=lambda: 1000)
time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 100)
- mocker.patch.object(input, "enter_value", new=lambda: PRESSED)
+ mocker.patch.object(time, "ticks_ms", new=lambda: 1100)
time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 200)
mocker.patch.object(input, "enter_value", new=lambda: RELEASED)
assert input.entropy == 0
@@ -281,13 +289,14 @@ def test_wait_for_button_blocks_until_page_released(mocker, m5stickv):
input = Input()
input = reset_input_states(mocker, input)
+ mocker.patch.object(input, "page_event", new=lambda: True)
+ mocker.patch.object(input, "page_value", new=lambda: PRESSED)
+
def release():
- mocker.patch.object(time, "ticks_ms", new=lambda: 0)
+ mocker.patch.object(time, "ticks_ms", new=lambda: 1000)
time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 100)
- mocker.patch.object(input, "page_value", new=lambda: PRESSED)
+ mocker.patch.object(time, "ticks_ms", new=lambda: 1100)
time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 200)
mocker.patch.object(input, "page_value", new=lambda: RELEASED)
assert input.entropy == 0
@@ -302,7 +311,7 @@ def release():
krux.input.wdt.feed.assert_called()
-def test_wait_for_button_blocks_until_page_prev_released(mocker, amigo_tft):
+def test_wait_for_button_blocks_until_page_prev_released(mocker, m5stickv):
import threading
import krux
from krux.input import Input, RELEASED, PRESSED, BUTTON_PAGE_PREV
@@ -310,27 +319,19 @@ def test_wait_for_button_blocks_until_page_prev_released(mocker, amigo_tft):
input = Input()
input = reset_input_states(mocker, input)
- def click():
- mocker.patch.object(time, "ticks_ms", new=lambda: 0)
+ mocker.patch.object(input, "page_prev_event", new=lambda: True)
+ mocker.patch.object(input, "page_prev_value", new=lambda: PRESSED)
+
+ def release():
+ mocker.patch.object(time, "ticks_ms", new=lambda: 1000)
time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 100)
- # if input.page_prev, "value" is mocked, in what apparently is bug,
- # other input buttons get the same "value", invalidating the test
- mocker.patch.object(input, "page_prev_value", new=lambda: PRESSED)
+ mocker.patch.object(time, "ticks_ms", new=lambda: 1100)
time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 200)
mocker.patch.object(input, "page_prev_value", new=lambda: RELEASED)
assert input.entropy == 0
- # first one click to enable physical buttons
- t = threading.Thread(target=click)
- t.start()
- btn = input.wait_for_button(True)
- t.join()
-
- # than other click to be counted
- t = threading.Thread(target=click)
+ t = threading.Thread(target=release)
t.start()
btn = input.wait_for_button(True)
t.join()
@@ -343,51 +344,20 @@ def click():
def test_wait_for_button_blocks_until_touch_released(mocker, amigo_tft):
import threading
import krux
- from krux.input import Input, BUTTON_TOUCH
+ from krux.input import Input, BUTTON_TOUCH, PRESSED, RELEASED
input = Input()
input = reset_input_states(mocker, input)
- def click():
- mocker.patch.object(time, "ticks_ms", new=lambda: 0)
- time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 100)
- mocker.patch.object(input.touch, "current_state", new=lambda: 1)
- time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 200)
- mocker.patch.object(input.touch, "current_state", new=lambda: 0)
-
- assert input.entropy == 0
-
- t = threading.Thread(target=click)
- t.start()
- btn = input.wait_for_button(True)
- t.join()
-
- assert btn == BUTTON_TOUCH
- assert input.entropy > 0
- krux.input.wdt.feed.assert_called()
-
-
-def test_wait_for_button_waits_for_existing_press_to_release(mocker, m5stickv):
- import threading
- import krux
- from krux.input import Input, RELEASED, PRESSED, BUTTON_ENTER
-
- input = Input()
- input = reset_input_states(mocker, input)
+ mocker.patch.object(input.touch, "event", new=lambda: True)
+ mocker.patch.object(input, "touch_value", new=lambda: PRESSED)
def release():
- mocker.patch.object(time, "ticks_ms", new=lambda: 0)
- time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 100)
- mocker.patch.object(input, "page_value", new=lambda: RELEASED)
+ mocker.patch.object(time, "ticks_ms", new=lambda: 1000)
time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 200)
- mocker.patch.object(input, "enter_value", new=lambda: PRESSED)
+ mocker.patch.object(time, "ticks_ms", new=lambda: 1100)
time.sleep(0.1)
- mocker.patch.object(time, "ticks_ms", new=lambda: 300)
- mocker.patch.object(input, "enter_value", new=lambda: RELEASED)
+ mocker.patch.object(input, "touch_value", new=lambda: RELEASED)
assert input.entropy == 0
@@ -396,7 +366,7 @@ def release():
btn = input.wait_for_button(True)
t.join()
- assert btn == BUTTON_ENTER
+ assert btn == BUTTON_TOUCH
assert input.entropy > 0
krux.input.wdt.feed.assert_called()
@@ -435,8 +405,11 @@ def release():
mocker.patch.object(time, "ticks_ms", new=lambda: 0)
time.sleep(0.1)
mocker.patch.object(time, "ticks_ms", new=lambda: 100)
+ mocker.patch.object(input, "page_event", new=lambda: True)
mocker.patch.object(input, "page_value", new=lambda: PRESSED)
time.sleep(0.1)
+ mocker.patch.object(input, "page_event", new=lambda: False)
+ time.sleep(0.1)
mocker.patch.object(time, "ticks_ms", new=lambda: LONG_PRESS_PERIOD + 500)
mocker.patch.object(input, "page_value", new=lambda: RELEASED)
@@ -464,8 +437,11 @@ def release():
mocker.patch.object(time, "ticks_ms", new=lambda: 0)
time.sleep(0.1)
mocker.patch.object(time, "ticks_ms", new=lambda: 100)
+ mocker.patch.object(input, "page_prev_event", new=lambda: True)
mocker.patch.object(input, "page_prev_value", new=lambda: PRESSED)
time.sleep(0.1)
+ mocker.patch.object(input, "page_prev_event", new=lambda: False)
+ time.sleep(0.1)
mocker.patch.object(time, "ticks_ms", new=lambda: LONG_PRESS_PERIOD + 500)
mocker.patch.object(input, "page_prev_value", new=lambda: RELEASED)
@@ -493,28 +469,32 @@ def test_touch_indexing(mocker, amigo_tft):
def time_control(point1, point2):
nonlocal elapsed_time
-
mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time)
time.sleep(0.1)
- elapsed_time += 500
+ elapsed_time += 200
mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time)
# touch on 3ΒΊ quadrant
+ mocker.patch.object(input.touch, "event", new=lambda: True)
+ mocker.patch.object(input.touch.touch_driver, "irq_point", new=lambda: point1)
mocker.patch.object(
input.touch.touch_driver, "current_point", new=lambda: point1
)
time.sleep(0.1)
- elapsed_time += 500
+ mocker.patch.object(input.touch, "event", new=lambda: False)
+ time.sleep(0.1)
+ elapsed_time += 200
mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time)
# touch slightly sideways before release
mocker.patch.object(
input.touch.touch_driver, "current_point", new=lambda: point2
)
time.sleep(0.1)
- elapsed_time += 500
+ elapsed_time += 200
mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time)
+ # release touch
mocker.patch.object(input.touch.touch_driver, "current_point", new=lambda: None)
time.sleep(0.1)
- elapsed_time += 500
+ elapsed_time += 200
mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time)
# full screen as single touch button
@@ -557,27 +537,31 @@ def test_touch_gestures(mocker, amigo_tft):
def time_control(point1, point2):
nonlocal elapsed_time
+ elapsed_time += 200
mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time)
- time.sleep(0.1)
- elapsed_time += 500
- mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time)
- # touch on 3ΒΊ quadrant
+ mocker.patch.object(input.touch, "event", new=lambda: True)
+ mocker.patch.object(input.touch.touch_driver, "irq_point", new=lambda: point1)
mocker.patch.object(
input.touch.touch_driver, "current_point", new=lambda: point1
)
time.sleep(0.1)
- elapsed_time += 500
+ # Detect press event
+ elapsed_time += 200
mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time)
- # swipe
+ time.sleep(0.1)
+ mocker.patch.object(input.touch.touch_driver, "event", new=lambda: False)
+ time.sleep(0.1)
+ # Swipe
mocker.patch.object(
input.touch.touch_driver, "current_point", new=lambda: point2
)
time.sleep(0.1)
- elapsed_time += 500
+ # Release
+ elapsed_time += 200
mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time)
mocker.patch.object(input.touch.touch_driver, "current_point", new=lambda: None)
time.sleep(0.1)
- elapsed_time += 500
+ elapsed_time += 200
mocker.patch.object(time, "ticks_ms", new=lambda: elapsed_time)
# Swipe Right
@@ -723,9 +707,12 @@ def release():
mocker.patch.object(time, "ticks_ms", new=lambda: 0)
time.sleep(0.1)
mocker.patch.object(time, "ticks_ms", new=lambda: 100)
+ mocker.patch.object(input, "enter_event", new=lambda: True)
mocker.patch.object(input, "enter_value", new=lambda: PRESSED)
time.sleep(0.1)
mocker.patch.object(time, "ticks_ms", new=lambda: 200)
+ mocker.patch.object(input, "enter_event", new=lambda: False)
+ time.sleep(0.1)
mocker.patch.object(input, "enter_value", new=lambda: RELEASED)
assert input.entropy == 0
@@ -754,9 +741,11 @@ def release():
mocker.patch.object(time, "ticks_ms", new=lambda: 0)
time.sleep(0.1)
mocker.patch.object(time, "ticks_ms", new=lambda: 100)
+ mocker.patch.object(input, "page_event", new=lambda: True)
mocker.patch.object(input, "page_value", new=lambda: PRESSED)
time.sleep(0.1)
mocker.patch.object(time, "ticks_ms", new=lambda: 200)
+ mocker.patch.object(input, "page_event", new=lambda: False)
mocker.patch.object(input, "page_value", new=lambda: RELEASED)
assert input.entropy == 0
@@ -785,9 +774,11 @@ def release():
mocker.patch.object(time, "ticks_ms", new=lambda: 0)
time.sleep(0.1)
mocker.patch.object(time, "ticks_ms", new=lambda: 100)
+ mocker.patch.object(input, "page_prev_event", new=lambda: True)
mocker.patch.object(input, "page_prev_value", new=lambda: PRESSED)
time.sleep(0.1)
mocker.patch.object(time, "ticks_ms", new=lambda: 200)
+ mocker.patch.object(input, "page_prev_event", new=lambda: False)
mocker.patch.object(input, "page_prev_value", new=lambda: RELEASED)
assert input.entropy == 0
@@ -801,3 +792,31 @@ def release():
assert btn is None
assert input.entropy > 0
krux.input.wdt.feed.assert_called()
+
+
+def test_wait_for_press_screensaver(mocker, m5stickv):
+ from krux.input import Input
+ from krux.krux_settings import Settings
+ import krux
+
+ input = Input(screensaver_fallback=mocker.MagicMock())
+ input = reset_input_states(mocker, input)
+ input.buttons_active = False
+
+ # Make test faster
+ Settings().appearance.screensaver_time = 0.0001
+
+ time_seq = []
+ tmp = 100
+ for _ in range(10):
+ time_seq.append(tmp)
+ tmp += 100
+
+ time.ticks_ms = mocker.MagicMock(side_effect=time_seq)
+
+ input.wait_for_press(True, enable_screensaver=True)
+ input.screensaver_fallback.assert_called()
+
+ Settings().appearance.screensaver_time = 0
+ input.wait_for_button(block=False)
+ input.screensaver_fallback.assert_called_once()
diff --git a/tests/test_qr.py b/tests/test_qr.py
index 212aa4961..6993efc83 100644
--- a/tests/test_qr.py
+++ b/tests/test_qr.py
@@ -79,7 +79,10 @@ def test_parser(mocker, m5stickv, tdata):
(FORMAT_UR, tdata.TEST_PARTS_FORMAT_SINGLEPART_UR),
(FORMAT_UR, tdata.TEST_PARTS_FORMAT_MULTIPART_UR),
]
+ num = 0
for case in cases:
+ print("case: ", num)
+ num += 1
fmt = case[0]
parts = case[1]
@@ -89,7 +92,10 @@ def test_parser(mocker, m5stickv, tdata):
assert parser.format == fmt
- assert parser.total_count() == len(parts)
+ if num == 4:
+ assert parser.total_count() == len(parts) * 2
+ else:
+ assert parser.total_count() == len(parts)
if parser.format == FORMAT_UR:
assert parser.parsed_count() > 0
else:
@@ -103,7 +109,11 @@ def test_parser(mocker, m5stickv, tdata):
# Re-parse the first part to test that redundant parts are ignored
parser.parse(parts[0])
- assert parser.total_count() == len(parts)
+ if num == 4:
+ assert parser.total_count() == len(parts) * 2
+ else:
+ assert parser.total_count() == len(parts)
+
if parser.format == FORMAT_UR:
assert parser.parsed_count() > 0
else:
@@ -120,19 +130,31 @@ def test_parser(mocker, m5stickv, tdata):
def test_to_qr_codes(mocker, m5stickv, tdata):
from krux.qr import to_qr_codes, FORMAT_NONE, FORMAT_PMOFN, FORMAT_UR
+ from krux.display import Display
cases = [
- (FORMAT_NONE, tdata.TEST_DATA_B58, 1),
- (FORMAT_PMOFN, tdata.TEST_DATA_B58, 3),
- (FORMAT_UR, tdata.TEST_DATA_UR, 3),
+ # Test 135 pixels wide display
+ (FORMAT_NONE, tdata.TEST_DATA_B58, 135, 1),
+ (FORMAT_PMOFN, tdata.TEST_DATA_B58, 135, 9),
+ (FORMAT_UR, tdata.TEST_DATA_UR, 135, 26),
+ # Test 320 pixels wide display
+ (FORMAT_NONE, tdata.TEST_DATA_B58, 320, 1),
+ (FORMAT_PMOFN, tdata.TEST_DATA_B58, 320, 3),
+ (FORMAT_UR, tdata.TEST_DATA_UR, 320, 6),
]
for case in cases:
+ mocker.patch(
+ "krux.display.lcd",
+ new=mocker.MagicMock(width=mocker.MagicMock(return_value=case[2])),
+ )
+ display = Display()
+ qr_data_width = display.qr_data_width()
fmt = case[0]
data = case[1]
- expected_parts = case[2]
+ expected_parts = case[3]
codes = []
- code_generator = to_qr_codes(data, 135, fmt)
+ code_generator = to_qr_codes(data, qr_data_width, fmt)
i = 0
while True:
try:
@@ -141,7 +163,8 @@ def test_to_qr_codes(mocker, m5stickv, tdata):
assert total == expected_parts
if i == total - 1:
break
- except:
+ except Exception as e:
+ print("Error:", e)
break
i += 1
assert len(codes) == expected_parts
@@ -155,3 +178,13 @@ def test_detect_plaintext_qr(mocker, m5stickv):
)
detect_format(PLAINTEXT_QR_DATA)
+
+
+def test_find_min_num_parts(m5stickv):
+ from krux.qr import find_min_num_parts
+
+ with pytest.raises(ValueError) as raised_ex:
+ find_min_num_parts("", 10, "format unknown")
+
+ assert raised_ex.type is ValueError
+ assert raised_ex.value.args[0] == "Invalid format type"
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 46f1d28fd..99d8aac6f 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -18,14 +18,6 @@ def test_init(mocker, m5stickv):
assert isinstance(s, Settings)
-# @pytest.fixture
-# def mocker_sd_card(mocker):
-# mocker.patch(
-# "os.listdir",
-# new=mocker.MagicMock(return_value=["somefile", "otherfile"]),
-# )
-
-
def test_store_init(mocker, m5stickv):
from krux.settings import Store, SETTINGS_FILENAME, SD_PATH
@@ -43,9 +35,7 @@ def test_store_init(mocker, m5stickv):
assert s.settings == case[1]
-def test_store_get(mocker, m5stickv):
- mo = mocker.mock_open()
- mocker.patch("builtins.open", mo)
+def test_store_get():
from krux.settings import Store
s = Store()
@@ -55,16 +45,20 @@ def test_store_get(mocker, m5stickv):
("ns1.ns2", "setting", "call1_defaultvalue2", "call2_defaultvalue2"),
("ns1.ns2.ns3", "setting", "call1_defaultvalue3", "call2_defaultvalue3"),
]
- for case in cases:
+ for i, case in enumerate(cases):
# First call, setting does not exist, so default value becomes the value
assert s.get(case[0], case[1], case[2]) == case[2]
+
+ # Getter does not populate settings dict. if nothing is set then settings is empty
+ if i == 0:
+ assert s.settings == {}
+
# Second call, setting does exist, so default value is ignored
+ s.set(case[0], case[1], case[2])
assert s.get(case[0], case[1], case[3]) == case[2]
-def test_store_set(mocker, m5stickv):
- mo = mocker.mock_open()
- mocker.patch("builtins.open", mo)
+def test_store_set():
from krux.settings import Store
s = Store()
@@ -74,6 +68,9 @@ def test_store_set(mocker, m5stickv):
("ns1.ns2", "setting", "call1_value2", 2, "call3_value2"),
("ns1.ns2.ns3", "setting", "call1_value3", 3, "call3_value3"),
]
+
+ assert s.dirty == False
+
for case in cases:
s.set(case[0], case[1], case[2])
assert s.get(case[0], case[1], "default") == case[2]
@@ -84,6 +81,101 @@ def test_store_set(mocker, m5stickv):
s.set(case[0], case[1], case[4])
assert s.get(case[0], case[1], "default") == case[4]
+ assert s.dirty == True
+
+
+def test_store_delete():
+ from krux.settings import Store
+
+ s = Store()
+
+ cases = [
+ ("ns1", "setting1", "value1"),
+ ("ns1.ns2", "setting2", "value2"),
+ ]
+
+ assert s.dirty == False
+
+ for case in cases:
+ s.set(case[0], case[1], case[2])
+
+ for i, case in enumerate(cases):
+ s.delete(case[0], case[1])
+
+ # value of deleted setting is its default
+ assert s.get(case[0], case[1], "default") == "default"
+
+ # when deleting a setting, its empty namespaces are deleted too
+ if i == len(cases) - 1:
+ assert s.settings == {}
+
+ assert s.dirty == True
+
+
+def test_store_update_file_location(mocker):
+ ms, mr = mocker.Mock(), mocker.Mock()
+ mocker.patch("os.stat", ms)
+ mocker.patch("os.remove", mr)
+ from krux.settings import Store, SD_PATH, FLASH_PATH, SETTINGS_FILENAME
+
+ s = Store()
+
+ # default is /flash/
+ assert s.file_location == "/" + FLASH_PATH + "/"
+
+ # from flash to sd removes /flash/settings.json
+ s.update_file_location(SD_PATH)
+ assert s.file_location == "/" + SD_PATH + "/"
+ ms.assert_called_once_with("/" + FLASH_PATH + "/" + SETTINGS_FILENAME)
+ mr.assert_called_once_with("/" + FLASH_PATH + "/" + SETTINGS_FILENAME)
+ ms.reset_mock()
+ mr.reset_mock()
+
+ # from sd to flash removes /sd/settings.json
+ s.update_file_location(FLASH_PATH)
+ assert s.file_location == "/" + FLASH_PATH + "/"
+ ms.assert_called_once_with("/" + SD_PATH + "/" + SETTINGS_FILENAME)
+ mr.assert_called_once_with("/" + SD_PATH + "/" + SETTINGS_FILENAME)
+ ms.reset_mock()
+ mr.reset_mock()
+
+ # updating with existing file_location does nothing
+ s.update_file_location(FLASH_PATH)
+ assert s.file_location == "/" + FLASH_PATH + "/"
+ ms.assert_not_called()
+ mr.assert_not_called()
+
+
+def test_store_save_settings(mocker):
+ mo = mocker.mock_open()
+ mocker.patch("builtins.open", mo)
+ from krux.settings import Store, SETTINGS_FILENAME
+
+ s = Store()
+ filename = s.file_location + SETTINGS_FILENAME
+
+ # new setting change: is dirty, save_settings() persists, file written
+ s.set("name.space", "setting", "custom_value")
+ assert s.dirty == True
+ assert s.save_settings() == True
+ mo.assert_called_with(filename, "w")
+
+ # no setting change: not dirty, save_settings() doesn't persist, file not even read
+ mo.reset_mock()
+ assert s.dirty == False
+ assert s.save_settings() == False
+ mo.assert_not_called()
+
+ # settings changed to original: dirty but save_settings() doesn't persist, file read -- not written
+ mo = mocker.mock_open(read_data='{"name": {"space": {"setting": "custom_value"}}}')
+ mocker.patch("builtins.open", mo)
+ s.set("name.space", "setting", "new_custom_value")
+ s.set("name.space", "setting", "custom_value")
+ assert s.dirty == True
+ assert s.save_settings() == False
+ mo.assert_called_once()
+ assert s.dirty == False
+
def test_setting(mocker, m5stickv):
mo = mocker.mock_open()
@@ -101,103 +193,48 @@ class TestClass:
assert t.some_setting == 2
-def test_encryption_pbkdf2_setting(m5stickv, mocker):
- from krux.pages.login import Login
- from krux.input import BUTTON_ENTER, BUTTON_PAGE, BUTTON_PAGE_PREV
- from krux.krux_settings import Settings, NumberSetting
-
- cases = [
- ( # 0
- (
- BUTTON_PAGE,
- # Encryption
- BUTTON_ENTER,
- # pbkdf2
- BUTTON_ENTER,
- # go to "<" keypad position
- BUTTON_PAGE_PREV,
- BUTTON_PAGE_PREV,
- BUTTON_PAGE_PREV,
- # delete last 0
- BUTTON_ENTER,
- # go to "1" keypad
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- # Enter "1"
- BUTTON_ENTER,
- # go to "Go" keypad position
- BUTTON_PAGE_PREV,
- BUTTON_PAGE_PREV,
- # Go
- BUTTON_ENTER,
- # Leave Encryption settings
- BUTTON_PAGE_PREV,
- BUTTON_ENTER,
- # Leave Settings
- BUTTON_PAGE_PREV,
- BUTTON_PAGE_PREV,
- BUTTON_ENTER,
- ),
- # Assert iterations didn't change to 100,001 (not multiple of 10,000)
- lambda: Settings().encryption.pbkdf2_iterations == 100000,
- NumberSetting,
- ),
- ( # 0
- (
- BUTTON_PAGE,
- # Encryption
- BUTTON_ENTER,
- # pbkdf2
- BUTTON_ENTER,
- # go to "<" keypad position
- BUTTON_PAGE_PREV,
- BUTTON_PAGE_PREV,
- BUTTON_PAGE_PREV,
- # delete last 5 zeros
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- # go to "1" keypad
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- BUTTON_PAGE,
- # Enter "1"
- BUTTON_ENTER,
- # Go to Zero
- BUTTON_PAGE_PREV,
- # Enter 4 zeros
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- BUTTON_ENTER,
- # go to "Go" keypad position
- BUTTON_PAGE_PREV,
- # Go
- BUTTON_ENTER,
- # Leave Encryption settings
- BUTTON_PAGE_PREV,
- BUTTON_ENTER,
- # Leave Settings
- BUTTON_PAGE_PREV,
- BUTTON_PAGE_PREV,
- BUTTON_ENTER,
- ),
- # Assert iterations changed to a multiple of 10,000
- lambda: Settings().encryption.pbkdf2_iterations == 110000,
- NumberSetting,
- ),
- ]
- for case in cases:
- ctx = create_ctx(mocker, case[0])
- login = Login(ctx)
-
- login.settings()
-
- assert ctx.input.wait_for_button.call_count == len(case[0])
+def test_all_labels(mocker, m5stickv):
+ from krux.krux_settings import (
+ BitcoinSettings,
+ I18nSettings,
+ LoggingSettings,
+ EncryptionSettings,
+ PrinterSettings,
+ ThermalSettings,
+ AdafruitPrinterSettings,
+ CNCSettings,
+ GRBLSettings,
+ PersistSettings,
+ ThemeSettings,
+ TouchSettings,
+ EncoderSettings,
+ )
- assert case[1]()
+ bitcoin = BitcoinSettings()
+ i18n = I18nSettings()
+ logging = LoggingSettings()
+ encryption = EncryptionSettings()
+ printer = PrinterSettings()
+ thermal = ThermalSettings()
+ adafruit = AdafruitPrinterSettings()
+ cnc = CNCSettings()
+ gbrl = GRBLSettings()
+ persist = PersistSettings()
+ appearance = ThemeSettings()
+ touch = TouchSettings()
+ encoder = EncoderSettings()
+
+ assert bitcoin.label("network")
+ assert i18n.label("locale")
+ assert logging.label("level")
+ assert encryption.label("version")
+ assert printer.label("thermal")
+ assert thermal.label("adafruit")
+ assert adafruit.label("tx_pin")
+ assert cnc.label("invert")
+ assert gbrl.label("tx_pin")
+ assert persist.label("location")
+ assert appearance.label("theme")
+ assert appearance.label("screensaver_time")
+ assert touch.label("threshold")
+ assert encoder.label("debounce")
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index a97d3e589..2cd42d059 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -118,16 +118,16 @@ def tdata(mocker):
),
)
- UNAMBIGUOUS_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/0h/0h]xpub6DPMTPxGMqdtzMwpqT1dDQaVdyaEppEm2qYSaJ7ANsuES7HkNzrXJst1Ed8D7NAnijUdgSDUFgph1oj5LKKAD5gyxWNhNP2AuDqaKYqzphA/{0,1}/*)"
+ UNAMBIGUOUS_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/0h/0h]xpub6DPMTPxGMqdtzMwpqT1dDQaVdyaEppEm2qYSaJ7ANsuES7HkNzrXJst1Ed8D7NAnijUdgSDUFgph1oj5LKKAD5gyxWNhNP2AuDqaKYqzphA/<0;1>/*)"
AMBIGUOUS_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/0h/0h]xpub6DPMTPxGMqdtzMwpqT1dDQaVdyaEppEm2qYSaJ7ANsuES7HkNzrXJst1Ed8D7NAnijUdgSDUFgph1oj5LKKAD5gyxWNhNP2AuDqaKYqzphA)"
- UNAMBIGUOUS_MULTISIG_DESCRIPTOR = "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/{0,1}/*,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/{0,1}/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/{0,1}/*))"
+ UNAMBIGUOUS_MULTISIG_DESCRIPTOR = "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*))"
AMBIGUOUS_MULTISIG_DESCRIPTOR = "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv))"
UNRELATED_SINGLESIG_DESCRIPTOR = "wpkh([55f8fc5d/84h/0h/0h]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB)"
UNRELATED_MULTISIG_DESCRIPTOR = "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv))"
- UNSORTED_MULTISIG_DESCRIPTOR = "wsh(multi(2,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/{0,1}/*,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/{0,1}/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/{0,1}/*))"
+ UNSORTED_MULTISIG_DESCRIPTOR = "wsh(multi(2,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*))"
return namedtuple(
"TestData",
@@ -346,7 +346,7 @@ def test_load_multisig(mocker, m5stickv, tdata):
],
},
),
- # TODO: Fix
+ # TODO: Fix - ValueError: xpub not a cosigner
# (tdata.UR_OUTPUT_MULTISIG_WALLET_DATA, FORMAT_UR, tdata.UR_OUTPUT_MULTISIG_DESCRIPTOR, '2 of 3', {
# 'type': 'p2wsh', 'm': 2, 'n': 3, 'cosigners': [
# tdata.MULTISIG_KEY1.xpub(),
@@ -436,8 +436,7 @@ def test_load_singlesig_fails_with_multisig_descriptor(mocker, m5stickv, tdata):
cases = [
(tdata.SPECTER_MULTISIG_WALLET_DATA, FORMAT_PMOFN),
(tdata.BLUEWALLET_MULTISIG_WALLET_DATA, FORMAT_NONE),
- # TODO: Fix
- # (tdata.UR_OUTPUT_MULTISIG_WALLET_DATA, FORMAT_UR),
+ (tdata.UR_OUTPUT_MULTISIG_WALLET_DATA, FORMAT_UR),
(tdata.UR_BYTES_MULTISIG_WALLET_DATA, FORMAT_UR),
]
for case in cases:
@@ -537,7 +536,10 @@ def test_parse_wallet(mocker, m5stickv, tdata):
),
]
+ case_n = 1
for case in cases:
+ print(case_n)
+ case_n += 1
descriptor, label = parse_wallet(case[0], NETWORKS["main"])
assert descriptor.to_string() == case[1]
assert label == case[2]
diff --git a/tests/touchscreens/test_ft6x36.py b/tests/touchscreens/test_ft6x36.py
index 45df99fde..f2895458f 100644
--- a/tests/touchscreens/test_ft6x36.py
+++ b/tests/touchscreens/test_ft6x36.py
@@ -13,7 +13,7 @@ def test_point_read(mocker, m5stickv):
# four bytes
reg_point = [0x00, 0x09, 0x00, 0x09]
point = (0x09, 0x09)
- touch.i2c.readfrom_mem = mocker.MagicMock(side_effect=([0x01], reg_point))
+ mocker.patch("krux.i2c.i2c_bus.readfrom_mem",side_effect=([0x01], reg_point))
data = touch.current_point()
assert data == point
@@ -24,7 +24,7 @@ def test_gesture_discarded(mocker, m5stickv):
touch = FT6X36()
# four bytes
reg_point = [0x00, 0x09, 0x00, 0x09]
- touch.i2c.readfrom_mem = mocker.MagicMock(side_effect=([0x02], reg_point))
+ mocker.patch("krux.i2c.i2c_bus.readfrom_mem",side_effect=([0x02], reg_point))
data = touch.current_point()
assert data is None
@@ -35,6 +35,6 @@ def test_point_read_error(mocker, m5stickv):
touch = FT6X36()
# four bytes
e = Exception("mocked error")
- touch.i2c.readfrom_mem = mocker.MagicMock(side_effect=([0x01], e))
+ mocker.patch("krux.i2c.i2c_bus.readfrom_mem",side_effect=([0x01], e))
data = touch.current_point()
assert data == e
diff --git a/vendor/embit b/vendor/embit
index be13c6c97..605ca51e9 160000
--- a/vendor/embit
+++ b/vendor/embit
@@ -1 +1 @@
-Subproject commit be13c6c9789055f39f515389ab3fbf2e1c6beaf3
+Subproject commit 605ca51e96d28b1deed96f790a6ef67c15262adc
diff --git a/vendor/urtypes b/vendor/urtypes
index 8ff8e6ebe..9b84f87c2 160000
--- a/vendor/urtypes
+++ b/vendor/urtypes
@@ -1 +1 @@
-Subproject commit 8ff8e6ebe484d7a0f98ad73f4441708704998b43
+Subproject commit 9b84f87c2b127baea0d665ddcbe4f677ee4f45be