Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72adfe1cec |
No files matched your search
@@ -7,4 +7,5 @@ dependencies:
|
|||||||
- python=3.12
|
- python=3.12
|
||||||
- pyserial>=3.5
|
- pyserial>=3.5
|
||||||
- pyside6>=6.7
|
- pyside6>=6.7
|
||||||
|
- esptool>=4.8,<5
|
||||||
- pytest>=8
|
- pytest>=8
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Official firmware images the technician tool may flash.
|
||||||
|
# Boxes normally receive new firmware over OTA; this is the factory image a
|
||||||
|
# box updates itself from once it is online. Add a new image only when the
|
||||||
|
# firmware team releases one: copy the .bin here and add an entry with its
|
||||||
|
# SHA-256 (the tool refuses files that do not match).
|
||||||
|
|
||||||
|
[[image]]
|
||||||
|
file = "new_system.bin"
|
||||||
|
sha256 = "1d58a8f4bb60f66009bd6df32e6e860c545c0c668de955af60d049ee3b9a212d"
|
||||||
|
name = "v1.0 factory image (built Jun 29 2026)"
|
||||||
|
notes = "Official factory image. Once online, the box registers for OTA and updates itself."
|
||||||
Binary file not shown.
@@ -1,3 +1,3 @@
|
|||||||
"""KOTA sensorbox technician tool."""
|
"""KOTA sensorbox technician tool."""
|
||||||
|
|
||||||
__version__ = "0.3.0"
|
__version__ = "0.4.0"
|
||||||
+38
-19
@@ -64,7 +64,7 @@ title = "Firmware is not running normally"
|
|||||||
text = "The heartbeat LED should blink once per second whenever the firmware runs. A steady heartbeat LED means the firmware has frozen."
|
text = "The heartbeat LED should blink once per second whenever the firmware runs. A steady heartbeat LED means the firmware has frozen."
|
||||||
actions = [
|
actions = [
|
||||||
"Switch the box off and on with the power switch.",
|
"Switch the box off and on with the power switch.",
|
||||||
"If it happens again: save the session log and re-flash the official firmware (Flash tab, coming in v0.4).",
|
"If it happens again: save the session log and re-flash the official firmware (Flash firmware tab, App only).",
|
||||||
]
|
]
|
||||||
|
|
||||||
# =============================================================== network
|
# =============================================================== network
|
||||||
@@ -597,7 +597,7 @@ title = "Box does not start"
|
|||||||
text = "Neither LED is lit: no power or the firmware does not start."
|
text = "Neither LED is lit: no power or the firmware does not start."
|
||||||
actions = [
|
actions = [
|
||||||
"Check the power supply and cable.",
|
"Check the power supply and cable.",
|
||||||
"If the USB port shows up in this tool, re-flash the official firmware (Flash tab, coming in v0.4).",
|
"If the USB port shows up in this tool, re-flash the official firmware (Flash firmware tab, App only).",
|
||||||
]
|
]
|
||||||
|
|
||||||
# =================================================================== clock
|
# =================================================================== clock
|
||||||
@@ -651,29 +651,48 @@ default = "clap"
|
|||||||
[[guide.step]]
|
[[guide.step]]
|
||||||
id = "switch"
|
id = "switch"
|
||||||
type = "question"
|
type = "question"
|
||||||
text = "The firmware reads the microphone as switched off. Does this box have a microphone ON/OFF switch or jumper (inside the case)?"
|
text = "The box reads its microphone switch as OFF. The switch is on the back of the case, just above the power switch (no need to open the box). Should this box measure noise?"
|
||||||
options = [
|
options = [
|
||||||
{ label = "Yes - I set it to ON", next = "switch_verify" },
|
{ label = "Yes - I flipped the microphone switch", next = "switch_verify" },
|
||||||
{ label = "No such switch / not sure", next = "end:mic_enable_line" },
|
{ label = "No - it is meant to be off", next = "end:mic_off_ok" },
|
||||||
{ label = "It is meant to be off", next = "end:mic_off_ok" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[guide.step]]
|
[[guide.step]]
|
||||||
id = "switch_verify"
|
id = "switch_verify"
|
||||||
type = "wait"
|
type = "wait"
|
||||||
text = "The tool restarts the box and waits for a microphone reading (up to 60 s)..."
|
# The firmware re-reads the switch before every reading (~10 s): no restart needed.
|
||||||
tool_action = "reset"
|
text = "Waiting for the next microphone reading (up to 30 s)..."
|
||||||
until = 'MICROPHONE: Read \d+ bytes'
|
until = 'MICROPHONE: Read \d+ bytes'
|
||||||
fail_on = 'Microphone is in shutdown state'
|
fail_on = 'Microphone is in shutdown state'
|
||||||
timeout_s = 60
|
timeout_s = 30
|
||||||
on_ok = "end:mic_on"
|
on_ok = "end:mic_on"
|
||||||
on_fail = "end:mic_enable_line"
|
on_fail = "flip_back"
|
||||||
on_timeout = "end:mic_enable_line"
|
on_timeout = "flip_back"
|
||||||
|
|
||||||
|
[[guide.step]]
|
||||||
|
id = "flip_back"
|
||||||
|
type = "question"
|
||||||
|
text = "The box still reads the switch as OFF. Flip the microphone switch to the other position (it may have been ON already)."
|
||||||
|
options = [
|
||||||
|
{ label = "Done - I flipped it to the other position", next = "switch_verify2" },
|
||||||
|
{ label = "Leave it off", next = "end:mic_off_ok" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[guide.step]]
|
||||||
|
id = "switch_verify2"
|
||||||
|
type = "wait"
|
||||||
|
text = "Waiting for the next microphone reading (up to 30 s)..."
|
||||||
|
until = 'MICROPHONE: Read \d+ bytes'
|
||||||
|
fail_on = 'Microphone is in shutdown state'
|
||||||
|
timeout_s = 30
|
||||||
|
on_ok = "end:mic_on"
|
||||||
|
on_fail = "end:mic_switch_faulty"
|
||||||
|
on_timeout = "end:mic_switch_faulty"
|
||||||
|
|
||||||
[[guide.step]]
|
[[guide.step]]
|
||||||
id = "clap"
|
id = "clap"
|
||||||
type = "wait"
|
type = "wait"
|
||||||
text = "Clap your hands or speak loudly right next to the box for about 20 seconds..."
|
text = "Check the microphone switch (back of the case, above the power switch) is ON. Then clap your hands or speak loudly right next to the box for about 20 seconds..."
|
||||||
until = 'dB peak: (?!-inf)'
|
until = 'dB peak: (?!-inf)'
|
||||||
timeout_s = 45
|
timeout_s = 45
|
||||||
on_ok = "end:mic_ok"
|
on_ok = "end:mic_ok"
|
||||||
@@ -683,8 +702,8 @@ on_timeout = "end:mic_faulty"
|
|||||||
id = "mic_on"
|
id = "mic_on"
|
||||||
severity = "ok"
|
severity = "ok"
|
||||||
title = "Microphone is on and recording"
|
title = "Microphone is on and recording"
|
||||||
text = "The firmware now reads audio samples."
|
text = "The firmware reads audio samples again."
|
||||||
actions = []
|
actions = ["Leave the microphone switch in this position."]
|
||||||
|
|
||||||
[[guide.end]]
|
[[guide.end]]
|
||||||
id = "mic_off_ok"
|
id = "mic_off_ok"
|
||||||
@@ -694,11 +713,11 @@ text = "No noise data is recorded for this box; everything else works."
|
|||||||
actions = []
|
actions = []
|
||||||
|
|
||||||
[[guide.end]]
|
[[guide.end]]
|
||||||
id = "mic_enable_line"
|
id = "mic_switch_faulty"
|
||||||
severity = "warning"
|
severity = "warning"
|
||||||
title = "Microphone enable line stays 'off'"
|
title = "Microphone switch has no effect"
|
||||||
text = "The firmware keeps reading the microphone as switched off (GPIO3)."
|
text = "The box reads the microphone as OFF in both switch positions (enable line GPIO3)."
|
||||||
actions = ["Check the microphone board and its cable/connector (inside the case); send the box MAC to the firmware team to confirm the hardware variant."]
|
actions = ["Check the switch and its cable to the main board (inside the case); replace the switch if needed."]
|
||||||
|
|
||||||
[[guide.end]]
|
[[guide.end]]
|
||||||
id = "mic_ok"
|
id = "mic_ok"
|
||||||
@@ -711,5 +730,5 @@ actions = []
|
|||||||
id = "mic_faulty"
|
id = "mic_faulty"
|
||||||
severity = "error"
|
severity = "error"
|
||||||
title = "Microphone records nothing"
|
title = "Microphone records nothing"
|
||||||
text = "Even loud noise next to the box gives no signal."
|
text = "Even loud noise next to the box gives no signal, although the switch is on."
|
||||||
actions = ["Check the microphone board/cable (inside the case); replace the microphone if needed."]
|
actions = ["Check the microphone board/cable (inside the case); replace the microphone if needed."]
|
||||||
@@ -558,8 +558,8 @@ id = "mic.disabled"
|
|||||||
pattern = 'MICROPHONE: Microphone is in shutdown state'
|
pattern = 'MICROPHONE: Microphone is in shutdown state'
|
||||||
severity = "info"
|
severity = "info"
|
||||||
title = "Microphone is switched off - no noise measurements"
|
title = "Microphone is switched off - no noise measurements"
|
||||||
meaning = "The firmware reads the microphone enable line (GPIO3) as 'off', which it treats as disabled on purpose. Noise levels are not recorded."
|
meaning = "The microphone switch on the back of the case (just above the power switch) is OFF, so noise levels are not recorded."
|
||||||
actions = ["If this box should measure noise: check the microphone enable switch/jumper and its wiring (inside the case)."]
|
actions = ["If this box should measure noise: flip the microphone switch (back of the case, above the power switch). No restart needed - it is picked up within about 10 s."]
|
||||||
|
|
||||||
[[rule]]
|
[[rule]]
|
||||||
id = "gather.empty_reading"
|
id = "gather.empty_reading"
|
||||||
|
|||||||
Whitespace-only changes.
@@ -0,0 +1,152 @@
|
|||||||
|
"""Official firmware images: validation and layout.
|
||||||
|
|
||||||
|
Only images listed in firmware/firmware.toml with a matching SHA-256 are
|
||||||
|
offered for flashing: boxes normally get new firmware over OTA, so the tool
|
||||||
|
only ever writes the official factory image.
|
||||||
|
|
||||||
|
The factory image is a merged ESP32-S3 image (bootloader at 0x0, partition
|
||||||
|
table at 0x8000, app at ota_0). App-only flashing takes the app out of it
|
||||||
|
and writes it to ota_0; the layout comes from the image's own partition
|
||||||
|
table, not from constants.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import struct
|
||||||
|
import tomllib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
FIRMWARE_DIR = Path(__file__).resolve().parents[2] / "firmware"
|
||||||
|
MANIFEST = FIRMWARE_DIR / "firmware.toml"
|
||||||
|
|
||||||
|
PT_OFFSET = 0x8000
|
||||||
|
PT_SIZE = 0xC00
|
||||||
|
ESP32S3_CHIP_ID = 9
|
||||||
|
IMAGE_MAGIC = 0xE9
|
||||||
|
APP_DESC_MAGIC = 0xABCD5432
|
||||||
|
FLASH_SIZES = {0: "1MB", 1: "2MB", 2: "4MB", 3: "8MB", 4: "16MB", 5: "32MB"}
|
||||||
|
|
||||||
|
|
||||||
|
class ImageError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Partition:
|
||||||
|
label: str
|
||||||
|
type: int
|
||||||
|
subtype: int
|
||||||
|
offset: int
|
||||||
|
size: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AppInfo:
|
||||||
|
project: str
|
||||||
|
version: str
|
||||||
|
built: str
|
||||||
|
idf: str
|
||||||
|
elf_sha: str # hex
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FirmwareImage:
|
||||||
|
path: Path
|
||||||
|
name: str
|
||||||
|
notes: str
|
||||||
|
sha256: str
|
||||||
|
flash_size: str
|
||||||
|
partitions: tuple[Partition, ...]
|
||||||
|
app: AppInfo
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> bytes:
|
||||||
|
return self.path.read_bytes()
|
||||||
|
|
||||||
|
def partition(self, label: str) -> Partition:
|
||||||
|
for p in self.partitions:
|
||||||
|
if p.label == label:
|
||||||
|
return p
|
||||||
|
raise ImageError(f"partition {label!r} not in image")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def partition_table(self) -> bytes:
|
||||||
|
return self.data[PT_OFFSET:PT_OFFSET + PT_SIZE]
|
||||||
|
|
||||||
|
def app_bytes(self) -> bytes:
|
||||||
|
"""The application image stored in the ota_0 region of the merged image."""
|
||||||
|
# No stripping of trailing 0xFF: the image ends in a SHA-256 digest
|
||||||
|
# that may legitimately end in 0xFF. The merged file ends with the app.
|
||||||
|
ota0 = self.partition("ota_0")
|
||||||
|
return self.data[ota0.offset:ota0.offset + ota0.size]
|
||||||
|
|
||||||
|
def blank_otadata(self) -> bytes:
|
||||||
|
"""Erased otadata: the bootloader then boots ota_0."""
|
||||||
|
return b"\xff" * self.partition("otadata").size
|
||||||
|
|
||||||
|
|
||||||
|
def parse_partition_table(raw: bytes) -> tuple[Partition, ...]:
|
||||||
|
parts = []
|
||||||
|
for i in range(0, len(raw) - 31, 32):
|
||||||
|
entry = raw[i:i + 32]
|
||||||
|
magic = entry[:2]
|
||||||
|
if magic == b"\xaa\x50":
|
||||||
|
_, ptype, sub, off, size = struct.unpack_from("<HBBII", entry)
|
||||||
|
label = entry[12:28].split(b"\0")[0].decode("ascii", "replace")
|
||||||
|
parts.append(Partition(label, ptype, sub, off, size))
|
||||||
|
elif magic in (b"\xeb\xeb", b"\xff\xff"): # MD5 entry / end
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ImageError(f"corrupt partition table entry at 0x{PT_OFFSET + i:x}")
|
||||||
|
return tuple(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _app_info(data: bytes, offset: int) -> AppInfo:
|
||||||
|
if len(data) < offset + 0x20 + 256 or data[offset] != IMAGE_MAGIC:
|
||||||
|
raise ImageError(f"no application image at 0x{offset:x}")
|
||||||
|
d = data[offset + 0x20:offset + 0x20 + 256]
|
||||||
|
if struct.unpack_from("<I", d)[0] != APP_DESC_MAGIC:
|
||||||
|
raise ImageError("application has no ESP-IDF app descriptor")
|
||||||
|
s = lambda o, n: d[o:o + n].split(b"\0")[0].decode("ascii", "replace")
|
||||||
|
return AppInfo(project=s(48, 32), version=s(16, 32), built=f"{s(96, 16)} {s(80, 16)}",
|
||||||
|
idf=s(112, 32), elf_sha=d[144:176].hex())
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_image(path: Path, name: str = "", notes: str = "") -> FirmwareImage:
|
||||||
|
data = path.read_bytes()
|
||||||
|
if len(data) < PT_OFFSET + PT_SIZE or data[0] != IMAGE_MAGIC:
|
||||||
|
raise ImageError("not a merged ESP32 image (no bootloader at offset 0)")
|
||||||
|
chip_id = struct.unpack_from("<H", data, 12)[0]
|
||||||
|
if chip_id != ESP32S3_CHIP_ID:
|
||||||
|
raise ImageError(f"image is for chip id {chip_id}, not ESP32-S3")
|
||||||
|
flash_size = FLASH_SIZES.get(data[3] >> 4, "?")
|
||||||
|
parts = parse_partition_table(data[PT_OFFSET:PT_OFFSET + PT_SIZE])
|
||||||
|
labels = {p.label for p in parts}
|
||||||
|
for need in ("nvs", "otadata", "ota_0"):
|
||||||
|
if need not in labels:
|
||||||
|
raise ImageError(f"partition table has no {need!r} partition")
|
||||||
|
ota0 = next(p for p in parts if p.label == "ota_0")
|
||||||
|
app = _app_info(data, ota0.offset)
|
||||||
|
if app.project != "kota-sensorboard":
|
||||||
|
raise ImageError(f"image is {app.project!r}, not kota-sensorboard")
|
||||||
|
return FirmwareImage(path, name or path.name, notes, hashlib.sha256(data).hexdigest(),
|
||||||
|
flash_size, parts, app)
|
||||||
|
|
||||||
|
|
||||||
|
def official_images(manifest: Path = MANIFEST) -> list[FirmwareImage]:
|
||||||
|
"""Images from the manifest whose file exists and matches its SHA-256."""
|
||||||
|
if not manifest.exists():
|
||||||
|
return []
|
||||||
|
entries = tomllib.loads(manifest.read_text(encoding="utf-8")).get("image", [])
|
||||||
|
out = []
|
||||||
|
for e in entries:
|
||||||
|
path = manifest.parent / e["file"]
|
||||||
|
if not path.exists():
|
||||||
|
continue
|
||||||
|
img = inspect_image(path, e.get("name", ""), e.get("notes", ""))
|
||||||
|
if img.sha256 != e["sha256"].lower():
|
||||||
|
raise ImageError(f"{path.name}: SHA-256 does not match the manifest - file changed or corrupt")
|
||||||
|
out.append(img)
|
||||||
|
return out
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""Flash plans: which esptool steps run, in which order.
|
||||||
|
|
||||||
|
App-only (default, repairs): check the box has the same partition table,
|
||||||
|
then write the app to ota_0 and erase otadata so ota_0 boots. NVS is kept:
|
||||||
|
Matter pairing, OTA registration and Wi-Fi survive.
|
||||||
|
|
||||||
|
Full (new or wiped boards): write the whole factory image at 0x0, the same
|
||||||
|
as the factory flash_all.bat. This erases NVS: pairing and OTA registration
|
||||||
|
are lost.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .image import PT_OFFSET, PT_SIZE, FirmwareImage
|
||||||
|
|
||||||
|
BAUD = "460800"
|
||||||
|
|
||||||
|
|
||||||
|
class Mode(str, enum.Enum):
|
||||||
|
APP_ONLY = "app_only"
|
||||||
|
FULL = "full"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Step:
|
||||||
|
kind: str # "backup" | "read_pt" | "write"
|
||||||
|
title: str
|
||||||
|
args: list[str] # esptool arguments (no interpreter prefix)
|
||||||
|
est_s: int # rough duration for the UI
|
||||||
|
output: Path | None = None # file produced by the step (backup / table)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Plan:
|
||||||
|
mode: Mode
|
||||||
|
image: FirmwareImage
|
||||||
|
port: str
|
||||||
|
steps: list[Step] = field(default_factory=list)
|
||||||
|
workdir: Path | None = None
|
||||||
|
|
||||||
|
def summary(self) -> list[str]:
|
||||||
|
return [f"{i}. {s.title} (~{_dur(s.est_s)})" for i, s in enumerate(self.steps, 1)]
|
||||||
|
|
||||||
|
|
||||||
|
def _dur(s: int) -> str:
|
||||||
|
return f"{s // 60} min" if s >= 90 else f"{s} s"
|
||||||
|
|
||||||
|
|
||||||
|
def _common(port: str, after: str) -> list[str]:
|
||||||
|
return ["--chip", "esp32s3", "--port", port, "--baud", BAUD,
|
||||||
|
"--before", "default_reset", "--after", after]
|
||||||
|
|
||||||
|
|
||||||
|
def build_plan(image: FirmwareImage, mode: Mode, port: str, workdir: Path,
|
||||||
|
backup_to: Path | None = None) -> Plan:
|
||||||
|
workdir.mkdir(parents=True, exist_ok=True)
|
||||||
|
plan = Plan(mode, image, port, workdir=workdir)
|
||||||
|
if backup_to is not None:
|
||||||
|
size = {"4MB": "0x400000", "8MB": "0x800000", "16MB": "0x1000000"}.get(image.flash_size, "0x800000")
|
||||||
|
plan.steps.append(Step(
|
||||||
|
"backup", f"Back up the box's complete flash to {backup_to.name}",
|
||||||
|
_common(port, "no_reset") + ["read_flash", "0", size, str(backup_to)],
|
||||||
|
est_s=200 if size == "0x800000" else 100, output=backup_to))
|
||||||
|
|
||||||
|
write_opts = ["write_flash", "--flash_mode", "keep", "--flash_freq", "keep", "--flash_size", "keep"]
|
||||||
|
if mode == Mode.APP_ONLY:
|
||||||
|
pt_file = workdir / "box_partition_table.bin"
|
||||||
|
plan.steps.append(Step(
|
||||||
|
"read_pt", "Check the box has the same partition layout",
|
||||||
|
_common(port, "no_reset") + ["read_flash", hex(PT_OFFSET), hex(PT_SIZE), str(pt_file)],
|
||||||
|
est_s=5, output=pt_file))
|
||||||
|
app_file = workdir / "app_ota0.bin"
|
||||||
|
ota_file = workdir / "otadata_blank.bin"
|
||||||
|
app_file.write_bytes(image.app_bytes())
|
||||||
|
ota_file.write_bytes(image.blank_otadata())
|
||||||
|
otadata, ota0 = image.partition("otadata"), image.partition("ota_0")
|
||||||
|
plan.steps.append(Step(
|
||||||
|
"write", "Write the firmware app (keeps pairing, OTA registration, settings)",
|
||||||
|
_common(port, "hard_reset") + write_opts + [
|
||||||
|
hex(otadata.offset), str(ota_file), hex(ota0.offset), str(app_file)],
|
||||||
|
est_s=20))
|
||||||
|
else:
|
||||||
|
plan.steps.append(Step(
|
||||||
|
"write", "Write the complete factory image (erases pairing and OTA registration)",
|
||||||
|
_common(port, "hard_reset") + write_opts + ["0x0", str(image.path)],
|
||||||
|
est_s=25))
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
|
def check_partition_table(box_table: bytes, image: FirmwareImage) -> str | None:
|
||||||
|
"""None if App-only is safe, otherwise the reason it is not."""
|
||||||
|
if box_table.strip(b"\xff") == b"":
|
||||||
|
return "The box has no partition table (blank or erased chip). Use Full mode."
|
||||||
|
if box_table != image.partition_table:
|
||||||
|
return ("The box uses a different partition layout than this image. "
|
||||||
|
"App-only could leave it unbootable - use Full mode.")
|
||||||
|
return None
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"""Runs a flash plan: one esptool subprocess per step, in a background thread.
|
||||||
|
|
||||||
|
A subprocess (instead of calling esptool in-process) keeps esptool's
|
||||||
|
stdout/sys.exit handling away from the GUI and lets the tool read progress
|
||||||
|
line by line. Callbacks are invoked from the worker thread.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .plan import Plan, check_partition_table
|
||||||
|
|
||||||
|
_PROGRESS = re.compile(r"\((\d{1,3}) ?%\)")
|
||||||
|
|
||||||
|
# esptool output -> technician-facing hint
|
||||||
|
HINTS = [
|
||||||
|
(re.compile(r"could not open port|Access is denied|PermissionError|Resource busy", re.I),
|
||||||
|
"The USB port is in use. Close other programs that use the sensorbox port (serial monitors, Arduino IDE) and try again."),
|
||||||
|
(re.compile(r"Failed to connect to ESP32-S3|No serial data received|Wrong boot mode", re.I),
|
||||||
|
"The box did not enter flashing mode by itself. Open the case, hold the BOOT button, press and release RESET, "
|
||||||
|
"release BOOT, then press Start again."),
|
||||||
|
(re.compile(r"device reports readiness to read but returned no data|The port disappeared|device disconnected", re.I),
|
||||||
|
"The USB connection dropped. Use a short data-capable USB cable directly on the laptop (no hub) and try again."),
|
||||||
|
(re.compile(r"Invalid head of packet|timed out", re.I),
|
||||||
|
"Communication errors. Try another USB cable or port."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Result:
|
||||||
|
ok: bool
|
||||||
|
message: str
|
||||||
|
hint: str = ""
|
||||||
|
failed_step: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class FlashRunner:
|
||||||
|
def __init__(self, plan: Plan,
|
||||||
|
on_step: Callable[[int, str], None],
|
||||||
|
on_progress: Callable[[int], None],
|
||||||
|
on_output: Callable[[str], None],
|
||||||
|
on_done: Callable[[Result], None],
|
||||||
|
esptool_cmd: list[str] | None = None):
|
||||||
|
self.plan = plan
|
||||||
|
self._on_step, self._on_progress = on_step, on_progress
|
||||||
|
self._on_output, self._on_done = on_output, on_done
|
||||||
|
self._cmd = esptool_cmd or [sys.executable, "-m", "esptool"]
|
||||||
|
self._thread: threading.Thread | None = None
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
self._thread = threading.Thread(target=self._run, name="flash", daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def join(self, timeout: float | None = None) -> None:
|
||||||
|
if self._thread:
|
||||||
|
self._thread.join(timeout)
|
||||||
|
|
||||||
|
def _run(self) -> None:
|
||||||
|
try:
|
||||||
|
self._on_done(self._execute())
|
||||||
|
except Exception as exc: # never leave the UI waiting
|
||||||
|
self._on_done(Result(False, f"Internal error: {exc}"))
|
||||||
|
|
||||||
|
def _execute(self) -> Result:
|
||||||
|
for i, step in enumerate(self.plan.steps):
|
||||||
|
self._on_step(i, step.title)
|
||||||
|
self._on_progress(0)
|
||||||
|
code, output = self._esptool(step.args)
|
||||||
|
if code != 0:
|
||||||
|
return Result(False, f"Step {i + 1} failed: {step.title}", _hint(output), i)
|
||||||
|
if step.kind == "read_pt":
|
||||||
|
problem = check_partition_table(step.output.read_bytes(), self.plan.image)
|
||||||
|
if problem:
|
||||||
|
return Result(False, problem, failed_step=i)
|
||||||
|
self._on_progress(100)
|
||||||
|
return Result(True, "Firmware written and verified by esptool.")
|
||||||
|
|
||||||
|
def _esptool(self, args: list[str]) -> tuple[int, str]:
|
||||||
|
cmd = self._cmd + args
|
||||||
|
self._on_output("$ esptool " + " ".join(args))
|
||||||
|
flags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
||||||
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
|
creationflags=flags)
|
||||||
|
collected: list[str] = []
|
||||||
|
buf = b""
|
||||||
|
assert proc.stdout is not None
|
||||||
|
while chunk := proc.stdout.read1(4096): # returns as soon as bytes arrive
|
||||||
|
buf += chunk
|
||||||
|
# esptool redraws progress with '\r'; treat it as a line break.
|
||||||
|
*lines, buf = re.split(rb"[\r\n]", buf)
|
||||||
|
for raw in lines:
|
||||||
|
self._line(raw, collected)
|
||||||
|
if buf:
|
||||||
|
self._line(buf, collected)
|
||||||
|
return proc.wait(), "\n".join(collected)
|
||||||
|
|
||||||
|
def _line(self, raw: bytes, collected: list[str]) -> None:
|
||||||
|
text = raw.decode("utf-8", "replace").strip()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
collected.append(text)
|
||||||
|
if m := _PROGRESS.search(text):
|
||||||
|
self._on_progress(min(int(m.group(1)), 100))
|
||||||
|
return # progress lines are not worth listing one by one
|
||||||
|
self._on_output(text)
|
||||||
|
|
||||||
|
|
||||||
|
def _hint(output: str) -> str:
|
||||||
|
for pattern, hint in HINTS:
|
||||||
|
if pattern.search(output):
|
||||||
|
return hint
|
||||||
|
return "See the esptool output below. Save it and send it to the firmware team if the problem repeats."
|
||||||
@@ -40,7 +40,8 @@ HINTS = [
|
|||||||
"reboot name the cause.",
|
"reboot name the cause.",
|
||||||
"<b>Small LED next to the RGB LED</b> blinks once per second while the "
|
"<b>Small LED next to the RGB LED</b> blinks once per second while the "
|
||||||
"firmware runs. Not blinking = firmware hung or not running.",
|
"firmware runs. Not blinking = firmware hung or not running.",
|
||||||
"<b>Outside the case</b>: power switch and the Matter button. Holding the "
|
"<b>Outside the case</b> (back): power switch, the <b>microphone switch</b> just above it "
|
||||||
|
"(off = no noise measurements), and the Matter button. Holding the "
|
||||||
"Matter button <b>5 s</b> factory-resets the box (it forgets its Matter "
|
"Matter button <b>5 s</b> factory-resets the box (it forgets its Matter "
|
||||||
"network and must be commissioned again with the sticker QR code).",
|
"network and must be commissioned again with the sticker QR code).",
|
||||||
"<b>Inside the case</b> (unscrew): RESET and BOOT buttons and the SD card. "
|
"<b>Inside the case</b> (unscrew): RESET and BOOT buttons and the SD card. "
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
"""Flash firmware tab: official image only, App-only by default."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PySide6.QtCore import QObject, Qt, QTimer, Signal
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QButtonGroup,
|
||||||
|
QCheckBox,
|
||||||
|
QComboBox,
|
||||||
|
QGroupBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QMessageBox,
|
||||||
|
QPlainTextEdit,
|
||||||
|
QProgressBar,
|
||||||
|
QPushButton,
|
||||||
|
QRadioButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..core.session import SESSIONS_DIR
|
||||||
|
from ..flash.image import FirmwareImage, ImageError, official_images
|
||||||
|
from ..flash.plan import Mode, build_plan
|
||||||
|
from ..flash.runner import FlashRunner, Result
|
||||||
|
from ..serialio.ports import PortInfo, list_serial_ports
|
||||||
|
|
||||||
|
BACKUP_DIR = SESSIONS_DIR.parent / "backups"
|
||||||
|
VERIFY_TIMEOUT_MS = 45_000
|
||||||
|
|
||||||
|
HELP = (
|
||||||
|
"<b>If flashing cannot connect:</b> use a data-capable USB cable directly on the laptop, close "
|
||||||
|
"other programs using the port, then retry. Last resort (case must be opened): hold <b>BOOT</b>, "
|
||||||
|
"press and release <b>RESET</b>, release <b>BOOT</b>, and press Start again."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _Bridge(QObject):
|
||||||
|
step = Signal(int, str)
|
||||||
|
progress = Signal(int)
|
||||||
|
output = Signal(str)
|
||||||
|
done = Signal(object)
|
||||||
|
|
||||||
|
|
||||||
|
class FlashTab(QWidget):
|
||||||
|
def __init__(self, serial_tab, parent: QWidget | None = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.serial_tab = serial_tab
|
||||||
|
self._images: list[FirmwareImage] = []
|
||||||
|
self._ports: list[PortInfo] = [] # before any signal can call _update()
|
||||||
|
self._runner: FlashRunner | None = None
|
||||||
|
self._port: PortInfo | None = None
|
||||||
|
self._image: FirmwareImage | None = None
|
||||||
|
|
||||||
|
# 1. box
|
||||||
|
box = QGroupBox("1. Sensorbox")
|
||||||
|
self.port_combo = QComboBox(minimumWidth=360)
|
||||||
|
refresh = QPushButton("Refresh")
|
||||||
|
refresh.clicked.connect(self.refresh_ports)
|
||||||
|
self.box_fw = QLabel(wordWrap=True)
|
||||||
|
bl = QVBoxLayout(box)
|
||||||
|
row = QHBoxLayout()
|
||||||
|
row.addWidget(self.port_combo)
|
||||||
|
row.addWidget(refresh)
|
||||||
|
row.addStretch(1)
|
||||||
|
bl.addLayout(row)
|
||||||
|
bl.addWidget(self.box_fw)
|
||||||
|
|
||||||
|
# 2. firmware
|
||||||
|
fwbox = QGroupBox("2. Firmware (official images only)")
|
||||||
|
self.image_combo = QComboBox(minimumWidth=360)
|
||||||
|
self.image_info = QLabel(wordWrap=True)
|
||||||
|
self.image_info.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||||
|
fl = QVBoxLayout(fwbox)
|
||||||
|
fl.addWidget(self.image_combo)
|
||||||
|
fl.addWidget(self.image_info)
|
||||||
|
|
||||||
|
# 3. mode
|
||||||
|
modebox = QGroupBox("3. What to write")
|
||||||
|
self.rb_app = QRadioButton("App only (recommended for repairs)")
|
||||||
|
self.rb_full = QRadioButton("Full factory image (new or completely wiped boards)")
|
||||||
|
self.mode_group = QButtonGroup(self)
|
||||||
|
self.mode_group.addButton(self.rb_app)
|
||||||
|
self.mode_group.addButton(self.rb_full)
|
||||||
|
self.rb_app.setChecked(True)
|
||||||
|
app_note = QLabel("Replaces the firmware only. Keeps the Matter pairing, the OTA registration and the "
|
||||||
|
"settings stored on the box.", wordWrap=True)
|
||||||
|
full_note = QLabel("Writes everything like the factory. <b>Erases the Matter pairing and the OTA "
|
||||||
|
"registration</b> - the box must be paired again afterwards.", wordWrap=True)
|
||||||
|
for n in (app_note, full_note):
|
||||||
|
n.setContentsMargins(24, 0, 0, 6)
|
||||||
|
n.setStyleSheet("color:#555")
|
||||||
|
self.confirm_full = QCheckBox("I understand the pairing and OTA registration will be erased")
|
||||||
|
self.backup = QCheckBox("Back up the box's complete flash first (8 MB, about 3 minutes)")
|
||||||
|
ml = QVBoxLayout(modebox)
|
||||||
|
for w in (self.rb_app, app_note, self.rb_full, full_note, self.confirm_full, self.backup):
|
||||||
|
ml.addWidget(w)
|
||||||
|
|
||||||
|
# 4. run
|
||||||
|
runbox = QGroupBox("4. Flash")
|
||||||
|
self.plan_label = QLabel(wordWrap=True)
|
||||||
|
self.start_btn = QPushButton("Start flashing")
|
||||||
|
self.start_btn.setMinimumHeight(36)
|
||||||
|
self.start_btn.clicked.connect(self._start)
|
||||||
|
self.step_label = QLabel()
|
||||||
|
self.bar = QProgressBar()
|
||||||
|
self.result = QLabel(wordWrap=True)
|
||||||
|
self.result.setMargin(8)
|
||||||
|
self.result.setTextFormat(Qt.TextFormat.RichText)
|
||||||
|
self.details_btn = QPushButton("Show esptool output")
|
||||||
|
self.details_btn.setCheckable(True)
|
||||||
|
self.output = QPlainTextEdit(readOnly=True)
|
||||||
|
self.output.setVisible(False)
|
||||||
|
self.details_btn.toggled.connect(self.output.setVisible)
|
||||||
|
rl = QVBoxLayout(runbox)
|
||||||
|
for w in (self.plan_label, self.start_btn, self.step_label, self.bar, self.result, self.details_btn, self.output):
|
||||||
|
rl.addWidget(w)
|
||||||
|
help_lbl = QLabel(HELP, wordWrap=True)
|
||||||
|
help_lbl.setStyleSheet("color:#555")
|
||||||
|
rl.addWidget(help_lbl)
|
||||||
|
rl.addStretch(1) # keep the run column packed at the top
|
||||||
|
|
||||||
|
left = QVBoxLayout()
|
||||||
|
for w in (box, fwbox, modebox):
|
||||||
|
left.addWidget(w)
|
||||||
|
left.addStretch(1)
|
||||||
|
lay = QHBoxLayout(self)
|
||||||
|
lay.addLayout(left, 1)
|
||||||
|
lay.addWidget(runbox, 1)
|
||||||
|
|
||||||
|
self._bridge = _Bridge()
|
||||||
|
self._bridge.step.connect(self._on_step)
|
||||||
|
self._bridge.progress.connect(self.bar.setValue)
|
||||||
|
self._bridge.output.connect(self.output.appendPlainText)
|
||||||
|
self._bridge.done.connect(self._on_done)
|
||||||
|
self._verify_timer = QTimer(self, interval=500)
|
||||||
|
self._verify_timer.timeout.connect(self._check_verify)
|
||||||
|
|
||||||
|
for sig in (self.rb_app.toggled, self.confirm_full.toggled, self.backup.toggled):
|
||||||
|
sig.connect(self._update)
|
||||||
|
self.rb_full.toggled.connect(lambda on: self.backup.setChecked(on)) # backup default on for Full
|
||||||
|
self.image_combo.currentIndexChanged.connect(self._update)
|
||||||
|
self.port_combo.currentIndexChanged.connect(self._update)
|
||||||
|
|
||||||
|
self._load_images()
|
||||||
|
self.refresh_ports()
|
||||||
|
self._show_result("", None)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ setup
|
||||||
|
def _load_images(self) -> None:
|
||||||
|
try:
|
||||||
|
self._images = official_images()
|
||||||
|
except ImageError as exc:
|
||||||
|
self._images = []
|
||||||
|
self.image_info.setText(f"<span style='color:#b03a2e'><b>Firmware file problem:</b> {exc}</span>")
|
||||||
|
self.image_combo.clear()
|
||||||
|
for img in self._images:
|
||||||
|
self.image_combo.addItem(img.name)
|
||||||
|
if not self._images:
|
||||||
|
self.image_combo.addItem("No official firmware image found in the firmware folder")
|
||||||
|
|
||||||
|
def refresh_ports(self) -> None:
|
||||||
|
current = self.port_combo.currentData()
|
||||||
|
self._ports = list_serial_ports()
|
||||||
|
self.port_combo.clear()
|
||||||
|
for p in self._ports:
|
||||||
|
self.port_combo.addItem(p.label, p.device)
|
||||||
|
if not self._ports:
|
||||||
|
self.port_combo.addItem("No sensorbox found - check USB cable and power switch", None)
|
||||||
|
self.port_combo.setCurrentIndex(max(self.port_combo.findData(current), 0))
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
def showEvent(self, event): # noqa: N802 - refresh when the tab is opened
|
||||||
|
self.refresh_ports()
|
||||||
|
super().showEvent(event)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ state
|
||||||
|
def _selected_image(self) -> FirmwareImage | None:
|
||||||
|
i = self.image_combo.currentIndex()
|
||||||
|
return self._images[i] if 0 <= i < len(self._images) else None
|
||||||
|
|
||||||
|
def _selected_port(self) -> PortInfo | None:
|
||||||
|
dev = self.port_combo.currentData()
|
||||||
|
return next((p for p in self._ports if p.device == dev), None)
|
||||||
|
|
||||||
|
def _update(self) -> None:
|
||||||
|
img = self._selected_image()
|
||||||
|
if img:
|
||||||
|
a = img.app
|
||||||
|
self.image_info.setText(
|
||||||
|
f"{a.project} {a.version}, built {a.built}, ESP-IDF {a.idf}<br>"
|
||||||
|
f"<small>build {a.elf_sha[:9]} · file SHA-256 {img.sha256[:12]}... verified</small><br>"
|
||||||
|
f"<small>{img.notes}</small>")
|
||||||
|
fw = self.serial_tab.engine.firmware
|
||||||
|
if fw.elf_sha and img:
|
||||||
|
same = fw.elf_sha.startswith(img.app.elf_sha[:9]) or img.app.elf_sha.startswith(fw.elf_sha)
|
||||||
|
msg = f"Box currently runs: {fw.describe()}"
|
||||||
|
if same:
|
||||||
|
msg += "<br><i>It already has this firmware. Re-flash only if you suspect the firmware is damaged.</i>"
|
||||||
|
else:
|
||||||
|
msg += ("<br><b style='color:#c77c02'>The box runs a different build (probably updated over OTA). "
|
||||||
|
"Flashing puts the older factory image back; the box updates itself again once online.</b>")
|
||||||
|
else:
|
||||||
|
msg = "Firmware on the box: unknown - connect in the Serial monitor and press Reset & capture to see it."
|
||||||
|
self.box_fw.setText(msg)
|
||||||
|
|
||||||
|
self.confirm_full.setVisible(self.rb_full.isChecked())
|
||||||
|
ok = bool(img and self._selected_port() and self._runner is None
|
||||||
|
and (self.rb_app.isChecked() or self.confirm_full.isChecked()))
|
||||||
|
self.start_btn.setEnabled(ok)
|
||||||
|
if img and self._selected_port():
|
||||||
|
mode = Mode.APP_ONLY if self.rb_app.isChecked() else Mode.FULL
|
||||||
|
steps = ["Back up the complete flash (~3 min)"] if self.backup.isChecked() else []
|
||||||
|
if mode == Mode.APP_ONLY:
|
||||||
|
steps += ["Check the box has the same partition layout", "Write the firmware app (~20 s)"]
|
||||||
|
else:
|
||||||
|
steps += ["Write the complete factory image (~25 s)"]
|
||||||
|
steps += ["Restart the box and verify the new firmware boots"]
|
||||||
|
self.plan_label.setText("<b>Plan:</b><ol>" + "".join(f"<li>{s}</li>" for s in steps) + "</ol>")
|
||||||
|
else:
|
||||||
|
self.plan_label.setText("Select a sensorbox and a firmware image.")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ flash
|
||||||
|
def _start(self) -> None:
|
||||||
|
img, port = self._selected_image(), self._selected_port()
|
||||||
|
if not img or not port:
|
||||||
|
return
|
||||||
|
mode = Mode.APP_ONLY if self.rb_app.isChecked() else Mode.FULL
|
||||||
|
what = ("the firmware app (pairing and settings are kept)" if mode == Mode.APP_ONLY
|
||||||
|
else "the COMPLETE factory image (pairing and OTA registration are erased)")
|
||||||
|
if QMessageBox.question(self, "Flash firmware",
|
||||||
|
f"Write {what}\n\nImage: {img.name}\nBox: {port.label}\n\n"
|
||||||
|
"Keep the box powered and the USB cable connected until it finishes.",
|
||||||
|
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel
|
||||||
|
) != QMessageBox.StandardButton.Ok:
|
||||||
|
return
|
||||||
|
backup_to = None
|
||||||
|
if self.backup.isChecked():
|
||||||
|
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
stamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
backup_to = BACKUP_DIR / f"{(port.mac or port.device).replace(':', '')}_{stamp}.bin"
|
||||||
|
plan = build_plan(img, mode, port.device, Path(tempfile.mkdtemp(prefix="kota_flash_")), backup_to)
|
||||||
|
|
||||||
|
self._port, self._image = port, img
|
||||||
|
self.serial_tab.release_port() # esptool needs the port exclusively
|
||||||
|
self.output.clear()
|
||||||
|
self.output.appendPlainText("\n".join(plan.summary()))
|
||||||
|
self._show_result("", None)
|
||||||
|
self._runner = FlashRunner(plan, self._bridge.step.emit, self._bridge.progress.emit,
|
||||||
|
self._bridge.output.emit, self._bridge.done.emit)
|
||||||
|
self._set_busy(True)
|
||||||
|
self._runner.start()
|
||||||
|
|
||||||
|
def _set_busy(self, busy: bool) -> None:
|
||||||
|
for w in (self.port_combo, self.image_combo, self.rb_app, self.rb_full, self.backup, self.confirm_full):
|
||||||
|
w.setEnabled(not busy)
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
def _on_step(self, i: int, title: str) -> None:
|
||||||
|
n = len(self._runner.plan.steps) if self._runner else 0
|
||||||
|
self.step_label.setText(f"Step {i + 1} of {n + 1}: {title}")
|
||||||
|
|
||||||
|
def _on_done(self, res: Result) -> None:
|
||||||
|
self._runner = None
|
||||||
|
if not res.ok:
|
||||||
|
self._set_busy(False)
|
||||||
|
self.step_label.setText("Stopped.")
|
||||||
|
hint = f"<p>{res.hint}</p>" if res.hint else ""
|
||||||
|
self._show_result(f"<b>Flashing failed.</b> {res.message}{hint}", "error")
|
||||||
|
self.details_btn.setChecked(True)
|
||||||
|
return
|
||||||
|
self.step_label.setText("Final step: restarting the box and checking which firmware it boots...")
|
||||||
|
self.bar.setValue(0)
|
||||||
|
self._show_result("Firmware written. Waiting for the box to start...", "info")
|
||||||
|
self.serial_tab.connect_port(self._port, capture=True)
|
||||||
|
self._verify_ms = 0
|
||||||
|
self._verify_timer.start()
|
||||||
|
|
||||||
|
def _check_verify(self) -> None:
|
||||||
|
self._verify_ms += self._verify_timer.interval()
|
||||||
|
self.bar.setValue(min(100, int(100 * self._verify_ms / VERIFY_TIMEOUT_MS)))
|
||||||
|
fw = self.serial_tab.engine.firmware
|
||||||
|
want = self._image.app.elf_sha[:9]
|
||||||
|
done = fw.elf_sha is not None and fw.elf_sha.startswith(want)
|
||||||
|
if done or self._verify_ms >= VERIFY_TIMEOUT_MS:
|
||||||
|
self._verify_timer.stop()
|
||||||
|
self._set_busy(False)
|
||||||
|
self.bar.setValue(100)
|
||||||
|
if done:
|
||||||
|
self.step_label.setText("Done.")
|
||||||
|
self._show_result(f"<b>Success.</b> The box boots the new firmware (build {want}). "
|
||||||
|
"Check the Diagnosis panel in the Serial monitor.", "ok")
|
||||||
|
else:
|
||||||
|
seen = fw.elf_sha[:9] if fw.elf_sha else "no boot seen"
|
||||||
|
self._show_result(f"<b>Written, but not verified.</b> Expected build {want}, saw: {seen}. "
|
||||||
|
"Open the Serial monitor and press Reset & capture.", "warning")
|
||||||
|
|
||||||
|
def _show_result(self, html: str, kind: str | None) -> None:
|
||||||
|
colours = {"ok": ("#2e9e44", "#e3f4e7"), "info": ("#2e6da4", "#e3edf7"),
|
||||||
|
"warning": ("#c77c02", "#fdf0d6"), "error": ("#d9534f", "#f9e0df")}
|
||||||
|
self.result.setVisible(bool(html))
|
||||||
|
if html:
|
||||||
|
border, bg = colours[kind or "info"]
|
||||||
|
self.result.setStyleSheet(f"QLabel {{ border:2px solid {border}; background:{bg}; border-radius:6px; }}")
|
||||||
|
self.result.setText(html)
|
||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QObject, Qt, QTimer, QUrl, Signal
|
from PySide6.QtCore import QCoreApplication, QObject, Qt, QTimer, QUrl, Signal
|
||||||
from PySide6.QtGui import QColor, QDesktopServices, QFont, QGuiApplication, QTextCharFormat, QTextCursor
|
from PySide6.QtGui import QColor, QDesktopServices, QFont, QGuiApplication, QTextCharFormat, QTextCursor
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
@@ -30,6 +30,7 @@ from ..serialio.manager import SerialManager, State
|
|||||||
from ..serialio.ports import PortInfo, list_serial_ports
|
from ..serialio.ports import PortInfo, list_serial_ports
|
||||||
from .cheatsheet import cheatsheet_html
|
from .cheatsheet import cheatsheet_html
|
||||||
from .diag_panel import DiagnosticsPanel
|
from .diag_panel import DiagnosticsPanel
|
||||||
|
from .flash_tab import FlashTab
|
||||||
from .guide_panel import GuidePanel
|
from .guide_panel import GuidePanel
|
||||||
|
|
||||||
MAX_LINES = 20000
|
MAX_LINES = 20000
|
||||||
@@ -180,6 +181,9 @@ class SerialTab(QWidget):
|
|||||||
port = self._selected_port()
|
port = self._selected_port()
|
||||||
if port is None:
|
if port is None:
|
||||||
return
|
return
|
||||||
|
self._connect(port)
|
||||||
|
|
||||||
|
def _connect(self, port: PortInfo) -> None:
|
||||||
self._counts = {"E": 0, "W": 0, "boots": 0}
|
self._counts = {"E": 0, "W": 0, "boots": 0}
|
||||||
self._engine.reset()
|
self._engine.reset()
|
||||||
self._runner.cancel()
|
self._runner.cancel()
|
||||||
@@ -187,8 +191,36 @@ class SerialTab(QWidget):
|
|||||||
self._session.start(port.mac or port.device)
|
self._session.start(port.mac or port.device)
|
||||||
self._manager.connect(port)
|
self._manager.connect(port)
|
||||||
|
|
||||||
|
# ---- hand-off with the flash tab ------------------------------------
|
||||||
|
@property
|
||||||
|
def engine(self) -> DiagnosticsEngine:
|
||||||
|
return self._engine
|
||||||
|
|
||||||
|
def release_port(self) -> None:
|
||||||
|
"""Stop monitoring so esptool can open the port exclusively."""
|
||||||
|
if self._manager.is_running:
|
||||||
|
self._manager.disconnect() # joins the reader thread
|
||||||
|
QCoreApplication.processEvents() # deliver its queued final lines/state now
|
||||||
|
|
||||||
|
def connect_port(self, port: PortInfo, capture: bool = True) -> None:
|
||||||
|
"""Monitor `port`; with capture, restart the box once connected to record its boot."""
|
||||||
|
self.release_port()
|
||||||
|
idx = self.port_combo.findData(port.device)
|
||||||
|
if idx < 0:
|
||||||
|
self.port_combo.addItem(port.label, port.device)
|
||||||
|
self._ports.append(port)
|
||||||
|
idx = self.port_combo.count() - 1
|
||||||
|
self.port_combo.setCurrentIndex(idx)
|
||||||
|
self._reset_on_connect = capture
|
||||||
|
self._connect(port)
|
||||||
|
|
||||||
def _on_state(self, state: State, detail: str) -> None:
|
def _on_state(self, state: State, detail: str) -> None:
|
||||||
running = state != State.DISCONNECTED
|
running = state != State.DISCONNECTED
|
||||||
|
if not running and self._manager.is_running:
|
||||||
|
return # stale event from a session that was already replaced
|
||||||
|
if state == State.CONNECTED and getattr(self, "_reset_on_connect", False):
|
||||||
|
self._reset_on_connect = False
|
||||||
|
QTimer.singleShot(300, self._manager.reset_board)
|
||||||
if not running and self._session.path is not None:
|
if not running and self._session.path is not None:
|
||||||
self._session.write_report(self._engine.report().to_text())
|
self._session.write_report(self._engine.report().to_text())
|
||||||
self._session.stop()
|
self._session.stop()
|
||||||
@@ -315,8 +347,10 @@ class MainWindow(QMainWindow):
|
|||||||
guide = QTextBrowser(openExternalLinks=True)
|
guide = QTextBrowser(openExternalLinks=True)
|
||||||
guide.setHtml(cheatsheet_html())
|
guide.setHtml(cheatsheet_html())
|
||||||
self.serial_tab = SerialTab()
|
self.serial_tab = SerialTab()
|
||||||
|
self.flash_tab = FlashTab(self.serial_tab)
|
||||||
tabs.addTab(guide, "Quick start")
|
tabs.addTab(guide, "Quick start")
|
||||||
tabs.addTab(self.serial_tab, "Serial monitor")
|
tabs.addTab(self.serial_tab, "Serial monitor")
|
||||||
|
tabs.addTab(self.flash_tab, "Flash firmware")
|
||||||
self.setCentralWidget(tabs)
|
self.setCentralWidget(tabs)
|
||||||
|
|
||||||
def closeEvent(self, event): # noqa: N802 (Qt naming)
|
def closeEvent(self, event): # noqa: N802 (Qt naming)
|
||||||
|
|||||||
+2
-1
@@ -1,11 +1,12 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "kota-technician-tool"
|
name = "kota-technician-tool"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
description = "KOTA sensorbox technician diagnostic tool (demo)"
|
description = "KOTA sensorbox technician diagnostic tool (demo)"
|
||||||
requires-python = ">=3.11,<3.14"
|
requires-python = ">=3.11,<3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyserial>=3.5",
|
"pyserial>=3.5",
|
||||||
"PySide6-Essentials>=6.7",
|
"PySide6-Essentials>=6.7",
|
||||||
|
"esptool~=4.8", # 4.x CLI (write_flash/read_flash); v5 renamed commands
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
cd "$(dirname "$0")" || exit 1
|
cd "$(dirname "$0")" || exit 1
|
||||||
|
|
||||||
# Keep the virtual environment out of the (Google Drive) tool folder.
|
# Keep the virtual environment out of the (Google Drive) tool folder.
|
||||||
export UV_PROJECT_ENVIRONMENT="$HOME/Library/Caches/KotaTechnicianTool/venv-0.3"
|
export UV_PROJECT_ENVIRONMENT="$HOME/Library/Caches/KotaTechnicianTool/venv-0.4"
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
if ! command -v uv >/dev/null 2>&1; then
|
if ! command -v uv >/dev/null 2>&1; then
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ setlocal
|
|||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
|
|
||||||
rem Keep the virtual environment out of the (Google Drive) tool folder.
|
rem Keep the virtual environment out of the (Google Drive) tool folder.
|
||||||
set "UV_PROJECT_ENVIRONMENT=%LOCALAPPDATA%\KotaTechnicianTool\venv-0.3"
|
set "UV_PROJECT_ENVIRONMENT=%LOCALAPPDATA%\KotaTechnicianTool\venv-0.4"
|
||||||
set "PATH=%USERPROFILE%\.local\bin;%PATH%"
|
set "PATH=%USERPROFILE%\.local\bin;%PATH%"
|
||||||
|
|
||||||
where uv >nul 2>nul
|
where uv >nul 2>nul
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""Flashing: image validation on the real official image, plan building,
|
||||||
|
and the runner against a fake esptool (no hardware)."""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from kota_tech.flash.image import ImageError, inspect_image, official_images
|
||||||
|
from kota_tech.flash.plan import Mode, build_plan, check_partition_table
|
||||||
|
from kota_tech.flash.runner import FlashRunner
|
||||||
|
|
||||||
|
FW = Path(__file__).resolve().parents[1] / "firmware" / "new_system.bin"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def image():
|
||||||
|
imgs = official_images()
|
||||||
|
assert len(imgs) == 1
|
||||||
|
return imgs[0]
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ image
|
||||||
|
|
||||||
|
def test_official_image_is_the_deployed_v1_build(image):
|
||||||
|
assert image.app.project == "kota-sensorboard"
|
||||||
|
assert image.app.elf_sha.startswith("45fa9d5b9") # same as the live board
|
||||||
|
assert image.flash_size == "8MB"
|
||||||
|
assert image.partition("ota_0").offset == 0x40000
|
||||||
|
assert image.partition("otadata").offset == 0x31000
|
||||||
|
assert image.partition("nvs").offset == 0x10000
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_bytes_are_a_complete_app_image(image):
|
||||||
|
app = image.app_bytes()
|
||||||
|
assert app[0] == 0xE9
|
||||||
|
assert len(app) == len(image.data) - 0x40000 # ends where the file ends
|
||||||
|
assert len(image.blank_otadata()) == 0x2000 and set(image.blank_otadata()) == {0xFF}
|
||||||
|
|
||||||
|
|
||||||
|
def test_modified_image_is_refused(tmp_path):
|
||||||
|
data = bytearray(FW.read_bytes())
|
||||||
|
data[-1] ^= 0xFF
|
||||||
|
bad = tmp_path / "new_system.bin"
|
||||||
|
bad.write_bytes(data)
|
||||||
|
manifest = tmp_path / "firmware.toml"
|
||||||
|
manifest.write_text(f'[[image]]\nfile = "new_system.bin"\nsha256 = "{hashlib.sha256(FW.read_bytes()).hexdigest()}"\n')
|
||||||
|
with pytest.raises(ImageError, match="SHA-256"):
|
||||||
|
official_images(manifest)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("patch,msg", [
|
||||||
|
((0, 0x00), "not a merged"),
|
||||||
|
((12, 0x05), "not ESP32-S3"),
|
||||||
|
])
|
||||||
|
def test_wrong_images_are_rejected(tmp_path, patch, msg):
|
||||||
|
data = bytearray(FW.read_bytes())
|
||||||
|
data[patch[0]] = patch[1]
|
||||||
|
p = tmp_path / "x.bin"
|
||||||
|
p.write_bytes(data)
|
||||||
|
with pytest.raises(ImageError, match=msg):
|
||||||
|
inspect_image(p)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- plan
|
||||||
|
|
||||||
|
def test_app_only_plan_keeps_nvs(image, tmp_path):
|
||||||
|
plan = build_plan(image, Mode.APP_ONLY, "COM5", tmp_path)
|
||||||
|
kinds = [s.kind for s in plan.steps]
|
||||||
|
assert kinds == ["read_pt", "write"]
|
||||||
|
write = plan.steps[-1].args
|
||||||
|
offsets = [a for a in write if a.startswith("0x")]
|
||||||
|
assert offsets == ["0x31000", "0x40000"] # otadata + ota_0 only
|
||||||
|
assert "0x10000" not in write and "0x0" not in write # NVS / bootloader untouched
|
||||||
|
assert write[write.index("--after") + 1] == "hard_reset"
|
||||||
|
assert (tmp_path / "app_ota0.bin").read_bytes() == image.app_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_plan_with_backup(image, tmp_path):
|
||||||
|
backup = tmp_path / "bk.bin"
|
||||||
|
plan = build_plan(image, Mode.FULL, "COM5", tmp_path, backup_to=backup)
|
||||||
|
assert [s.kind for s in plan.steps] == ["backup", "write"]
|
||||||
|
assert plan.steps[0].args[-3:] == ["0", "0x800000", str(backup)]
|
||||||
|
assert plan.steps[0].args[plan.steps[0].args.index("--after") + 1] == "no_reset"
|
||||||
|
assert plan.steps[1].args[-2:] == ["0x0", str(image.path)]
|
||||||
|
assert "Back up" in plan.summary()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_partition_table_check(image):
|
||||||
|
assert check_partition_table(image.partition_table, image) is None
|
||||||
|
assert "blank" in check_partition_table(b"\xff" * 0xC00, image)
|
||||||
|
other = bytearray(image.partition_table)
|
||||||
|
other[40] ^= 1
|
||||||
|
assert "different partition layout" in check_partition_table(bytes(other), image)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- runner
|
||||||
|
|
||||||
|
FAKE = """
|
||||||
|
import sys, pathlib
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if "read_flash" in args:
|
||||||
|
out = pathlib.Path(args[-1])
|
||||||
|
src = pathlib.Path(r"{fw}").read_bytes()
|
||||||
|
off, size = int(args[-3], 0), int(args[-2], 0)
|
||||||
|
{pt_line}
|
||||||
|
print("Read ... (50 %)", end="\\r"); print("Read ... (100 %)")
|
||||||
|
sys.exit(0)
|
||||||
|
if "write_flash" in args:
|
||||||
|
for p in (10, 60, 100):
|
||||||
|
print(f"Writing at 0x00040000... ({{p}} %)", end="\\r", flush=True)
|
||||||
|
print(); print("Hash of data verified.")
|
||||||
|
sys.exit({write_code})
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _fake(tmp_path, write_code=0, pt="same", fail_text=""):
|
||||||
|
pt_line = {"same": "out.write_bytes(src[off:off+size])",
|
||||||
|
"blank": "out.write_bytes(b'\\xff' * size)"}[pt]
|
||||||
|
script = FAKE.format(fw=FW, pt_line=pt_line, write_code=write_code)
|
||||||
|
if fail_text:
|
||||||
|
script = script.replace('print("Hash of data verified.")', f'print("{fail_text}")')
|
||||||
|
f = tmp_path / "fake_esptool.py"
|
||||||
|
f.write_text(script)
|
||||||
|
return [sys.executable, str(f)]
|
||||||
|
|
||||||
|
|
||||||
|
def _run(plan, cmd):
|
||||||
|
steps, progress, output, done = [], [], [], threading.Event()
|
||||||
|
result = {}
|
||||||
|
r = FlashRunner(plan, lambda i, t: steps.append(i), progress.append, output.append,
|
||||||
|
lambda res: (result.setdefault("r", res), done.set()), esptool_cmd=cmd)
|
||||||
|
r.start()
|
||||||
|
assert done.wait(30)
|
||||||
|
return result["r"], steps, progress, output
|
||||||
|
|
||||||
|
|
||||||
|
def test_runner_app_only_success(image, tmp_path):
|
||||||
|
plan = build_plan(image, Mode.APP_ONLY, "COM5", tmp_path)
|
||||||
|
res, steps, progress, output = _run(plan, _fake(tmp_path))
|
||||||
|
assert res.ok, res
|
||||||
|
assert steps == [0, 1]
|
||||||
|
assert 60 in progress and progress[-1] == 100
|
||||||
|
assert any("Hash of data verified" in o for o in output)
|
||||||
|
|
||||||
|
|
||||||
|
def test_runner_refuses_app_only_on_blank_chip(image, tmp_path):
|
||||||
|
plan = build_plan(image, Mode.APP_ONLY, "COM5", tmp_path)
|
||||||
|
res, steps, *_ = _run(plan, _fake(tmp_path, pt="blank"))
|
||||||
|
assert not res.ok and "Full mode" in res.message
|
||||||
|
assert steps == [0] # never reached the write step
|
||||||
|
|
||||||
|
|
||||||
|
def test_runner_reports_connect_failure_with_hint(image, tmp_path):
|
||||||
|
plan = build_plan(image, Mode.FULL, "COM5", tmp_path)
|
||||||
|
cmd = _fake(tmp_path, write_code=2,
|
||||||
|
fail_text="A fatal error occurred: Failed to connect to ESP32-S3: No serial data received.")
|
||||||
|
res, *_ = _run(plan, cmd)
|
||||||
|
assert not res.ok and res.failed_step == 0
|
||||||
|
assert "BOOT" in res.hint
|
||||||
+17
-4
@@ -182,17 +182,30 @@ def test_reset_action_failure_is_shown(rig):
|
|||||||
assert "connect" in runner.view().note.lower()
|
assert "connect" in runner.view().note.lower()
|
||||||
|
|
||||||
|
|
||||||
def test_mic_disabled_switch_on_verified(rig):
|
def test_mic_switch_flip_verified_without_restart(rig):
|
||||||
engine, runner, clock, actions, board, _ = rig
|
engine, runner, clock, actions, board, _ = rig
|
||||||
_bench(engine)
|
_bench(engine)
|
||||||
runner.start("mic")
|
runner.start("mic")
|
||||||
assert runner.step.id == "switch"
|
assert runner.step.id == "switch"
|
||||||
runner.answer("0") # set it to ON
|
assert "above the power switch" in runner.view().text
|
||||||
assert actions == ["reset"]
|
runner.answer("0") # flipped the external switch
|
||||||
board(*BOOT, "I (20000) MICROPHONE: Read 300000 bytes in 4936 ms")
|
assert actions == [] # the firmware re-reads it; no reset
|
||||||
|
board("I (95000) MICROPHONE: Read 300000 bytes in 4936 ms")
|
||||||
assert runner.end.id == "mic_on"
|
assert runner.end.id == "mic_on"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mic_switch_no_effect_in_both_positions(rig):
|
||||||
|
engine, runner, clock, actions, board, _ = rig
|
||||||
|
_bench(engine)
|
||||||
|
runner.start("mic")
|
||||||
|
runner.answer("0")
|
||||||
|
board("W (95000) MICROPHONE: Microphone is in shutdown state, skipping sample collection")
|
||||||
|
assert runner.step.id == "flip_back"
|
||||||
|
runner.answer("0")
|
||||||
|
board("W (105000) MICROPHONE: Microphone is in shutdown state, skipping sample collection")
|
||||||
|
assert runner.end.id == "mic_switch_faulty"
|
||||||
|
|
||||||
|
|
||||||
def test_mic_clap_test_detects_dead_mic(rig):
|
def test_mic_clap_test_detects_dead_mic(rig):
|
||||||
engine, runner, clock, _, board, _ = rig
|
engine, runner, clock, _, board, _ = rig
|
||||||
board(*BOOT, *["I (20036) MICROPHONE: Sum SQ: 0.000000, RMS pressure: 0.000000, "
|
board(*BOOT, *["I (20036) MICROPHONE: Sum SQ: 0.000000, RMS pressure: 0.000000, "
|
||||||
|
|||||||
@@ -1,6 +1,129 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.11, <3.14"
|
requires-python = ">=3.11, <3.14"
|
||||||
|
resolution-markers = [
|
||||||
|
"platform_machine == 'x86_64' and sys_platform == 'darwin'",
|
||||||
|
"platform_machine != 'x86_64' or sys_platform != 'darwin'",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argcomplete"
|
||||||
|
version = "3.7.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/87/6f/5a73f04007ca950701765949209f068da628bd11f9c2da287278ce91e0ee/argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9", size = 74473, upload-time = "2026-08-06T04:53:21.662Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/bd/551ee6af426af84ca33e02622be722925c196608e9127d731ef17c47f06e/argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19", size = 43294, upload-time = "2026-08-06T04:53:20.246Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitarray"
|
||||||
|
version = "3.11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/04/f7/6765577df59e2345036e435f7e983e1c291d67b7d76a51918eff04ad1494/bitarray-3.11.0.tar.gz", hash = "sha256:bf19437ec00ec3d40aef82eaeedc14cf4000be9b635c4f5049796506e6630dd8", size = 174911, upload-time = "2026-09-01T15:21:52.076Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/55/aeb2eb3870a31257a29d3830f7d5804253d2070727ef6475629455491d33/bitarray-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:470940879d8553fecddc3cf306b7d4b69fa0f5b7a6e3790b238fe553578061c1", size = 170436, upload-time = "2026-09-01T15:19:54.02Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/b4/38b6131ecd500f8f853367339b9e95ad4644d47729bcbf0fa2c5e4c06a91/bitarray-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56a2aa49e900aa927c147328e571da8a38a50ae99277ed093290845498dedefc", size = 166780, upload-time = "2026-09-01T15:19:55.173Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/28/d1e0493106911b08d3d328e463aa140ca76fcab6cdcda06912ffb59d53cf/bitarray-3.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55370f00c52b5e46800fee0218a27a1b99db6e09113c1b804190c039e4350ea1", size = 380740, upload-time = "2026-09-01T15:19:56.253Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/85/1ebb39e36b7102358942609583aa5a027b7e1eb83e140ad3fba86e53adba/bitarray-3.11.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:155205159f3c38180a705253341b8200414c0198e86a9b9e2db49010236ff785", size = 404566, upload-time = "2026-09-01T15:19:57.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/8d/e94bfa36a7cadcd16415afe531e0a01c6e2dc2bb11095c6c52fca384c642/bitarray-3.11.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c49fc5f57eea2e859a3f2936a1fed84510af6f7464572b641485de2d22148cb", size = 418899, upload-time = "2026-09-01T15:19:58.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/3a/859196ab78c6fa0aa8f04155308dd314265c8c27e09350bcd3894fc333d8/bitarray-3.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41545b21384fd91767d294a5b3526c6306f7db577cf59ba72b51498e90bd52a7", size = 383414, upload-time = "2026-09-01T15:19:59.649Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/fc/5fafae3bee337c31372293fcc6259960e9bcaf89825f07798514213c5686/bitarray-3.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:afe8ee837c085eeeaead62429f260a2de3a0602d1a34bcdead8b9f0388ef6114", size = 378658, upload-time = "2026-09-01T15:20:00.751Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/c8/39a1e59453b99a67ee449d9d0c378f5e282ec4c00645127a9aaa4a5adba7/bitarray-3.11.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4bddbc76e619a36d2abad14713c4dd970a8fa46a75a85ebbb626a2f26f1821d8", size = 401990, upload-time = "2026-09-01T15:20:01.951Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/aa/add67250e559e24fdc585921062e388395e0b47f40c6aa13e1ee493a13d4/bitarray-3.11.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:027766e71da6063cb51fcd4c51f031ac14d5088ecb55e4500ffd692d78630dd3", size = 399837, upload-time = "2026-09-01T15:20:03.202Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/80/c758fa566da3180e43fba1a9bc2c84b29fc5e25ce0149155378e89ee4172/bitarray-3.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:116a23f655f394f80f41fdc03368862485ac05534a56991eefb9f18f72e17f17", size = 380673, upload-time = "2026-09-01T15:20:04.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/8a/fcb7bef53c2aacdbc2ca07f498e5249ab7d87dbfed666f184bb8d16fec0f/bitarray-3.11.0-cp311-cp311-win32.whl", hash = "sha256:30597fa6f4116eb6e374568d4f4de7e220462368ec7ce37d93403871bdf27a8c", size = 160605, upload-time = "2026-09-01T15:20:05.525Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/2c/3d3f3f7846432bc1f630c99fa5cd595674b5318c77c774b6c028bdbf05ad/bitarray-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7b16df7ddef03e30622d894cce86a560b7888415ddcac470eb99940a5adf1a35", size = 171179, upload-time = "2026-09-01T15:20:06.579Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/c8cb10050d1afed77e0511861ba49d56724fd846b4a0b596f52e7ebe5711/bitarray-3.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:ec3d8b99a92220bdba382acf4eff370d6a6cda34a5ce936726cb9e8cf2427c47", size = 166363, upload-time = "2026-09-01T15:20:07.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/74/c878e209fd15fdf0460763199892fc4e74c6a7345f5161ad673af586f2fe/bitarray-3.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:93a7f9f9f848ec79f19fdd39f3b8f750d9abf8e65cd6fb3a045df192de23855d", size = 170727, upload-time = "2026-09-01T15:20:08.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/f8/4463653a8c909796c8f8931d0fd64344c6745a144e432d0310435197b0ed/bitarray-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3889cf12ae0c04076772c8be66655bfe5925003b44dec056b569a68a121f7e93", size = 166746, upload-time = "2026-09-01T15:20:09.719Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/30/edaec4a0fffd04ee98b1425a597dc1d350c4def690b0a34815a75e5c2c33/bitarray-3.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:823638d0a900cea53c173fb31f5f1f4b913065a92fa3406d9b20eb7b7ebf189d", size = 383620, upload-time = "2026-09-01T15:20:10.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/1d/8fba2e884d5e5e7fa6828b4ca461e819259bac0f6f99073388157ea1c6d6/bitarray-3.11.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:301763ffde9d288c5a58dda51c5290bc1081c2bc0faf266c4aef227ca59004d5", size = 406908, upload-time = "2026-09-01T15:20:12.061Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/71/414853d582267d8e397ea5bf894503236d81660c5d7a34b2a0d0ec6e9f2d/bitarray-3.11.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:664f02e6e2c63fcc1c02718d2daa21b04f77a18ef3850a9b348597c0c8a0f222", size = 422031, upload-time = "2026-09-01T15:20:13.157Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/1c/35b3f34fee6f91f708eb81af03438afc137b2e5fc0e27ec11d948287f64c/bitarray-3.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52741a1d24a3723a8151259906cf1befeaf3d4ccf7d3e27e8be4334590b59bcd", size = 387692, upload-time = "2026-09-01T15:20:14.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/a9/7ebe3e04548cee84fba250425935a500ff02e7fef1b020ed182a3f5227c4/bitarray-3.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a1093eb1a756f08c7bdeb22969fb463aa62f036dc191ad371af5e292108c611", size = 381169, upload-time = "2026-09-01T15:20:15.515Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/48/cdf80b633f326451de801504f8718dd71ce63f534d29348822896d9c5bd2/bitarray-3.11.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ab0988829e4ed10fa971842e9fbf77595b8a4557a078a3589b0754f06417ddff", size = 404500, upload-time = "2026-09-01T15:20:16.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/cf/a64cab1bb0f48101ac45fb45a7f0d000af76e26ed0ab26b52573140a9a64/bitarray-3.11.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c4cabc2439ab3ce76881037741bf41e9fafda217ff721fdee92701e5fe3c8b9c", size = 402903, upload-time = "2026-09-01T15:20:18.127Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/b8/c3937572587a0503ea0a05ae61543916d6d93ffe48c26532cad9c4bfd9e4/bitarray-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9db61968efb834dacda6bcd7ef1f8a033253a53ac31426a684a28a7e4c9f2f29", size = 384582, upload-time = "2026-09-01T15:20:19.481Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/13/824648ef8f3fc14f56c4f9903e6c06979385f0c3a4d76371f6ccd8e9d1b2/bitarray-3.11.0-cp312-cp312-win32.whl", hash = "sha256:97cd5913000e4e56d211f7db053284d1f753aed34838368769bf084474a76f0a", size = 160902, upload-time = "2026-09-01T15:20:21.037Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/c4/0a1608187e6d21b30c341ebcce6f75efbc8fe289f87ad58aa4149ce59828/bitarray-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:5db38c0a36d07190c3629c65dab2504233a56b1c35d8b1cc3d7369943d028711", size = 171542, upload-time = "2026-09-01T15:20:22.114Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/59/f21b457eb6ab683ccf755093550ec1408e8ec8a66b980653c99740b85552/bitarray-3.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:9aafe7dc6d568470f5c297d0b15acddd3afc1677c983f0460b28f8cac4e9360c", size = 166561, upload-time = "2026-09-01T15:20:23.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/3d/578c4b63b3c1f206dfe7f7f60e941ca2d0aafd9da43943c076c01e976ec0/bitarray-3.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ac322a81c3c938c13093d8d77c419093f50118c29fbfa9eb80813d93af1a2d95", size = 170422, upload-time = "2026-09-01T15:20:24.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/30/3f982be25cf0c858351490b6d0cf6168e8668bb81e3daac25d8a3b2b8e44/bitarray-3.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ece8caff2fb31d6c69430d3dfc0efff70083e66b012f7f5de60cfe5dca98ba87", size = 166475, upload-time = "2026-09-01T15:20:25.649Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/ad/f94c267873f0c80705dcef602af0a8d3f42838197e8fff01033f2a8084c9/bitarray-3.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:962dc3e5622a08bb1c1ad429a9f7dace7efadd7fef1fcdf1628a6db142333256", size = 381856, upload-time = "2026-09-01T15:20:26.799Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/ce/e9ba5d080302c4d6fa269bccc7ad6ed4a5ad36a1dc75baae334714bbc183/bitarray-3.11.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de25b8c76fe9fe7d9c6bce2832096f020133e1d21e2841abd7280937bf90e56e", size = 405246, upload-time = "2026-09-01T15:20:27.888Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/4c/cce77df63010304704eb802d884d9b11c84f63a02d7c4081a41225e25343/bitarray-3.11.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:86dfaab663a2ad16ed2f6252116cf034cfc84282aa744f19a9a66542e85c9ca9", size = 420083, upload-time = "2026-09-01T15:20:29.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/1f/2c7b1a995af9814b6ec5ee69333a0a6131924ed0f3f10ffbd73c22c0a6ad/bitarray-3.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5605c8e74f95f77dd1b93c8c83d6fefe0d41d2704683d4a72152ca5284e2546d", size = 385572, upload-time = "2026-09-01T15:20:30.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/58/53845d1ddbe6077b61e7f39f586f7f35f18ce7dc47f178331e4fd64f59e1/bitarray-3.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d62e73fcdd273d07b1e9699de0e66e4d367b28c554c86bfe7c92029337c2a414", size = 379323, upload-time = "2026-09-01T15:20:31.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/66/b67544886466ede3d26e58c43d9841cc427d4fdcd81ded4087313687241a/bitarray-3.11.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9808734d1e5fd2839660a9db1abfe93e9d64b2aa9ed59aa480193ad93e8d620e", size = 402087, upload-time = "2026-09-01T15:20:33.113Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/a7/4ffc5984f3f4a12458adbceff60def49752f74807945771428f97694aec0/bitarray-3.11.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8b77aa18131418b869be882df1c5e229fb02f90861783bbe638d9ebe296e9209", size = 401000, upload-time = "2026-09-01T15:20:34.658Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/15/2d5dd4107e49b2c0040bc9cc48ee5af7f45772fedae3bc54fa987aeeedf2/bitarray-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d084fa59c94d3fec7eac6793e08162fa697aa4e70b9f519618c29d3e54eda752", size = 382431, upload-time = "2026-09-01T15:20:36.013Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/8b/8d95483688bcf297654c87b65ef1847fad794adb083d294da65ac275d4fc/bitarray-3.11.0-cp313-cp313-win32.whl", hash = "sha256:387b64dc54be3732a0988bf3731fc06d7d4f52dc9470d1a5479e8ce2b67d368b", size = 160760, upload-time = "2026-09-01T15:20:37.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/9b/b10ebf5e9a479815d2d54908617c0c55b255965389d3132927c9cd9ccf86/bitarray-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:b4afbb464a1db2d37858bd3b87bd59a76b4aae5e29ec007a9fd00bf5a911e1da", size = 171278, upload-time = "2026-09-01T15:20:38.646Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/3a/b0a29fc2379c567eb49730022c5913ad85b2f9ccda1b841737f756e1fb87/bitarray-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:36da946f64e10e28b23f345fde90f4d14e9c2e9013364fe04d9b2013e7291285", size = 166426, upload-time = "2026-09-01T15:20:39.862Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitstring"
|
||||||
|
version = "4.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "bitarray" },
|
||||||
|
{ name = "tibs" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/36/d3/de6fe4e7065df8c2f1ac1766f5fdccbe75bc18af2cf2dbeecd34d68e1518/bitstring-4.4.0.tar.gz", hash = "sha256:e682ac522bb63e041d16cbc9d0ca86a4f00194db16d0847c7efe066f836b2e37", size = 255209, upload-time = "2026-03-10T20:29:14.824Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/02/1a870bab76f2896d827aa4963be95e56675ffa1453e53525d13c43036edf/bitstring-4.4.0-py3-none-any.whl", hash = "sha256:feac49524fcf3ef27e6081e86f02b10d2adf6c3773bf22fbe0e7eea9534bc737", size = 76846, upload-time = "2026-03-10T20:29:12.832Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
@@ -11,6 +134,79 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "48.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/d3/eb4e394e587341fdad09a09101fa76478ead3a78b0ad63e55c22f0d75c02/cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a", size = 3951747, upload-time = "2026-06-09T22:31:23.871Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/8c/ce3823c06c2804f194f9e64f0d67fa3f4094a39f2bb1a990cd03603af8fc/cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a", size = 3742204, upload-time = "2026-06-09T22:31:34.773Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ecdsa"
|
||||||
|
version = "0.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/25/ca/8de7744cb3bc966c85430ca2d0fcaeea872507c6a4cf6e007f7fe269ed9d/ecdsa-0.19.2.tar.gz", hash = "sha256:62635b0ac1ca2e027f82122b5b81cb706edc38cd91c63dda28e4f3455a2bf930", size = 202432, upload-time = "2026-03-26T09:58:17.675Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl", hash = "sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399", size = 150818, upload-time = "2026-03-26T09:58:15.808Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esptool"
|
||||||
|
version = "4.12.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "argcomplete", marker = "sys_platform != 'win32'" },
|
||||||
|
{ name = "bitstring" },
|
||||||
|
{ name = "cryptography" },
|
||||||
|
{ name = "ecdsa" },
|
||||||
|
{ name = "intelhex" },
|
||||||
|
{ name = "pyserial" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "reedsolo" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/84/19/f68b27fd43564d69277e956d13b8c2b352e7f1af4fb5d6e5c43e5419e61c/esptool-4.12.0.tar.gz", hash = "sha256:d797f6529d582173508baf0572542ddadf53aae8c92c84ea9f420f242464103c", size = 463840, upload-time = "2026-07-14T08:36:51.891Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -20,11 +216,21 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "intelhex"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/37/1e7522494557d342a24cb236e2aec5d078fac8ed03ad4b61372586406b01/intelhex-2.3.0.tar.gz", hash = "sha256:892b7361a719f4945237da8ccf754e9513db32f5628852785aea108dcd250093", size = 44513, upload-time = "2020-10-20T20:35:51.526Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/78/79461288da2b13ed0a13deb65c4ad1428acb674b95278fa9abf1cefe62a2/intelhex-2.3.0-py2.py3-none-any.whl", hash = "sha256:87cc5225657524ec6361354be928adfd56bcf2a3dcc646c40f8f094c39c07db4", size = 50914, upload-time = "2020-10-20T20:35:50.162Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kota-technician-tool"
|
name = "kota-technician-tool"
|
||||||
version = "0.1.0"
|
version = "0.4.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "esptool" },
|
||||||
{ name = "pyserial" },
|
{ name = "pyserial" },
|
||||||
{ name = "pyside6-essentials" },
|
{ name = "pyside6-essentials" },
|
||||||
]
|
]
|
||||||
@@ -36,6 +242,7 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "esptool", specifier = "~=4.8" },
|
||||||
{ name = "pyserial", specifier = ">=3.5" },
|
{ name = "pyserial", specifier = ">=3.5" },
|
||||||
{ name = "pyside6-essentials", specifier = ">=6.7" },
|
{ name = "pyside6-essentials", specifier = ">=6.7" },
|
||||||
]
|
]
|
||||||
@@ -61,6 +268,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.21.0"
|
version = "2.21.0"
|
||||||
@@ -110,6 +326,52 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reedsolo"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f7/61/a67338cbecf370d464e71b10e9a31355f909d6937c3a8d6b17dd5d5beb5e/reedsolo-1.7.0.tar.gz", hash = "sha256:c1359f02742751afe0f1c0de9f0772cc113835aa2855d2db420ea24393c87732", size = 59723, upload-time = "2023-01-17T05:10:19.733Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/19/1bb346c0e581557c88946d2bb979b2bee8992e72314cfb418b5440e383db/reedsolo-1.7.0-py3-none-any.whl", hash = "sha256:2b6a3e402a1ee3e1eea3f932f81e6c0b7bbc615588074dca1dbbcdeb055002bd", size = 32360, upload-time = "2023-01-17T05:10:17.652Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shiboken6"
|
name = "shiboken6"
|
||||||
version = "6.11.2"
|
version = "6.11.2"
|
||||||
@@ -121,3 +383,35 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ce/6b/fee1b4814317e4664e318644c9c007676c65a71e5b2b9d3ac4121a94dc0a/shiboken6-6.11.2-cp310-abi3-win_amd64.whl", hash = "sha256:6ab0eba1c904455df621f9a6df3ca2bb896bab8670572d2bc4e37804ae91f19a", size = 1226578, upload-time = "2026-08-18T06:37:18.002Z" },
|
{ url = "https://files.pythonhosted.org/packages/ce/6b/fee1b4814317e4664e318644c9c007676c65a71e5b2b9d3ac4121a94dc0a/shiboken6-6.11.2-cp310-abi3-win_amd64.whl", hash = "sha256:6ab0eba1c904455df621f9a6df3ca2bb896bab8670572d2bc4e37804ae91f19a", size = 1226578, upload-time = "2026-08-18T06:37:18.002Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/b5/80c4e5aafeb8ac777cf5a1becfe87ac7197c35051df00b5f7819787e09d5/shiboken6-6.11.2-cp310-abi3-win_arm64.whl", hash = "sha256:97c49432488df958f0308736f3d15b6915d8826fc380af17bb4b9ec5c4a5e81b", size = 1784669, upload-time = "2026-08-18T06:37:21.046Z" },
|
{ url = "https://files.pythonhosted.org/packages/4d/b5/80c4e5aafeb8ac777cf5a1becfe87ac7197c35051df00b5f7819787e09d5/shiboken6-6.11.2-cp310-abi3-win_arm64.whl", hash = "sha256:97c49432488df958f0308736f3d15b6915d8826fc380af17bb4b9ec5c4a5e81b", size = 1784669, upload-time = "2026-08-18T06:37:21.046Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tibs"
|
||||||
|
version = "0.5.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/57/cd/6cf028decf1c2df4d26077dd5d0532587d93d4917233d5e004133166a940/tibs-0.5.7.tar.gz", hash = "sha256:173dfbecb2309edd9771f453580c88cf251e775613461566b23dbd756b3d54cb", size = 78255, upload-time = "2026-03-12T13:06:29.79Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/2d/de2c579d3eea0f18212b5b16decb04568b7a0ef912d00581a77492609d4e/tibs-0.5.7-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:859f05315ffb307d3474c505d694f3a547f00730a024c982f5f60316a5505b3c", size = 411352, upload-time = "2026-03-12T13:06:52.016Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/71/4c21ccc5c2e1672f9cd91ed2c46604c250cffd9d386113772dded128b5cf/tibs-0.5.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:a883ca13a922a66b2c1326a9c188123a574741a72510a4bf52fd6f97db191e44", size = 383971, upload-time = "2026-03-12T13:06:50.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/85/399940ac5393772792a209911a5efa42cf55cf621771e48b863211ac5a2a/tibs-0.5.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f70bd250769381c73110d6f24feaf8b6fcd44f680b3cb28a20ea06db3d04fb6f", size = 416256, upload-time = "2026-03-12T13:06:24.222Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/94/481a73e74d398949f57d297b1809a10a951d252e7ec94b6715ed952ce500/tibs-0.5.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76746f01b3db9dbd802f5e615f11f68df7a29ecef521b082dca53f3fa7d0084f", size = 428003, upload-time = "2026-03-12T13:06:23.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/e0/72db1760a7f7fec1d5f3690e0855fbbccbcf0a4a2fd318c9d71f3b33f3a7/tibs-0.5.7-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:847709c108800ad6a45efaf9a040628278956938a4897f7427a2587013dc3b98", size = 455589, upload-time = "2026-03-12T13:06:53.144Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/26/9cd3395914bf705d6ae1e9a6c323f727e9dc88fef716327ce7f486e0b55a/tibs-0.5.7-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad61df93b50f875b277ab736c5d37b6bce56f9abce489a22f4e02d9daa2966e3", size = 459266, upload-time = "2026-03-12T13:06:21.678Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/3b/267f19a008d13c704dc0b044138a56239272a43531ccb05464129d0fbd01/tibs-0.5.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e13b9c7ff2604b0146772025e1ac6f85c8c625bf6ac73736ff671eaf357dda41", size = 423466, upload-time = "2026-03-12T13:06:41.212Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/d4/424ae3515e0e013ad83186074bf3beb53399b9052c00da703415ccc316ca/tibs-0.5.7-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a7ce857ef05c59dc61abadc31c4b9b1e3c62f9e5fb29217988c308936aea71e", size = 452080, upload-time = "2026-03-12T13:06:32.112Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/15/ab80beba83a134745439d33763e1d3b017f994abeb9c309a3ac9fd94e90e/tibs-0.5.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d5521cc6768bfa6282a0c591ba06b079ab91b5c7d5696925ad2abac59779a54", size = 592311, upload-time = "2026-03-12T13:06:47.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/21/f5cf41c15431e63aeaefb494e714d48d9e9061b4e01fcc01d1987e2e5faa/tibs-0.5.7-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:477608f9b87e24a22ab6d50b81da04a5cb59bfa49598ff7ec5165035a18fb392", size = 703400, upload-time = "2026-03-12T13:06:16.968Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/ec/b3bdb7dcc3de8513c5678a685f4e25bb85ef48526d7d535ddc592f9e8602/tibs-0.5.7-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:ac0aa2aae38f7325c91c261ce1d18f769c4c7033c98d6ea3ea5534585cf16452", size = 664623, upload-time = "2026-03-12T13:06:48.894Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/71/7b85af3ad1b2cd9871c8f50ba0eb17e54e12481b467678535e58aced0d98/tibs-0.5.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b56583db148e5094d781c3d746815dbcbb6378c6f813c8ce291efd4ab21da8b", size = 635199, upload-time = "2026-03-12T13:06:34.798Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/63/60220fb502beb857306afd4a5bac4a8617ae496f3b1f4968d127380fdefe/tibs-0.5.7-cp38-abi3-win32.whl", hash = "sha256:d4f3ff613d486650816bc5516760c0382a2cc0ca8aeddd8914d011bc3b81d9a2", size = 288454, upload-time = "2026-03-12T13:06:30.978Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/ab/aab78827ba7e0d65fe346b86d1d61e0792c38d5f9b7547e0f71b7027c835/tibs-0.5.7-cp38-abi3-win_amd64.whl", hash = "sha256:a61d36155f8ab8642e1b6744e13822f72050fc7ec4f86ec6965295afa04949e2", size = 304135, upload-time = "2026-03-12T13:06:35.884Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/59/e9e6a610928a4bcbf04f0ac1436ee320aa8cbe95181f1aa32687c50e858b/tibs-0.5.7-cp38-abi3-win_arm64.whl", hash = "sha256:130bc68ff500fc8185677df7a97350b5d5339e6ba7e325bc3031337f6424ede7", size = 289272, upload-time = "2026-03-12T13:06:19.247Z" },
|
||||||
|
]
|
||||||
Reference in new issue
Block a user