diff --git a/.ci/create-spec.py b/.ci/create-spec.py
new file mode 100755
index 0000000..384e2eb
--- /dev/null
+++ b/.ci/create-spec.py
@@ -0,0 +1,129 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2024 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.
+"""
+build.py
+"""
+from re import findall
+from os import listdir
+from os.path import join, isfile
+from pathlib import Path
+from platform import system
+import argparse
+import PyInstaller.building.makespec
+
+import sys
+sys.path.append(".")
+from seedqreader import VERSION
+
+if __name__ == "__main__":
+ p = argparse.ArgumentParser()
+ PyInstaller.building.makespec.__add_options(p)
+ PyInstaller.log.__add_options(p)
+
+ SYSTEM = system()
+
+ # build executable for following systems
+ if SYSTEM not in ("Linux", "Windows", "Darwin"):
+ raise OSError(f"OS '{system()}' not implemented")
+
+ # Get root path to properly setup
+ DIR = Path(__file__).parents
+ ROOT_PATH = Path(__file__).parent.parent.absolute()
+ PYNAME = "seedqreader"
+ PYFILE = f"{PYNAME}.py"
+ KFILE = str(ROOT_PATH / PYFILE)
+ ASSETS = str(ROOT_PATH / "assets")
+ ICON = join(ASSETS, "icon.png")
+ # I18NS = str(ROOT_PATH / "src" / "i18n")
+
+ BUILDER_ARGS = [ ]
+
+ # The app name
+ BUILDER_ARGS.append(f"--name={PYNAME}_{VERSION}")
+
+ # The application has window
+ BUILDER_ARGS.append("--windowed")
+
+ # Icon
+ BUILDER_ARGS.append(f"--icon={ICON}")
+
+ # Specifics about operational system
+ # on how will behave as file or bundled app
+ if SYSTEM == "Linux":
+ # Tha application is a GUI
+ BUILDER_ARGS.append("--onefile")
+
+ elif SYSTEM == "Windows":
+ # Tha application is a GUI with a hidden console
+ # to keep `sys` module enabled (necessary for Kboot)
+ BUILDER_ARGS.append("--onefile")
+ BUILDER_ARGS.append("--console")
+ BUILDER_ARGS.append("--hidden-import=win32timezone")
+ BUILDER_ARGS.append("--hide-console=minimize-early")
+ BUILDER_ARGS.append("--add-binary=assets/libiconv.dll:.")
+ BUILDER_ARGS.append("--add-binary=assets/libzbar-64.dll:.")
+
+ elif SYSTEM == "Darwin":
+ # Tha application is a GUI in a bundled .app
+ BUILDER_ARGS.append("--onefile")
+ BUILDER_ARGS.append("--noconsole")
+
+ # For darwin system, will be necessary
+ # to add a hidden import for ssl
+ # (necessary for request module)
+ BUILDER_ARGS.append("--hidden-import=ssl")
+ BUILDER_ARGS.append("--hidden-import=pillow")
+ BUILDER_ARGS.append("--optimize=2")
+
+ # Necessary for get version and
+ # another infos in application
+ # BUILDER_ARGS.append("--add-data=pyproject.toml:.")
+ BUILDER_ARGS.append("--add-data=form.ui:.")
+
+ # some assets
+ for f in listdir(ASSETS):
+ asset = join(ASSETS, f)
+ if isfile(asset):
+ if asset.endswith("png") or asset.endswith("gif") or asset.endswith("ttf"):
+ BUILDER_ARGS.append(f"--add-data={asset}:assets")
+
+ # Add i18n translations
+ # for f in listdir(I18NS):
+ # i18n_abs = join(I18NS, f)
+ # i18n_rel = join("src", "i18n")
+ # if isfile(i18n_abs):
+ # if findall(r"^[a-z]+\_[A-Z]+\.UTF-8\.json$", f):
+ # BUILDER_ARGS.append(f"--add-data={i18n_abs}:{i18n_rel}")
+
+
+ args = p.parse_args(BUILDER_ARGS)
+
+ # Now generate spec
+ print("============================")
+ print("create-spec.py")
+ print("============================")
+ print()
+ for k, v in vars(args).items():
+ print(f"{k}: {v}")
+
+ print()
+ PyInstaller.building.makespec.main([PYFILE], **vars(args))
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index e87e33c..3cfacc8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,9 @@
__pycache__/
venv/
config
+.seedqrenv/
+.venv/
+.env/
+build/
+*.spec
+dist/
\ No newline at end of file
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
index 04bd362..06f67be
--- a/README.md
+++ b/README.md
@@ -1,34 +1,83 @@
-SeedQReader
----
+# SeedQReader
-SeedQReader is a simple tool made for communicate with airgapped Bitcoin Signer.
+
+
+**SeedQReader** is a simple tool made for communicate with airgapped Bitcoin Signer.

It actually can send/receive:
- 1 Frame QRCodes
-- Multiframes QRCodes using the `Specter` format (_of_)
+- Multiframes QRCodes using the `Specter` format (_p M of N_)
- Multiframes QRCodes using the `UR` format are partially supported (PSBT and Bytes)
+- Multiframes QRCodes using the `BBQR` format are partially supported (PSBT)
+
+## Download the latest releases
+[
](https://github.com/tadeubas/seedQReader/releases)
-Dependencies:
-opencv might be installed
+## Install
-Install:
-Go into this repo and run:
+To install on **Linux**, enter the repo folder and run:
```
+# create environment to install dependencies
+python3 -m venv .seedqrenv
+
+# activate the environment on the current terminal
+source .seedqrenv/bin/activate
+
+# install python dependencies on this environment
pip install -r requirements.txt
```
-Run under Linux/MacOS:
+If you get this error on **Linux**, please install libxcb-cursor:
+```
+# qt.qpa.plugin: From 6.5.0, xcb-cursor0 or libxcb-cursor0 is needed to load the Qt xcb platform plugin.
+sudo apt install libxcb-cursor0
+```
+
+To install on **Windows**:
+```
+# create environment to install dependencies
+python -m venv .seedqrenv
+
+# activate the environment on the current cmd
+.seedqrenv\Scripts\activate
+
+# install python dependencies on this environment
+pip install -r requirements.txt
+```
+
+If you get this error on **Windows**, install `vcredist_x64.exe` [Visual C++ Redistributable Packages for Visual Studio 2013](https://www.microsoft.com/en-US/download/details.aspx?id=40784). Then uninstall and install `pyzbar` lib again:
+```
+FileNotFoundError: Could not find module 'libiconv.dll' (or one of its dependencies). Try using the full path with constructor syntax.
+pyimod03_ctypes.install..PyInstallerImportError: Failed to load dynlib/dll 'libiconv.dll'. Most likely this dynlib/dll was not found when the application was frozen.
+[PYI-5780:ERROR] Failed to execute script 'seedqreader' due to unhandled exception!
+```
+
+## Run
+
+**Linux/MacOS**:
```
python3 seedqreader.py
```
-Run under Windows:
+**Windows**:
```
python seedqreader.py
```
-If you want i build more cool tools you can support me with bitcoin:
-`bc1q5pgfrt09f4vuxyryg95erge38nw94usvpe5gg0`
+## Build binaries
+
+```
+pip install PyInstaller
+rm seedqreader_*.spec
+python3 .ci/create-spec.py
+python3 -m PyInstaller seedqreader_*.spec
+```
+
+## Acknowledgements & Alternatives
+
+Big thanks to [pythcoiner](https://github.com/pythcoiner) for originally creating SeedQReader! π
+
+If SeedQReader isnβt your vibe, check out [bitcoin-qr-tools](https://github.com/andreasgriffin/bitcoin-qr-tools) for another awesome QR code tool for BTC stuff. π
diff --git a/assets/badge_github.png b/assets/badge_github.png
new file mode 100755
index 0000000..326d254
Binary files /dev/null and b/assets/badge_github.png differ
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 0000000..1f6f899
Binary files /dev/null and b/assets/icon.png differ
diff --git a/assets/libiconv.dll b/assets/libiconv.dll
new file mode 100755
index 0000000..a31664d
Binary files /dev/null and b/assets/libiconv.dll differ
diff --git a/assets/libzbar-64.dll b/assets/libzbar-64.dll
new file mode 100755
index 0000000..5353d43
Binary files /dev/null and b/assets/libzbar-64.dll differ
diff --git a/bbqr.py b/bbqr.py
new file mode 100644
index 0000000..8c07315
--- /dev/null
+++ b/bbqr.py
@@ -0,0 +1,323 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2024 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 code an adaptation of Coinkite's BBQr python implementation for Krux environment
+# https://github.com/coinkite/BBQr
+
+import gc
+
+# BBQR
+KNOWN_ENCODINGS = {"H", "2", "Z"}
+
+# File types
+# P='PSBT', T='Transaction', J='JSON', C='CBOR'
+# U='Unicode Text', X='Executable', B='Binary'
+KNOWN_FILETYPES = {"P", "T", "J", "U"}
+
+BBQR_ALWAYS_COMPRESS_THRESHOLD = 5000 # bytes
+
+B32CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
+assert len(B32CHARS) == 32
+
+BBQR_PREFIX_LENGTH = 8
+
+QR_CAPACITY_ALPHANUMERIC = [
+ 25,
+ 47,
+ 77,
+ 114,
+ 154,
+ 195,
+ 224,
+ 279,
+ 335,
+ 395,
+ 468,
+ 535,
+ 619,
+ 667,
+ 758,
+ 854,
+ 938,
+ 1046,
+ 1153,
+ 1249,
+]
+
+class BBQrCode:
+ """A BBQr code, containing the data, encoding, and file type"""
+
+
+ def __init__(self, payload, encoding=None, file_type=None):
+ """Initializes the BBQr code with the given data, encoding, and file type"""
+
+ if encoding not in KNOWN_ENCODINGS:
+ raise ValueError("Invalid BBQr encoding")
+ if file_type not in KNOWN_FILETYPES:
+ raise ValueError("Invalid BBQr file type")
+ self.payload = payload
+ self.encoding = encoding
+ self.file_type = file_type
+
+ def find_min_num_parts(self, max_width):
+ qr_capacity = self.max_qr_bytes(max_width)
+ data_length = len(self.payload)
+ max_part_size = qr_capacity - BBQR_PREFIX_LENGTH
+ if data_length < max_part_size:
+ return 1, data_length
+ # Round max_part_size to the nearest lower multiple of 8
+ max_part_size = (max_part_size // 8) * 8
+ # Calculate the number of parts required (rounded up)
+ num_parts = (data_length + max_part_size - 1) // max_part_size
+ # Calculate the optimal part size
+ part_size = data_length // num_parts
+ # Round to the nearest higher multiple of 8
+ part_size = ((part_size + 7) // 8) * 8
+ # Check if the part size is within the limits
+ if part_size > max_part_size:
+ num_parts += 1
+ part_size = data_length // num_parts
+ # Round to the nearest higher multiple of 8 again
+ part_size = ((part_size + 7) // 8) * 8
+ return num_parts, part_size
+
+ def max_qr_bytes(self, 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
+ capacity_list = QR_CAPACITY_ALPHANUMERIC
+
+ try:
+ return capacity_list[qr_version - 1]
+ except:
+ # Limited to version 20
+ return capacity_list[-1]
+
+ def to_qr_code(self, max_width):
+ num_parts, part_size = self.find_min_num_parts(max_width)
+ part_index = 0
+ while True:
+ header = "B$%s%s%s%s" % (
+ self.encoding,
+ self.file_type,
+ int2base36(num_parts),
+ int2base36(part_index),
+ )
+ part = None
+ if part_index == num_parts - 1:
+ part = header + self.payload[part_index * part_size :]
+ part_index = 0
+ else:
+ part = (
+ header
+ + self.payload[
+ part_index * part_size : (part_index + 1) * part_size
+ ]
+ )
+ part_index += 1
+ yield (part, num_parts)
+
+ def parse(self, data):
+ part, index, total = parse_bbqr(data)
+ self.parts[index] = part
+ self.total = total
+ return index
+
+
+
+def parse_bbqr(data):
+ """
+ Parses the QR as a BBQR part, extracting the part's content,
+ encoding, file format, index, and total
+ """
+ if len(data) < 8:
+ raise ValueError("Invalid BBQR format")
+
+ encoding = data[2]
+ if encoding not in KNOWN_ENCODINGS:
+ raise ValueError("Invalid encoding")
+
+ file_type = data[3]
+ if file_type not in KNOWN_FILETYPES:
+ raise ValueError("Invalid file type")
+
+ try:
+ part_total = int(data[4:6], 36)
+ part_index = int(data[6:8], 36)
+ except ValueError:
+ raise ValueError("Invalid BBQR format")
+
+ if part_index >= part_total:
+ raise ValueError("Invalid part index")
+
+ return data[8:], part_index, part_total
+
+
+def deflate_compress(data):
+ """Compresses the given data using deflate module"""
+ try:
+ import deflate
+ from io import BytesIO
+
+ stream = BytesIO()
+ with deflate.DeflateIO(stream) as d:
+ d.write(data)
+ return stream.getvalue()
+ except Exception as e:
+ print(e)
+ raise ValueError("Error compressing BBQR")
+
+
+def deflate_decompress(data):
+ """Decompresses the given data using deflate module"""
+ try:
+ import deflate
+ from io import BytesIO
+
+ with deflate.DeflateIO(BytesIO(data)) as d:
+ return d.read()
+ except:
+ raise ValueError("Error decompressing BBQR")
+
+
+def decode_bbqr(parts, encoding, file_type):
+ """Decodes the given data as BBQR, returning the decoded data"""
+
+ if encoding == "H":
+ from binascii import unhexlify
+
+ data_bytes = bytearray()
+ for _, part in sorted(parts.items()):
+ data_bytes.extend(unhexlify(part))
+ return bytes(data_bytes)
+
+ binary_data = b""
+ for _, part in sorted(parts.items()):
+ padding = (8 - (len(part) % 8)) % 8
+ padded_part = part + (padding * "=")
+ binary_data += base32_decode_stream(padded_part)
+
+ if encoding == "Z":
+ if file_type in "JU":
+ return deflate_decompress(binary_data).decode("utf-8")
+ return deflate_decompress(binary_data)
+ if file_type in "JU":
+ return binary_data.decode("utf-8")
+ return binary_data
+
+
+def encode_bbqr(data, encoding="Z", file_type="P"):
+ """Encodes the given data as BBQR, returning the encoded data and format"""
+
+ if encoding == "H":
+ from binascii import hexlify
+
+ data = hexlify(data).decode()
+ return BBQrCode(data.upper(), encoding, file_type)
+
+ if encoding == "Z":
+ if len(data) > BBQR_ALWAYS_COMPRESS_THRESHOLD:
+ # RAM won't be enough to have both compressed and not compressed data
+ # It will always be beneficial to compress large data
+ data = deflate_compress(data)
+ else:
+ # Check if compression is beneficial
+ cmp = deflate_compress(data)
+ if len(cmp) >= len(data):
+ encoding = "2"
+ else:
+ encoding = "Z"
+ data = cmp
+
+ data = data.encode("utf-8") if isinstance(data, str) else data
+ gc.collect()
+ return BBQrCode("".join(base32_encode_stream(data)), encoding, file_type)
+
+
+# Base 32 encoding/decoding, used in BBQR only
+
+
+def base32_decode_stream(encoded_str):
+ """Decodes a Base32 string"""
+ base32_index = {ch: index for index, ch in enumerate(B32CHARS)}
+
+ # Strip padding
+ encoded_str = encoded_str.rstrip("=")
+
+ buffer = 0
+ bits_left = 0
+ decoded_bytes = bytearray()
+
+ for char in encoded_str:
+ if char not in base32_index:
+ raise ValueError("Invalid Base32 character: %s" % char)
+ index = base32_index[char]
+ buffer = (buffer << 5) | index
+ bits_left += 5
+
+ while bits_left >= 8:
+ bits_left -= 8
+ decoded_bytes.append((buffer >> bits_left) & 0xFF)
+ buffer &= (1 << bits_left) - 1 # Keep only the remaining bits
+
+ return bytes(decoded_bytes)
+
+
+def base32_encode_stream(data, add_padding=False):
+ """A streaming base32 encoder"""
+ buffer = 0
+ bits_left = 0
+
+ for byte in data:
+ buffer = (buffer << 8) | byte
+ bits_left += 8
+
+ while bits_left >= 5:
+ bits_left -= 5
+ yield B32CHARS[(buffer >> bits_left) & 0x1F]
+ buffer &= (1 << bits_left) - 1 # Keep only the remaining bits
+
+ if bits_left > 0:
+ buffer <<= 5 - bits_left
+ yield B32CHARS[buffer & 0x1F]
+
+ # Padding
+ if add_padding:
+ encoded_length = (len(data) * 8 + 4) // 5
+ padding_length = (8 - (encoded_length % 8)) % 8
+ for _ in range(padding_length):
+ yield "="
+
+
+def int2base36(n):
+ """Convert integer n to a base36 string."""
+ if not 0 <= n <= 1295: # ensure the number is within the valid range
+ raise ValueError("Number out of range")
+
+ def tostr(x):
+ """Convert integer x to a base36 character."""
+ return chr(48 + x) if x < 10 else chr(65 + x - 10)
+
+ quotient, remainder = divmod(n, 36)
+ return tostr(quotient) + tostr(remainder)
+
diff --git a/deflate.py b/deflate.py
new file mode 100644
index 0000000..4015314
--- /dev/null
+++ b/deflate.py
@@ -0,0 +1,35 @@
+import sys
+import zlib
+
+
+class DeflateIO:
+
+ def __init__(self, stream) -> None:
+ self.stream = stream
+ self.data = stream.read()
+
+ def read(self):
+ return zlib.decompress(self.data, wbits=-10)
+
+ def write(self, input_data):
+ compressor = zlib.compressobj(wbits=-10)
+ compressed_data = compressor.compress(input_data)
+ compressed_data += compressor.flush()
+ self.stream.seek(0) # Ensure we overwrite the stream from the beginning
+ self.stream.write(compressed_data)
+ self.stream.truncate() # Remove any remaining part of the old data
+
+ def __enter__(self):
+ # Return the instance itself when entering the context
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ # Handle cleanup here if necessary
+ pass
+
+
+class Deflate:
+ DeflateIO = DeflateIO
+
+if "deflate" not in sys.modules:
+ sys.modules["deflate"] = Deflate
diff --git a/form.ui b/form.ui
index cad1542..0bba286 100644
--- a/form.ui
+++ b/form.ui
@@ -1,448 +1,699 @@
-
-
-
-
- 0
- 0
- 811
- 650
-
-
-
+ centralwidget
+
+
+
+
+ 0
+ 0
+ 812
+ 660
+
+
+
+ Read
+
+
+ 0
+
+
+
Read
-
-
- 0
-
-
-
- Read
-
-
-
-
- 190
- 10
- 400
- 300
-
-
-
-
-
-
-
-
-
- 10
- 350
- 771
- 201
-
-
-
- QPlainTextEdit::WidgetWidth
-
-
- true
-
-
-
-
-
- 190
- 320
- 401
- 20
-
-
-
- 0
-
-
-
-
-
-
-
-
- 10
- 320
- 161
- 27
-
-
-
- Start Read
-
-
-
-
-
- 10
- 30
- 111
- 27
-
-
-
-
-
-
- 20
- 10
- 91
- 17
-
-
-
- Camera:
-
-
-
-
-
- 90
- 3
- 31
- 27
-
-
-
-
-
-
+
+
+
+
+ 190
+ 10
+ 400
+ 300
+
+
+
+
+
-
-
- Send
-
-
-
-
- 10
- 10
- 781
- 81
-
-
-
-
-
-
- 250
- 140
- 450
- 450
-
-
-
-
-
-
-
-
-
- 10
- 100
- 97
- 27
-
-
-
- Generate
-
-
-
-
-
- 380
- 100
- 291
- 28
-
-
-
- 10
-
-
- 500
-
-
- 10
-
-
- 100
-
-
- Qt::Horizontal
-
-
- 15
-
-
-
-
-
- 690
- 100
- 96
- 22
-
-
-
- No split
-
-
-
-
-
- 120
- 100
- 71
- 27
-
-
-
- Clear
-
-
-
-
-
- 20
- 400
- 116
- 22
-
-
-
- Key 1
-
-
-
-
-
- 20
- 420
- 116
- 22
-
-
-
- Key 2
-
-
-
-
-
- 20
- 440
- 116
- 22
-
-
-
- Key 3
-
-
-
-
-
- 20
- 460
- 116
- 22
-
-
-
- Key 4
-
-
-
-
-
- 20
- 480
- 116
- 22
-
-
-
- Key 5
-
-
-
-
-
- 200
- 100
- 71
- 27
-
-
-
- Save
-
-
-
-
-
- 20
- 220
- 116
- 22
-
-
-
- Descriptor 1
-
-
- true
-
-
-
-
-
- 20
- 260
- 116
- 22
-
-
-
- Descriptor 3
-
-
-
-
-
- 20
- 290
- 116
- 22
-
-
-
- PSBT 1
-
-
-
-
-
- 20
- 240
- 116
- 22
-
-
-
- Descriptor 2
-
-
-
-
-
- 20
- 310
- 116
- 22
-
-
-
- PSBT 2
-
-
-
-
-
- 20
- 330
- 116
- 22
-
-
-
- PSBT 3
-
-
-
-
-
- 20
- 350
- 116
- 22
-
-
-
- PSBT 4
-
-
-
-
-
- 20
- 370
- 116
- 22
-
-
-
- PSBT 5
-
-
-
-
-
- 10
- 180
- 111
- 27
-
-
-
-
-
-
- 10
- 140
- 111
- 27
-
-
-
-
+
+
+
+ 10
+ 350
+ 771
+ 201
+
+
+
+ QPlainTextEdit::WidgetWidth
+
+
+ true
+
+
+
+
+
+ 190
+ 320
+ 401
+ 20
+
+
+
+ 0
+
+
+
+
+
+
+
+
+ 10
+ 20
+ 154
+ 94
+
+
+
+ -
+
+
+
+ 11
+ true
+
+
+
+ Camera:
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ π
+
+
+
+
+
+ -
+
+
+ π· Camera Scan
+
+
+
+
+
+
+
+
+ 10
+ 150
+ 154
+ 94
+
+
+
+ -
+
+
+
+ 11
+ true
+
+
+
+ Monitor:
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ π
+
+
+
+
+
+ -
+
+
+ π₯οΈ Screen Scan
+
+
+
+
+
+
+
+
+ Send
+
+
+
+
+ 250
+ 140
+ 450
+ 450
+
+
+
+
+
+
+
+
+
+ 10
+ 180
+ 111
+ 27
+
+
+
+
+
+
+ 10
+ 140
+ 111
+ 27
+
+
+
+
+
+
+ 440
+ 590
+ 67
+ 17
+
+
+
+
+
+
+
+
+
+ 10
+ 220
+ 121
+ 400
+
+
+
+ -
+
+
+
+ 12
+ true
+
+
+
+ Selected slot:
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Qt::LeftToRight
+
+
+ Descriptor 1
+
+
+ true
+
+
+
+ -
+
+
+ Descriptor 2
+
+
+
+ -
+
+
+ Descriptor 3
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ PSBT 1
+
+
+
+ -
+
+
+ PSBT 2
+
+
+
+ -
+
+
+ PSBT 3
+
+
+
+ -
+
+
+ PSBT 4
+
+
+
+ -
+
+
+ PSBT 5
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Key 1
+
+
+
+ -
+
+
+ Key 2
+
+
+
+ -
+
+
+ Key 3
+
+
+
+ -
+
+
+ Key 4
+
+
+
+ -
+
+
+ Key 5
+
+
+
+
+
+
+
+
+ 10
+ 10
+ 781
+ 88
+
+
+
+ -
+
+
+
+
+
+ Type the QR code content here, then click Generate QR
+
+
+
+ -
+
+
-
+
+
+ Generate QR
+
+
+
+ -
+
+
+ Cleart text
+
+
+
+ -
+
+
+ Save on selected slot
+
+
+
+
+
+
+
+
+
+
+ 280
+ 100
+ 511
+ 31
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 80
+ 0
+
+
+
+ true
+
+
+
+
+
- 440
- 590
- 67
- 17
+ 0
+ 0
+ 511
+ 32
-
-
-
+
+
+ 6
+
+ -
+
+
+ 10
+
+
+ 500
+
+
+ 10
+
+
+ 10
+
+
+ 100
+
+
+ Qt::Horizontal
+
+
+ 15
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 0
+
+
+
+ QR split size:
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 10
+ 0
+
+
+
+ :disabled { background-color: darkgray; }
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 50
+ 0
+
+
+
+ No split
+
+
+ 6
+
+
+
+
+
+
-
+
+
+
+
+ 10
+ 100
+ 261
+ 31
+
+
+
+ true
+
+
+
+
+
- 284
- 104
- 91
- 17
+ 0
+ 0
+ 261
+ 31
-
- Split size:
-
+
+ -
+
+
+ 100
+
+
+ 4000
+
+
+ 100
+
+
+ 100
+
+
+ 400
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+
+ 81
+ 0
+
+
+
+ QR delay:
+
+
+
+
-
+
diff --git a/foundation/fountain_decoder.py b/foundation/fountain_decoder.py
index c0b3f87..d9d5460 100644
--- a/foundation/fountain_decoder.py
+++ b/foundation/fountain_decoder.py
@@ -76,8 +76,8 @@ def estimated_percent_complete(self):
return 1
if self.expected_part_indexes == None:
return 0
- estimated_input_parts = self.expected_part_count() * 1.75
- return min(0.99, self.processed_parts_count / estimated_input_parts)
+ estimated_input_parts = self.expected_part_count() * .7
+ return min(0.99, len(self.received_part_indexes) / estimated_input_parts)
def receive_part(self, encoder_part):
# Don't process the part if we're already done
diff --git a/qr_type.py b/qr_type.py
index 6519691..e928d85 100644
--- a/qr_type.py
+++ b/qr_type.py
@@ -1,4 +1,4 @@
SPECTER = "specter"
UR = "ur"
-
+BBQR = "bbqr"
diff --git a/requirements.txt b/requirements.txt
index 7314bb3..123ec31 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,12 +1,10 @@
-embit==0.7.0
-numpy==1.26.4
-opencv-python==4.7.0.72
-Pillow==9.5.0
-pypng==0.20220715.0
-PyYAML==6.0.1
+PyYAML==6.0.2
+PySide6==6.9.0
+Pillow==11.2.1
pyzbar==0.1.9
-qrcode==7.4.2
-typing_extensions==4.11.0
-PySide6==6.6.3.1
-shiboken6==6.6.3.1
-urtypes @ git+https://github.com/selfcustody/urtypes.git@7fb280eab3b3563dfc57d2733b0bf5cbc0a96a6a
+qrcode==8.1
+opencv-python==4.11.0.86
+urtypes==1.0.0
+embit==0.8.0
+mss==10.0.0
+numpy==2.2.6
diff --git a/screenshot.png b/screenshot.png
index d155d60..8e755bd 100644
Binary files a/screenshot.png and b/screenshot.png differ
diff --git a/seedqreader.py b/seedqreader.py
index ac77883..13404a2 100644
--- a/seedqreader.py
+++ b/seedqreader.py
@@ -10,7 +10,7 @@
from yaml.loader import SafeLoader as Loader
from PySide6.QtWidgets import QApplication, QMainWindow
-from PySide6.QtGui import QImage, QPixmap, QPalette, QColor
+from PySide6.QtGui import QImage, QPixmap, QPalette, QColor, QColorConstants, QIcon
from PySide6.QtCore import Qt, QFile, QThread, Signal
from PySide6.QtUiTools import QUiLoader
from PySide6.QtGui import QTextOption
@@ -35,10 +35,29 @@
from embit.psbt import PSBT
+from mss import mss
+import numpy as np
+
+VERSION="1.1.0"
+
MAX_LEN = 100
-QR_DELAY = 400
FILL_COLOR = "#434343"
+STOP_QR_TXT = 'Remove QR'
+STOP_READ_TXT = ' Stop'
+START_READ_TXT = ' Scan'
+GENERATE_TXT = 'Generate QR'
+
+ANIMATED_QR_FIRST_FRAME_DELAY = 900 #ms
+
+FORMAT_UR = 'UR'
+FORMAT_SPECTER = 'Specter'
+FORMAT_BBQR = 'BBQR'
+
+# helper obj to handle bbqr encoding and file_type
+bbqr_obj = None
+
+
def to_str(bin_):
return bin_.decode('utf-8')
@@ -74,7 +93,7 @@ class MultiQRCode(QRCode):
encoder = None
def step(self):
- if self.qr_type == qr_type.SPECTER:
+ if self.qr_type in (qr_type.SPECTER, qr_type.BBQR):
self.total_sequences = len(self.data_stack)
return f"{self.current + 1}/{self.total_sequences}"
@@ -89,6 +108,42 @@ def append(self, data: tuple):
elif self.qr_type == qr_type.UR:
self.append_ur(data)
+ elif self.qr_type == qr_type.BBQR:
+ self.append_bbqr(data)
+
+ def append_bbqr(self, data: tuple):
+ data, sequence, total_sequences = data
+
+ if not self.is_init:
+ self.data_init(total_sequences)
+ self.is_init = True
+
+ if not self.data_stack[sequence]:
+ self.data_stack[sequence] = data
+ else:
+ if data != self.data_stack[sequence]:
+ raise ValueError('Same sequences have different data!')
+ self.check_complete_bbrq()
+
+ def check_complete_bbrq(self):
+ global bbqr_obj
+
+ fill_sequences = 0
+ for i in self.data_stack:
+ if i:
+ fill_sequences += 1
+
+ self.sequences_count = fill_sequences
+
+ if fill_sequences == self.total_sequences:
+ from bbqr import decode_bbqr
+ my_dict = {}
+ for i, val in enumerate(self.data_stack):
+ my_dict[i] = val
+ self.data = decode_bbqr(my_dict, bbqr_obj.encoding, bbqr_obj.file_type)
+ self.is_completed = True
+
+
def append_specter(self, data: tuple):
# print(f'MultiQRCode.append({data})')
sequence = data[0]
@@ -169,20 +224,22 @@ def check_complete_ur(self):
print(self.decoder.result_error())
@staticmethod
- def from_string(data, max=MAX_LEN, type=None, format=None):
-
- if (max and len(data) > max) or format == 'UR':
+ def from_string(data, _max=MAX_LEN, type=None, format=None):
+ if (_max and len(data) > _max) or format == FORMAT_UR:
out = MultiQRCode()
out.data = data
- if format == 'UR':
+
+ if format == FORMAT_UR:
out.qr_type = qr_type.UR
- elif format == 'Specter':
+ elif format == FORMAT_SPECTER:
out.qr_type = qr_type.SPECTER
+ elif format == FORMAT_BBQR:
+ out.qr_type = qr_type.BBQR
- if format == 'Specter':
- while len(data) > max:
- sequence = data[:max]
- data = data[max:]
+ if format == FORMAT_SPECTER:
+ while len(data) > _max:
+ sequence = data[:_max]
+ data = data[_max:]
out.data_stack.append(sequence)
if len(data):
out.data_stack.append(data)
@@ -191,7 +248,34 @@ def from_string(data, max=MAX_LEN, type=None, format=None):
out.sequences_count = out.total_sequences
out.is_completed = True
- elif format == 'UR':
+ elif format == FORMAT_BBQR:
+ from bbqr import encode_bbqr
+
+ data_bytes = bytes(data, "utf-8")
+
+ bb = encode_bbqr(data_bytes)
+
+ # adjust BBQR size from 10-500 to 23-200
+ old_min, old_max = 10, 500
+ new_min, new_max = 23, 100
+
+ scaled_value = new_min + ((_max - old_min) * (new_max - new_min)) / (old_max - old_min)
+ _max = int(round(scaled_value))
+
+ count = 1
+ for sequence, total in bb.to_qr_code(_max):
+ out.data_stack.append(sequence)
+ count += 1
+ if count > total:
+ break
+ out.total_sequences = total
+ out.sequences_count = out.total_sequences
+ out.is_completed = True
+
+ if total == 1:
+ out.data = sequence
+
+ elif format == FORMAT_UR:
_UR = None
if type == 'PSBT':
out.data_type = 'crypto-psbt'
@@ -201,7 +285,6 @@ def from_string(data, max=MAX_LEN, type=None, format=None):
out.data_type = 'bytes'
_UR = Bytes
elif type == 'Key':
- print("key")
out.data_type = 'bytes'
_UR = Bytes
elif type == 'Bytes':
@@ -209,12 +292,11 @@ def from_string(data, max=MAX_LEN, type=None, format=None):
_UR = Bytes
else:
return
- if not max:
- max = 100000
+ if not _max:
+ _max = 100000
ur = UR(out.data_type, _UR(data).to_cbor())
- out.encoder = UREncoder(ur, max)
+ out.encoder = UREncoder(ur, _max)
out.total_sequences = out.encoder.fountain_encoder.seq_len()
-
else:
out = QRCode()
out.data = data
@@ -223,30 +305,31 @@ def from_string(data, max=MAX_LEN, type=None, format=None):
return out
def next(self) -> str:
+ data = None
if self.qr_type == qr_type.SPECTER:
- self.current += 1
- if self.current >= self.total_sequences:
- self.current = 0
-
data = self.data_stack[self.current]
digit_a = self.current + 1
digit_b = self.total_sequences
data = f"p{digit_a}of{digit_b} {data}"
- print(data)
-
- return data
+ self.current += 1
+ if self.current >= self.total_sequences:
+ self.current = 0
elif self.qr_type == qr_type.UR:
self.current = self.encoder.fountain_encoder.seq_num
data = self.encoder.next_part().upper()
- print(data)
- return data
+ elif self.qr_type == qr_type.BBQR:
+ data = self.data_stack[self.current]
+ self.current += 1
+ if self.current >= self.total_sequences:
+ self.current = 0
+
+ return data
class ReadQR(QThread):
-
data = Signal(object)
video_stream = Signal(object)
@@ -257,29 +340,83 @@ def __init__(self, parent):
self.qr_data: QRCode | MultiQRCode = None
self.capture = None
self.end = False
+ self.viaCamera = True
def run(self):
+ from PIL import Image
self.qr_data: QRCode | MultiQRCode = None
- # Initialize the camera
- camera_id = self.parent.get_camera_id()
- if camera_id is None:
- return
- self.capture = cv2.VideoCapture(camera_id)
+ if self.viaCamera:
+ # Initialize the camera
+ camera_id = self.parent.get_camera_id()
+
+ if camera_id is None:
+ return
+
+ self.capture = cv2.VideoCapture(camera_id)
+
+ self.parent.ui.btn_start_read.setText(' '.join(self.parent.ui.btn_start_read.text().split(' ')[:-1]) + STOP_READ_TXT)
+ self.parent.ui.monitor_group.setDisabled(True)
+ else:
+ # Initialize the monitor
+ monitor_id = self.parent.get_monitor_id()
+
+ if monitor_id is None:
+ return
+ else:
+ monitor_id += 1
+
+ self.parent.ui.btn_start_read_monitor.setText(' '.join(self.parent.ui.btn_start_read_monitor.text().split(' ')[:-1]) + STOP_READ_TXT)
+ self.parent.ui.camera_group.setDisabled(True)
+
- self.parent.ui.btn_start_read.setText('Stop')
while not self.end:
self.msleep(30)
- ret, frame = self.capture.read()
+ if self.viaCamera:
+ ret, frame = self.capture.read()
+ else:
+ ret = True
+ with mss() as sct:
+ # Get a screenshot of the monitor
+ monitor = sct.monitors[monitor_id]
+ width = monitor['width']
+ height = monitor['height']
+ screenshot = sct.grab(sct.monitors[monitor_id])
if ret:
- # Convert the frame to RGB format
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
+ if self.viaCamera:
+ # Convert the frame to RGB format
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
- # Create a QImage from the frame data
- height, width, channel = frame.shape
- image = QImage(frame.data, width, height, QImage.Format_RGB888)
+ # Create a QImage from the frame data
+ height, width, _ = frame.shape
+ image = QImage(frame.data, width, height, QImage.Format_RGB888)
+ else:
+ # Convert to numpy array (BGRA format)
+ img_data = np.frombuffer(screenshot.rgb, dtype=np.uint8)
+ frame = img_data.reshape((screenshot.height, screenshot.width, 3))
+
+ # Add an alpha channel to convert RGB to RGBA
+ alpha_channel = np.full((height, width, 1), 255, dtype=np.uint8) # Fully opaque
+ img_data = np.concatenate([frame, alpha_channel], axis=2) # Append alpha
+
+ # Convert RGB to RGBA (ensure correct channel order for QImage)
+ img_data = img_data[:, :, [0, 1, 2, 3]] # Already in correct order, but explicit for clarity
+
+ img_data = np.ascontiguousarray(img_data)
+
+ # Create QImage from the data
+ image = QImage(
+ img_data.data,
+ screenshot.width,
+ screenshot.height,
+ screenshot.width * 4, # Bytes per line
+ QImage.Format_RGBA8888
+ )
+
+ # Ensure the data is not garbage-collected
+ image.ndarray = img_data
# Create a QPixmap from the QImage
pixmap = QPixmap.fromImage(image)
@@ -309,8 +446,7 @@ def run(self):
return
def decode(self, data):
-
- # Multipart QR Code case
+ '''Multipart QR Code case'''
# specter format
if re.match(r'^p\d+of\d+\s', data, re.IGNORECASE):
@@ -331,7 +467,8 @@ def decode(self, data):
self.parent.ui.read_progress.setValue(progress)
self.parent.ui.read_progress.setFormat(f"{self.qr_data.sequences_count}/{self.qr_data.total_sequences}")
self.parent.ui.read_progress.setVisible(True)
-
+
+ # UR format
elif re.match(r'^UR:', data, re.IGNORECASE):
if not self.qr_data:
@@ -341,87 +478,116 @@ def decode(self, data):
self.qr_data.append(data)
progress = self.qr_data.decoder.estimated_percent_complete() * 100
- self.qr_data.total_sequences = self.qr_data.decoder.expected_part_count()
- self.qr_data.sequences_count = self.qr_data.decoder.processed_parts_count()
+ try:
+ self.qr_data.total_sequences = self.qr_data.decoder.expected_part_count()
+ except:
+ self.qr_data.total_sequences = 0
+ self.qr_data.sequences_count = len(self.qr_data.decoder.received_part_indexes())
self.parent.ui.read_progress.setValue(progress)
self.parent.ui.read_progress.setFormat(f"{self.qr_data.sequences_count}/{self.qr_data.total_sequences}")
self.parent.ui.read_progress.setVisible(True)
+ elif data.startswith("B$"):
+ global bbqr_obj
+ if bbqr_obj is None:
+ from bbqr import BBQrCode, KNOWN_ENCODINGS, KNOWN_FILETYPES
+
+ if data[3] in KNOWN_FILETYPES:
+ bbqr_file_type = data[3]
+ if data[2] in KNOWN_ENCODINGS:
+ bbqr_encoding = data[2]
+ bbqr_obj = BBQrCode(None, bbqr_encoding, bbqr_file_type)
+
+ from bbqr import parse_bbqr
+ parsed_data = parse_bbqr(data)
+
+ if not self.qr_data:
+ self.qr_data = MultiQRCode()
+ self.qr_data.qr_type = qr_type.BBQR
+
+ self.qr_data.append(parsed_data)
+
+ progress = round(self.qr_data.sequences_count / self.qr_data.total_sequences * 100)
+ self.parent.ui.read_progress.setValue(progress)
+ self.parent.ui.read_progress.setFormat(f"{self.qr_data.sequences_count}/{self.qr_data.total_sequences}")
+ self.parent.ui.read_progress.setVisible(True)
+
+ # Other format
else:
self.qr_data = QRCode()
self.qr_data.append(data)
-
-
def on_finnish(self):
if self.capture:
self.capture.release()
self.parent.ui.read_progress.setValue(0)
self.parent.ui.read_progress.setVisible(False)
self.parent.ui.read_progress.setFormat('')
- self.parent.ui.btn_start_read.setText('Start read')
+ self.parent.ui.btn_start_read.setText(' '.join(self.parent.ui.btn_start_read.text().split(' ')[:-1]) + START_READ_TXT)
+ self.parent.ui.btn_start_read_monitor.setText(' '.join(self.parent.ui.btn_start_read_monitor.text().split(' ')[:-1]) + START_READ_TXT)
+ self.parent.ui.monitor_group.setDisabled(False)
+ self.parent.ui.camera_group.setDisabled(False)
class DisplayQR(QThread):
-
video_stream = Signal(object)
- def __init__(self, parent):
+ def __init__(self, parent, delay):
QThread.__init__(self)
self.parent = parent
+ self.set_delay(delay)
self.qr_data: QRCode | MultiQRCode = None
- self.stop = False
+ self.stop = True
+
+ def set_delay(self, delay):
+ self.delay = delay
def run(self):
self.stop = False
if self.qr_data.total_sequences > 1 or self.qr_data.qr_type == qr_type.UR:
+ remove_qr = True
+ firstFrame = True
while not self.stop:
+ self.parent.ui.steps.setText(self.qr_data.step())
data = self.qr_data.next()
-
+ if self.qr_data.qr_type == qr_type.UR:
+ self.parent.ui.steps.setText(self.qr_data.step())
self.display_qr(data)
- self.parent.ui.steps.setText(self.qr_data.step())
+ self.msleep(self.delay)
if self.qr_data.total_sequences == 1:
+ remove_qr = False
break
- if not self.stop:
- self.msleep(QR_DELAY)
-
- if self.qr_data.total_sequences == 1:
- while not self.stop:
- self.msleep(QR_DELAY)
-
- self.parent.ui.steps.setText('')
-
+ if firstFrame:
+ firstFrame = False
+ self.msleep(ANIMATED_QR_FIRST_FRAME_DELAY)
+ if remove_qr:
+ self.video_stream.emit(None)
elif self.qr_data.total_sequences == 1:
data = self.qr_data.data
self.display_qr(data)
- while not self.stop:
- self.msleep(QR_DELAY)
+ self.parent.ui.steps.setText('')
def display_qr(self, data):
+ try:
+ qr = qrcode.QRCode()
+ qr.add_data(data)
+ qr.make(fit=False)
+ img = qr.make_image()
+ pil_image = img.convert("RGB")
+ qimage = ImageQt.ImageQt(pil_image)
+ qimage = qimage.convertToFormat(QImage.Format_RGB888)
- qr = qrcode.QRCode()
- qr.add_data(data)
- qr.make(fit=False)
- img = qr.make_image()
- pil_image = img.convert("RGB")
- qimage = ImageQt.ImageQt(pil_image)
- qimage = qimage.convertToFormat(QImage.Format_RGB888)
-
- # Create a QPixmap from the QImage
- pixmap = QPixmap.fromImage(qimage)
-
- scaled_pixmap = pixmap.scaled(self.parent.ui.video_out.size(), Qt.KeepAspectRatio)
- self.video_stream.emit(scaled_pixmap)
+ # Create a QPixmap from the QImage
+ pixmap = QPixmap.fromImage(qimage)
- def on_stop(self):
- self.video_stream.emit(None)
- self.stop = True
+ scaled_pixmap = pixmap.scaled(self.parent.ui.video_out.size(), Qt.KeepAspectRatio)
+ self.video_stream.emit(scaled_pixmap)
+ except Exception as e:
+ print("error making QR", e)
class MainWindow(QMainWindow):
- stop_display = Signal()
-
def __init__(self, loader):
super().__init__()
@@ -431,17 +597,21 @@ def __init__(self, loader):
ui_file.open(QFile.ReadOnly)
self.ui = loader.load(ui_file, self)
ui_file.close()
- self.setWindowTitle("SeedQReader")
- self.setFixedSize(812,670)
+ self.setWindowTitle("SeedQReader " + VERSION)
+ self.setWindowIcon(QIcon('assets/icon.png'))
+ self.setFixedSize(self.ui.tabWidget.width(),self.ui.tabWidget.height())
self.setCentralWidget(self.ui)
self.load_config()
- self.ui.btn_start_read.clicked.connect(self.on_qr_read)
+ self.ui.btn_start_read.clicked.connect(self.on_qr_read_camera)
+ self.ui.btn_start_read_monitor.clicked.connect(self.on_qr_read_monitor)
self.ui.btn_generate.clicked.connect(self.on_btn_generate)
self.ui.btn_clear.clicked.connect(self.on_btn_clear)
self.ui.send_slider.valueChanged.connect(self.on_slider_move)
+ self.ui.delay_slider.valueChanged.connect(self.on_delay_slider_move)
+ self.ui.no_split.stateChanged.connect(self.on_no_split_change)
self.ui.data_out.setWordWrapMode(QTextOption.WrapAnywhere)
@@ -469,7 +639,7 @@ def __init__(self, loader):
self.ui.btn_save.clicked.connect(self.on_btn_save)
- self.ui.combo_format.addItems(['Specter', 'UR'])
+ self.ui.combo_format.addItems([FORMAT_SPECTER, FORMAT_UR, FORMAT_BBQR])
self.format = self.ui.combo_format.currentText()
self.ui.combo_format.currentIndexChanged.connect(self.on_format_change)
self.ui.combo_type.currentIndexChanged.connect(self.on_data_type_change)
@@ -479,24 +649,24 @@ def __init__(self, loader):
self.data_type = None
self.ui.btn_camera_update.clicked.connect(self.on_camera_update)
+ self.ui.btn_monitor_update.clicked.connect(self.on_monitor_update)
self.on_slider_move()
+ self.on_delay_slider_move()
self.on_camera_update()
+ self.on_monitor_update()
self.init_qr()
def init_qr(self):
-
self.read_qr = ReadQR(self)
self.read_qr.video_stream.connect(self.upd_camera_stream)
self.read_qr.data.connect(self.on_qr_data_read)
- self.display_qr = DisplayQR(self)
+ self.display_qr = DisplayQR(self, self.ui.delay_slider.value())
self.display_qr.video_stream.connect(self.on_qr_display)
- self.stop_display.connect(self.display_qr.on_stop)
def load_config(self):
-
if not os.path.exists('config'):
f = open('config', 'w')
f.close()
@@ -533,6 +703,27 @@ def list_available_cameras():
continue
return available_cameras
+
+ @staticmethod
+ def list_available_monitors():
+ with mss() as sct:
+ return [str(i) for i in list(range(len(sct.monitors)-1))]
+
+ def on_monitor_update(self):
+ last = self.get_monitor_id()
+
+ monitors = self.list_available_monitors()
+ self.ui.combo_monitor.clear()
+ self.ui.combo_monitor.addItems(monitors)
+ if last and str(last) in monitors:
+ self.ui.combo_type.setCurrentText(str(last))
+
+ def get_monitor_id(self) -> int | None:
+ try:
+ id = self.ui.combo_monitor.currentText()
+ return int(id)
+ except :
+ return None
def get_camera_id(self) -> int | None:
try:
@@ -553,16 +744,16 @@ def on_camera_update(self):
def on_format_change(self):
self.format = self.ui.combo_format.currentText()
- if self.format != 'Specter':
+ if self.format == FORMAT_UR:
self.ui.combo_type.show()
self.on_data_type_change()
- else:
+ elif self.format in (FORMAT_SPECTER, FORMAT_BBQR):
self.ui.combo_type.hide()
self.data_type = None
def on_data_type_change(self):
- if self.format == 'UR':
+ if self.format == FORMAT_UR:
self.data_type = self.ui.combo_type.currentText()
def on_qr_display(self, frame):
@@ -572,6 +763,14 @@ def on_qr_display(self, frame):
self.ui.video_out.setPixmap(frame)
+ def on_qr_read_camera(self):
+ self.read_qr.viaCamera = True
+ self.on_qr_read()
+
+ def on_qr_read_monitor(self):
+ self.read_qr.viaCamera = False
+ self.on_qr_read()
+
def on_qr_read(self):
if not self.read_qr.isRunning():
self.read_qr.end = False
@@ -582,6 +781,16 @@ def on_qr_read(self):
def on_qr_data_read(self, data):
self.ui.data_in.setWordWrapMode(QTextOption.WrapAnywhere)
+ if isinstance(data, bytes):
+ try:
+ data = data.decode("utf-8")
+ except:
+ try:
+ import base64
+ data = base64.b64encode(data).decode("utf-8")
+ except Exception as e:
+ print("Could not identify data", e)
+
self.ui.data_in.setPlainText(data)
def upd_camera_stream(self, frame):
@@ -592,35 +801,66 @@ def upd_camera_stream(self, frame):
self.ui.video_in.setPixmap(frame)
def on_slider_move(self):
- self.ui.split_size.setText(f"Split size: {self.ui.send_slider.value()}")
+ self.set_split_slider(self.ui.send_slider.value())
+
+ def on_no_split_change(self):
+ self.ui.send_slider.setDisabled(self.ui.no_split.isChecked())
+ self.ui.split_size.setDisabled(self.ui.no_split.isChecked())
+ self.disableQRCombo(self.ui.no_split.isChecked())
+
+ if self.ui.no_split.isChecked():
+ self.set_split_slider('-')
+ else:
+ self.set_split_slider(self.ui.send_slider.value())
+
+ def set_split_slider(self, val):
+ self.ui.split_size.setText(f"QR split size: {val}")
+
+ def on_delay_slider_move(self):
+ self.ui.delay_size.setText(f"QR delay: {self.ui.delay_slider.value()}")
+ try:
+ self.display_qr.set_delay(self.ui.delay_slider.value())
+ except:
+ pass
def on_btn_generate(self):
data: str = self.ui.data_out.toPlainText()
data.replace(' ', '').replace('\n', '')
- if not self.display_qr.isRunning() and data != '':
- if self.ui.no_split.isChecked():
- _max = None
- else:
- _max = self.ui.send_slider.value()
+ if not self.display_qr.isRunning() and self.display_qr.stop and data != '':
+ _max = None if self.ui.no_split.isChecked() else self.ui.send_slider.value()
- # print(f"max={_max}")
- qr = MultiQRCode.from_string(data, max=_max, type=self.data_type, format=self.format)
+ try:
+ qr = MultiQRCode.from_string(data, _max=_max, type=self.data_type, format=self.format)
+ except Exception as e:
+ print("error creating MultiQRCode", self.format, e)
+ return
+
if not qr:
print("error creating MultiQRCode")
return
+
+ self.ui.split_group.setDisabled(True)
self.display_qr.qr_data = qr
self.display_qr.start()
- self.ui.btn_generate.setText('Stop')
-
+ self.ui.btn_generate.setText(STOP_QR_TXT)
+ self.disableQRCombo(True)
else:
- self.stop_display.emit()
- self.ui.btn_generate.setText('Generate')
+ self.display_qr.stop = True
+ self.display_qr.video_stream.emit(None)
+
+ self.ui.split_group.setDisabled(False)
+ self.ui.btn_generate.setText(GENERATE_TXT)
+ self.disableQRCombo(False)
def on_btn_clear(self):
self.ui.data_out.setPlainText('')
+ def disableQRCombo(self, val):
+ self.ui.combo_type.setDisabled(val)
+ self.ui.combo_format.setDisabled(val)
+
def select_data_type(self, data_type):
self.data_type = data_type
self.ui.combo_type.setCurrentText(data_type)
@@ -682,7 +922,6 @@ def radio_select(self):
return
def on_radio_toggled(self):
-
self.radio_select()
self.load_config()
@@ -692,7 +931,6 @@ def on_radio_toggled(self):
self.ui.data_out.setPlainText('')
def on_btn_save(self):
-
self.load_config()
self.config[self.radio_selected] = self.ui.data_out.toPlainText()
self.dump_config()
@@ -714,12 +952,17 @@ def on_btn_save(self):
palette.setColor(QPalette.ToolTipBase, Qt.black)
palette.setColor(QPalette.ToolTipText, Qt.white)
palette.setColor(QPalette.Text, Qt.white)
+ palette.setColor(QPalette.PlaceholderText, Qt.gray)
palette.setColor(QPalette.Button, QColor(53, 53, 53))
palette.setColor(QPalette.ButtonText, Qt.white)
palette.setColor(QPalette.BrightText, Qt.red)
palette.setColor(QPalette.Link, QColor(42, 130, 218))
palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
palette.setColor(QPalette.HighlightedText, Qt.black)
+ palette.setColor(QPalette.ColorGroup.Disabled, QPalette.Button, QColorConstants.DarkGray)
+ palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ButtonText, QColorConstants.Black)
+ palette.setColor(QPalette.ColorGroup.Disabled, QPalette.WindowText, QColorConstants.DarkGray)
+
app.setPalette(palette)
main_win = MainWindow(loader)