Compare commits
27
Commits
mux-v0.1.0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c539133488
|
||
|
|
d353c2acdf
|
||
|
|
2a97e87c16
|
||
|
|
8066f5135e
|
||
|
|
1c93b77d0b
|
||
|
|
e60465f181
|
||
|
|
35aecf07fc
|
||
|
|
c78e2ceaa9
|
||
|
|
d67c0699c9
|
||
|
|
adc76921e1
|
||
|
|
acd97966aa
|
||
|
|
317236acd1
|
||
|
|
c145065642
|
||
|
|
dee5b46716
|
||
|
|
5728d57caf
|
||
|
|
bdb1897b2f
|
||
|
|
4760615fc9
|
||
|
|
ef2b574ae2
|
||
|
|
073d1d5bdb
|
||
|
|
c7f145c787
|
||
|
|
82701c183b
|
||
|
|
7cb1d7a8f8
|
||
|
|
bd23468235
|
||
|
|
9bad8f7304
|
||
|
|
c2c56f3886
|
||
|
|
0d07bd2f83
|
||
|
|
aebb8aedb8
|
No files matched your search
@@ -1,2 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
.hypothesis/
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
||||||
|
|
||||||
## [0.1.0] - Unreleased
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Transparent LoRa radio over USB CDC-ACM (VID `1209`, PID `5741`)
|
|
||||||
- COBS-framed fixed-size LE binary protocol (9 commands, 7 responses)
|
|
||||||
- Radio configuration: frequency, bandwidth, spreading factor, coding rate, TX power
|
|
||||||
- Continuous RX mode with RSSI/SNR per packet
|
|
||||||
- Single-shot TX with optional per-packet config override
|
|
||||||
- SSD1306 OLED display support with status dashboard
|
|
||||||
- Radio state, frequency, modulation parameters
|
|
||||||
- Packet counters (RX/TX)
|
|
||||||
- RSSI sparkline (1-minute history, TX shown as dotted bars)
|
|
||||||
- Combined splash/waiting screen on disconnect
|
|
||||||
- Auto-detect USB host connect/disconnect via DTR
|
|
||||||
- Config validation against per-board hardware limits
|
|
||||||
- `TX_POWER_MAX` sentinel (`-128`): auto-use the board's maximum TX power
|
|
||||||
- Board support: Heltec V3, Heltec V4, RAK WisBlock 4631
|
|
||||||
- Python host library (`clients/python/`) with `pip install` and `donglora-mux` CLI
|
|
||||||
- USB multiplexer for sharing one dongle with multiple applications
|
|
||||||
- Two-way LoRa bridge over TCP (`examples/lora_bridge.py`)
|
|
||||||
|
|
||||||
### Technical
|
|
||||||
|
|
||||||
- Embassy async runtime (no threads, CPU sleeps when idle)
|
|
||||||
- Zero-config: boots and waits for host commands
|
|
||||||
- Protocol-agnostic: firmware has no opinion about what you transmit
|
|
||||||
- Tasks never panic: all errors reported to host or logged
|
|
||||||
- Board abstraction: one `.rs` file + one Cargo feature per board
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# Contributing to DongLoRa
|
|
||||||
|
|
||||||
## Adding Board Support
|
|
||||||
|
|
||||||
DongLoRa is designed to make adding new boards easy:
|
|
||||||
|
|
||||||
1. Create `firmware/src/board/your_board.rs` implementing the `init()` function
|
|
||||||
2. Add a Cargo feature in `firmware/Cargo.toml` with the board's dependencies
|
|
||||||
3. Add the board's feature/target/chip to the `justfile` boards list
|
|
||||||
4. `firmware/build.rs` auto-discovers the new file and generates the module selector
|
|
||||||
|
|
||||||
See existing board files (`heltec_v3.rs`, `rak_wisblock_4631.rs`) as templates.
|
|
||||||
Each board file owns all hardware init and returns peripheral bundles that the
|
|
||||||
common tasks consume. No shared board code — each board is self-contained.
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
- **Clippy-clean.** `just clippy <board>` must pass with no warnings.
|
|
||||||
- **No panics in tasks.** Use `match`/`if let`/`warn!` — never `.unwrap()` in async task code. Board init (runs once at startup) may use `.expect("reason")`.
|
|
||||||
- **Display rendering uses `.ok()`.** Drawing is best-effort; display errors are not recoverable.
|
|
||||||
- **Every `let _ =` gets an inline comment** explaining why the result is discarded.
|
|
||||||
|
|
||||||
## Client Libraries
|
|
||||||
|
|
||||||
Client libraries exist in [Python](clients/python/) and [Rust](clients/rust/).
|
|
||||||
To add a library in another language:
|
|
||||||
|
|
||||||
1. Create a directory with the language's standard project layout
|
|
||||||
2. Implement the wire protocol from [firmware/PROTOCOL.md](firmware/PROTOCOL.md)
|
|
||||||
3. Include device discovery (USB VID `1209`, PID `5741`)
|
|
||||||
4. Include COBS framing, command encoding, and response decoding
|
|
||||||
5. Add a mux client (connect to the mux daemon's Unix socket or TCP)
|
|
||||||
|
|
||||||
The protocol is intentionally simple — 8 commands, 7 responses, fixed-size LE.
|
|
||||||
A minimal client library should be implementable in a weekend.
|
|
||||||
|
|
||||||
## Building and Testing
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd firmware
|
|
||||||
just check-all # Compile-check all boards
|
|
||||||
just build-all # Build release firmware for all boards
|
|
||||||
just clippy <board> # Lint a specific board
|
|
||||||
just test # Host-side protocol unit tests
|
|
||||||
```
|
|
||||||
|
|
||||||
Tool versions are pinned in each project's `mise.toml`.
|
|
||||||
Tools are installed automatically on first run.
|
|
||||||
|
|
||||||
## Commits
|
|
||||||
|
|
||||||
- [Conventional Commits](https://www.conventionalcommits.org/) format
|
|
||||||
- Logically grouped (one concern per commit)
|
|
||||||
- We use [jj](https://martinvonz.github.io/jj/) for version control
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
# Design Principles
|
|
||||||
|
|
||||||
These are the non-negotiable first principles of this project. Every design decision,
|
|
||||||
naming choice, module boundary, and abstraction is evaluated against them. When in
|
|
||||||
doubt, choose simplicity and clarity over cleverness.
|
|
||||||
|
|
||||||
## What This Is
|
|
||||||
|
|
||||||
A transparent LoRa radio over USB. Plug the dongle into your PC, talk to any LoRa
|
|
||||||
radio within range. That's it.
|
|
||||||
|
|
||||||
## What This Is Not
|
|
||||||
|
|
||||||
Not a mesh network node. Not a Meshtastic device. Not a protocol implementation. Not
|
|
||||||
an application framework. This firmware has no opinion about what you do with your
|
|
||||||
radio — it just gives you clean access to it.
|
|
||||||
|
|
||||||
## Principles
|
|
||||||
|
|
||||||
1. **Single purpose.** Transparent LoRa radio over USB. No mesh, no routing, no
|
|
||||||
application logic. Ever.
|
|
||||||
|
|
||||||
2. **Protocol-agnostic.** The firmware is a dumb pipe. It exposes clean LoRa
|
|
||||||
parameters (frequency, bandwidth, spreading factor, coding rate, sync word, TX
|
|
||||||
power) and nothing else. No raw register access, no chip-specific leakage.
|
|
||||||
|
|
||||||
3. **Host-driven.** The firmware makes zero autonomous decisions. The radio idles on
|
|
||||||
boot until the host commands it. No persistence across reboots. The host is always
|
|
||||||
in control.
|
|
||||||
|
|
||||||
4. **Zero-config.** Boots, enumerates USB, waits for commands. The host plugs in and
|
|
||||||
goes.
|
|
||||||
|
|
||||||
5. **Robustness over features.** Tasks never panic. Errors are reported to the host.
|
|
||||||
The radio self-recovers. Ship fewer features, but make them bulletproof.
|
|
||||||
|
|
||||||
6. **Minimal footprint.** Avoid heavy dependencies. Keep the binary small. Every
|
|
||||||
dependency must justify its inclusion.
|
|
||||||
|
|
||||||
7. **Auto-detect peripherals.** If the board has a display, keyboard, or sensors,
|
|
||||||
use them. No user configuration required.
|
|
||||||
|
|
||||||
8. **World-class Rust.** Fully leverage the type system for safety. Strict `no_std`.
|
|
||||||
Always clippy-clean. Thoughtfully designed — everything has an obvious home.
|
|
||||||
Anyone who reads this code should immediately think *"this is nice."*
|
|
||||||
|
|
||||||
9. **Easy board support.** Adding a new board means dropping a `.rs` file and adding
|
|
||||||
a Cargo feature. Nothing else changes.
|
|
||||||
|
|
||||||
10. **Support all common LoRa boards.** This project supports every common LoRa
|
|
||||||
board, and even some uncommon ones. The architecture is designed to accommodate
|
|
||||||
them cleanly. If the code isn't structured for a new board, fix the code.
|
|
||||||
|
|
||||||
11. **Minimal overhead.** The dongle should add as little latency as possible
|
|
||||||
between the radio and the host. No unnecessary copies, no blocking operations
|
|
||||||
in the radio path, no computation that isn't strictly needed.
|
|
||||||
|
|
||||||
12. **Standard toolchain.** No custom Rust forks or special SDKs. Use stock `rustup`
|
|
||||||
channels (stable, nightly). Xtensa targets use nightly + `-Zbuild-std`; Cortex-M
|
|
||||||
and RISC-V targets use stable. Boards that need the `esp` toolchain (via `espup`)
|
|
||||||
are supported but gracefully skipped when the toolchain isn't installed.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Host PC
|
|
||||||
│ USB CDC-ACM (COBS-framed fixed-size LE)
|
|
||||||
▼
|
|
||||||
usb_task ──Command──► radio_task ──► SX1262
|
|
||||||
◄──Response── │
|
|
||||||
StatusWatch
|
|
||||||
▼
|
|
||||||
display_task (optional) ──► OLED/TFT
|
|
||||||
```
|
|
||||||
|
|
||||||
Each task exclusively owns its peripheral. No shared state except typed channels and
|
|
||||||
a watch signal. Pure actor model.
|
|
||||||
|
|
||||||
## Peripheral Model
|
|
||||||
|
|
||||||
Boards can have optional peripherals beyond the radio and USB. The board's
|
|
||||||
`into_parts()` returns `Option<T>` slots for each, and the corresponding task is
|
|
||||||
only spawned when present.
|
|
||||||
|
|
||||||
| Peripheral | Slot | Task | Examples |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Display | `Option<DisplayParts>` | `display_task` | SSD1306 OLED, ST7789 TFT |
|
|
||||||
| Input | `Option<InputParts>` | `input_task` | T-Deck keyboard, buttons |
|
|
||||||
| Telemetry | `Option<TelemetryParts>` | `telemetry_task` | Battery voltage, temperature |
|
|
||||||
|
|
||||||
**Board files are always separate.** A T-Deck is `t_deck.rs`, not a variant of
|
|
||||||
Heltec V3. Each board owns its own init code and pin mappings. Shared logic between
|
|
||||||
boards that use the same MCU can be factored into helper modules if it earns its
|
|
||||||
keep, but never prematurely.
|
|
||||||
|
|
||||||
## Technical Decisions
|
|
||||||
|
|
||||||
| Decision | Choice | Why |
|
|
||||||
|---|---|---|
|
|
||||||
| USB framing | COBS | Compact, `no_std`, zero-overhead frame delimiting |
|
|
||||||
| Serialization | Fixed-size LE | Every field at a known offset. No varints, no surprises |
|
|
||||||
| Async runtime | Embassy | De facto standard for async embedded Rust |
|
|
||||||
| Display | Radio status dashboard | Freq, BW, SF, packet counts, RSSI/SNR |
|
|
||||||
| Boot behavior | Idle until host commands | Host-driven principle |
|
|
||||||
| Persistence | None | Always start fresh, true dumb pipe |
|
|
||||||
| Board codegen | Jinja2 via build.rs | Auto-discovers boards from filesystem |
|
|
||||||
| Rust toolchain | Stock `rustup` (stable/nightly) | No forks; `espup` for Xtensa boards only |
|
|
||||||
| Firmware output | `firmware/builds/donglora-{board}-{profile}.elf` | Readable names, not buried in `target/` |
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 Stephen Waits <steve@waits.net>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,214 +1,17 @@
|
|||||||
# DongLoRa
|
# DongLoRa has moved
|
||||||
|
|
||||||
**/ˈdɒŋ.ɡəl.ɔːr.ə/** — a [portmanteau](https://en.wikipedia.org/wiki/Portmanteau) of **dongle** and **LoRa**.
|
This repository has been split into individual repos under the
|
||||||
|
[**donglora** organization](https://git.swaits.com/donglora).
|
||||||
|
|
||||||
Transparent LoRa radio over USB. Flash the firmware onto a supported
|
| What | New location |
|
||||||
board, plug it in, and send/receive LoRa packets from your computer.
|
|------|--------------|
|
||||||
The firmware handles the radio; your code decides what to do with it.
|
| Firmware | [donglora/firmware](https://git.swaits.com/donglora/firmware) |
|
||||||
|
| Rust client library | [donglora/client-rs](https://git.swaits.com/donglora/client-rs) |
|
||||||
|
| Python client library | [donglora/client-py](https://git.swaits.com/donglora/client-py) |
|
||||||
|
| Rust mux daemon | [donglora/mux-rs](https://git.swaits.com/donglora/mux-rs) |
|
||||||
|
| Python mux daemon | [donglora/mux-py](https://git.swaits.com/donglora/mux-py) |
|
||||||
|
| MeshCore bridge | [donglora/bridge](https://git.swaits.com/donglora/bridge) |
|
||||||
|
| MeshCore AI bot | [donglora/ai-bot](https://git.swaits.com/donglora/ai-bot) |
|
||||||
|
| Example scripts | [donglora/examples](https://git.swaits.com/donglora/examples) |
|
||||||
|
|
||||||
Any LoRa board becomes a reusable peripheral that works with any host —
|
This repo is archived and will not receive further updates.
|
||||||
a laptop, a desktop, a Raspberry Pi, a server. The built-in multiplexer
|
|
||||||
lets multiple applications share one dongle at the same time, so you can
|
|
||||||
run a MeshCore bot, a packet analyzer, and a bridge all on the same
|
|
||||||
radio without dedicated hardware for each.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
```
|
|
||||||
Your code ──► USB serial ──► firmware ──► LoRa radio
|
|
||||||
|
|
||||||
Your code ──► client library ──► USB serial ──► firmware ──► LoRa radio
|
|
||||||
|
|
||||||
Your code ──► client library ──┐
|
|
||||||
Your code ──► client library ──┤
|
|
||||||
... ├──► mux daemon ──► USB serial ──► firmware ──► LoRa radio
|
|
||||||
Your code ──► client library ──┤
|
|
||||||
Your code ──► client library ──┘
|
|
||||||
```
|
|
||||||
|
|
||||||
Four components, each self-contained with its own dependencies and tooling:
|
|
||||||
|
|
||||||
| Component | What it is | Language |
|
|
||||||
|-----------|-----------|----------|
|
|
||||||
| **[firmware/](firmware/)** | Embedded firmware flashed onto a LoRa board. Speaks a simple [binary protocol](firmware/PROTOCOL.md) over USB. | Rust |
|
|
||||||
| **[clients/](clients/)** | Client libraries that handle device discovery, COBS framing, and protocol encoding/decoding. This is what your code imports. | [Python](clients/python/), [Rust](clients/rust/) |
|
|
||||||
| **[mux/](mux/)** | Optional daemon that lets multiple applications share one dongle. Owns the USB port, exposes a Unix socket (and optional TCP). | [Python](mux/python/), [Rust](mux/rust/) |
|
|
||||||
| **[examples/](examples/)** | Ready-to-run scripts demonstrating the client library — receive, transmit, bridge, MeshCore decoder, AI bot. | Python |
|
|
||||||
|
|
||||||
The client library can talk directly to the firmware over USB, or connect
|
|
||||||
through the mux when multiple applications need the same dongle.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
1. Get a [supported board](#supported-boards)
|
|
||||||
2. `cd firmware && just flash heltec_v4`
|
|
||||||
3. `cd examples && just rx`
|
|
||||||
4. See packets
|
|
||||||
|
|
||||||
## Supported Boards
|
|
||||||
|
|
||||||
| Board | MCU | Radio | Display |
|
|
||||||
|-------|-----|-------|---------|
|
|
||||||
| Heltec V3 | ESP32-S3 | SX1262 | SSD1306 OLED |
|
|
||||||
| Heltec V4 | ESP32-S3 | SX1262 | SSD1315 OLED |
|
|
||||||
| RAK WisBlock 4631 | nRF52840 | SX1262 | SSD1306 OLED (optional) |
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
Dependencies are handled automatically:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd examples
|
|
||||||
just rx # receive packets
|
|
||||||
just tx # transmit a packet
|
|
||||||
just ping-pong --role tx # two-dongle ping-pong demo
|
|
||||||
just test-commands # exercise all DongLoRa commands
|
|
||||||
just bridge --mode server # LoRa bridge over TCP
|
|
||||||
just meshcore # MeshCore packet decoder
|
|
||||||
just orac # MeshCore AI bot (needs ANTHROPIC_API_KEY)
|
|
||||||
just telemetry # MeshCore repeater telemetry monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
## Multiplexer
|
|
||||||
|
|
||||||
To share one dongle across multiple applications, run the mux daemon:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Python
|
|
||||||
cd mux/python
|
|
||||||
just run # start the mux daemon
|
|
||||||
just verbose # start with verbose logging
|
|
||||||
just run --tcp 5741 --port /dev/ttyACM0 # with explicit options
|
|
||||||
|
|
||||||
# Rust
|
|
||||||
cd mux/rust
|
|
||||||
just run # start the mux daemon
|
|
||||||
just verbose # start with verbose logging
|
|
||||||
just run -- --tcp 5741 --port /dev/ttyACM0 # with explicit options
|
|
||||||
```
|
|
||||||
|
|
||||||
The mux owns the USB serial port exclusively and exposes a Unix socket
|
|
||||||
(and optional TCP) speaking the same protocol. Client libraries connect
|
|
||||||
through it transparently.
|
|
||||||
|
|
||||||
| Script | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| [`simple_rx.py`](examples/simple_rx.py) | Configure radio, receive and print packets |
|
|
||||||
| [`simple_tx.py`](examples/simple_tx.py) | Transmit a single packet |
|
|
||||||
| [`ping_pong.py`](examples/ping_pong.py) | Two-dongle demo (`--role tx` / `--role rx`) |
|
|
||||||
| [`all_commands.py`](examples/all_commands.py) | Exercise all 9 commands |
|
|
||||||
| [`lora_bridge.py`](examples/lora_bridge.py) | Two-way LoRa bridge over TCP (works over Tailscale, WireGuard, etc.) |
|
|
||||||
| [`meshcore/`](examples/meshcore/) | Full MeshCore packet decoder, AI bot, telemetry monitor |
|
|
||||||
|
|
||||||
## Device Permissions (Linux)
|
|
||||||
|
|
||||||
Your user needs read/write access to the serial device (`/dev/ttyACM*` or
|
|
||||||
`/dev/ttyUSB*`). The cleanest fix is a udev rule:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# /etc/udev/rules.d/99-donglora.rules
|
|
||||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="5741", MODE="0666"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then reload:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo udevadm control --reload-rules && sudo udevadm trigger
|
|
||||||
```
|
|
||||||
|
|
||||||
Without this, you'll get permission errors when opening the serial port.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
Requires [just](https://github.com/casey/just) and [mise](https://mise.jdx.dev/).
|
|
||||||
Everything else is installed automatically on first run.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd firmware
|
|
||||||
just build heltec_v4 # build a specific board
|
|
||||||
just build-all # build firmware for all boards
|
|
||||||
just flash heltec_v4 # build + flash
|
|
||||||
just check-all # compile-check only
|
|
||||||
just clippy heltec_v4 # lint
|
|
||||||
just test # host-side protocol unit tests
|
|
||||||
```
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
### Now: Solidify the Foundation
|
|
||||||
|
|
||||||
- [ ] Test on more boards (Heltec V2, T-Beam Supreme, T-Echo, RAK 3112)
|
|
||||||
- [ ] Python client library on PyPI — the existing library, properly packaged
|
|
||||||
- [ ] CI for all boards — automated build/clippy/size-check on every commit
|
|
||||||
- [ ] Protocol documentation polish
|
|
||||||
|
|
||||||
### Next: Cross-Language Client Libraries
|
|
||||||
|
|
||||||
The protocol is 8 commands over COBS-framed LE. Any language that can
|
|
||||||
open a serial port can speak DongLoRa.
|
|
||||||
|
|
||||||
- [x] **Rust** crate — async, tokio-native, zero-copy COBS
|
|
||||||
- [ ] **Go** module — goroutine-friendly, cross-platform
|
|
||||||
- [ ] **C** library — `libdonglora` for FFI from any language
|
|
||||||
- [ ] **Ruby** gem — simple protocol, weekend gem, new community
|
|
||||||
- [ ] **Python** on PyPI — type hints and async support
|
|
||||||
|
|
||||||
### Then: Infrastructure and Tooling
|
|
||||||
|
|
||||||
- [x] Cross-platform mux daemon rewritten in Rust (single static binary, no Python required)
|
|
||||||
- [ ] `donglora-ctl` CLI — ping, configure, receive, transmit, scan frequencies. Think `ip link` for LoRa.
|
|
||||||
- [ ] Protocol versioning — version handshake so client libraries can detect firmware capabilities
|
|
||||||
- [ ] Firmware OTA over USB — field-upgradeable without a flash tool
|
|
||||||
|
|
||||||
### Future: Things That Would Be Amazing
|
|
||||||
|
|
||||||
All host-driven, firmware stays dumb. Every item passes the pipe test.
|
|
||||||
|
|
||||||
- [ ] **Spectrum analyzer** — host sweeps frequencies, radio reports RSSI at each step. Instant RF site survey tool.
|
|
||||||
- [ ] **Multi-dongle coordination** — one host, multiple dongles on different frequencies. Frequency-hopping, parallel monitoring, diversity reception — all host-driven.
|
|
||||||
- [ ] **LoRa packet capture** — PCAP-NG with a LoRa link type. Wireshark dissectors. Record, replay, analyze LoRa traffic with standard tools.
|
|
||||||
- [ ] **Time-synchronized RX** — "start RX at T, stop at T+N" for precise duty-cycle control and TDMA-style protocols. Firmware just follows the schedule.
|
|
||||||
- [ ] **Raw IQ streaming** — SDR-lite for LoRa. Demodulation and signal analysis on the host.
|
|
||||||
- [ ] **Mesh protocol test harness** — host-side framework for testing any mesh protocol over real radios. Inject packets, measure latency, simulate topology. The dongle is just the radio — test logic lives entirely on the host.
|
|
||||||
|
|
||||||
*Have an idea? Open an issue. The protocol is stable and the firmware is
|
|
||||||
intentionally boring — the interesting stuff happens on the host.*
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Board Roadmap</strong></summary>
|
|
||||||
|
|
||||||
### Next Up
|
|
||||||
|
|
||||||
| Board | MCU | Radio |
|
|
||||||
|-------|-----|-------|
|
|
||||||
| Heltec V2 | ESP32 | SX1276 |
|
|
||||||
| Heltec Wireless Tracker | ESP32-S3 | SX1262 |
|
|
||||||
| Heltec Vision Master E213/E290 | ESP32-S3 | SX1262 |
|
|
||||||
| LilyGo T-Beam (SX1262) | ESP32 | SX1262 |
|
|
||||||
| LilyGo T-Beam Supreme | ESP32-S3 | SX1262 |
|
|
||||||
| LilyGo T-Echo | nRF52840 | SX1262 |
|
|
||||||
| LilyGo T3 S3 | ESP32-S3 | SX1262 |
|
|
||||||
| Seeed Studio Xiao nRF52 WIO | nRF52840 | SX1262 |
|
|
||||||
|
|
||||||
### Future
|
|
||||||
|
|
||||||
| Board | MCU | Radio |
|
|
||||||
|-------|-----|-------|
|
|
||||||
| Elecrow ThinkNode M1-M6 | various | various |
|
|
||||||
| Heltec Wireless Paper / MeshPocket / T114 | various | SX1262 |
|
|
||||||
| LilyGo T-Deck / T-Echo Lite / LoRa32 | various | various |
|
|
||||||
| RAK WisBlock 3112 / WisMesh variants | various | SX1262 |
|
|
||||||
| RPI Pico 2040 + WaveShare SX1262 | RP2040 | SX1262 |
|
|
||||||
| Seeed Studio SenseCAP / Xiao variants | various | SX1262 |
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](LICENSE) — Stephen Waits
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.venv/
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# DongLoRa Python Client
|
|
||||||
|
|
||||||
Python client library for talking to a DongLoRa device — either directly
|
|
||||||
over USB or through the [mux daemon](../../mux/).
|
|
||||||
|
|
||||||
## What's in Here
|
|
||||||
|
|
||||||
- `donglora/` — the library: device discovery, COBS framing, protocol
|
|
||||||
encoding/decoding, mux client support
|
|
||||||
- Installable as `donglora` via pip/uv
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```python
|
|
||||||
import donglora as dl
|
|
||||||
|
|
||||||
ser = dl.connect()
|
|
||||||
dl.send(ser, "SetConfig", config=dl.DEFAULT_CONFIG)
|
|
||||||
dl.send(ser, "StartRx")
|
|
||||||
while True:
|
|
||||||
pkt = dl.recv(ser)
|
|
||||||
if pkt:
|
|
||||||
print(pkt["rssi"], pkt["payload"].hex())
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- `cobs` — COBS framing
|
|
||||||
- `pyserial` — USB serial communication
|
|
||||||
|
|
||||||
Optional extras: `meshcore` (crypto), `orac` (AI bot).
|
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
"""DongLoRa host library — connect, configure, send/receive LoRa packets.
|
|
||||||
|
|
||||||
Implements the DongLoRa USB protocol (COBS-framed fixed-size LE).
|
|
||||||
See firmware/PROTOCOL.md for the full specification.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
import serial
|
|
||||||
import time
|
|
||||||
from cobs import cobs
|
|
||||||
|
|
||||||
# ── USB device discovery ──────────────────────────────────────────
|
|
||||||
|
|
||||||
USB_VID_PID = "1209:5741"
|
|
||||||
|
|
||||||
|
|
||||||
def find_port() -> str | None:
|
|
||||||
"""Find the DongLoRa serial port by USB VID:PID."""
|
|
||||||
for path in sorted(glob.glob("/dev/ttyACM*")) + sorted(glob.glob("/dev/ttyUSB*")):
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["udevadm", "info", "--query=property", f"--name={path}"],
|
|
||||||
capture_output=True, text=True, timeout=2,
|
|
||||||
)
|
|
||||||
props = dict(
|
|
||||||
line.split("=", 1) for line in result.stdout.splitlines() if "=" in line
|
|
||||||
)
|
|
||||||
vid = props.get("ID_VENDOR_ID", "").lower()
|
|
||||||
pid = props.get("ID_MODEL_ID", "").lower()
|
|
||||||
if f"{vid}:{pid}" == USB_VID_PID:
|
|
||||||
return path
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
ports = sorted(glob.glob("/dev/ttyACM*"))
|
|
||||||
return ports[0] if ports else None
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_device() -> str:
|
|
||||||
"""Block until a DongLoRa device appears."""
|
|
||||||
print("Waiting for DongLoRa...", end="", flush=True)
|
|
||||||
while True:
|
|
||||||
port = find_port()
|
|
||||||
if port:
|
|
||||||
print(f" found {port}")
|
|
||||||
time.sleep(0.3)
|
|
||||||
return port
|
|
||||||
print(".", end="", flush=True)
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
|
|
||||||
# ── COBS framing ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def cobs_encode(data: bytes) -> bytes:
|
|
||||||
return cobs.encode(data) + b"\x00"
|
|
||||||
|
|
||||||
|
|
||||||
def read_frame(ser: serial.Serial) -> bytes | None:
|
|
||||||
"""Read one COBS frame. Returns decoded bytes or None on timeout."""
|
|
||||||
buf = b""
|
|
||||||
while True:
|
|
||||||
b = ser.read(1)
|
|
||||||
if not b:
|
|
||||||
return None
|
|
||||||
if b == b"\x00":
|
|
||||||
break
|
|
||||||
buf += b
|
|
||||||
if not buf:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return cobs.decode(buf)
|
|
||||||
except cobs.DecodeError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# ── Fixed-size LE encoding ────────────────────────────────────────
|
|
||||||
|
|
||||||
def encode_config(cfg: dict) -> bytes:
|
|
||||||
"""Encode a RadioConfig to 13 fixed-size LE bytes."""
|
|
||||||
return struct.pack("<IBBBHBHB",
|
|
||||||
cfg["freq_hz"],
|
|
||||||
cfg["bw"],
|
|
||||||
cfg["sf"],
|
|
||||||
cfg["cr"],
|
|
||||||
cfg["sync_word"],
|
|
||||||
cfg["tx_power_dbm"] & 0xFF, # i8 as unsigned byte
|
|
||||||
cfg.get("preamble_len", 0), # 0 = firmware default (16)
|
|
||||||
cfg.get("cad", 1), # 0 = off, 1 = on (default)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def encode_command(cmd: str, **kwargs) -> bytes:
|
|
||||||
"""Encode a command to fixed-size LE bytes."""
|
|
||||||
tags = {
|
|
||||||
"Ping": 0, "GetConfig": 1, "SetConfig": 2, "StartRx": 3,
|
|
||||||
"StopRx": 4, "Transmit": 5, "DisplayOn": 6, "DisplayOff": 7,
|
|
||||||
"GetMac": 8,
|
|
||||||
}
|
|
||||||
out = bytes([tags[cmd]])
|
|
||||||
if cmd == "SetConfig":
|
|
||||||
out += encode_config(kwargs["config"])
|
|
||||||
elif cmd == "Transmit":
|
|
||||||
config = kwargs.get("config")
|
|
||||||
if config is None:
|
|
||||||
out += b"\x00"
|
|
||||||
else:
|
|
||||||
out += b"\x01" + encode_config(config)
|
|
||||||
payload = kwargs["payload"]
|
|
||||||
out += struct.pack("<H", len(payload)) + payload
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
# ── Response decoding ────────────────────────────────────────────
|
|
||||||
|
|
||||||
def decode_response(data: bytes) -> dict:
|
|
||||||
"""Decode a fixed-size LE response."""
|
|
||||||
if not data:
|
|
||||||
return {"type": "Empty"}
|
|
||||||
tag = data[0]
|
|
||||||
rest = data[1:]
|
|
||||||
if tag == 0:
|
|
||||||
return {"type": "Pong"}
|
|
||||||
elif tag == 1:
|
|
||||||
if len(rest) >= 13:
|
|
||||||
freq_hz, bw, sf, cr, sync_word, pwr, preamble_len, cad = struct.unpack_from("<IBBBHBHB", rest, 0)
|
|
||||||
tx_power_dbm = struct.unpack_from("<b", rest, 9)[0]
|
|
||||||
return {"type": "Config", "freq_hz": freq_hz, "bw": bw, "sf": sf,
|
|
||||||
"cr": cr, "sync_word": sync_word, "tx_power_dbm": tx_power_dbm,
|
|
||||||
"preamble_len": preamble_len, "cad": cad}
|
|
||||||
return {"type": "Config", "raw": rest.hex()}
|
|
||||||
elif tag == 2:
|
|
||||||
rssi = struct.unpack_from("<h", rest, 0)[0]
|
|
||||||
snr = struct.unpack_from("<h", rest, 2)[0]
|
|
||||||
plen = struct.unpack_from("<H", rest, 4)[0]
|
|
||||||
payload = rest[6:6 + plen]
|
|
||||||
return {"type": "RxPacket", "rssi": rssi, "snr": snr, "payload": payload}
|
|
||||||
elif tag == 3:
|
|
||||||
return {"type": "TxDone"}
|
|
||||||
elif tag == 4:
|
|
||||||
return {"type": "Ok"}
|
|
||||||
elif tag == 5:
|
|
||||||
code = rest[0] if rest else -1
|
|
||||||
return {"type": "Error", "code": code}
|
|
||||||
elif tag == 6:
|
|
||||||
if len(rest) >= 6:
|
|
||||||
mac = ":".join(f"{b:02X}" for b in rest[:6])
|
|
||||||
return {"type": "MacAddress", "mac": mac}
|
|
||||||
return {"type": "MacAddress", "raw": rest.hex()}
|
|
||||||
else:
|
|
||||||
return {"type": f"Unknown({tag})"}
|
|
||||||
|
|
||||||
|
|
||||||
# ── High-level helpers ───────────────────────────────────────────
|
|
||||||
|
|
||||||
# Buffer for RxPacket frames encountered while waiting for solicited responses.
|
|
||||||
# These are real received LoRa data — don't discard them.
|
|
||||||
_rx_queue: collections.deque = collections.deque(maxlen=256)
|
|
||||||
|
|
||||||
|
|
||||||
def send(ser: serial.Serial, cmd: str, **kwargs) -> dict:
|
|
||||||
"""Send a command and return the solicited response.
|
|
||||||
|
|
||||||
Any unsolicited RxPacket frames encountered while waiting are buffered
|
|
||||||
in the module-level receive queue (retrievable via recv/drain_rx).
|
|
||||||
"""
|
|
||||||
ser.write(cobs_encode(encode_command(cmd, **kwargs)))
|
|
||||||
ser.flush()
|
|
||||||
for _ in range(50): # safety bound — don't loop forever
|
|
||||||
data = read_frame(ser)
|
|
||||||
if data is None:
|
|
||||||
return {"type": "Timeout"}
|
|
||||||
resp = decode_response(data)
|
|
||||||
if resp["type"] == "RxPacket":
|
|
||||||
_rx_queue.append(resp)
|
|
||||||
continue
|
|
||||||
return resp
|
|
||||||
return {"type": "Timeout"}
|
|
||||||
|
|
||||||
|
|
||||||
def recv(ser: serial.Serial) -> dict | None:
|
|
||||||
"""Return the next RxPacket from the buffer or the wire.
|
|
||||||
|
|
||||||
Returns None on timeout (no packet available).
|
|
||||||
"""
|
|
||||||
if _rx_queue:
|
|
||||||
return _rx_queue.popleft()
|
|
||||||
data = read_frame(ser)
|
|
||||||
if data is None:
|
|
||||||
return None
|
|
||||||
resp = decode_response(data)
|
|
||||||
if resp["type"] == "RxPacket":
|
|
||||||
return resp
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def drain_rx(ser: serial.Serial) -> list[dict]:
|
|
||||||
"""Drain all buffered and pending RxPacket frames."""
|
|
||||||
packets = list(_rx_queue)
|
|
||||||
_rx_queue.clear()
|
|
||||||
old_timeout = ser.timeout
|
|
||||||
ser.timeout = 0.01
|
|
||||||
while True:
|
|
||||||
data = read_frame(ser)
|
|
||||||
if data is None:
|
|
||||||
break
|
|
||||||
resp = decode_response(data)
|
|
||||||
if resp["type"] == "RxPacket":
|
|
||||||
packets.append(resp)
|
|
||||||
ser.timeout = old_timeout
|
|
||||||
return packets
|
|
||||||
|
|
||||||
|
|
||||||
# ── Mux client support ──────────────────────────────────────────
|
|
||||||
|
|
||||||
class MuxConnection:
|
|
||||||
"""Drop-in replacement for serial.Serial that talks to the mux daemon."""
|
|
||||||
|
|
||||||
def __init__(self, sock: socket.socket, timeout: float = 2.0):
|
|
||||||
self._sock = sock
|
|
||||||
self._timeout = timeout
|
|
||||||
self._sock.settimeout(timeout)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def timeout(self) -> float:
|
|
||||||
return self._timeout
|
|
||||||
|
|
||||||
@timeout.setter
|
|
||||||
def timeout(self, value: float) -> None:
|
|
||||||
self._timeout = value
|
|
||||||
self._sock.settimeout(value)
|
|
||||||
|
|
||||||
def read(self, n: int = 1) -> bytes:
|
|
||||||
try:
|
|
||||||
data = self._sock.recv(n)
|
|
||||||
if not data:
|
|
||||||
raise ConnectionError("mux disconnected")
|
|
||||||
return data
|
|
||||||
except socket.timeout:
|
|
||||||
return b""
|
|
||||||
|
|
||||||
def write(self, data: bytes) -> int:
|
|
||||||
self._sock.sendall(data)
|
|
||||||
return len(data)
|
|
||||||
|
|
||||||
def flush(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def reset_input_buffer(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self._sock.close()
|
|
||||||
|
|
||||||
|
|
||||||
def _mux_socket_path() -> str | None:
|
|
||||||
"""Resolve the mux socket path, or None if no socket exists."""
|
|
||||||
env = os.environ.get("DONGLORA_MUX")
|
|
||||||
if env:
|
|
||||||
return env if os.path.exists(env) else None
|
|
||||||
xdg = os.environ.get("XDG_RUNTIME_DIR")
|
|
||||||
if xdg:
|
|
||||||
p = os.path.join(xdg, "donglora", "mux.sock")
|
|
||||||
if os.path.exists(p):
|
|
||||||
return p
|
|
||||||
p = "/tmp/donglora-mux.sock"
|
|
||||||
return p if os.path.exists(p) else None
|
|
||||||
|
|
||||||
|
|
||||||
def mux_connect(path: str | None = None, timeout: float = 2.0) -> MuxConnection:
|
|
||||||
"""Connect to the DongLoRa mux daemon via Unix socket."""
|
|
||||||
if path is None:
|
|
||||||
path = _mux_socket_path()
|
|
||||||
if path is None:
|
|
||||||
raise FileNotFoundError("No mux socket found")
|
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
||||||
sock.connect(path)
|
|
||||||
return MuxConnection(sock, timeout)
|
|
||||||
|
|
||||||
|
|
||||||
def mux_tcp_connect(host: str, port: int, timeout: float = 2.0) -> MuxConnection:
|
|
||||||
"""Connect to the DongLoRa mux daemon via TCP."""
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.connect((host, port))
|
|
||||||
return MuxConnection(sock, timeout)
|
|
||||||
|
|
||||||
|
|
||||||
_mux_mode: tuple | None = None # ("unix",) or ("tcp", host, port) once sticky
|
|
||||||
|
|
||||||
|
|
||||||
def _reconnect_mux(timeout: float) -> MuxConnection:
|
|
||||||
"""Retry the mux connection until it comes back."""
|
|
||||||
assert _mux_mode is not None
|
|
||||||
print("Waiting for mux...", end="", flush=True)
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
if _mux_mode[0] == "unix":
|
|
||||||
return mux_connect(timeout=timeout)
|
|
||||||
else:
|
|
||||||
return mux_tcp_connect(_mux_mode[1], _mux_mode[2], timeout)
|
|
||||||
except (FileNotFoundError, ConnectionRefusedError, OSError):
|
|
||||||
print(".", end="", flush=True)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
def connect(port: str | None = None, timeout: float = 2.0) -> serial.Serial | MuxConnection:
|
|
||||||
"""Open a connection to a DongLoRa device.
|
|
||||||
|
|
||||||
Priority: DONGLORA_MUX_TCP env var → Unix socket mux → direct USB serial.
|
|
||||||
Once connected via mux, subsequent calls only retry mux (never steals USB).
|
|
||||||
"""
|
|
||||||
global _mux_mode
|
|
||||||
|
|
||||||
# Sticky mux — if we connected via mux before, only retry mux
|
|
||||||
if _mux_mode is not None and port is None:
|
|
||||||
return _reconnect_mux(timeout)
|
|
||||||
|
|
||||||
if port is None:
|
|
||||||
# Try local Unix socket mux
|
|
||||||
sock_path = _mux_socket_path()
|
|
||||||
if sock_path is not None:
|
|
||||||
try:
|
|
||||||
conn = mux_connect(sock_path, timeout=timeout)
|
|
||||||
_mux_mode = ("unix",)
|
|
||||||
return conn
|
|
||||||
except (ConnectionRefusedError, OSError):
|
|
||||||
pass # stale socket — fall through
|
|
||||||
|
|
||||||
# Try TCP mux (e.g. remote access over Tailscale)
|
|
||||||
tcp = os.environ.get("DONGLORA_MUX_TCP")
|
|
||||||
if tcp:
|
|
||||||
try:
|
|
||||||
host, _, p = tcp.rpartition(":")
|
|
||||||
host = host or "localhost"
|
|
||||||
conn = mux_tcp_connect(host, int(p), timeout)
|
|
||||||
_mux_mode = ("tcp", host, int(p))
|
|
||||||
return conn
|
|
||||||
except (ConnectionRefusedError, OSError):
|
|
||||||
pass # mux not reachable — fall through
|
|
||||||
|
|
||||||
# No mux available — direct USB serial
|
|
||||||
if port is None:
|
|
||||||
port = wait_for_device()
|
|
||||||
ser = serial.Serial(port, timeout=timeout)
|
|
||||||
ser.reset_input_buffer()
|
|
||||||
return ser
|
|
||||||
|
|
||||||
|
|
||||||
# Sentinel: set TX power to the board's maximum
|
|
||||||
TX_POWER_MAX = -128 # i8::MIN on the wire
|
|
||||||
|
|
||||||
# Sentinel: use firmware default preamble length (16 symbols)
|
|
||||||
PREAMBLE_DEFAULT = 0
|
|
||||||
|
|
||||||
# Default radio config: 915 MHz, 125 kHz BW, SF7, CR 4/5, max power, default preamble
|
|
||||||
DEFAULT_CONFIG = {
|
|
||||||
"freq_hz": 915_000_000,
|
|
||||||
"bw": 7, # 125 kHz
|
|
||||||
"sf": 7,
|
|
||||||
"cr": 5, # CR 4/5
|
|
||||||
"sync_word": 0x1424,
|
|
||||||
"tx_power_dbm": TX_POWER_MAX,
|
|
||||||
"preamble_len": PREAMBLE_DEFAULT,
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
[tools]
|
|
||||||
python = "3.14"
|
|
||||||
uv = "latest"
|
|
||||||
just = "latest"
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "donglora"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "DongLoRa host library — talk LoRa from Python over USB"
|
|
||||||
license = "MIT"
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
authors = [{ name = "Stephen Waits" }]
|
|
||||||
keywords = ["lora", "radio", "usb", "embedded"]
|
|
||||||
classifiers = [
|
|
||||||
"Development Status :: 3 - Alpha",
|
|
||||||
"Topic :: Communications :: Ham Radio",
|
|
||||||
"Topic :: System :: Hardware",
|
|
||||||
]
|
|
||||||
dependencies = [
|
|
||||||
"cobs",
|
|
||||||
"pyserial",
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
meshcore = ["pycryptodome", "pynacl"]
|
|
||||||
orac = ["pycryptodome", "pynacl", "anthropic"]
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["hatchling"]
|
|
||||||
build-backend = "hatchling.build"
|
|
||||||
Generated
-606
@@ -1,606 +0,0 @@
|
|||||||
version = 1
|
|
||||||
revision = 3
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "annotated-types"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anthropic"
|
|
||||||
version = "0.88.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "anyio" },
|
|
||||||
{ name = "distro" },
|
|
||||||
{ name = "docstring-parser" },
|
|
||||||
{ name = "httpx" },
|
|
||||||
{ name = "jiter" },
|
|
||||||
{ name = "pydantic" },
|
|
||||||
{ name = "sniffio" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/86/68/565f13059c0a6a6fd5f96f306f2a0fb478a0e1174ec18a4df16b5fac9379/anthropic-0.88.0.tar.gz", hash = "sha256:f4c7f6863d08c869913516f08d658fe53caaf8bcc4fbea3218df343d2a876c58", size = 596654, upload-time = "2026-04-01T19:59:05.287Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/ac/68f646998160c9f2e6f9353a31dd87292ef02b915b455aaf70a52a059a75/anthropic-0.88.0-py3-none-any.whl", hash = "sha256:71898b32332bc75d9739bc10095288d40a29605da6d00da2fe832b1aa036552f", size = 478338, upload-time = "2026-04-01T19:59:03.832Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anyio"
|
|
||||||
version = "4.13.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "idna" },
|
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "certifi"
|
|
||||||
version = "2026.2.25"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cffi"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cobs"
|
|
||||||
version = "1.2.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/34/ef/ea149311227a4fc3160cc885fce06da7c7d76782a308ef070b8065c69953/cobs-1.2.2.tar.gz", hash = "sha256:dbdd5e32111d72786f83d0c269215dcd6ac629b1ac1962c6878221f3b2ca98da", size = 14582, upload-time = "2025-07-20T01:08:35.434Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/e0/9574590639a624a9a84c89a569f36343523a306fff2ffddc9468a2b9b05d/cobs-1.2.2-cp312-cp312-win32.whl", hash = "sha256:3afe0cec5d91164f84dcf35b471949aa36285664d21ee984350e39d485707d11", size = 26400, upload-time = "2025-07-20T01:08:30.288Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/4f/5418d9decdc4cf27a814fc189bc4fa44d216d37b871d633654f37ed331f8/cobs-1.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:4da6048f6e16cbd6c8ec5e95e8520e226248ffb8d31e9d6fc5e0888b3b65c54b", size = 27776, upload-time = "2025-07-20T01:08:31.377Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/df/7f07559954d53d09680aa4f5c72db999e7643253e2ca4e431023fe5c1c40/cobs-1.2.2-cp313-cp313-win32.whl", hash = "sha256:a8fdf6ca70247e7271caea6f2f6f9c0343f5e872e2fd47e21f71831fa9ababa8", size = 26401, upload-time = "2025-07-20T01:08:32.508Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/00/5b063105ff567bb7fb72c3cd3e96116a3889381fa2c0e7747497391dbecc/cobs-1.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe3f7194d5fbbc02e8fb03d26ad626a33fa0d420b50d66ed60ac4bf516322d8e", size = 27774, upload-time = "2025-07-20T01:08:33.692Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/35/de1e889c26fcfc9ae6852b8f99e744e6690a3baae99f63e2b9fffdd21710/cobs-1.2.2-cp314-cp314-win32.whl", hash = "sha256:bcf08cb0b68c1824a87cfa89e0684d5a3eaee9b9e08b02ce4a54d5589536f01c", size = 26779, upload-time = "2026-03-09T02:18:48.711Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/64/390e226843cc83066acc60257e0e71ebdbce6dd915ec4f3d34101b9ac31b/cobs-1.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:1f27b7a553d809487287e4ed88540b2c30ed9c9bb596173bfcf549e705af7bf0", size = 28205, upload-time = "2026-03-09T02:18:20.193Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "distro"
|
|
||||||
version = "1.9.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "docstring-parser"
|
|
||||||
version = "0.17.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "donglora"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { editable = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "cobs" },
|
|
||||||
{ name = "pyserial" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.optional-dependencies]
|
|
||||||
meshcore = [
|
|
||||||
{ name = "pycryptodome" },
|
|
||||||
{ name = "pynacl" },
|
|
||||||
]
|
|
||||||
orac = [
|
|
||||||
{ name = "anthropic" },
|
|
||||||
{ name = "pycryptodome" },
|
|
||||||
{ name = "pynacl" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "anthropic", marker = "extra == 'orac'" },
|
|
||||||
{ name = "cobs" },
|
|
||||||
{ name = "pycryptodome", marker = "extra == 'meshcore'" },
|
|
||||||
{ name = "pycryptodome", marker = "extra == 'orac'" },
|
|
||||||
{ name = "pynacl", marker = "extra == 'meshcore'" },
|
|
||||||
{ name = "pynacl", marker = "extra == 'orac'" },
|
|
||||||
{ name = "pyserial" },
|
|
||||||
]
|
|
||||||
provides-extras = ["meshcore", "orac"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "exceptiongroup"
|
|
||||||
version = "1.3.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "h11"
|
|
||||||
version = "0.16.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "httpcore"
|
|
||||||
version = "1.0.9"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "h11" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "httpx"
|
|
||||||
version = "0.28.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "anyio" },
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "httpcore" },
|
|
||||||
{ name = "idna" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idna"
|
|
||||||
version = "3.11"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiter"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[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]]
|
|
||||||
name = "pycryptodome"
|
|
||||||
version = "3.23.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pydantic"
|
|
||||||
version = "2.12.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "annotated-types" },
|
|
||||||
{ name = "pydantic-core" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
{ name = "typing-inspection" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pydantic-core"
|
|
||||||
version = "2.41.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pynacl"
|
|
||||||
version = "1.6.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyserial"
|
|
||||||
version = "3.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sniffio"
|
|
||||||
version = "1.3.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typing-extensions"
|
|
||||||
version = "4.15.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typing-inspection"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
|
||||||
]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/target
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## 0.1.0 — 2026-04-06
|
|
||||||
|
|
||||||
Initial release.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- High-level `Client<T>` with send/recv and command helpers (ping, set_config,
|
|
||||||
start_rx, stop_rx, transmit, get_config, get_mac, display_on, display_off)
|
|
||||||
- COBS wire framing via `ucobs` (matches firmware implementation)
|
|
||||||
- Auto-detection connection: TCP mux, Unix socket mux, direct USB serial
|
|
||||||
- Bounded RX packet buffering (256 packets, FIFO eviction)
|
|
||||||
- `FrameReader` accumulator for streaming byte sources
|
|
||||||
- USB device discovery by VID:PID with blocking wait
|
|
||||||
|
|
||||||
### Resilience
|
|
||||||
|
|
||||||
- Cross-platform timeout handling: `TimedOut` (Windows) and `WouldBlock`
|
|
||||||
(Linux/macOS) both treated as clean timeouts in `read_frame`
|
|
||||||
- `EINTR`/`Interrupted` signals retried automatically in `read_frame`
|
|
||||||
- `drain_rx` always restores the original timeout, even on I/O errors
|
|
||||||
- TCP mux connections use `connect_timeout` (bounded by caller's timeout)
|
|
||||||
- Mux sockets set both read and write timeouts
|
|
||||||
- `SerialTransport` tracks timeout accurately for save/restore
|
|
||||||
- Unexpected unsolicited frames logged via `tracing::warn` instead of silently
|
|
||||||
discarded
|
|
||||||
Generated
-326
@@ -1,326 +0,0 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 4
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anyhow"
|
|
||||||
version = "1.0.102"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "1.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "2.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "1.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "core-foundation"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "core-foundation-sys"
|
|
||||||
version = "0.8.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "donglora-client"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"serialport",
|
|
||||||
"tracing",
|
|
||||||
"ucobs",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "io-kit-sys"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"mach2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libc"
|
|
||||||
version = "0.2.184"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libudev"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"libudev-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libudev-sys"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mach2"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nix"
|
|
||||||
version = "0.26.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell"
|
|
||||||
version = "1.21.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-project-lite"
|
|
||||||
version = "0.2.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pkg-config"
|
|
||||||
version = "0.3.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.106"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.45"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scopeguard"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serialport"
|
|
||||||
version = "4.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4d91116f97173694f1642263b2ff837f80d933aa837e2314969f6728f661df3"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.0",
|
|
||||||
"cfg-if",
|
|
||||||
"core-foundation",
|
|
||||||
"core-foundation-sys",
|
|
||||||
"io-kit-sys",
|
|
||||||
"libudev",
|
|
||||||
"mach2",
|
|
||||||
"nix",
|
|
||||||
"scopeguard",
|
|
||||||
"unescaper",
|
|
||||||
"windows-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "2.0.117"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "2.0.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "2.0.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing"
|
|
||||||
version = "0.1.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
|
||||||
dependencies = [
|
|
||||||
"pin-project-lite",
|
|
||||||
"tracing-attributes",
|
|
||||||
"tracing-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-attributes"
|
|
||||||
version = "0.1.31"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-core"
|
|
||||||
version = "0.1.36"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ucobs"
|
|
||||||
version = "0.3.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "311fbab511bafd13c109aff41f67d4c4426bf919a6670647e69f0d2af95ac01d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unescaper"
|
|
||||||
version = "0.1.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ident"
|
|
||||||
version = "1.0.24"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.52.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm",
|
|
||||||
"windows_aarch64_msvc",
|
|
||||||
"windows_i686_gnu",
|
|
||||||
"windows_i686_gnullvm",
|
|
||||||
"windows_i686_msvc",
|
|
||||||
"windows_x86_64_gnu",
|
|
||||||
"windows_x86_64_gnullvm",
|
|
||||||
"windows_x86_64_msvc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnullvm"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "donglora-client"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
|
||||||
license = "MIT"
|
|
||||||
description = "DongLoRa host library — connect, configure, send/receive LoRa packets"
|
|
||||||
repository = "https://github.com/swaits/donglora"
|
|
||||||
keywords = ["lora", "radio", "usb", "serial"]
|
|
||||||
categories = ["hardware-support"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1"
|
|
||||||
ucobs = "0.3"
|
|
||||||
serialport = "4"
|
|
||||||
tracing = "0.1"
|
|
||||||
|
|
||||||
[lints.clippy]
|
|
||||||
unwrap_used = "deny"
|
|
||||||
expect_used = "deny"
|
|
||||||
panic = "deny"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 Stephen Waits <steve@waits.net>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# DongLoRa Rust Client
|
|
||||||
|
|
||||||
Rust client library for talking to a DongLoRa device — either directly
|
|
||||||
over USB or through the [mux daemon](../../mux/).
|
|
||||||
|
|
||||||
## What's in Here
|
|
||||||
|
|
||||||
- `src/protocol.rs` — wire protocol types (`RadioConfig`, `Command`, `Response`, `ErrorCode`)
|
|
||||||
- `src/codec.rs` — COBS framing, frame accumulator
|
|
||||||
- `src/discovery.rs` — USB VID:PID device discovery
|
|
||||||
- `src/transport.rs` — serial and mux socket transports
|
|
||||||
- `src/client.rs` — high-level `Client<T>` with send/recv
|
|
||||||
- `src/connect.rs` — auto-detection (mux socket, TCP, direct USB)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use donglora_client::*;
|
|
||||||
|
|
||||||
let mut client = connect_default()?;
|
|
||||||
client.ping()?;
|
|
||||||
client.set_config(RadioConfig::default())?;
|
|
||||||
client.start_rx()?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if let Some(Response::RxPacket { rssi, snr, payload }) = client.recv()? {
|
|
||||||
println!("RX rssi={rssi} snr={snr} len={}", payload.len());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- `ucobs` — COBS framing (same implementation as the firmware)
|
|
||||||
- `serialport` — USB serial communication
|
|
||||||
- `anyhow` — error handling
|
|
||||||
- `tracing` — logging
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
set shell := ["bash", "-c"]
|
|
||||||
|
|
||||||
default: check
|
|
||||||
|
|
||||||
# Run clippy and tests
|
|
||||||
check:
|
|
||||||
cargo clippy -- -D warnings && cargo test
|
|
||||||
|
|
||||||
# Build release
|
|
||||||
build:
|
|
||||||
cargo build --release
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
//! High-level DongLoRa client.
|
|
||||||
//!
|
|
||||||
//! The [`Client`] wraps a transport and provides ergonomic send/recv methods
|
|
||||||
//! that handle the command/response discipline (buffering unsolicited RxPackets
|
|
||||||
//! while waiting for solicited responses).
|
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
|
|
||||||
use crate::codec::{encode_frame, read_frame};
|
|
||||||
use crate::protocol::{Command, ErrorCode, RadioConfig, Response};
|
|
||||||
use crate::transport::Transport;
|
|
||||||
|
|
||||||
/// Maximum number of buffered RxPackets before oldest are dropped.
|
|
||||||
const RX_BUFFER_CAP: usize = 256;
|
|
||||||
|
|
||||||
/// Maximum frames to read while waiting for a solicited response before giving up.
|
|
||||||
const MAX_UNSOLICITED_BEFORE_TIMEOUT: usize = 50;
|
|
||||||
|
|
||||||
/// High-level DongLoRa client, generic over transport.
|
|
||||||
///
|
|
||||||
/// Works with any [`Transport`]: direct USB serial, Unix socket mux, or TCP mux.
|
|
||||||
pub struct Client<T: Transport> {
|
|
||||||
transport: T,
|
|
||||||
rx_buffer: VecDeque<Response>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Transport> Client<T> {
|
|
||||||
/// Create a new client wrapping the given transport.
|
|
||||||
pub fn new(transport: T) -> Self {
|
|
||||||
Self {
|
|
||||||
transport,
|
|
||||||
rx_buffer: VecDeque::with_capacity(64),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a command and wait for the solicited response.
|
|
||||||
///
|
|
||||||
/// Any unsolicited `RxPacket` frames encountered while waiting are buffered
|
|
||||||
/// and retrievable via [`recv`](Self::recv) or [`drain_rx`](Self::drain_rx).
|
|
||||||
pub fn send(&mut self, cmd: Command) -> anyhow::Result<Response> {
|
|
||||||
let frame = encode_frame(&cmd.to_bytes());
|
|
||||||
std::io::Write::write_all(&mut self.transport, &frame)?;
|
|
||||||
std::io::Write::flush(&mut self.transport)?;
|
|
||||||
|
|
||||||
for _ in 0..MAX_UNSOLICITED_BEFORE_TIMEOUT {
|
|
||||||
let data = read_frame(&mut self.transport)?
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("timeout waiting for response"))?;
|
|
||||||
let resp = Response::from_bytes(&data)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
let hex: Vec<String> = data.iter().map(|b| format!("{b:02x}")).collect();
|
|
||||||
anyhow::anyhow!("malformed response ({} bytes): [{}]", data.len(), hex.join(" "))
|
|
||||||
})?;
|
|
||||||
if resp.is_rx_packet() {
|
|
||||||
self.buffer_rx(resp);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return Ok(resp);
|
|
||||||
}
|
|
||||||
anyhow::bail!("no solicited response after {MAX_UNSOLICITED_BEFORE_TIMEOUT} frames")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the next RxPacket from the buffer or the wire.
|
|
||||||
///
|
|
||||||
/// Returns `Ok(None)` on timeout (no packet available).
|
|
||||||
pub fn recv(&mut self) -> anyhow::Result<Option<Response>> {
|
|
||||||
if let Some(pkt) = self.rx_buffer.pop_front() {
|
|
||||||
return Ok(Some(pkt));
|
|
||||||
}
|
|
||||||
let Some(data) = read_frame(&mut self.transport)? else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let resp = Response::from_bytes(&data)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("malformed response ({} bytes)", data.len()))?;
|
|
||||||
if resp.is_rx_packet() {
|
|
||||||
Ok(Some(resp))
|
|
||||||
} else {
|
|
||||||
tracing::warn!("recv: discarding unexpected unsolicited response: {resp:?}");
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drain all buffered and pending RxPacket frames.
|
|
||||||
///
|
|
||||||
/// Temporarily reduces the read timeout to quickly drain any frames still
|
|
||||||
/// in flight, then restores the original timeout.
|
|
||||||
pub fn drain_rx(&mut self) -> anyhow::Result<Vec<Response>> {
|
|
||||||
let mut packets: Vec<Response> = self.rx_buffer.drain(..).collect();
|
|
||||||
|
|
||||||
let old_timeout = self.transport.timeout();
|
|
||||||
self.transport
|
|
||||||
.set_timeout(std::time::Duration::from_millis(10))?;
|
|
||||||
|
|
||||||
let result: anyhow::Result<()> = (|| {
|
|
||||||
loop {
|
|
||||||
let Some(data) = read_frame(&mut self.transport)? else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
if let Some(resp) = Response::from_bytes(&data)
|
|
||||||
&& resp.is_rx_packet()
|
|
||||||
{
|
|
||||||
packets.push(resp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Always restore timeout, even if the loop errored
|
|
||||||
self.transport.set_timeout(old_timeout)?;
|
|
||||||
result?;
|
|
||||||
Ok(packets)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a Ping and verify the Pong response.
|
|
||||||
pub fn ping(&mut self) -> anyhow::Result<()> {
|
|
||||||
match self.send(Command::Ping)? {
|
|
||||||
Response::Pong => Ok(()),
|
|
||||||
other => anyhow::bail!("unexpected response to Ping: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the radio configuration.
|
|
||||||
pub fn set_config(&mut self, config: RadioConfig) -> anyhow::Result<()> {
|
|
||||||
match self.send(Command::SetConfig(config))? {
|
|
||||||
Response::Ok => Ok(()),
|
|
||||||
Response::Error(code) => anyhow::bail!("SetConfig failed: {code}"),
|
|
||||||
other => anyhow::bail!("unexpected response to SetConfig: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start receiving LoRa packets.
|
|
||||||
pub fn start_rx(&mut self) -> anyhow::Result<()> {
|
|
||||||
match self.send(Command::StartRx)? {
|
|
||||||
Response::Ok => Ok(()),
|
|
||||||
Response::Error(code) => anyhow::bail!("StartRx failed: {code}"),
|
|
||||||
other => anyhow::bail!("unexpected response to StartRx: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stop receiving LoRa packets.
|
|
||||||
pub fn stop_rx(&mut self) -> anyhow::Result<()> {
|
|
||||||
match self.send(Command::StopRx)? {
|
|
||||||
Response::Ok => Ok(()),
|
|
||||||
Response::Error(code) => anyhow::bail!("StopRx failed: {code}"),
|
|
||||||
other => anyhow::bail!("unexpected response to StopRx: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transmit a LoRa packet.
|
|
||||||
pub fn transmit(
|
|
||||||
&mut self,
|
|
||||||
payload: &[u8],
|
|
||||||
config: Option<RadioConfig>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let cmd = Command::Transmit {
|
|
||||||
config,
|
|
||||||
payload: payload.to_vec(),
|
|
||||||
};
|
|
||||||
match self.send(cmd)? {
|
|
||||||
Response::TxDone => Ok(()),
|
|
||||||
Response::Error(ErrorCode::TxTimeout) => anyhow::bail!("transmit timed out"),
|
|
||||||
Response::Error(code) => anyhow::bail!("Transmit failed: {code}"),
|
|
||||||
other => anyhow::bail!("unexpected response to Transmit: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the board's MAC address.
|
|
||||||
pub fn get_mac(&mut self) -> anyhow::Result<[u8; 6]> {
|
|
||||||
match self.send(Command::GetMac)? {
|
|
||||||
Response::MacAddress(mac) => Ok(mac),
|
|
||||||
Response::Error(code) => anyhow::bail!("GetMac failed: {code}"),
|
|
||||||
other => anyhow::bail!("unexpected response to GetMac: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current radio configuration from the device.
|
|
||||||
pub fn get_config(&mut self) -> anyhow::Result<RadioConfig> {
|
|
||||||
match self.send(Command::GetConfig)? {
|
|
||||||
Response::Config(cfg) => Ok(cfg),
|
|
||||||
Response::Error(code) => anyhow::bail!("GetConfig failed: {code}"),
|
|
||||||
other => anyhow::bail!("unexpected response to GetConfig: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Turn on the display (if present).
|
|
||||||
pub fn display_on(&mut self) -> anyhow::Result<()> {
|
|
||||||
match self.send(Command::DisplayOn)? {
|
|
||||||
Response::Ok => Ok(()),
|
|
||||||
Response::Error(code) => anyhow::bail!("DisplayOn failed: {code}"),
|
|
||||||
other => anyhow::bail!("unexpected response to DisplayOn: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Turn off the display (if present).
|
|
||||||
pub fn display_off(&mut self) -> anyhow::Result<()> {
|
|
||||||
match self.send(Command::DisplayOff)? {
|
|
||||||
Response::Ok => Ok(()),
|
|
||||||
Response::Error(code) => anyhow::bail!("DisplayOff failed: {code}"),
|
|
||||||
other => anyhow::bail!("unexpected response to DisplayOff: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Consume the client and return the inner transport.
|
|
||||||
pub fn into_inner(self) -> T {
|
|
||||||
self.transport
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a reference to the inner transport.
|
|
||||||
pub fn transport(&self) -> &T {
|
|
||||||
&self.transport
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a mutable reference to the inner transport.
|
|
||||||
pub fn transport_mut(&mut self) -> &mut T {
|
|
||||||
&mut self.transport
|
|
||||||
}
|
|
||||||
|
|
||||||
fn buffer_rx(&mut self, resp: Response) {
|
|
||||||
if self.rx_buffer.len() >= RX_BUFFER_CAP {
|
|
||||||
self.rx_buffer.pop_front(); // drop oldest
|
|
||||||
}
|
|
||||||
self.rx_buffer.push_back(resp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
//! COBS framing: encode/decode frames and accumulate partial reads.
|
|
||||||
//!
|
|
||||||
//! Each frame is COBS-encoded and terminated with a `0x00` sentinel byte.
|
|
||||||
//! The sentinel never appears in the encoded data, so it unambiguously
|
|
||||||
//! marks frame boundaries.
|
|
||||||
//!
|
|
||||||
//! Uses the [`ucobs`] crate — the same COBS implementation as the firmware.
|
|
||||||
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
/// COBS-encode data and append the `0x00` sentinel.
|
|
||||||
pub fn encode_frame(data: &[u8]) -> Vec<u8> {
|
|
||||||
let max_encoded = ucobs::max_encoded_len(data.len());
|
|
||||||
let mut buf = vec![0u8; max_encoded];
|
|
||||||
let n = match ucobs::encode(data, &mut buf) {
|
|
||||||
Some(n) => n,
|
|
||||||
None => return vec![0x00], // empty/failed encode → just sentinel
|
|
||||||
};
|
|
||||||
buf.truncate(n);
|
|
||||||
buf.push(0x00);
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
|
|
||||||
/// COBS-decode a frame (without the sentinel). Returns `None` on decode error.
|
|
||||||
pub fn decode_frame(encoded: &[u8]) -> Option<Vec<u8>> {
|
|
||||||
if encoded.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut buf = vec![0u8; encoded.len()];
|
|
||||||
let n = ucobs::decode(encoded, &mut buf)?;
|
|
||||||
buf.truncate(n);
|
|
||||||
Some(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stateful COBS frame accumulator.
|
|
||||||
///
|
|
||||||
/// Feed raw byte chunks (from serial reads, socket reads, etc.) and extract
|
|
||||||
/// complete decoded frames. Handles partial reads, multiple frames per chunk,
|
|
||||||
/// and empty inter-frame gaps gracefully.
|
|
||||||
///
|
|
||||||
/// Used by both the sync client (`read_frame`) and the async mux to accumulate
|
|
||||||
/// data from serial and client socket streams.
|
|
||||||
pub struct FrameReader {
|
|
||||||
buf: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FrameReader {
|
|
||||||
/// Create a new empty frame reader.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
buf: Vec::with_capacity(512),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Feed raw bytes and return all complete decoded frames.
|
|
||||||
///
|
|
||||||
/// COBS decode errors are logged and the bad frame is skipped.
|
|
||||||
/// Empty inter-frame gaps (consecutive `0x00` bytes) are ignored.
|
|
||||||
pub fn feed(&mut self, data: &[u8]) -> Vec<Vec<u8>> {
|
|
||||||
self.buf.extend_from_slice(data);
|
|
||||||
|
|
||||||
let mut frames = Vec::new();
|
|
||||||
while let Some(sentinel_pos) = self.buf.iter().position(|&b| b == 0x00) {
|
|
||||||
let encoded = &self.buf[..sentinel_pos];
|
|
||||||
if !encoded.is_empty() {
|
|
||||||
match decode_frame(encoded) {
|
|
||||||
Some(decoded) => frames.push(decoded),
|
|
||||||
None => warn!("bad COBS frame ({} bytes) — skipped", encoded.len()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove the consumed bytes including the sentinel
|
|
||||||
self.buf.drain(..=sentinel_pos);
|
|
||||||
}
|
|
||||||
frames
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the number of buffered bytes not yet forming a complete frame.
|
|
||||||
pub fn buffered(&self) -> usize {
|
|
||||||
self.buf.len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FrameReader {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read one complete COBS frame from a blocking `Read` source.
|
|
||||||
///
|
|
||||||
/// Reads byte-by-byte until a `0x00` sentinel is found, then COBS-decodes.
|
|
||||||
/// Returns `Ok(None)` on timeout (zero-length read from the underlying source).
|
|
||||||
/// Returns `Err` on I/O errors or COBS decode failures.
|
|
||||||
pub fn read_frame(reader: &mut dyn std::io::Read) -> anyhow::Result<Option<Vec<u8>>> {
|
|
||||||
let mut buf = Vec::with_capacity(280);
|
|
||||||
let mut byte = [0u8; 1];
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match reader.read(&mut byte) {
|
|
||||||
Ok(0) => {
|
|
||||||
// Timeout or EOF
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Ok(_) => {
|
|
||||||
if byte[0] == 0x00 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
buf.push(byte[0]);
|
|
||||||
}
|
|
||||||
Err(e)
|
|
||||||
if matches!(
|
|
||||||
e.kind(),
|
|
||||||
std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if buf.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
decode_frame(&buf)
|
|
||||||
.map(Some)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("COBS decode error"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tests ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn encode_decode_roundtrip() {
|
|
||||||
let data = b"hello world";
|
|
||||||
let encoded = encode_frame(data);
|
|
||||||
// Encoded should end with 0x00 sentinel
|
|
||||||
assert_eq!(encoded.last(), Some(&0x00));
|
|
||||||
// Encoded should not contain 0x00 except the trailing sentinel
|
|
||||||
assert!(!encoded[..encoded.len() - 1].contains(&0x00));
|
|
||||||
// Decode (without sentinel)
|
|
||||||
let decoded = decode_frame(&encoded[..encoded.len() - 1]);
|
|
||||||
assert_eq!(decoded.as_deref(), Some(data.as_slice()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn encode_decode_empty() {
|
|
||||||
// COBS encoding of empty data produces just the 0x00 sentinel.
|
|
||||||
// This is equivalent to an empty inter-frame gap and is correctly
|
|
||||||
// skipped by both FrameReader and read_frame.
|
|
||||||
let encoded = encode_frame(b"");
|
|
||||||
assert_eq!(*encoded.last().unwrap(), 0x00);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn encode_decode_with_zeros() {
|
|
||||||
let data = &[0x00, 0x01, 0x00, 0x02, 0x00];
|
|
||||||
let encoded = encode_frame(data);
|
|
||||||
let decoded = decode_frame(&encoded[..encoded.len() - 1]);
|
|
||||||
assert_eq!(decoded.as_deref(), Some(data.as_slice()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn decode_frame_invalid() {
|
|
||||||
assert!(decode_frame(&[]).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn frame_reader_single_frame() {
|
|
||||||
let mut reader = FrameReader::new();
|
|
||||||
let data = b"test";
|
|
||||||
let frame = encode_frame(data);
|
|
||||||
let frames = reader.feed(&frame);
|
|
||||||
assert_eq!(frames.len(), 1);
|
|
||||||
assert_eq!(frames[0], data);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn frame_reader_multiple_frames_at_once() {
|
|
||||||
let mut reader = FrameReader::new();
|
|
||||||
let f1 = encode_frame(b"one");
|
|
||||||
let f2 = encode_frame(b"two");
|
|
||||||
let mut combined = f1;
|
|
||||||
combined.extend_from_slice(&f2);
|
|
||||||
let frames = reader.feed(&combined);
|
|
||||||
assert_eq!(frames.len(), 2);
|
|
||||||
assert_eq!(frames[0], b"one");
|
|
||||||
assert_eq!(frames[1], b"two");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn frame_reader_partial_then_complete() {
|
|
||||||
let mut reader = FrameReader::new();
|
|
||||||
let frame = encode_frame(b"split");
|
|
||||||
let mid = frame.len() / 2;
|
|
||||||
|
|
||||||
// Feed first half — no complete frames yet
|
|
||||||
let frames = reader.feed(&frame[..mid]);
|
|
||||||
assert!(frames.is_empty());
|
|
||||||
assert!(reader.buffered() > 0);
|
|
||||||
|
|
||||||
// Feed second half — now we get the frame
|
|
||||||
let frames = reader.feed(&frame[mid..]);
|
|
||||||
assert_eq!(frames.len(), 1);
|
|
||||||
assert_eq!(frames[0], b"split");
|
|
||||||
assert_eq!(reader.buffered(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn frame_reader_empty_gaps() {
|
|
||||||
let mut reader = FrameReader::new();
|
|
||||||
// Multiple sentinels in a row (empty gaps between frames)
|
|
||||||
let f = encode_frame(b"data");
|
|
||||||
let mut input = vec![0x00, 0x00]; // leading empty gaps
|
|
||||||
input.extend_from_slice(&f);
|
|
||||||
input.push(0x00); // trailing empty gap
|
|
||||||
let frames = reader.feed(&input);
|
|
||||||
assert_eq!(frames.len(), 1);
|
|
||||||
assert_eq!(frames[0], b"data");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn frame_reader_bad_cobs_skipped() {
|
|
||||||
let mut reader = FrameReader::new();
|
|
||||||
// Feed a known-bad COBS frame followed by a good one.
|
|
||||||
// A frame where the first byte claims a run longer than the frame is invalid.
|
|
||||||
let bad = &[0xFF, 0x01, 0x00]; // 0xFF says next 254 bytes, but only 1 present
|
|
||||||
let good = encode_frame(b"ok");
|
|
||||||
let mut input = bad.to_vec();
|
|
||||||
input.extend_from_slice(&good);
|
|
||||||
let frames = reader.feed(&input);
|
|
||||||
// Bad frame skipped, good frame decoded
|
|
||||||
assert_eq!(frames.len(), 1);
|
|
||||||
assert_eq!(frames[0], b"ok");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn read_frame_from_bytes() {
|
|
||||||
let data = b"frame";
|
|
||||||
let encoded = encode_frame(data);
|
|
||||||
let mut cursor = std::io::Cursor::new(encoded);
|
|
||||||
let result = read_frame(&mut cursor);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert_eq!(result.ok().flatten().as_deref(), Some(data.as_slice()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn read_frame_timeout() {
|
|
||||||
// Empty reader → returns None (timeout)
|
|
||||||
let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
|
|
||||||
let result = read_frame(&mut cursor);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert!(result.ok().flatten().is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn read_frame_empty_gap_then_data() {
|
|
||||||
let data = b"after_gap";
|
|
||||||
let mut input = vec![0x00]; // empty gap
|
|
||||||
input.extend_from_slice(&encode_frame(data));
|
|
||||||
let mut cursor = std::io::Cursor::new(input);
|
|
||||||
// First read_frame returns None for the empty gap
|
|
||||||
let r1 = read_frame(&mut cursor);
|
|
||||||
assert_eq!(r1.ok().flatten(), None);
|
|
||||||
// Second read returns the actual frame
|
|
||||||
let r2 = read_frame(&mut cursor);
|
|
||||||
assert_eq!(r2.ok().flatten().as_deref(), Some(data.as_slice()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn encode_all_byte_values_roundtrip() {
|
|
||||||
// Test that all possible byte values survive encode/decode
|
|
||||||
let data: Vec<u8> = (0..=255).collect();
|
|
||||||
let encoded = encode_frame(&data);
|
|
||||||
let decoded = decode_frame(&encoded[..encoded.len() - 1]);
|
|
||||||
assert_eq!(decoded, Some(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn config_response_cobs_roundtrip() {
|
|
||||||
// Exact Config response bytes: tag=1, freq=910525000, bw=62.5k, sf=7,
|
|
||||||
// cr=5, sync=0x3444, tx=22dBm, preamble=16, cad=1
|
|
||||||
// Note: preamble high byte is 0x00 — tests COBS with embedded zero.
|
|
||||||
let data = vec![
|
|
||||||
0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05,
|
|
||||||
0x44, 0x34, 0x16, 0x10, 0x00, 0x01,
|
|
||||||
];
|
|
||||||
assert_eq!(data.len(), 14);
|
|
||||||
let encoded = encode_frame(&data);
|
|
||||||
let decoded = decode_frame(&encoded[..encoded.len() - 1]);
|
|
||||||
assert_eq!(decoded, Some(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn config_response_cobs_roundtrip_cad_zero() {
|
|
||||||
// Same but with cad=0 — two consecutive 0x00 bytes at end.
|
|
||||||
let data = vec![
|
|
||||||
0x01, 0x48, 0x82, 0x45, 0x36, 0x06, 0x07, 0x05,
|
|
||||||
0x44, 0x34, 0x16, 0x10, 0x00, 0x00,
|
|
||||||
];
|
|
||||||
assert_eq!(data.len(), 14);
|
|
||||||
let encoded = encode_frame(&data);
|
|
||||||
let decoded = decode_frame(&encoded[..encoded.len() - 1]);
|
|
||||||
assert_eq!(decoded, Some(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
#[test]
|
|
||||||
fn read_frame_socket_timeout_returns_none() {
|
|
||||||
use std::os::unix::net::UnixStream;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
let (mut a, _b) = UnixStream::pair().unwrap();
|
|
||||||
a.set_read_timeout(Some(Duration::from_millis(10)))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// No data written to _b, so read on `a` will time out with WouldBlock on Linux.
|
|
||||||
let result = read_frame(&mut a);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"socket timeout should be Ok(None), got: {result:?}"
|
|
||||||
);
|
|
||||||
assert!(result.unwrap().is_none());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
//! Connection auto-detection and mux client helpers.
|
|
||||||
//!
|
|
||||||
//! The [`connect`] function tries mux connections first (TCP via env var, then
|
|
||||||
//! Unix socket), falling back to direct USB serial. This matches the Python
|
|
||||||
//! client's `connect()` behavior.
|
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use crate::client::Client;
|
|
||||||
use crate::discovery;
|
|
||||||
use crate::transport::{AnyTransport, MuxTransport, SerialTransport};
|
|
||||||
|
|
||||||
/// Default read timeout for connections.
|
|
||||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
|
|
||||||
|
|
||||||
/// Resolve the mux socket path in priority order.
|
|
||||||
///
|
|
||||||
/// 1. `$DONGLORA_MUX` environment variable
|
|
||||||
/// 2. `$XDG_RUNTIME_DIR/donglora/mux.sock`
|
|
||||||
/// 3. `/tmp/donglora-mux.sock`
|
|
||||||
pub fn default_socket_path() -> String {
|
|
||||||
if let Ok(env) = std::env::var("DONGLORA_MUX") {
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
|
|
||||||
return format!("{xdg}/donglora/mux.sock");
|
|
||||||
}
|
|
||||||
"/tmp/donglora-mux.sock".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find an existing mux socket path, or `None` if no socket file exists.
|
|
||||||
fn find_mux_socket() -> Option<String> {
|
|
||||||
if let Ok(env) = std::env::var("DONGLORA_MUX") {
|
|
||||||
if std::path::Path::new(&env).exists() {
|
|
||||||
return Some(env);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
|
|
||||||
let p = format!("{xdg}/donglora/mux.sock");
|
|
||||||
if std::path::Path::new(&p).exists() {
|
|
||||||
return Some(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let p = "/tmp/donglora-mux.sock";
|
|
||||||
if std::path::Path::new(p).exists() {
|
|
||||||
return Some(p.to_string());
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Connect to the mux daemon via Unix domain socket.
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub fn mux_connect(
|
|
||||||
path: Option<&str>,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> anyhow::Result<Client<MuxTransport>> {
|
|
||||||
let path = match path {
|
|
||||||
Some(p) => p.to_string(),
|
|
||||||
None => find_mux_socket()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("no mux socket found"))?,
|
|
||||||
};
|
|
||||||
let transport = MuxTransport::unix(&path, timeout)?;
|
|
||||||
Ok(Client::new(transport))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Connect to the mux daemon via TCP.
|
|
||||||
pub fn mux_tcp_connect(
|
|
||||||
host: &str,
|
|
||||||
port: u16,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> anyhow::Result<Client<MuxTransport>> {
|
|
||||||
let transport = MuxTransport::tcp(host, port, timeout)?;
|
|
||||||
Ok(Client::new(transport))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Auto-detect and connect to a DongLoRa device.
|
|
||||||
///
|
|
||||||
/// Priority:
|
|
||||||
/// 1. `DONGLORA_MUX_TCP` env var → TCP mux connection
|
|
||||||
/// 2. Unix socket mux (if socket file exists)
|
|
||||||
/// 3. Direct USB serial (auto-detect by VID:PID, blocks until device appears)
|
|
||||||
///
|
|
||||||
/// If `port` is `Some`, skips mux detection and connects directly to that serial port.
|
|
||||||
pub fn connect(
|
|
||||||
port: Option<&str>,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> anyhow::Result<Client<AnyTransport>> {
|
|
||||||
// If explicit port given, go direct
|
|
||||||
if let Some(port) = port {
|
|
||||||
debug!("opening serial port {port}");
|
|
||||||
let transport = SerialTransport::open(port, timeout)?;
|
|
||||||
return Ok(Client::new(AnyTransport::Serial(transport)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try TCP mux via environment variable
|
|
||||||
if let Ok(tcp) = std::env::var("DONGLORA_MUX_TCP")
|
|
||||||
&& let Some(transport) = try_tcp_mux(&tcp, timeout)
|
|
||||||
{
|
|
||||||
debug!("connected to TCP mux at {tcp}");
|
|
||||||
return Ok(Client::new(AnyTransport::Mux(transport)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try Unix socket mux
|
|
||||||
#[cfg(unix)]
|
|
||||||
if let Some(path) = find_mux_socket() {
|
|
||||||
match MuxTransport::unix(&path, timeout) {
|
|
||||||
Ok(transport) => {
|
|
||||||
debug!("connected to mux socket at {path}");
|
|
||||||
return Ok(Client::new(AnyTransport::Mux(transport)));
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// Stale socket or connection refused — fall through
|
|
||||||
debug!("mux socket at {path} not reachable, falling back to USB");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct USB serial — auto-detect or wait
|
|
||||||
let port_path = discovery::find_port()
|
|
||||||
.unwrap_or_else(discovery::wait_for_device);
|
|
||||||
debug!("opening serial port {port_path}");
|
|
||||||
let transport = SerialTransport::open(&port_path, timeout)?;
|
|
||||||
Ok(Client::new(AnyTransport::Serial(transport)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience: connect with default timeout.
|
|
||||||
pub fn connect_default() -> anyhow::Result<Client<AnyTransport>> {
|
|
||||||
connect(None, DEFAULT_TIMEOUT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Connect to a mux daemon only (TCP via env var, then Unix socket).
|
|
||||||
///
|
|
||||||
/// Unlike [`connect`], this **never** falls back to direct USB serial.
|
|
||||||
/// Returns an error if no mux is reachable — the caller can retry with backoff.
|
|
||||||
///
|
|
||||||
/// This is the Rust equivalent of the Python client's "sticky mux" behavior:
|
|
||||||
/// once you decide to use the mux, you stay on the mux.
|
|
||||||
pub fn connect_mux_auto(timeout: Duration) -> anyhow::Result<Client<AnyTransport>> {
|
|
||||||
// Try TCP mux via environment variable.
|
|
||||||
if let Ok(tcp) = std::env::var("DONGLORA_MUX_TCP")
|
|
||||||
&& let Some(transport) = try_tcp_mux(&tcp, timeout)
|
|
||||||
{
|
|
||||||
debug!("connected to TCP mux at {tcp}");
|
|
||||||
return Ok(Client::new(AnyTransport::Mux(transport)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try Unix socket mux.
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
let path = find_mux_socket()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("no mux socket found"))?;
|
|
||||||
let transport = MuxTransport::unix(&path, timeout)?;
|
|
||||||
debug!("connected to mux socket at {path}");
|
|
||||||
Ok(Client::new(AnyTransport::Mux(transport)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
anyhow::bail!("mux-only mode requires Unix socket support or DONGLORA_MUX_TCP")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_tcp_mux(addr: &str, timeout: Duration) -> Option<MuxTransport> {
|
|
||||||
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
|
|
||||||
let host = if h.is_empty() { "localhost" } else { h };
|
|
||||||
let port: u16 = p.parse().ok()?;
|
|
||||||
(host.to_string(), port)
|
|
||||||
} else {
|
|
||||||
let port: u16 = addr.parse().ok()?;
|
|
||||||
("localhost".to_string(), port)
|
|
||||||
};
|
|
||||||
MuxTransport::tcp(&host, port, timeout).ok()
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
//! USB device discovery for DongLoRa dongles.
|
|
||||||
//!
|
|
||||||
//! Finds the serial port by matching USB VID:PID using the `serialport` crate.
|
|
||||||
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
/// DongLoRa USB Vendor ID.
|
|
||||||
pub const USB_VID: u16 = 0x1209;
|
|
||||||
|
|
||||||
/// DongLoRa USB Product ID.
|
|
||||||
pub const USB_PID: u16 = 0x5741;
|
|
||||||
|
|
||||||
/// Find the DongLoRa serial port by USB VID:PID.
|
|
||||||
///
|
|
||||||
/// Returns the first matching port path, or `None` if no device is found.
|
|
||||||
pub fn find_port() -> Option<String> {
|
|
||||||
let ports = serialport::available_ports().ok()?;
|
|
||||||
ports
|
|
||||||
.into_iter()
|
|
||||||
.find(|p| {
|
|
||||||
matches!(
|
|
||||||
&p.port_type,
|
|
||||||
serialport::SerialPortType::UsbPort(info)
|
|
||||||
if info.vid == USB_VID && info.pid == USB_PID
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map(|p| p.port_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Block until a DongLoRa device appears on USB.
|
|
||||||
///
|
|
||||||
/// Polls [`find_port`] every 500ms and returns the port path once found.
|
|
||||||
pub fn wait_for_device() -> String {
|
|
||||||
info!("waiting for DongLoRa device...");
|
|
||||||
loop {
|
|
||||||
if let Some(port) = find_port() {
|
|
||||||
info!("found device at {port}");
|
|
||||||
// Brief delay for USB enumeration to settle
|
|
||||||
thread::sleep(Duration::from_millis(300));
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_millis(500));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
//! DongLoRa host library — connect, configure, send/receive LoRa packets.
|
|
||||||
//!
|
|
||||||
//! Implements the DongLoRa USB protocol (COBS-framed fixed-size LE).
|
|
||||||
//! See `firmware/PROTOCOL.md` for the full specification.
|
|
||||||
//!
|
|
||||||
//! # Quick start
|
|
||||||
//!
|
|
||||||
//! ```no_run
|
|
||||||
//! use donglora_client::*;
|
|
||||||
//!
|
|
||||||
//! let mut client = connect_default()?;
|
|
||||||
//! client.ping()?;
|
|
||||||
//! client.set_config(RadioConfig::default())?;
|
|
||||||
//! client.start_rx()?;
|
|
||||||
//!
|
|
||||||
//! loop {
|
|
||||||
//! if let Some(Response::RxPacket { rssi, snr, payload }) = client.recv()? {
|
|
||||||
//! println!("RX rssi={rssi} snr={snr} len={}", payload.len());
|
|
||||||
//! }
|
|
||||||
//! }
|
|
||||||
//! # Ok::<(), anyhow::Error>(())
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
pub mod client;
|
|
||||||
pub mod codec;
|
|
||||||
pub mod connect;
|
|
||||||
pub mod discovery;
|
|
||||||
pub mod protocol;
|
|
||||||
pub mod transport;
|
|
||||||
|
|
||||||
// Flat re-exports for convenience
|
|
||||||
pub use client::Client;
|
|
||||||
pub use codec::{decode_frame, encode_frame, read_frame, FrameReader};
|
|
||||||
pub use connect::{connect, connect_default, connect_mux_auto, default_socket_path};
|
|
||||||
pub use discovery::{find_port, wait_for_device, USB_PID, USB_VID};
|
|
||||||
pub use protocol::{
|
|
||||||
Bandwidth, Command, ErrorCode, RadioConfig, Response, CMD_TAG_SET_CONFIG, CMD_TAG_START_RX,
|
|
||||||
CMD_TAG_STOP_RX, ERROR_INVALID_CONFIG, MAX_PAYLOAD, PREAMBLE_DEFAULT, RADIO_CONFIG_SIZE,
|
|
||||||
RESP_TAG_ERROR, RESP_TAG_OK, RESP_TAG_RX_PACKET, TX_POWER_MAX,
|
|
||||||
};
|
|
||||||
pub use transport::{AnyTransport, MuxTransport, SerialTransport, Transport};
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub use connect::mux_connect;
|
|
||||||
pub use connect::mux_tcp_connect;
|
|
||||||
@@ -1,693 +0,0 @@
|
|||||||
//! Wire protocol types and fixed-size little-endian serialization.
|
|
||||||
//!
|
|
||||||
//! Mirrors the firmware's `protocol.rs` using std types. Every integer is
|
|
||||||
//! fixed-width LE. See `firmware/PROTOCOL.md` for the full specification.
|
|
||||||
|
|
||||||
/// Maximum LoRa payload size in bytes.
|
|
||||||
pub const MAX_PAYLOAD: usize = 256;
|
|
||||||
|
|
||||||
/// RadioConfig wire size (fixed).
|
|
||||||
pub const RADIO_CONFIG_SIZE: usize = 13;
|
|
||||||
|
|
||||||
/// Sentinel value for `tx_power_dbm`: use the board's maximum TX power.
|
|
||||||
pub const TX_POWER_MAX: i8 = i8::MIN; // -128 on the wire
|
|
||||||
|
|
||||||
/// Sentinel value for `preamble_len`: use the firmware default (16 symbols).
|
|
||||||
pub const PREAMBLE_DEFAULT: u16 = 0;
|
|
||||||
|
|
||||||
// ── Tag constants ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Command tag for SetConfig.
|
|
||||||
pub const CMD_TAG_SET_CONFIG: u8 = 2;
|
|
||||||
/// Command tag for StartRx.
|
|
||||||
pub const CMD_TAG_START_RX: u8 = 3;
|
|
||||||
/// Command tag for StopRx.
|
|
||||||
pub const CMD_TAG_STOP_RX: u8 = 4;
|
|
||||||
|
|
||||||
/// Response tag for RxPacket.
|
|
||||||
pub const RESP_TAG_RX_PACKET: u8 = 2;
|
|
||||||
/// Response tag for Ok.
|
|
||||||
pub const RESP_TAG_OK: u8 = 4;
|
|
||||||
/// Response tag for Error.
|
|
||||||
pub const RESP_TAG_ERROR: u8 = 5;
|
|
||||||
|
|
||||||
/// Error code for InvalidConfig.
|
|
||||||
pub const ERROR_INVALID_CONFIG: u8 = 0;
|
|
||||||
|
|
||||||
// ── Bandwidth ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// LoRa signal bandwidth.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum Bandwidth {
|
|
||||||
Khz7 = 0,
|
|
||||||
Khz10 = 1,
|
|
||||||
Khz15 = 2,
|
|
||||||
Khz20 = 3,
|
|
||||||
Khz31 = 4,
|
|
||||||
Khz41 = 5,
|
|
||||||
Khz62 = 6,
|
|
||||||
Khz125 = 7,
|
|
||||||
Khz250 = 8,
|
|
||||||
Khz500 = 9,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bandwidth {
|
|
||||||
/// Convert from raw byte. Returns `None` for invalid values.
|
|
||||||
pub fn from_u8(v: u8) -> Option<Self> {
|
|
||||||
match v {
|
|
||||||
0 => Some(Self::Khz7),
|
|
||||||
1 => Some(Self::Khz10),
|
|
||||||
2 => Some(Self::Khz15),
|
|
||||||
3 => Some(Self::Khz20),
|
|
||||||
4 => Some(Self::Khz31),
|
|
||||||
5 => Some(Self::Khz41),
|
|
||||||
6 => Some(Self::Khz62),
|
|
||||||
7 => Some(Self::Khz125),
|
|
||||||
8 => Some(Self::Khz250),
|
|
||||||
9 => Some(Self::Khz500),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── RadioConfig ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Complete LoRa radio configuration.
|
|
||||||
///
|
|
||||||
/// Wire layout (13 bytes, all little-endian):
|
|
||||||
/// ```text
|
|
||||||
/// [freq_hz:4] [bw:1] [sf:1] [cr:1] [sync_word:2] [tx_power_dbm:1] [preamble_len:2] [cad:1]
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct RadioConfig {
|
|
||||||
/// Frequency in Hz (150_000_000 - 960_000_000 for SX1262).
|
|
||||||
pub freq_hz: u32,
|
|
||||||
pub bw: Bandwidth,
|
|
||||||
/// Spreading factor (5-12).
|
|
||||||
pub sf: u8,
|
|
||||||
/// Coding rate denominator (5-8). E.g. 5 = CR 4/5, 8 = CR 4/8.
|
|
||||||
pub cr: u8,
|
|
||||||
pub sync_word: u16,
|
|
||||||
/// Transmit power in dBm. Set to [`TX_POWER_MAX`] (-128) for the board's maximum.
|
|
||||||
pub tx_power_dbm: i8,
|
|
||||||
/// Preamble length in symbols. Set to [`PREAMBLE_DEFAULT`] (0) for firmware default (16).
|
|
||||||
pub preamble_len: u16,
|
|
||||||
/// Channel Activity Detection (listen-before-talk). 0 = disabled, non-zero = enabled.
|
|
||||||
pub cad: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RadioConfig {
|
|
||||||
/// Serialize to fixed-size LE bytes.
|
|
||||||
pub fn to_bytes(&self) -> [u8; RADIO_CONFIG_SIZE] {
|
|
||||||
let mut buf = [0u8; RADIO_CONFIG_SIZE];
|
|
||||||
buf[0..4].copy_from_slice(&self.freq_hz.to_le_bytes());
|
|
||||||
buf[4] = self.bw as u8;
|
|
||||||
buf[5] = self.sf;
|
|
||||||
buf[6] = self.cr;
|
|
||||||
buf[7..9].copy_from_slice(&self.sync_word.to_le_bytes());
|
|
||||||
buf[9] = self.tx_power_dbm as u8;
|
|
||||||
buf[10..12].copy_from_slice(&self.preamble_len.to_le_bytes());
|
|
||||||
buf[12] = self.cad;
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deserialize from fixed-size LE bytes. Returns `None` if too short or invalid.
|
|
||||||
pub fn from_bytes(buf: &[u8]) -> Option<Self> {
|
|
||||||
if buf.len() < RADIO_CONFIG_SIZE {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(Self {
|
|
||||||
freq_hz: u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
|
|
||||||
bw: Bandwidth::from_u8(buf[4])?,
|
|
||||||
sf: buf[5],
|
|
||||||
cr: buf[6],
|
|
||||||
sync_word: u16::from_le_bytes([buf[7], buf[8]]),
|
|
||||||
tx_power_dbm: buf[9] as i8,
|
|
||||||
preamble_len: u16::from_le_bytes([buf[10], buf[11]]),
|
|
||||||
cad: buf[12],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Default: 915 MHz, 125 kHz BW, SF7, CR 4/5, sync 0x1424, max power, default preamble, CAD on.
|
|
||||||
impl Default for RadioConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
freq_hz: 915_000_000,
|
|
||||||
bw: Bandwidth::Khz125,
|
|
||||||
sf: 7,
|
|
||||||
cr: 5,
|
|
||||||
sync_word: 0x1424,
|
|
||||||
tx_power_dbm: TX_POWER_MAX,
|
|
||||||
preamble_len: PREAMBLE_DEFAULT,
|
|
||||||
cad: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Command ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Host → firmware commands.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Command {
|
|
||||||
Ping,
|
|
||||||
GetConfig,
|
|
||||||
SetConfig(RadioConfig),
|
|
||||||
StartRx,
|
|
||||||
StopRx,
|
|
||||||
Transmit {
|
|
||||||
config: Option<RadioConfig>,
|
|
||||||
payload: Vec<u8>,
|
|
||||||
},
|
|
||||||
DisplayOn,
|
|
||||||
DisplayOff,
|
|
||||||
GetMac,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command {
|
|
||||||
/// Returns the wire tag byte for this command.
|
|
||||||
pub fn tag(&self) -> u8 {
|
|
||||||
match self {
|
|
||||||
Self::Ping => 0,
|
|
||||||
Self::GetConfig => 1,
|
|
||||||
Self::SetConfig(_) => 2,
|
|
||||||
Self::StartRx => 3,
|
|
||||||
Self::StopRx => 4,
|
|
||||||
Self::Transmit { .. } => 5,
|
|
||||||
Self::DisplayOn => 6,
|
|
||||||
Self::DisplayOff => 7,
|
|
||||||
Self::GetMac => 8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize to fixed-size LE bytes.
|
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
|
||||||
match self {
|
|
||||||
Self::Ping => vec![0],
|
|
||||||
Self::GetConfig => vec![1],
|
|
||||||
Self::SetConfig(cfg) => {
|
|
||||||
let mut out = vec![2];
|
|
||||||
out.extend_from_slice(&cfg.to_bytes());
|
|
||||||
out
|
|
||||||
}
|
|
||||||
Self::StartRx => vec![3],
|
|
||||||
Self::StopRx => vec![4],
|
|
||||||
Self::Transmit { config, payload } => {
|
|
||||||
let mut out = vec![5];
|
|
||||||
match config {
|
|
||||||
None => out.push(0),
|
|
||||||
Some(cfg) => {
|
|
||||||
out.push(1);
|
|
||||||
out.extend_from_slice(&cfg.to_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.extend_from_slice(&(payload.len() as u16).to_le_bytes());
|
|
||||||
out.extend_from_slice(payload);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
Self::DisplayOn => vec![6],
|
|
||||||
Self::DisplayOff => vec![7],
|
|
||||||
Self::GetMac => vec![8],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deserialize from a decoded frame. Returns `None` on malformed input.
|
|
||||||
pub fn from_bytes(buf: &[u8]) -> Option<Self> {
|
|
||||||
let tag = *buf.first()?;
|
|
||||||
let rest = &buf[1..];
|
|
||||||
match tag {
|
|
||||||
0 => Some(Self::Ping),
|
|
||||||
1 => Some(Self::GetConfig),
|
|
||||||
2 => Some(Self::SetConfig(RadioConfig::from_bytes(rest)?)),
|
|
||||||
3 => Some(Self::StartRx),
|
|
||||||
4 => Some(Self::StopRx),
|
|
||||||
5 => {
|
|
||||||
if rest.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (config, pos) = if rest[0] == 0 {
|
|
||||||
(None, 1)
|
|
||||||
} else if rest[0] == 1 && rest.len() > RADIO_CONFIG_SIZE {
|
|
||||||
(Some(RadioConfig::from_bytes(&rest[1..])?), 1 + RADIO_CONFIG_SIZE)
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
if rest.len() < pos + 2 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let len = u16::from_le_bytes([rest[pos], rest[pos + 1]]) as usize;
|
|
||||||
let data_start = pos + 2;
|
|
||||||
if rest.len() < data_start + len || len > MAX_PAYLOAD {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(Self::Transmit {
|
|
||||||
config,
|
|
||||||
payload: rest[data_start..data_start + len].to_vec(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
6 => Some(Self::DisplayOn),
|
|
||||||
7 => Some(Self::DisplayOff),
|
|
||||||
8 => Some(Self::GetMac),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── ErrorCode ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Error codes reported by the firmware.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum ErrorCode {
|
|
||||||
InvalidConfig = 0,
|
|
||||||
RadioBusy = 1,
|
|
||||||
TxTimeout = 2,
|
|
||||||
CrcError = 3,
|
|
||||||
NotConfigured = 4,
|
|
||||||
NoDisplay = 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ErrorCode {
|
|
||||||
/// Convert from raw byte. Returns `None` for unknown codes.
|
|
||||||
pub fn from_u8(v: u8) -> Option<Self> {
|
|
||||||
match v {
|
|
||||||
0 => Some(Self::InvalidConfig),
|
|
||||||
1 => Some(Self::RadioBusy),
|
|
||||||
2 => Some(Self::TxTimeout),
|
|
||||||
3 => Some(Self::CrcError),
|
|
||||||
4 => Some(Self::NotConfigured),
|
|
||||||
5 => Some(Self::NoDisplay),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ErrorCode {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::InvalidConfig => write!(f, "InvalidConfig"),
|
|
||||||
Self::RadioBusy => write!(f, "RadioBusy"),
|
|
||||||
Self::TxTimeout => write!(f, "TxTimeout"),
|
|
||||||
Self::CrcError => write!(f, "CrcError"),
|
|
||||||
Self::NotConfigured => write!(f, "NotConfigured"),
|
|
||||||
Self::NoDisplay => write!(f, "NoDisplay"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Response ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Firmware → host responses.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Response {
|
|
||||||
Pong,
|
|
||||||
Config(RadioConfig),
|
|
||||||
RxPacket {
|
|
||||||
rssi: i16,
|
|
||||||
snr: i16,
|
|
||||||
payload: Vec<u8>,
|
|
||||||
},
|
|
||||||
TxDone,
|
|
||||||
Ok,
|
|
||||||
Error(ErrorCode),
|
|
||||||
MacAddress([u8; 6]),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
/// Returns the wire tag byte for this response.
|
|
||||||
pub fn tag(&self) -> u8 {
|
|
||||||
match self {
|
|
||||||
Self::Pong => 0,
|
|
||||||
Self::Config(_) => 1,
|
|
||||||
Self::RxPacket { .. } => 2,
|
|
||||||
Self::TxDone => 3,
|
|
||||||
Self::Ok => 4,
|
|
||||||
Self::Error(_) => 5,
|
|
||||||
Self::MacAddress(_) => 6,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether this is an unsolicited RxPacket.
|
|
||||||
pub fn is_rx_packet(&self) -> bool {
|
|
||||||
matches!(self, Self::RxPacket { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize to fixed-size LE bytes.
|
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
|
||||||
match self {
|
|
||||||
Self::Pong => vec![0],
|
|
||||||
Self::Config(cfg) => {
|
|
||||||
let mut out = vec![1];
|
|
||||||
out.extend_from_slice(&cfg.to_bytes());
|
|
||||||
out
|
|
||||||
}
|
|
||||||
Self::RxPacket { rssi, snr, payload } => {
|
|
||||||
let mut out = vec![2];
|
|
||||||
out.extend_from_slice(&rssi.to_le_bytes());
|
|
||||||
out.extend_from_slice(&snr.to_le_bytes());
|
|
||||||
out.extend_from_slice(&(payload.len() as u16).to_le_bytes());
|
|
||||||
out.extend_from_slice(payload);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
Self::TxDone => vec![3],
|
|
||||||
Self::Ok => vec![4],
|
|
||||||
Self::Error(code) => vec![5, *code as u8],
|
|
||||||
Self::MacAddress(mac) => {
|
|
||||||
let mut out = vec![6];
|
|
||||||
out.extend_from_slice(mac);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deserialize from a decoded frame. Returns `None` on malformed input.
|
|
||||||
pub fn from_bytes(buf: &[u8]) -> Option<Self> {
|
|
||||||
let tag = *buf.first()?;
|
|
||||||
let rest = &buf[1..];
|
|
||||||
match tag {
|
|
||||||
0 => Some(Self::Pong),
|
|
||||||
1 => Some(Self::Config(RadioConfig::from_bytes(rest)?)),
|
|
||||||
2 => {
|
|
||||||
if rest.len() < 6 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let rssi = i16::from_le_bytes([rest[0], rest[1]]);
|
|
||||||
let snr = i16::from_le_bytes([rest[2], rest[3]]);
|
|
||||||
let len = u16::from_le_bytes([rest[4], rest[5]]) as usize;
|
|
||||||
if rest.len() < 6 + len || len > MAX_PAYLOAD {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(Self::RxPacket {
|
|
||||||
rssi,
|
|
||||||
snr,
|
|
||||||
payload: rest[6..6 + len].to_vec(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
3 => Some(Self::TxDone),
|
|
||||||
4 => Some(Self::Ok),
|
|
||||||
5 => Some(Self::Error(ErrorCode::from_u8(*rest.first()?)?)),
|
|
||||||
6 => {
|
|
||||||
if rest.len() < 6 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut mac = [0u8; 6];
|
|
||||||
mac.copy_from_slice(&rest[..6]);
|
|
||||||
Some(Self::MacAddress(mac))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tests ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn make_config() -> RadioConfig {
|
|
||||||
RadioConfig {
|
|
||||||
freq_hz: 915_000_000,
|
|
||||||
bw: Bandwidth::Khz125,
|
|
||||||
sf: 7,
|
|
||||||
cr: 5,
|
|
||||||
sync_word: 0x3444,
|
|
||||||
tx_power_dbm: 22,
|
|
||||||
preamble_len: 16,
|
|
||||||
cad: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── RadioConfig ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_roundtrip() {
|
|
||||||
let cfg = make_config();
|
|
||||||
let bytes = cfg.to_bytes();
|
|
||||||
assert_eq!(bytes.len(), RADIO_CONFIG_SIZE);
|
|
||||||
assert_eq!(RadioConfig::from_bytes(&bytes), Some(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_default_roundtrip() {
|
|
||||||
let cfg = RadioConfig::default();
|
|
||||||
let bytes = cfg.to_bytes();
|
|
||||||
assert_eq!(RadioConfig::from_bytes(&bytes), Some(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_all_bandwidths() {
|
|
||||||
for bw_val in 0u8..=9 {
|
|
||||||
let bw = Bandwidth::from_u8(bw_val);
|
|
||||||
assert!(bw.is_some(), "bandwidth {bw_val} should be valid");
|
|
||||||
let cfg = RadioConfig { bw: bw.unwrap(), ..make_config() };
|
|
||||||
let bytes = cfg.to_bytes();
|
|
||||||
assert_eq!(RadioConfig::from_bytes(&bytes), Some(cfg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_invalid_bandwidth() {
|
|
||||||
let mut buf = make_config().to_bytes();
|
|
||||||
buf[4] = 255;
|
|
||||||
assert!(RadioConfig::from_bytes(&buf).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_negative_power() {
|
|
||||||
let cfg = RadioConfig { tx_power_dbm: TX_POWER_MAX, ..make_config() };
|
|
||||||
let bytes = cfg.to_bytes();
|
|
||||||
assert_eq!(RadioConfig::from_bytes(&bytes), Some(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_short_buffer() {
|
|
||||||
assert!(RadioConfig::from_bytes(&[0u8; 12]).is_none());
|
|
||||||
assert!(RadioConfig::from_bytes(&[]).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Command ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_simple_roundtrips() {
|
|
||||||
for cmd in [
|
|
||||||
Command::Ping,
|
|
||||||
Command::GetConfig,
|
|
||||||
Command::StartRx,
|
|
||||||
Command::StopRx,
|
|
||||||
Command::DisplayOn,
|
|
||||||
Command::DisplayOff,
|
|
||||||
Command::GetMac,
|
|
||||||
] {
|
|
||||||
let bytes = cmd.to_bytes();
|
|
||||||
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_set_config_roundtrip() {
|
|
||||||
let cmd = Command::SetConfig(make_config());
|
|
||||||
let bytes = cmd.to_bytes();
|
|
||||||
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_transmit_no_config_roundtrip() {
|
|
||||||
let cmd = Command::Transmit {
|
|
||||||
config: None,
|
|
||||||
payload: b"hello".to_vec(),
|
|
||||||
};
|
|
||||||
let bytes = cmd.to_bytes();
|
|
||||||
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_transmit_with_config_roundtrip() {
|
|
||||||
let cmd = Command::Transmit {
|
|
||||||
config: Some(make_config()),
|
|
||||||
payload: b"test".to_vec(),
|
|
||||||
};
|
|
||||||
let bytes = cmd.to_bytes();
|
|
||||||
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_transmit_empty_payload() {
|
|
||||||
let cmd = Command::Transmit {
|
|
||||||
config: None,
|
|
||||||
payload: vec![],
|
|
||||||
};
|
|
||||||
let bytes = cmd.to_bytes();
|
|
||||||
assert_eq!(Command::from_bytes(&bytes), Some(cmd));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_transmit_truncated() {
|
|
||||||
assert!(Command::from_bytes(&[5]).is_none()); // tag only
|
|
||||||
assert!(Command::from_bytes(&[5, 1]).is_none()); // has_config=1, no config
|
|
||||||
assert!(Command::from_bytes(&[5, 0]).is_none()); // has_config=0, no length
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_invalid_tag() {
|
|
||||||
assert!(Command::from_bytes(&[9]).is_none());
|
|
||||||
assert!(Command::from_bytes(&[255]).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_empty_buffer() {
|
|
||||||
assert!(Command::from_bytes(&[]).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_tags() {
|
|
||||||
assert_eq!(Command::Ping.tag(), 0);
|
|
||||||
assert_eq!(Command::GetConfig.tag(), 1);
|
|
||||||
assert_eq!(Command::SetConfig(make_config()).tag(), 2);
|
|
||||||
assert_eq!(Command::StartRx.tag(), 3);
|
|
||||||
assert_eq!(Command::StopRx.tag(), 4);
|
|
||||||
assert_eq!(Command::Transmit { config: None, payload: vec![] }.tag(), 5);
|
|
||||||
assert_eq!(Command::DisplayOn.tag(), 6);
|
|
||||||
assert_eq!(Command::DisplayOff.tag(), 7);
|
|
||||||
assert_eq!(Command::GetMac.tag(), 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Response ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_simple_roundtrips() {
|
|
||||||
for resp in [Response::Pong, Response::TxDone, Response::Ok] {
|
|
||||||
let bytes = resp.to_bytes();
|
|
||||||
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_config_roundtrip() {
|
|
||||||
let resp = Response::Config(make_config());
|
|
||||||
let bytes = resp.to_bytes();
|
|
||||||
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_rx_packet_roundtrip() {
|
|
||||||
let resp = Response::RxPacket {
|
|
||||||
rssi: -80,
|
|
||||||
snr: 10,
|
|
||||||
payload: b"data".to_vec(),
|
|
||||||
};
|
|
||||||
let bytes = resp.to_bytes();
|
|
||||||
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_rx_packet_empty_payload() {
|
|
||||||
let resp = Response::RxPacket {
|
|
||||||
rssi: -120,
|
|
||||||
snr: -5,
|
|
||||||
payload: vec![],
|
|
||||||
};
|
|
||||||
let bytes = resp.to_bytes();
|
|
||||||
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_error_codes() {
|
|
||||||
for code in [
|
|
||||||
ErrorCode::InvalidConfig,
|
|
||||||
ErrorCode::RadioBusy,
|
|
||||||
ErrorCode::TxTimeout,
|
|
||||||
ErrorCode::CrcError,
|
|
||||||
ErrorCode::NotConfigured,
|
|
||||||
ErrorCode::NoDisplay,
|
|
||||||
] {
|
|
||||||
let resp = Response::Error(code);
|
|
||||||
let bytes = resp.to_bytes();
|
|
||||||
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_mac_address_roundtrip() {
|
|
||||||
let resp = Response::MacAddress([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
|
||||||
let bytes = resp.to_bytes();
|
|
||||||
assert_eq!(Response::from_bytes(&bytes), Some(resp));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_tags() {
|
|
||||||
assert_eq!(Response::Pong.tag(), 0);
|
|
||||||
assert_eq!(Response::Config(make_config()).tag(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
Response::RxPacket { rssi: 0, snr: 0, payload: vec![] }.tag(),
|
|
||||||
2
|
|
||||||
);
|
|
||||||
assert_eq!(Response::TxDone.tag(), 3);
|
|
||||||
assert_eq!(Response::Ok.tag(), 4);
|
|
||||||
assert_eq!(Response::Error(ErrorCode::RadioBusy).tag(), 5);
|
|
||||||
assert_eq!(Response::MacAddress([0; 6]).tag(), 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_is_rx_packet() {
|
|
||||||
assert!(Response::RxPacket { rssi: 0, snr: 0, payload: vec![] }.is_rx_packet());
|
|
||||||
assert!(!Response::Ok.is_rx_packet());
|
|
||||||
assert!(!Response::Pong.is_rx_packet());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_invalid_tag() {
|
|
||||||
assert!(Response::from_bytes(&[7]).is_none());
|
|
||||||
assert!(Response::from_bytes(&[255]).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_truncated() {
|
|
||||||
assert!(Response::from_bytes(&[]).is_none());
|
|
||||||
assert!(Response::from_bytes(&[5]).is_none()); // Error with no code
|
|
||||||
assert!(Response::from_bytes(&[2, 0, 0]).is_none()); // RxPacket too short
|
|
||||||
assert!(Response::from_bytes(&[6, 0, 0, 0]).is_none()); // MacAddress too short
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── ErrorCode ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn error_code_from_u8() {
|
|
||||||
for v in 0..=5 {
|
|
||||||
assert!(ErrorCode::from_u8(v).is_some());
|
|
||||||
}
|
|
||||||
assert!(ErrorCode::from_u8(6).is_none());
|
|
||||||
assert!(ErrorCode::from_u8(255).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn error_code_display() {
|
|
||||||
assert_eq!(ErrorCode::InvalidConfig.to_string(), "InvalidConfig");
|
|
||||||
assert_eq!(ErrorCode::RadioBusy.to_string(), "RadioBusy");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Cross-compatibility with firmware encoding ─────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn firmware_worked_example() {
|
|
||||||
// From PROTOCOL.md: 915 MHz, 125 kHz BW, SF7, CR 4/5, sync 0x1424, max power
|
|
||||||
let cfg = RadioConfig {
|
|
||||||
freq_hz: 915_000_000,
|
|
||||||
bw: Bandwidth::Khz125,
|
|
||||||
sf: 7,
|
|
||||||
cr: 5,
|
|
||||||
sync_word: 0x1424,
|
|
||||||
tx_power_dbm: TX_POWER_MAX,
|
|
||||||
preamble_len: PREAMBLE_DEFAULT,
|
|
||||||
cad: 1,
|
|
||||||
};
|
|
||||||
let cmd = Command::SetConfig(cfg);
|
|
||||||
let bytes = cmd.to_bytes();
|
|
||||||
// Expected: 02 C0 CA 89 36 07 07 05 24 14 80 00 00 01
|
|
||||||
assert_eq!(
|
|
||||||
bytes,
|
|
||||||
[0x02, 0xC0, 0xCA, 0x89, 0x36, 0x07, 0x07, 0x05, 0x24, 0x14, 0x80, 0x00, 0x00, 0x01]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
//! Transport abstraction for byte-stream connections.
|
|
||||||
//!
|
|
||||||
//! The [`Transport`] trait unifies serial ports and mux socket connections,
|
|
||||||
//! allowing the [`Client`](crate::client::Client) to work over any of them.
|
|
||||||
|
|
||||||
use std::io::{self, Read, Write};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
/// A byte-stream transport that the client can talk over.
|
|
||||||
pub trait Transport: Read + Write + Send {
|
|
||||||
/// Set the read timeout. A read that takes longer returns zero bytes.
|
|
||||||
fn set_timeout(&mut self, timeout: Duration) -> anyhow::Result<()>;
|
|
||||||
|
|
||||||
/// Get the current read timeout.
|
|
||||||
fn timeout(&self) -> Duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Serial transport ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Direct USB serial port connection to a DongLoRa dongle.
|
|
||||||
pub struct SerialTransport {
|
|
||||||
port: Box<dyn serialport::SerialPort>,
|
|
||||||
timeout: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SerialTransport {
|
|
||||||
/// Open a serial port with the given timeout.
|
|
||||||
///
|
|
||||||
/// Baud rate is irrelevant for USB CDC-ACM but we pass a conventional value.
|
|
||||||
pub fn open(path: &str, timeout: Duration) -> anyhow::Result<Self> {
|
|
||||||
let port = serialport::new(path, 115_200)
|
|
||||||
.timeout(timeout)
|
|
||||||
.open()
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to open serial port {path}: {e}"))?;
|
|
||||||
Ok(Self { port, timeout })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the serial input buffer.
|
|
||||||
pub fn reset_input_buffer(&self) -> anyhow::Result<()> {
|
|
||||||
self.port
|
|
||||||
.clear(serialport::ClearBuffer::Input)
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to clear input buffer: {e}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Read for SerialTransport {
|
|
||||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
||||||
self.port.read(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write for SerialTransport {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
||||||
self.port.write(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
self.port.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Transport for SerialTransport {
|
|
||||||
fn set_timeout(&mut self, timeout: Duration) -> anyhow::Result<()> {
|
|
||||||
self.port
|
|
||||||
.set_timeout(timeout)
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to set serial timeout: {e}"))?;
|
|
||||||
self.timeout = timeout;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn timeout(&self) -> Duration {
|
|
||||||
self.timeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Mux transport ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Connection to the DongLoRa mux daemon via Unix socket or TCP.
|
|
||||||
pub struct MuxTransport {
|
|
||||||
stream: MuxStream,
|
|
||||||
timeout: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MuxStream {
|
|
||||||
#[cfg(unix)]
|
|
||||||
Unix(std::os::unix::net::UnixStream),
|
|
||||||
Tcp(std::net::TcpStream),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MuxTransport {
|
|
||||||
/// Connect to the mux daemon via Unix domain socket.
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub fn unix(path: &str, timeout: Duration) -> anyhow::Result<Self> {
|
|
||||||
let stream = std::os::unix::net::UnixStream::connect(path)
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to connect to mux socket {path}: {e}"))?;
|
|
||||||
stream
|
|
||||||
.set_read_timeout(Some(timeout))
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to set socket read timeout: {e}"))?;
|
|
||||||
stream
|
|
||||||
.set_write_timeout(Some(timeout))
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to set socket write timeout: {e}"))?;
|
|
||||||
Ok(Self {
|
|
||||||
stream: MuxStream::Unix(stream),
|
|
||||||
timeout,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Connect to the mux daemon via TCP.
|
|
||||||
pub fn tcp(host: &str, port: u16, timeout: Duration) -> anyhow::Result<Self> {
|
|
||||||
use std::net::ToSocketAddrs;
|
|
||||||
|
|
||||||
let addr = format!("{host}:{port}");
|
|
||||||
let sock_addr = addr
|
|
||||||
.to_socket_addrs()
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to resolve mux address {addr}: {e}"))?
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("no addresses found for {addr}"))?;
|
|
||||||
let stream = std::net::TcpStream::connect_timeout(&sock_addr, timeout)
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to connect to mux at {addr}: {e}"))?;
|
|
||||||
stream
|
|
||||||
.set_read_timeout(Some(timeout))
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to set TCP read timeout: {e}"))?;
|
|
||||||
stream
|
|
||||||
.set_write_timeout(Some(timeout))
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to set TCP write timeout: {e}"))?;
|
|
||||||
Ok(Self {
|
|
||||||
stream: MuxStream::Tcp(stream),
|
|
||||||
timeout,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Read for MuxTransport {
|
|
||||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
||||||
match &mut self.stream {
|
|
||||||
#[cfg(unix)]
|
|
||||||
MuxStream::Unix(s) => s.read(buf),
|
|
||||||
MuxStream::Tcp(s) => s.read(buf),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write for MuxTransport {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
||||||
match &mut self.stream {
|
|
||||||
#[cfg(unix)]
|
|
||||||
MuxStream::Unix(s) => s.write(buf),
|
|
||||||
MuxStream::Tcp(s) => s.write(buf),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
match &mut self.stream {
|
|
||||||
#[cfg(unix)]
|
|
||||||
MuxStream::Unix(s) => s.flush(),
|
|
||||||
MuxStream::Tcp(s) => s.flush(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Transport for MuxTransport {
|
|
||||||
fn set_timeout(&mut self, timeout: Duration) -> anyhow::Result<()> {
|
|
||||||
self.timeout = timeout;
|
|
||||||
let result = match &self.stream {
|
|
||||||
#[cfg(unix)]
|
|
||||||
MuxStream::Unix(s) => s
|
|
||||||
.set_read_timeout(Some(timeout))
|
|
||||||
.and_then(|()| s.set_write_timeout(Some(timeout))),
|
|
||||||
MuxStream::Tcp(s) => s
|
|
||||||
.set_read_timeout(Some(timeout))
|
|
||||||
.and_then(|()| s.set_write_timeout(Some(timeout))),
|
|
||||||
};
|
|
||||||
result.map_err(|e| anyhow::anyhow!("failed to set timeout: {e}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn timeout(&self) -> Duration {
|
|
||||||
self.timeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── AnyTransport ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Type-erased transport for the [`connect`](crate::connect::connect) return type.
|
|
||||||
pub enum AnyTransport {
|
|
||||||
Serial(SerialTransport),
|
|
||||||
Mux(MuxTransport),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Read for AnyTransport {
|
|
||||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
||||||
match self {
|
|
||||||
Self::Serial(t) => t.read(buf),
|
|
||||||
Self::Mux(t) => t.read(buf),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write for AnyTransport {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
||||||
match self {
|
|
||||||
Self::Serial(t) => t.write(buf),
|
|
||||||
Self::Mux(t) => t.write(buf),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
Self::Serial(t) => t.flush(),
|
|
||||||
Self::Mux(t) => t.flush(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Transport for AnyTransport {
|
|
||||||
fn set_timeout(&mut self, timeout: Duration) -> anyhow::Result<()> {
|
|
||||||
match self {
|
|
||||||
Self::Serial(t) => t.set_timeout(timeout),
|
|
||||||
Self::Mux(t) => t.set_timeout(timeout),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn timeout(&self) -> Duration {
|
|
||||||
match self {
|
|
||||||
Self::Serial(t) => t.timeout(),
|
|
||||||
Self::Mux(t) => t.timeout(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
.venv/
|
|
||||||
meshcore/corpus/generated/
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# DongLoRa Examples
|
|
||||||
|
|
||||||
Example scripts demonstrating the [Python client library](../clients/python/).
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd examples
|
|
||||||
just rx # receive packets
|
|
||||||
just tx # transmit a packet
|
|
||||||
just ping-pong --role tx # two-dongle ping-pong demo
|
|
||||||
just test-commands # exercise all DongLoRa commands
|
|
||||||
just bridge --mode server # LoRa bridge over TCP
|
|
||||||
just meshcore # MeshCore packet decoder
|
|
||||||
just orac # MeshCore AI bot (needs ANTHROPIC_API_KEY)
|
|
||||||
just telemetry # MeshCore repeater telemetry monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
## Depends On
|
|
||||||
|
|
||||||
- [clients/python](../clients/python/) — the `donglora` client library
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Exercise every DongLoRa command to verify the API works.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python examples/all_commands.py [PORT]
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import serial
|
|
||||||
|
|
||||||
import donglora as dl
|
|
||||||
|
|
||||||
try:
|
|
||||||
ser = dl.connect(sys.argv[1] if len(sys.argv) > 1 else None)
|
|
||||||
|
|
||||||
print("── Ping ──")
|
|
||||||
print(dl.send(ser, "Ping"))
|
|
||||||
|
|
||||||
print("\n── SetConfig ──")
|
|
||||||
print(dl.send(ser, "SetConfig", config=dl.DEFAULT_CONFIG))
|
|
||||||
|
|
||||||
print("\n── GetConfig ──")
|
|
||||||
print(dl.send(ser, "GetConfig"))
|
|
||||||
|
|
||||||
print("\n── StartRx ──")
|
|
||||||
print(dl.send(ser, "StartRx"))
|
|
||||||
|
|
||||||
print("\n── StopRx ──")
|
|
||||||
print(dl.send(ser, "StopRx"))
|
|
||||||
|
|
||||||
print("\n── Transmit ──")
|
|
||||||
print(dl.send(ser, "Transmit", payload=b"Hello from all_commands.py!"))
|
|
||||||
|
|
||||||
print("\n── DisplayOff ──")
|
|
||||||
print(dl.send(ser, "DisplayOff"))
|
|
||||||
|
|
||||||
print("\n── DisplayOn ──")
|
|
||||||
print(dl.send(ser, "DisplayOn"))
|
|
||||||
|
|
||||||
print("\n── GetMac ──")
|
|
||||||
print(dl.send(ser, "GetMac"))
|
|
||||||
|
|
||||||
print("\nAll 9 commands exercised successfully.")
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\nInterrupted.")
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"\nSerial error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
set shell := ["bash", "-c"]
|
|
||||||
|
|
||||||
_proj := source_directory()
|
|
||||||
_uv := "uv run --project " + _proj
|
|
||||||
_uv_meshcore := "uv run --project " + _proj + " --extra meshcore"
|
|
||||||
_uv_orac := "uv run --project " + _proj + " --extra orac"
|
|
||||||
|
|
||||||
[private]
|
|
||||||
_ensure_tools:
|
|
||||||
@mise trust --yes . 2>/dev/null; mise install --quiet
|
|
||||||
|
|
||||||
# Receive LoRa packets (Ctrl-C to stop)
|
|
||||||
rx *args: _ensure_tools
|
|
||||||
@{{_uv}} {{source_directory()}}/simple_rx.py {{args}}
|
|
||||||
|
|
||||||
# Transmit a single packet
|
|
||||||
tx *args: _ensure_tools
|
|
||||||
@{{_uv}} {{source_directory()}}/simple_tx.py {{args}}
|
|
||||||
|
|
||||||
# Two-dongle ping-pong demo (--role tx|rx)
|
|
||||||
ping-pong *args: _ensure_tools
|
|
||||||
@{{_uv}} {{source_directory()}}/ping_pong.py {{args}}
|
|
||||||
|
|
||||||
# Exercise all DongLoRa commands
|
|
||||||
test-commands *args: _ensure_tools
|
|
||||||
@{{_uv}} {{source_directory()}}/all_commands.py {{args}}
|
|
||||||
|
|
||||||
# Two-way LoRa bridge over TCP
|
|
||||||
bridge *args: _ensure_tools
|
|
||||||
@{{_uv}} {{source_directory()}}/lora_bridge.py {{args}}
|
|
||||||
|
|
||||||
# MeshCore packet decoder/monitor
|
|
||||||
meshcore *args: _ensure_tools
|
|
||||||
@{{_uv_meshcore}} {{source_directory()}}/meshcore/meshcore_rx.py {{args}}
|
|
||||||
|
|
||||||
# MeshCore AI bot (requires ANTHROPIC_API_KEY)
|
|
||||||
orac *args: _ensure_tools
|
|
||||||
@{{_uv_orac}} {{source_directory()}}/meshcore/ai_bot.py {{args}}
|
|
||||||
|
|
||||||
# MeshCore repeater telemetry monitor
|
|
||||||
telemetry *args: _ensure_tools
|
|
||||||
@{{_uv_meshcore}} {{source_directory()}}/meshcore/telemetry_monitor.py {{args}}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Two-way LoRa bridge over TCP — relay packets between two DongLoRa dongles
|
|
||||||
across any IP network (LAN, Tailscale, WireGuard, internet).
|
|
||||||
|
|
||||||
Architecture:
|
|
||||||
[Radio A] <-USB-> [DongLoRa A] <-TCP-> [DongLoRa B] <-USB-> [Radio B]
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
# Machine A (server):
|
|
||||||
python examples/lora_bridge.py --mode server --port 9100
|
|
||||||
|
|
||||||
# Machine B (client):
|
|
||||||
python examples/lora_bridge.py --mode client --host machineA --port 9100
|
|
||||||
|
|
||||||
Both sides:
|
|
||||||
- Open local DongLoRa, configure radio, start RX
|
|
||||||
- Forward received LoRa packets -> TCP -> remote side -> TX
|
|
||||||
- Bidirectional: both sides receive AND transmit
|
|
||||||
"""
|
|
||||||
import argparse
|
|
||||||
import serial
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
import donglora as dl
|
|
||||||
|
|
||||||
|
|
||||||
def tcp_send(sock: socket.socket, payload: bytes):
|
|
||||||
"""Send a length-prefixed message over TCP."""
|
|
||||||
sock.sendall(struct.pack("<I", len(payload)) + payload)
|
|
||||||
|
|
||||||
|
|
||||||
def tcp_recv(sock: socket.socket) -> bytes | None:
|
|
||||||
"""Receive a length-prefixed message from TCP. Returns None on disconnect."""
|
|
||||||
header = b""
|
|
||||||
while len(header) < 4:
|
|
||||||
chunk = sock.recv(4 - len(header))
|
|
||||||
if not chunk:
|
|
||||||
return None
|
|
||||||
header += chunk
|
|
||||||
length = struct.unpack("<I", header)[0]
|
|
||||||
if length > 65536:
|
|
||||||
return None # sanity check
|
|
||||||
data = b""
|
|
||||||
while len(data) < length:
|
|
||||||
chunk = sock.recv(length - len(data))
|
|
||||||
if not chunk:
|
|
||||||
return None
|
|
||||||
data += chunk
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
_ser_lock = threading.Lock()
|
|
||||||
|
|
||||||
|
|
||||||
def radio_to_tcp(ser: serial.Serial, sock: socket.socket):
|
|
||||||
"""Thread: forward LoRa RX packets to TCP."""
|
|
||||||
ser.timeout = 1
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
with _ser_lock:
|
|
||||||
data = dl.read_frame(ser)
|
|
||||||
if data is None:
|
|
||||||
continue
|
|
||||||
resp = dl.decode_response(data)
|
|
||||||
if resp["type"] == "RxPacket":
|
|
||||||
payload = resp["payload"]
|
|
||||||
snr = resp["snr"]
|
|
||||||
rssi = resp["rssi"]
|
|
||||||
sf = dl.DEFAULT_CONFIG["sf"]
|
|
||||||
min_snr = -2.5 * (sf - 4)
|
|
||||||
|
|
||||||
# Grade per PROTOCOL.md — drop likely-corrupt packets
|
|
||||||
if snr < -32 or snr > 32:
|
|
||||||
grade = "INVALID"
|
|
||||||
elif snr < min_snr:
|
|
||||||
grade = "UNRELIABLE"
|
|
||||||
elif snr < min_snr + 3:
|
|
||||||
grade = "MARGINAL"
|
|
||||||
else:
|
|
||||||
grade = "GOOD"
|
|
||||||
|
|
||||||
if grade in ("INVALID", "UNRELIABLE"):
|
|
||||||
print(f" RX drop len:{len(payload):3d} rssi:{rssi}dBm snr:{snr}dB [{grade}]")
|
|
||||||
continue
|
|
||||||
|
|
||||||
tag = f" [{grade}]" if grade == "MARGINAL" else ""
|
|
||||||
print(f" RX→TCP len:{len(payload):3d} rssi:{rssi}dBm snr:{snr}dB{tag}")
|
|
||||||
tcp_send(sock, payload)
|
|
||||||
except (serial.SerialException, OSError) as e:
|
|
||||||
print(f" [radio→tcp error: {e}]")
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def tcp_to_radio(ser: serial.Serial, sock: socket.socket):
|
|
||||||
"""Thread: forward TCP packets to LoRa TX."""
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
payload = tcp_recv(sock)
|
|
||||||
if payload is None:
|
|
||||||
print(" [TCP disconnected]")
|
|
||||||
break
|
|
||||||
print(f" TCP→TX len:{len(payload):3d}")
|
|
||||||
with _ser_lock:
|
|
||||||
dl.send(ser, "Transmit", payload=payload)
|
|
||||||
except (serial.SerialException, OSError) as e:
|
|
||||||
print(f" [tcp→radio error: {e}]")
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def run_bridge(ser: serial.Serial, sock: socket.socket):
|
|
||||||
"""Run bidirectional bridge between serial and TCP."""
|
|
||||||
print("Bridge active — forwarding packets bidirectionally\n")
|
|
||||||
|
|
||||||
t1 = threading.Thread(target=radio_to_tcp, args=(ser, sock), daemon=True)
|
|
||||||
t2 = threading.Thread(target=tcp_to_radio, args=(ser, sock), daemon=True)
|
|
||||||
t1.start()
|
|
||||||
t2.start()
|
|
||||||
|
|
||||||
try:
|
|
||||||
while t1.is_alive() and t2.is_alive():
|
|
||||||
time.sleep(0.5)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
print("\nBridge stopped.")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description="DongLoRa two-way LoRa bridge over TCP")
|
|
||||||
parser.add_argument("--mode", choices=["server", "client"], required=True)
|
|
||||||
parser.add_argument("--host", default="localhost", help="Remote host (client mode)")
|
|
||||||
parser.add_argument("--port", type=int, default=9100, help="TCP port")
|
|
||||||
parser.add_argument("--serial", default=None, help="Serial port (auto-detect if omitted)")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Open DongLoRa
|
|
||||||
ser = dl.connect(args.serial)
|
|
||||||
print(dl.send(ser, "SetConfig", config=dl.DEFAULT_CONFIG))
|
|
||||||
print(dl.send(ser, "StartRx"))
|
|
||||||
|
|
||||||
# Establish TCP connection
|
|
||||||
if args.mode == "server":
|
|
||||||
print(f"Listening on port {args.port}...")
|
|
||||||
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
srv.bind(("0.0.0.0", args.port))
|
|
||||||
srv.listen(1)
|
|
||||||
sock, addr = srv.accept()
|
|
||||||
print(f"Connected from {addr}")
|
|
||||||
srv.close()
|
|
||||||
else:
|
|
||||||
print(f"Connecting to {args.host}:{args.port}...")
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.connect((args.host, args.port))
|
|
||||||
print("Connected")
|
|
||||||
|
|
||||||
run_bridge(ser, sock)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\nInterrupted.")
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"\nSerial error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
except OSError as e:
|
|
||||||
print(f"\nNetwork error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# MeshCore Example
|
|
||||||
|
|
||||||
This is a **protocol implementation** built on top of DongLoRa. The DongLoRa
|
|
||||||
firmware itself is protocol-agnostic — it just gives you a LoRa radio pipe.
|
|
||||||
MeshCore is one of many possible protocols you can run over it.
|
|
||||||
|
|
||||||
## What's here
|
|
||||||
|
|
||||||
- **meshcore_rx.py** — MeshCore packet decoder, Watchman mesh health reporter
|
|
||||||
- **channels.csv** — 276 known MeshCore channels (Public + hashtag channels)
|
|
||||||
- **corpus/** — Test vectors for MeshCore packet decoders
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
From the `examples/` directory:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
just meshcore [PORT]
|
|
||||||
```
|
|
||||||
|
|
||||||
Connects to a DongLoRa device, configures the radio for the MeshCore
|
|
||||||
frequency/modulation, and decodes received packets in real time.
|
|
||||||
|
|
||||||
## Running the test corpus
|
|
||||||
|
|
||||||
From the `examples/` directory:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
uv run --extra meshcore meshcore/corpus/validate.py
|
|
||||||
```
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,281 +0,0 @@
|
|||||||
channel_name,hashtag,key_hex,channel_hash,description,region
|
|
||||||
Public,False,8b3387e9c5cdea6ac9e5edbaa115cd72,17,"Default public channel (fixed PSK, not hashtag-derived)",global
|
|
||||||
#test,True,9cd8fcf22a47333b591d96a2b848b73f,156,Global testing channel,global
|
|
||||||
#testing,True,cde5e82cf515647dcb547a79a4f065d1,205,Testing (alternate name),global
|
|
||||||
#ping,True,3cae16fd067ba9c32a98be22e9b98525,60,Ping-bot testing channel,global
|
|
||||||
#emergency,True,e1ad578d25108e344808f30dfdaaf926,225,Emergency communications,global
|
|
||||||
#hamradio,True,83c8b01997654265938da8765cbc7db9,131,HAM radio operators,global
|
|
||||||
#qrv,True,4d6de18b9bb6ef3f7c7197e8441044ed,77,HAM QSO coordination/logging,global
|
|
||||||
#hard-software,True,c4419524d9ce72ac093fdd2a0a856595,196,Hardware/software discussion & troubleshooting,global
|
|
||||||
#blackout,True,75420460f335577fa27cdd20dacf9edd,117,Power outage info/notifications,global
|
|
||||||
#warnings,True,31d44d29ed676f8020dc09211a2ee9d8,49,General warnings channel,global
|
|
||||||
#aurora-alert,True,e823127951fc6a425abe0c65ac07b323,232,Aurora/northern lights alerts,global
|
|
||||||
#flachwitze,True,ec98cd8821fada7fe21ff9e1df6c619f,236,Jokes channel (German flat jokes),global
|
|
||||||
#general,True,4c49f3f24629f5ee4ad5b3965db47985,76,General chat (common default),global
|
|
||||||
#switzerland,True,8ad1ce57ad257627090ed28413c1f0b7,138,Swiss national channel,CH
|
|
||||||
#svizzera,True,e571e6921d9c19d1c393f2ba9286abba,229,Italian-speaking Switzerland,CH
|
|
||||||
#suisse,True,2b8dd2d22f0820d9945316be97359ab1,43,French-speaking Switzerland,CH
|
|
||||||
#chtest,True,ee95ca5221f5b41941b26d89fd702c37,238,Swiss testing channel,CH
|
|
||||||
#bern,True,7658cadca2e40ba98cb13ce19782e994,118,Bern region,CH
|
|
||||||
#zuerich,True,15d09e6787401c4fdbef4bf10ffcef53,21,Zürich canton/city,CH
|
|
||||||
#wallis,True,1e65af5328cec2c29d03c77a5dfb8cb8,30,Wallis/Valais canton,CH
|
|
||||||
#freiburg,True,ec1c88f03aad858a6d74ec36ac04c420,236,"Freiburg (DE, near CH border)",CH/DE
|
|
||||||
#netzstatus,True,e84d5ce15046d5a7f69db3a40f604851,232,"Network status, mainly Germany",DE
|
|
||||||
#muenchen,True,3f50b3350a8ed17a303f07e9641dff4a,63,Munich area,DE
|
|
||||||
#hansemesh,True,21ddee26dc5215153c3903165fd4262f,33,HanseMesh community (Hamburg),DE
|
|
||||||
#de-sn-dd,True,c252cf274c306e76d9531fdf96f8dfa0,194,Dresden/Saxony test channel,DE
|
|
||||||
#berlin,True,8bffd7b0bf481d92afc625e409b88a16,139,Berlin (mentioned in MeshCore tutorial),DE
|
|
||||||
#hamburg,True,6ee6b4b98d5856392596cc1929eb2e36,110,Hamburg area,DE
|
|
||||||
#koeln,True,cc2de011842824b10ee2e28e97f91339,204,Cologne area,DE
|
|
||||||
#frankfurt,True,0d8e969330d62f9356b5e4d415ea7f98,13,Frankfurt area,DE
|
|
||||||
#stuttgart,True,74710f9e61866f184734bc367d9f492a,116,Stuttgart area,DE
|
|
||||||
#deutschland,True,6870d6013221f0278122cb8c88990794,104,Germany national,DE
|
|
||||||
#ruhrgebiet,True,6b5f2c549d9de0116749aa087e3caa4e,107,Ruhr area,DE
|
|
||||||
#nuernberg,True,a6e4f905a64512843b12ec20750f4161,166,Nuremberg area,DE
|
|
||||||
#dresden,True,a6f1bb7dbc598884b7a15be59e61ecd6,166,Dresden area,DE
|
|
||||||
#leipzig,True,0cc55d55809a7b048c39dd01c3254381,12,Leipzig area,DE
|
|
||||||
#hannover,True,b9430bfc2908b23efcae886f16eb425b,185,Hannover area,DE
|
|
||||||
#duesseldorf,True,1dfb747e50229101f2232e391d1eb5a7,29,Düsseldorf area,DE
|
|
||||||
#dortmund,True,37951d1ca4e428fd24c57f3087f160f0,55,Dortmund area,DE
|
|
||||||
#essen,True,80174f513e9099612b537bc1cd450a41,128,Essen area,DE
|
|
||||||
#austria,True,faaa5ef01081222e319a8205357321f4,250,Austria national channel,AT
|
|
||||||
#wien,True,aba9d5e2fb16d2b02111db5926819065,171,Vienna,AT
|
|
||||||
#graz,True,ea00635b84c245d519d29dc4044a43a1,234,Graz,AT
|
|
||||||
#salzburg,True,132d8883191473fc8a82f3447909a37d,19,Salzburg,AT
|
|
||||||
#innsbruck,True,dee607ccd01ca7ef375c0567c554db97,222,Innsbruck,AT
|
|
||||||
#linz,True,9b417e09eb452be6d068c90b3c46303d,155,Linz,AT
|
|
||||||
#italy,True,f8d4dd36f6b9476eb4cec18a1536b17d,248,Italy (English name variant),IT
|
|
||||||
#italia,True,17fa9f15bec81ed7da12116253db5a20,23,Italy (Italian name variant),IT
|
|
||||||
#roma,True,3523b8022329e6648738f0e5448ffcc1,53,Rome,IT
|
|
||||||
#milano,True,ea8929998e608b67fe5c2bcf29473d8f,234,Milan,IT
|
|
||||||
#nederland,True,c14f7b1e0416e4fa74f5f7b81a707b50,193,Netherlands national,NL
|
|
||||||
#amsterdam,True,d768f5a0aa65f8c54e4ea521bd49eb4f,215,Amsterdam,NL
|
|
||||||
#rotterdam,True,e3f3ad251e512a029bd27161b14f0bdd,227,Rotterdam,NL
|
|
||||||
#utrecht,True,042e458dfb05b17cbdee57f93df4be4c,4,Utrecht,NL
|
|
||||||
#denhaag,True,84eed36f62c7b22527dbf8883585ad14,132,The Hague,NL
|
|
||||||
#eindhoven,True,aceb8cceae584a3890c8db173668ae2b,172,Eindhoven,NL
|
|
||||||
#groningen,True,6a18329035bd3d2be69fed9094ffa5f4,106,Groningen,NL
|
|
||||||
#localmesh,True,13934f45750ce5f54311771bc52dc2a9,19,LocalMesh NL community,NL
|
|
||||||
#belgium,True,7085b78ed010599094f8c8e7d1aa0e27,112,Belgium national,BE
|
|
||||||
#belgie,True,559a87a20b1170a11656ffbe15d27c07,85,Belgium (Dutch),BE
|
|
||||||
#brussel,True,58648303d8e2b4e35f4301c4551d6307,88,Brussels,BE
|
|
||||||
#antwerpen,True,5900168627ddb2963d3fd8dba3fecbb9,89,Antwerp,BE
|
|
||||||
#gent,True,82f82c2835bd5fbda1ab0eb134364532,130,Ghent,BE
|
|
||||||
#france,True,edcb362b38e74b99025a7e551d925d20,237,France national,FR
|
|
||||||
#paris,True,d0fc2b1ec400eb8669010ce0311a00fb,208,Paris,FR
|
|
||||||
#lyon,True,0b4b602516088ea6d245be43102643b0,11,Lyon,FR
|
|
||||||
#marseille,True,c91ed99a23e560300b60325c0b9fb3ea,201,Marseille,FR
|
|
||||||
#uk,True,22b2eed34b5cc429ce1dc5e88635ff84,34,United Kingdom,UK
|
|
||||||
#london,True,9881d2b7ab9105a41a8d0f6ba449447e,152,London (mentioned in MeshCore tutorial),UK
|
|
||||||
#manchester,True,5ddc4f781fa3f504e9f514e77df13790,93,Manchester,UK
|
|
||||||
#birmingham,True,13b1c76ef007b4ad0c3b168610337513,19,Birmingham,UK
|
|
||||||
#scotland,True,f4fce403cfd56f7089920d065718f29c,244,Scotland,UK
|
|
||||||
#ireland,True,1b2a12acc5db1517d9d407946756b1da,27,Ireland,IE
|
|
||||||
#dublin,True,8792638977132bc05a1f72d6bb913694,135,Dublin,IE
|
|
||||||
#seattle,True,ef627a9bbbb549347fdb76bf0cd3bc14,239,Seattle (Puget Mesh),US-WA
|
|
||||||
#bot,True,eb50a1bcb3e4e5d7bf69a57c9dada211,235,Bot channel (Puget Mesh),US-WA
|
|
||||||
#bot-tacoma,True,278df637e12a0dda01c826e1fd9779fd,39,Bot channel Tacoma,US-WA
|
|
||||||
#bot-van,True,bf07fbf914e37710561cb741e914efeb,191,Bot channel Vancouver,US-WA/CA
|
|
||||||
#capitolhill,True,7f281916c8ec32e13c5ef687d182160a,127,Capitol Hill Seattle,US-WA
|
|
||||||
#protest,True,195e9167baf010c4538d8532b6d7296e,25,Protest coordination,US-WA
|
|
||||||
#queer,True,5754476f162d93bbee3de0efba136860,87,LGBTQ+ community,US-WA
|
|
||||||
#seahawks,True,4973d6ceb6c35a09894a83688847c8a3,73,Seattle Seahawks,US-WA
|
|
||||||
#sports,True,e8ee81f3aabf105d9ba2d2d4bd94fe4a,232,Sports discussion,US-WA
|
|
||||||
#salishmesh,True,c1ec55555132c7598393c869715b7555,193,Salish Mesh (Vancouver BC area),CA-BC
|
|
||||||
#dwebyvr,True,b8f956aab74dae02f70f11ddba36de53,184,DWeb Vancouver,CA-BC
|
|
||||||
#zspace,True,fc86b8cceea87091d45f166c7a698f78,252,ZSpace Vancouver,CA-BC
|
|
||||||
#portland,True,45c6bc719c15b9fb809f48f594359877,69,Portland OR,US-OR
|
|
||||||
#pnw,True,98059014c708581fbf0a398cfe8a486d,152,Pacific Northwest,US-PNW
|
|
||||||
#tacoma,True,9ba83a22a865c9baaffc6bc8b6a33d2f,155,Tacoma WA,US-WA
|
|
||||||
#idahomesh,True,60baad727ec5235cee359135aeecb3f4,96,Idaho statewide mesh,US-ID
|
|
||||||
#nodakmesh,True,156134e4a3f6d9826a39e0b7f85afdcd,21,North Dakota mesh,US-ND
|
|
||||||
#florida,True,c44bd74eac2c81dcb6dfb217727b05cd,196,Florida statewide,US-FL
|
|
||||||
#miami,True,d81c566a5d337d588ebd250df6fc1b63,216,Miami,US-FL
|
|
||||||
#tampa,True,8d10508c39d5d8e6c5e3e8fab41d1c09,141,Tampa,US-FL
|
|
||||||
#orlando,True,3553bdbd9b3a54da760624a27dcda156,53,Orlando,US-FL
|
|
||||||
#jacksonville,True,6cf63df8459d99d4a4a6177acf6661f6,108,Jacksonville,US-FL
|
|
||||||
#texas,True,c4a214e133de5e9ad276f99fdbd7216f,196,Texas statewide,US-TX
|
|
||||||
#austin,True,b2e6f9af95d959734d71cdf90ca62533,178,Austin TX,US-TX
|
|
||||||
#dallas,True,5b2efa4e2ad0a2b83c5486ca4dd244de,91,Dallas TX,US-TX
|
|
||||||
#houston,True,c001fbacad2d97676395ca37e2576345,192,Houston TX,US-TX
|
|
||||||
#sanantonio,True,c2e72548c615d1cf2796a6f77b9545b0,194,San Antonio TX,US-TX
|
|
||||||
#dfw,True,5b7dc809ba579affccab5462c537244e,91,Dallas-Fort Worth,US-TX
|
|
||||||
#california,True,55da17570958a6a311939c99582e6f60,85,California statewide,US-CA
|
|
||||||
#losangeles,True,3dd9373dcea0294bd05ab067cc58e9d4,61,Los Angeles,US-CA
|
|
||||||
#sanfrancisco,True,ae3608083279fd2ad03f0d6a99617d25,174,San Francisco,US-CA
|
|
||||||
#sandiego,True,08623bbd90a96ecdc1f5c34e7292b35f,8,San Diego,US-CA
|
|
||||||
#bayarea,True,7f9a5fd3070ad14e337ba100ca53a89b,127,Bay Area,US-CA
|
|
||||||
#sacramento,True,a6d6927f0b48762cf1346e2ae95cec14,166,Sacramento,US-CA
|
|
||||||
#socal,True,f4018307615ac79d2e5ef17bb44654d4,244,Southern California,US-CA
|
|
||||||
#norcal,True,f2438510715f5d9d55eb4370664330f5,242,Northern California,US-CA
|
|
||||||
#sanjose,True,ce964c28170b1c043d06073e6fcd83a7,206,San Jose,US-CA
|
|
||||||
#sfbay,True,5bd50243f42644439c21dfe367eea031,91,SF Bay,US-CA
|
|
||||||
#baymesh,True,1be0e569ad371f72d1e36da7242ae2f0,27,Bay Mesh community,US-CA
|
|
||||||
#boston,True,9587d847a7208da684c89cc1f525bc03,149,Boston MA,US-MA
|
|
||||||
#newyork,True,82a78024dd7edf6c9be298c919632e25,130,New York,US-NY
|
|
||||||
#nyc,True,6e6554655f84ca26fbc09d81d15d6b96,110,New York City,US-NY
|
|
||||||
#chicago,True,c1c289b131e5222370cbc2048445844b,193,Chicago IL,US-IL
|
|
||||||
#denver,True,b24355a0d22ed2bf393ec530d75810b4,178,Denver CO,US-CO
|
|
||||||
#phoenix,True,027850d9410fa98809819c96644ec04c,2,Phoenix AZ,US-AZ
|
|
||||||
#atlanta,True,3c8f15665f99a349a97427e7c312ee0e,60,Atlanta GA,US-GA
|
|
||||||
#nashville,True,717490d2561c2e1f211b13e95073fb16,113,Nashville TN,US-TN
|
|
||||||
#charlotte,True,b67efd607080d98640774cb31181b0ce,182,Charlotte NC,US-NC
|
|
||||||
#raleigh,True,1a2999ed1fea10e2cdf456ff118193d9,26,Raleigh NC,US-NC
|
|
||||||
#minneapolis,True,3283cca82b7b0ac50e8014c344cb8a86,50,Minneapolis MN,US-MN
|
|
||||||
#stlouis,True,f366422991a19e745f65096e59b43d51,243,St. Louis MO,US-MO
|
|
||||||
#pittsburgh,True,926cb5461a1ef572619da576ef7de04b,146,Pittsburgh PA,US-PA
|
|
||||||
#detroit,True,bd01f26bf7d8c90952753157a41c61ac,189,Detroit MI,US-MI
|
|
||||||
#washingtondc,True,0067d451cc79f26bc9924b3fc53f28d7,0,Washington DC,US-DC
|
|
||||||
#dc,True,0f3aa71fed514f5c16ebaf265ef05b2e,15,DC (short form),US-DC
|
|
||||||
#colorado,True,ef61c9e5a3286053746f7603044bcb08,239,Colorado statewide,US-CO
|
|
||||||
#ohio,True,e89e8c2f72ebae476c4ded78a360d0ea,232,Ohio statewide,US-OH
|
|
||||||
#virginia,True,07655778c8adeb16c1a0470cc3516b01,7,Virginia statewide,US-VA
|
|
||||||
#georgia,True,6621a99c4c21a1f4897a9833a95ac962,102,Georgia statewide,US-GA
|
|
||||||
#michigan,True,4b0dc02774aa2ad8fe120d4ab0d17f1c,75,Michigan statewide,US-MI
|
|
||||||
#newengland,True,4ad1e19832319b916ab4f3a0fbb0a38e,74,New England region,US-NE
|
|
||||||
#appalachia,True,ab353b2bf5d281d97cd73d897ef8e149,171,Appalachian region,US
|
|
||||||
#hawaii,True,3fd57495820328594e1641d14583faa6,63,Hawaii,US-HI
|
|
||||||
#alaska,True,2a5841192b151422baec71537b0b5238,42,Alaska,US-AK
|
|
||||||
#vegas,True,d45435b6467e7ded33c28f6796bf5183,212,Las Vegas,US-NV
|
|
||||||
#lasvegas,True,b82b823d6dcc845ead47cee8cfa758b3,184,Las Vegas (long form),US-NV
|
|
||||||
#saltlakecity,True,07e180549b35782901d407832b3ec6a6,7,Salt Lake City,US-UT
|
|
||||||
#slc,True,fd31a70b220eb3e2430e59c15432ef8c,253,SLC (short form),US-UT
|
|
||||||
#utah,True,ee42ff5f055f2e83527e4efb91b10d0a,238,Utah statewide,US-UT
|
|
||||||
#canada,True,8373bb1055f34164c6dd2663927cb6d9,131,Canada national,CA
|
|
||||||
#toronto,True,e0d3774dd1da4dc5c55d8bd731555334,224,Toronto,CA-ON
|
|
||||||
#vancouver,True,16d6034be448ab86d11858cfa4c57c9c,22,Vancouver,CA-BC
|
|
||||||
#montreal,True,0c4c03b5fbea5b80f89e2a2a16ed3f40,12,Montreal,CA-QC
|
|
||||||
#calgary,True,bbbb1ad23fe1cdd7739667418204a57d,187,Calgary,CA-AB
|
|
||||||
#ottawa,True,7871ec72b45617696c35c970bddd8124,120,Ottawa,CA-ON
|
|
||||||
#edmonton,True,ce451a3a24ee89ee31564ae29e9dee04,206,Edmonton,CA-AB
|
|
||||||
#winnipeg,True,3c3294aac8a83b1143ab6bedee5b5226,60,Winnipeg,CA-MB
|
|
||||||
#australia,True,eadb84fa1da64c44b77c40fd11b9d78e,234,Australia national,AU
|
|
||||||
#sydney,True,57fe5284a5b905835193868d9bdbe1e9,87,Sydney,AU-NSW
|
|
||||||
#melbourne,True,4d73731d9450ccb9673eb923c0f40af2,77,Melbourne,AU-VIC
|
|
||||||
#brisbane,True,e4bd09784621decac600d0d4d857e3b2,228,Brisbane (MBUG community),AU-QLD
|
|
||||||
#perth,True,9873c6cf14dac409b5a573574cff5ce1,152,Perth,AU-WA
|
|
||||||
#adelaide,True,9ad0c536f3eabc7c2f0434d68e242009,154,Adelaide,AU-SA
|
|
||||||
#canberra,True,c716951221e6c17faf5f22b6245ce64b,199,Canberra,AU-ACT
|
|
||||||
#newzealand,True,1870a961e4aa06f62b02b835efcacb71,24,New Zealand national,NZ
|
|
||||||
#nz,True,eb87ee8817ba71315ac7be9c733b523a,235,NZ (short form),NZ
|
|
||||||
#auckland,True,7c6f1c71a5a3d6823a1d7bfd2a349ac8,124,Auckland,NZ
|
|
||||||
#wellington,True,c2da57d74f78996ad71bbe3e22446f16,194,Wellington,NZ
|
|
||||||
#christchurch,True,7b88b875fc5ae76305c8a15349549a3b,123,Christchurch,NZ
|
|
||||||
#gisborne,True,d8b45a1eb52d0bd45655d5f2a72d571f,216,Gisborne (mentioned in MeshCore tutorial),NZ
|
|
||||||
#weather,True,88f502554fee92a1625cfb311546e7cb,136,Weather updates,topical
|
|
||||||
#jokes,True,abe610d61dee508c0ac18793c36c41c3,171,Jokes channel,topical
|
|
||||||
#quotes,True,086be6f920280b7cc51d6ad91d6a786c,8,Quotes channel,topical
|
|
||||||
#secret,True,be657c0527e122bf93bc735999cd7e0d,190,Secret channel (Brisbane MBUG),topical
|
|
||||||
#chat,True,d0bdd6d71538138ed979eec00d98ad97,208,General chat,topical
|
|
||||||
#news,True,ecadb1a7d803db8958bea1302ca6e8be,236,News channel,topical
|
|
||||||
#help,True,dcc67fae2067046832af7b2b0b743165,220,Help channel,topical
|
|
||||||
#sos,True,9ed2c78bcd68ac7ce2a2fc3bb4045114,158,SOS / distress,topical
|
|
||||||
#prepper,True,1eec1a7df7080a392cb490473c4a9920,30,Prepper community,topical
|
|
||||||
#offgrid,True,aaa26662bebf122262692d0bca61dcef,170,Off-grid living,topical
|
|
||||||
#radio,True,266f225baced1b2a868dcc8e9c69a304,38,Radio hobby,topical
|
|
||||||
#lora,True,0749ea267c6be7b54ca1dbbae7dba0aa,7,LoRa technology discussion,topical
|
|
||||||
#meshcore,True,2fa78a5aef618e7c2a78f0ab5c8869b3,47,MeshCore project discussion,topical
|
|
||||||
#meshtastic,True,73a2e13ff0dea9ed19b24b2ab753650f,115,Meshtastic discussion,topical
|
|
||||||
#iot,True,233b42534ed82b8ae6a88d785f1b27fd,35,IoT / sensors,topical
|
|
||||||
#solar,True,7592a730ff1af2b40b95589fbafcca01,117,Solar power,topical
|
|
||||||
#emcomm,True,a9a49340642dfc4c562d7849b7c8a258,169,Emergency communications,topical
|
|
||||||
#ares,True,44419e394cde859e45710f288db939a8,68,ARES (Amateur Radio Emergency Service),topical
|
|
||||||
#gmrs,True,2ad811e84ab4ad0e3a648d8fcf3d650a,42,GMRS radio,topical
|
|
||||||
#cb,True,a35b5f87bbb67859a02e7666c823de7a,163,CB radio,topical
|
|
||||||
#hiking,True,2370a013053e384e5f18918bc2b26baf,35,Hiking,topical
|
|
||||||
#camping,True,c011b7c2abf33748cd9bd5a78c2b4955,192,Camping,topical
|
|
||||||
#hunting,True,0c0c28fd9bbf096ee06e01af840037af,12,Hunting,topical
|
|
||||||
#fishing,True,a671ca13cc499898aea4596e804ecf20,166,Fishing,topical
|
|
||||||
#skiing,True,b3baf3586d347ecaf2c113106a564207,179,Skiing,topical
|
|
||||||
#biking,True,9c0591d708b7c7970c9ad6712e3c4d05,156,Biking,topical
|
|
||||||
#ham,True,5270db3979da687fa133fec6684cd952,82,HAM radio (short form),topical
|
|
||||||
#cq,True,bc9da8bdebcaeee40fefff7dd16855f1,188,CQ (calling all stations),topical
|
|
||||||
#dx,True,adf77706da76b66bb0a313c1484d3be4,173,DX (long distance contacts),topical
|
|
||||||
#repeater,True,289991a3077903263f2d31493887c651,40,Repeater discussion,topical
|
|
||||||
#firmware,True,6163564e7449c1c842e58dcf2cd1ab4f,97,Firmware discussion,topical
|
|
||||||
#debug,True,fd7a60ed4796efcd1965c2de466105cb,253,Debug channel,topical
|
|
||||||
#dev,True,d41bcba61e9dca7177c7b8533d23a0bc,212,Development channel,topical
|
|
||||||
#admin,True,889334b7e486938c776dbdde120da9de,136,Admin channel,topical
|
|
||||||
#monitor,True,131479235e65b7c591307f699d64dc74,19,Monitoring channel,topical
|
|
||||||
#alert,True,678c7d2e08c019e113ace03eaaa128ad,103,Alert channel,topical
|
|
||||||
#local,True,d2d35fa76be9875ed254db80397483a5,210,Local community,topical
|
|
||||||
#neighborhood,True,53403881955f0e11573ba1e5d3a82c4b,83,Neighborhood chat,topical
|
|
||||||
#community,True,b9323ce55a729068ed8e3ed49b9b84b5,185,Community channel,topical
|
|
||||||
#events,True,d2cbcc606979cdb05c7bd66f49f23baa,210,Events channel,topical
|
|
||||||
#trade,True,c2be982890fb3a63113de637b9aee6fe,194,Trading/swap channel,topical
|
|
||||||
#marketplace,True,354a741c2975c843ec0706089390b968,53,Marketplace,topical
|
|
||||||
#gaming,True,3802c79121f195ea50ad9ab2aa2c402a,56,Gaming,topical
|
|
||||||
#music,True,b025ab29b0a5f56fb68a474741d7a2cc,176,Music,topical
|
|
||||||
#tech,True,5177b749eefbfab3c90a21b4e2518c5f,81,Technology,topical
|
|
||||||
#science,True,8abc980d3764fdabda5dfa6b6bc3a6cc,138,Science,topical
|
|
||||||
#crypto,True,0aa7c45d99be27024b82a1474c693951,10,Cryptocurrency,topical
|
|
||||||
#bitcoin,True,94bf6fd2688b1456c071ebad63d7021d,148,Bitcoin,topical
|
|
||||||
#linux,True,02c74eda5d8ec8b9bb945049fb2f55c6,2,Linux,topical
|
|
||||||
#arduino,True,ce8c596922eff9274f3b1e19ad296754,206,Arduino,topical
|
|
||||||
#raspberry,True,469bc476d8263b64b6f90ca868b04b91,70,Raspberry Pi,topical
|
|
||||||
#esp32,True,9f8aac4c48973b07bbdc47acbaea3e8e,159,ESP32 projects,topical
|
|
||||||
#heltec,True,71af5d9a209e8b17a19ee22aa7b6be23,113,Heltec devices,topical
|
|
||||||
#lilygo,True,a33dd4b025cc82b81f97318a63a9d5d3,163,LilyGo devices,topical
|
|
||||||
#rak,True,1f4734675fdfcabfaa67a0a059c65c5f,31,RAK Wireless devices,topical
|
|
||||||
#antenna,True,e8979157107155ade90822e29b186ee8,232,Antenna discussion,topical
|
|
||||||
#rf,True,46aaa16f78b3969040aad2b6b694b59e,70,RF engineering,topical
|
|
||||||
#preppers,True,4877173813de9668b9ef33adbc1b8f37,72,Preppers community,topical
|
|
||||||
#survival,True,e1f465ea51df09fc901389758f1c5f01,225,Survival skills,topical
|
|
||||||
#grid-down,True,94ddbd51f2ee0bd47426a6f3ed79be99,148,Grid-down scenarios,topical
|
|
||||||
#wildfire,True,b9124c3aecc0c813634f79f144b93f83,185,Wildfire alerts,topical
|
|
||||||
#hurricane,True,c13a2f9d18400e5d93bae4bb3afef40c,193,Hurricane alerts,topical
|
|
||||||
#earthquake,True,0c7082c04a1a90502a5f32fc6a9f6524,12,Earthquake alerts,topical
|
|
||||||
#flood,True,f2a0fd0abe4c9fc6865b5f8eebf319d6,242,Flood alerts,topical
|
|
||||||
#tornado,True,76e577e8187d0bab89052343f3835039,118,Tornado alerts,topical
|
|
||||||
#storm,True,113761b9e31a5c30e0ee4fc78dc7310b,17,Storm alerts,topical
|
|
||||||
#traffic,True,c1216dacbb7e4c3b9a3356e52f1c27fa,193,Traffic updates,topical
|
|
||||||
#fire,True,3d74a070077293ab66baf3aa724349ff,61,Fire alerts,topical
|
|
||||||
#police,True,79b89aa870ea85dbb8396f401e547d99,121,Police scanner / alerts,topical
|
|
||||||
#ems,True,f714461f26d43d9a17b2530c3ad29bc2,247,EMS / first responders,topical
|
|
||||||
#search-rescue,True,cd8ab9e4944ee656ae6e961a4437c551,205,Search and rescue,topical
|
|
||||||
#volunteer,True,2d3d64746075cb99407d681e547b99cb,45,Volunteer coordination,topical
|
|
||||||
#data,True,b0d9c01c8278a711c68ffdca98ef1245,176,Data/telemetry channel,topical
|
|
||||||
#sensor,True,6218df72386dd48259024327e8ebf66f,98,Sensor data channel,topical
|
|
||||||
#tracker,True,70258e69aeb03d1cd2e16072e1fe14b5,112,Asset/pet tracker,topical
|
|
||||||
#gps,True,af4b9f722a6200b1eb2696660017d0df,175,GPS tracking,topical
|
|
||||||
#boat,True,c03117578e38c3f1eb5ae8c3d0019753,192,Boating/maritime,topical
|
|
||||||
#sailing,True,c2f6052f8b89639f81c8ff4def32deae,194,Sailing,topical
|
|
||||||
#marine,True,7f4b40aac63642805a54a959beb07694,127,Marine channel,topical
|
|
||||||
#aviation,True,e17538c835ee0990b349ca1879b4f57a,225,Aviation,topical
|
|
||||||
#drone,True,4460931543103de42d2f85093a80273c,68,Drone / UAV,topical
|
|
||||||
#fpv,True,6d2752bc94797cfce43e2138eab7a772,109,FPV flying,topical
|
|
||||||
#farm,True,4a2f1b2147165cf942402a4c6bd7d42d,74,Farming / agriculture,topical
|
|
||||||
#ranch,True,5885e728fc06bfcbe7d6c9020c13ff10,88,Ranch / livestock,topical
|
|
||||||
#tucson,True,cf989ccea881cf5ddcf40d87bbfc441e,207,Tucson AZ,US-AZ
|
|
||||||
#albuquerque,True,2a59b6ce7d3ce661664217f9b8255e86,42,Albuquerque NM,US-NM
|
|
||||||
#boise,True,f4e641f9c90ac3bd5bc442ef379f6eb3,244,Boise ID,US-ID
|
|
||||||
#spokane,True,dfcf2a863b3baa99e3d3274bfb56439f,223,Spokane WA,US-WA
|
|
||||||
#olympia,True,baec83dd15ef758ad8bf8f357237c9cb,186,Olympia WA,US-WA
|
|
||||||
#bellingham,True,6000c8e3c1343fd920749eb3ad2a42b4,96,Bellingham WA,US-WA
|
|
||||||
#eugene,True,e6ce6f5e71d88bb90c0b7dacc2235662,230,Eugene OR,US-OR
|
|
||||||
#bend,True,291ddea6fc1c1742f7ad425819b66f7b,41,Bend OR,US-OR
|
|
||||||
#reno,True,e9edf1edf57a04b8f66e75829b4edc06,233,Reno NV,US-NV
|
|
||||||
#provo,True,72744153a7a2d84ed69338be4dc3fc31,63,Provo,US-UT
|
|
||||||
#orem,True,97236968b8a6c0cf297f1c1365539d0d,157,Orem,US-UT
|
|
||||||
#ogden,True,cdeec2a40203056a6c6f2d1fb5f50b81,245,Ogden,US-UT
|
|
||||||
#logan,True,b0ee430ede14470d5ff8fafbe092ee50,145,Logan,US-UT
|
|
||||||
#stgeorge,True,7792659def2811af89a02270b2918235,128,St. George,US-UT
|
|
||||||
#parkcity,True,bbb64d6ae8eaeaf4302f501844f64511,25,Park City,US-UT
|
|
||||||
#lehi,True,18f20a96d251665105d59df6ca5bcc5c,98,Lehi,US-UT
|
|
||||||
#byu,True,65633d6a99bbcb173cb01afeae3a51d0,194,Brigham Young University,US-UT
|
|
||||||
#uvu,True,cb305b9b3d0ab33fce2186ee6b9c58ff,91,Utah Valley University,US-UT
|
|
||||||
#uofu,True,c08fe0d4d37b4b7f233f2977296979d5,178,University of Utah,US-UT
|
|
||||||
#usu,True,ceb222637d010d1c5551a5035e86f89d,104,Utah State University,US-UT
|
|
||||||
#utahcounty,True,f191a8a833d2ae61b7fbfa8c9891cc0c,23,Utah County,US-UT
|
|
||||||
#daviscounty,True,926847daaed0baa7758df5ef8af76f32,11,Davis County,US-UT
|
|
||||||
#webercounty,True,364726e43d1fb9b8079ededb03f00764,172,Weber County,US-UT
|
|
||||||
#wasatchfront,True,eb9fa3bfa390eed3737634bed39c7460,44,Wasatch Front metro corridor,US-UT
|
|
||||||
#wasatch,True,15bf0e80d3d88092d38e4c82da7e523e,0,Wasatch region,US-UT
|
|
||||||
#saltlake,True,56327c6958d9108213f8638fb2a2b7a4,173,Salt Lake (alternate),US-UT
|
|
||||||
#tooele,True,092cbdcf351bdbe77ce6c00e1dbeb294,184,Tooele,US-UT
|
|
||||||
#sanpete,True,4ae01748b17bfc90e13004f19e734bbc,151,Sanpete County,US-UT
|
|
||||||
#nodeops,True,33677d99a0e707e37b57de3e3cd26cf3,151,Node operations channel,topical
|
|
||||||
#weekly-net,True,bdc4f5cb11e766c9a09eeb3586e65a58,18,Weekly net check-in,topical
|
|
||||||
#net,True,13aef6b8ce642cf4c482b91f0816fe68,157,Net channel,topical
|
|
||||||
#repeater-ops,True,915c91c726e0c9d558365a7ecc72bbd6,108,Repeater operations,topical
|
|
||||||
#watchman,True,11489fe661e776b3ccdcf864bd14f7d6,245,Watchman mesh health alerts,global
|
|
||||||
|
@@ -1,649 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Build hand-crafted MeshCore test vectors as individual JSON files."""
|
|
||||||
# /// script
|
|
||||||
# requires-python = ">=3.10"
|
|
||||||
# dependencies = []
|
|
||||||
# ///
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import struct
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
OUT = Path(__file__).parent / "hand"
|
|
||||||
OUT.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def write_case(name: str, case: dict):
|
|
||||||
path = OUT / f"{name}.json"
|
|
||||||
path.write_text(json.dumps(case, indent=2, ensure_ascii=False) + "\n")
|
|
||||||
print(f" {path.name}")
|
|
||||||
|
|
||||||
|
|
||||||
# ── Packet building helpers ───────────────────────────────────────
|
|
||||||
|
|
||||||
def mc_header(payload_type: int, route_type: int, ver: int = 0) -> int:
|
|
||||||
return (ver << 6) | (payload_type << 2) | route_type
|
|
||||||
|
|
||||||
|
|
||||||
def path_len_byte(hop_count: int, hash_size: int) -> int:
|
|
||||||
return ((hash_size - 1) << 6) | (hop_count & 0x3F)
|
|
||||||
|
|
||||||
|
|
||||||
DUMMY_PUBKEY = bytes(range(32))
|
|
||||||
DUMMY_SIG = bytes(64)
|
|
||||||
TS_2026 = struct.pack("<I", 1774600000) # ~2026-03-24
|
|
||||||
|
|
||||||
|
|
||||||
def advert_packet(app_data: bytes, route_type: int = 1,
|
|
||||||
path_hops: list[bytes] | None = None,
|
|
||||||
hash_size: int = 1, pubkey: bytes = DUMMY_PUBKEY,
|
|
||||||
timestamp: bytes = TS_2026) -> bytes:
|
|
||||||
hdr = bytes([mc_header(0x04, route_type)])
|
|
||||||
hops = path_hops or []
|
|
||||||
pl = bytes([path_len_byte(len(hops), hash_size)])
|
|
||||||
path = b"".join(hops)
|
|
||||||
payload = pubkey + timestamp + DUMMY_SIG + app_data
|
|
||||||
return hdr + pl + path + payload
|
|
||||||
|
|
||||||
|
|
||||||
def peer_packet(payload_type: int, route_type: int, dst: int, src: int,
|
|
||||||
mac: bytes, ciphertext: bytes,
|
|
||||||
path_hops: list[bytes] | None = None,
|
|
||||||
hash_size: int = 1, tc: tuple[int, int] | None = None) -> bytes:
|
|
||||||
hdr = bytes([mc_header(payload_type, route_type)])
|
|
||||||
hops = path_hops or []
|
|
||||||
pl_byte = bytes([path_len_byte(len(hops), hash_size)])
|
|
||||||
path = b"".join(hops)
|
|
||||||
body = bytes([dst, src]) + mac + ciphertext
|
|
||||||
tc_bytes = b""
|
|
||||||
if tc is not None:
|
|
||||||
tc_bytes = struct.pack("<HH", tc[0], tc[1])
|
|
||||||
return hdr + tc_bytes + pl_byte + path + body
|
|
||||||
|
|
||||||
|
|
||||||
def ack_packet(crc: int, route_type: int = 1,
|
|
||||||
path_hops: list[bytes] | None = None,
|
|
||||||
hash_size: int = 1) -> bytes:
|
|
||||||
hdr = bytes([mc_header(0x03, route_type)])
|
|
||||||
hops = path_hops or []
|
|
||||||
pl_byte = bytes([path_len_byte(len(hops), hash_size)])
|
|
||||||
path = b"".join(hops)
|
|
||||||
return hdr + pl_byte + path + struct.pack("<I", crc)
|
|
||||||
|
|
||||||
|
|
||||||
def group_packet(payload_type: int, ch_hash: int, mac: bytes,
|
|
||||||
ciphertext: bytes, route_type: int = 1,
|
|
||||||
path_hops: list[bytes] | None = None,
|
|
||||||
hash_size: int = 1) -> bytes:
|
|
||||||
hdr = bytes([mc_header(payload_type, route_type)])
|
|
||||||
hops = path_hops or []
|
|
||||||
pl_byte = bytes([path_len_byte(len(hops), hash_size)])
|
|
||||||
path = b"".join(hops)
|
|
||||||
return hdr + pl_byte + path + bytes([ch_hash]) + mac + ciphertext
|
|
||||||
|
|
||||||
|
|
||||||
def anon_req_packet(dst: int, ephem_pub: bytes, mac: bytes, ciphertext: bytes,
|
|
||||||
route_type: int = 1, path_hops: list[bytes] | None = None,
|
|
||||||
hash_size: int = 1) -> bytes:
|
|
||||||
hdr = bytes([mc_header(0x07, route_type)])
|
|
||||||
hops = path_hops or []
|
|
||||||
pl_byte = bytes([path_len_byte(len(hops), hash_size)])
|
|
||||||
path = b"".join(hops)
|
|
||||||
return hdr + pl_byte + path + bytes([dst]) + ephem_pub + mac + ciphertext
|
|
||||||
|
|
||||||
|
|
||||||
def trace_packet(tag: int, auth_code: int, flags: int,
|
|
||||||
trace_hashes: bytes = b"",
|
|
||||||
route_type: int = 1,
|
|
||||||
path_hops: list[bytes] | None = None,
|
|
||||||
hash_size: int = 1) -> bytes:
|
|
||||||
hdr = bytes([mc_header(0x09, route_type)])
|
|
||||||
hops = path_hops or []
|
|
||||||
pl_byte = bytes([path_len_byte(len(hops), hash_size)])
|
|
||||||
path = b"".join(hops)
|
|
||||||
payload = struct.pack("<II", tag, auth_code) + bytes([flags]) + trace_hashes
|
|
||||||
return hdr + pl_byte + path + payload
|
|
||||||
|
|
||||||
|
|
||||||
def multipart_packet(remaining: int, inner_type: int, inner_payload: bytes,
|
|
||||||
route_type: int = 1) -> bytes:
|
|
||||||
hdr = bytes([mc_header(0x0A, route_type)])
|
|
||||||
pl_byte = bytes([path_len_byte(0, 1)])
|
|
||||||
control = bytes([(remaining << 4) | (inner_type & 0x0F)])
|
|
||||||
return hdr + pl_byte + control + inner_payload
|
|
||||||
|
|
||||||
|
|
||||||
def control_packet(flags_byte: int, payload: bytes,
|
|
||||||
route_type: int = 1) -> bytes:
|
|
||||||
hdr = bytes([mc_header(0x0B, route_type)])
|
|
||||||
pl_byte = bytes([path_len_byte(0, 1)])
|
|
||||||
return hdr + pl_byte + bytes([flags_byte]) + payload
|
|
||||||
|
|
||||||
|
|
||||||
# ── Test cases ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
print("Building hand-crafted test vectors...")
|
|
||||||
|
|
||||||
# ADVERT: basic chat node with name
|
|
||||||
write_case("advert_basic", {
|
|
||||||
"description": "ADVERT flood, chat node with simple ASCII name",
|
|
||||||
"tags": ["advert", "flood", "name", "chat"],
|
|
||||||
"packet_hex": advert_packet(bytes([0x81]) + b"TestNode").hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": DUMMY_PUBKEY[:4].hex(),
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 0x81,
|
|
||||||
"node_type": "chat",
|
|
||||||
"location": None,
|
|
||||||
"name": "TestNode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# ADVERT: repeater with valid GPS location + name
|
|
||||||
write_case("advert_location", {
|
|
||||||
"description": "ADVERT flood, repeater with valid GPS coords and name",
|
|
||||||
"tags": ["advert", "flood", "location", "repeater"],
|
|
||||||
"packet_hex": advert_packet(
|
|
||||||
bytes([0x92])
|
|
||||||
+ struct.pack("<i", 37774900) + struct.pack("<i", -122419400)
|
|
||||||
+ b"HilltopRptr"
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": DUMMY_PUBKEY[:4].hex(),
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 0x92,
|
|
||||||
"node_type": "repeater",
|
|
||||||
"location": {"lat": 37.7749, "lon": -122.4194},
|
|
||||||
"name": "HilltopRptr"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# ADVERT: emoji name (multi-byte UTF-8)
|
|
||||||
emoji_name = "\U0001f985 Raptor" # 🦅 Raptor
|
|
||||||
write_case("advert_emoji_name", {
|
|
||||||
"description": "ADVERT flood, chat node with emoji in name (multi-byte UTF-8)",
|
|
||||||
"tags": ["advert", "flood", "emoji", "name", "utf8"],
|
|
||||||
"packet_hex": advert_packet(bytes([0x81]) + emoji_name.encode()).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": DUMMY_PUBKEY[:4].hex(),
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 0x81,
|
|
||||||
"node_type": "chat",
|
|
||||||
"location": None,
|
|
||||||
"name": emoji_name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# ADVERT: feat flags set but data is actually name (scoring test)
|
|
||||||
write_case("advert_feat_flags_as_name", {
|
|
||||||
"description": "ADVERT flood, feat1+feat2 bits set but bytes are actually the name",
|
|
||||||
"tags": ["advert", "flood", "feat", "scoring", "emoji"],
|
|
||||||
"packet_hex": advert_packet(
|
|
||||||
bytes([0xe1]) + "\U0001f3d4\ufe0f MtnNode".encode()
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": DUMMY_PUBKEY[:4].hex(),
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 0xe1,
|
|
||||||
"node_type": "chat",
|
|
||||||
"location": None,
|
|
||||||
"name": "\U0001f3d4\ufe0f MtnNode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# ADVERT: no app_data (bare 100-byte advert)
|
|
||||||
write_case("advert_no_appdata", {
|
|
||||||
"description": "ADVERT flood, bare minimum (no app_data)",
|
|
||||||
"tags": ["advert", "flood", "minimal"],
|
|
||||||
"packet_hex": advert_packet(b"").hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": DUMMY_PUBKEY[:4].hex(),
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# ADVERT: sensor node type
|
|
||||||
write_case("advert_sensor", {
|
|
||||||
"description": "ADVERT flood, sensor node type",
|
|
||||||
"tags": ["advert", "flood", "sensor"],
|
|
||||||
"packet_hex": advert_packet(bytes([0x84]) + b"TempSensor").hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": DUMMY_PUBKEY[:4].hex(),
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 0x84,
|
|
||||||
"node_type": "sensor",
|
|
||||||
"location": None,
|
|
||||||
"name": "TempSensor"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# ADVERT: with path (4 hops, 1-byte hash)
|
|
||||||
write_case("advert_with_path", {
|
|
||||||
"description": "ADVERT flood, 4 hops with 1-byte hashes",
|
|
||||||
"tags": ["advert", "flood", "path"],
|
|
||||||
"packet_hex": advert_packet(
|
|
||||||
bytes([0x81]) + b"RelayedNode",
|
|
||||||
path_hops=[b"\x2a", b"\x3d", b"\x35", b"\xed"]
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": ["2a", "3d", "35", "ed"]},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": DUMMY_PUBKEY[:4].hex(),
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 0x81,
|
|
||||||
"node_type": "chat",
|
|
||||||
"location": None,
|
|
||||||
"name": "RelayedNode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# ACK: basic
|
|
||||||
write_case("ack_basic", {
|
|
||||||
"description": "ACK flood, simple acknowledgment",
|
|
||||||
"tags": ["ack", "flood"],
|
|
||||||
"packet_hex": ack_packet(0xDEADBEEF).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ACK",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {"ack_crc": 0xDEADBEEF}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# ACK: with 1-byte hash path
|
|
||||||
write_case("ack_with_path", {
|
|
||||||
"description": "ACK flood, 2 hops with 1-byte hashes",
|
|
||||||
"tags": ["ack", "flood", "path"],
|
|
||||||
"packet_hex": ack_packet(
|
|
||||||
0x008DC29F, path_hops=[b"\xd1", b"\xac"]
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ACK",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": ["d1", "ac"]},
|
|
||||||
"payload": {"ack_crc": 0x008DC29F}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# TXT_MSG: direct, 2-byte hashes
|
|
||||||
write_case("txt_msg_direct_2b_hash", {
|
|
||||||
"description": "TXT_MSG direct, 2-byte hashes, 2 hops",
|
|
||||||
"tags": ["txt_msg", "direct", "2byte_hash", "encrypted"],
|
|
||||||
"packet_hex": peer_packet(
|
|
||||||
0x02, 2, 0x5b, 0x43, b"\xab\xcd", bytes(48),
|
|
||||||
path_hops=[b"\xac\x98", b"\xd1\x4d"], hash_size=2
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "direct",
|
|
||||||
"payload_type": "TXT_MSG",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 2, "hops": ["ac98", "d14d"]},
|
|
||||||
"payload": {"dst": "5b", "src": "43", "mac": "abcd", "ciphertext_len": 48}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# TXT_MSG: flood with loops (duplicate hops)
|
|
||||||
write_case("txt_msg_flood_with_loops", {
|
|
||||||
"description": "TXT_MSG flood with routing loop (ac98 appears 3x)",
|
|
||||||
"tags": ["txt_msg", "flood", "loop", "2byte_hash"],
|
|
||||||
"packet_hex": peer_packet(
|
|
||||||
0x02, 1, 0x5b, 0x43, b"\xab\xcd", bytes(32),
|
|
||||||
path_hops=[b"\xac\x98", b"\xd1\x4d", b"\xac\x98", b"\xff\x01", b"\xac\x98"],
|
|
||||||
hash_size=2
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "TXT_MSG",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 2, "hops": ["ac98", "d14d", "ac98", "ff01", "ac98"]},
|
|
||||||
"payload": {"dst": "5b", "src": "43", "mac": "abcd", "ciphertext_len": 32}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# REQ: with transport codes (transport direct)
|
|
||||||
write_case("req_transport_direct", {
|
|
||||||
"description": "REQ transport-direct with transport codes and 1-byte path",
|
|
||||||
"tags": ["req", "tdirect", "transport_codes", "encrypted"],
|
|
||||||
"packet_hex": peer_packet(
|
|
||||||
0x00, 3, 0xa1, 0xb2, b"\xef\x01", bytes(32),
|
|
||||||
path_hops=[b"\xcc", b"\xdd"], hash_size=1,
|
|
||||||
tc=(0x1234, 0x5678)
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "tdirect",
|
|
||||||
"payload_type": "REQ",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": {"tc1": 0x1234, "tc2": 0x5678},
|
|
||||||
"path": {"hash_size": 1, "hops": ["cc", "dd"]},
|
|
||||||
"payload": {"dst": "a1", "src": "b2", "mac": "ef01", "ciphertext_len": 32}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# ANON_REQ: route_type=3 but no actual TC (older firmware, TC fallback)
|
|
||||||
ephem = bytes(range(0x20, 0x40)) # 32-byte ephemeral pubkey
|
|
||||||
write_case("anon_req_tc_fallback", {
|
|
||||||
"description": "ANON_REQ tdirect header but NO transport codes (older firmware)",
|
|
||||||
"tags": ["anon_req", "tdirect", "tc_fallback"],
|
|
||||||
"packet_hex": (
|
|
||||||
bytes([mc_header(0x07, 3)])
|
|
||||||
+ bytes([path_len_byte(3, 1)]) + b"\x13\x35\x42"
|
|
||||||
+ bytes([0xd9]) + ephem + b"\xb2\xe2" + bytes(48)
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "tdirect",
|
|
||||||
"payload_type": "ANON_REQ",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": ["13", "35", "42"]},
|
|
||||||
"payload": {
|
|
||||||
"dst": "d9",
|
|
||||||
"ephem_prefix": ephem[:4].hex(),
|
|
||||||
"mac": "b2e2",
|
|
||||||
"ciphertext_len": 48
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# GRP_TXT: basic group text
|
|
||||||
write_case("grp_txt_basic", {
|
|
||||||
"description": "GRP_TXT flood, group text message",
|
|
||||||
"tags": ["grp_txt", "flood", "group", "encrypted"],
|
|
||||||
"packet_hex": group_packet(0x05, 0xCC, b"\xab\xcd", bytes(32)).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "GRP_TXT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {"channel_hash": "cc", "mac": "abcd", "ciphertext_len": 32}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# TRACE: basic trace packet
|
|
||||||
write_case("trace_basic", {
|
|
||||||
"description": "TRACE flood with tag, auth, and 2 trace hops",
|
|
||||||
"tags": ["trace", "flood"],
|
|
||||||
"packet_hex": trace_packet(
|
|
||||||
0x12345678, 0xAABBCCDD, 0x00, b"\x11\x22"
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "TRACE",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {"trace_tag": 0x12345678, "trace_hops": 2}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# TRACE: flags=2 means hash_size=4 (power of 2 encoding: 1 << 2 = 4)
|
|
||||||
write_case("trace_4byte_hash", {
|
|
||||||
"description": "TRACE flood with 4-byte trace hashes (flags=0x02, hash_size=4)",
|
|
||||||
"tags": ["trace", "flood", "4byte_hash"],
|
|
||||||
"packet_hex": trace_packet(
|
|
||||||
0xAABBCCDD, 0x11223344, 0x02, b"\xde\xad\xbe\xef\xca\xfe\xba\xbe"
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "TRACE",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {"trace_tag": 0xAABBCCDD, "trace_hops": 2}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# MULTIPART: ACK inner type
|
|
||||||
write_case("multipart_ack", {
|
|
||||||
"description": "MULTIPART flood, remaining=3, inner type=ACK",
|
|
||||||
"tags": ["multipart", "flood", "ack"],
|
|
||||||
"packet_hex": multipart_packet(
|
|
||||||
3, 0x03, struct.pack("<I", 0xCAFEBABE)
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "MULTIPART",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {"multipart_remaining": 3, "multipart_inner": "ACK"}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# CONTROL: DISCOVER_REQ
|
|
||||||
write_case("control_discover_req", {
|
|
||||||
"description": "CONTROL flood, DISCOVER_REQ sub-type",
|
|
||||||
"tags": ["control", "flood", "discover"],
|
|
||||||
"packet_hex": control_packet(
|
|
||||||
0x80, bytes([0x03]) + struct.pack("<I", 0xABCD1234)
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "CONTROL",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []},
|
|
||||||
"payload": {"control_subtype": "DISCOVER_REQ"}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# PATH: 3-byte hash, 1 hop
|
|
||||||
write_case("path_3byte_hash", {
|
|
||||||
"description": "PATH flood, 1 hop with 3-byte hash",
|
|
||||||
"tags": ["path", "flood", "3byte_hash", "encrypted"],
|
|
||||||
"packet_hex": peer_packet(
|
|
||||||
0x08, 1, 0xe6, 0xe9, b"\x2e\x47", bytes(46),
|
|
||||||
path_hops=[b"\x1a\xff\xf3"], hash_size=3
|
|
||||||
).hex(),
|
|
||||||
"valid": True,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "PATH",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 3, "hops": ["1afff3"]},
|
|
||||||
"payload": {"dst": "e6", "src": "e9", "mac": "2e47", "ciphertext_len": 46}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# PATH: 4-byte hash (hash_size_code 3 = RESERVED, must be rejected)
|
|
||||||
write_case("path_4byte_hash", {
|
|
||||||
"description": "PATH flood, 4-byte hash (reserved hash_size_code 3, invalid)",
|
|
||||||
"tags": ["invalid", "path", "flood", "4byte_hash", "reserved_hash_size"],
|
|
||||||
"packet_hex": peer_packet(
|
|
||||||
0x08, 1, 0xe6, 0xe9, b"\x2e\x47", bytes(16),
|
|
||||||
path_hops=[b"\xaa\xbb\xcc\xdd"], hash_size=4
|
|
||||||
).hex(),
|
|
||||||
"valid": False,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "PATH",
|
|
||||||
"error": "bad_framing"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# ── Invalid packets ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Too short: 1 byte
|
|
||||||
write_case("too_short_1byte", {
|
|
||||||
"description": "Packet too short (1 byte)",
|
|
||||||
"tags": ["invalid", "too_short"],
|
|
||||||
"packet_hex": "11",
|
|
||||||
"valid": False,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"error": "too_short"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Too short: 2 bytes (need at least 3: header + path_len + 1 byte payload)
|
|
||||||
write_case("too_short_2bytes", {
|
|
||||||
"description": "Packet too short (2 bytes, need >= 3)",
|
|
||||||
"tags": ["invalid", "too_short"],
|
|
||||||
"packet_hex": "1100",
|
|
||||||
"valid": False,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"error": "too_short"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Bad path_len: reserved hash_size_code 3 (bits 6-7 = 0b11)
|
|
||||||
bad_pkt = bytes([mc_header(0x02, 1), 0xC1]) + bytes(24)
|
|
||||||
write_case("bad_path_len_reserved_hash_size", {
|
|
||||||
"description": "TXT_MSG flood, path_len has reserved hash_size_code 3",
|
|
||||||
"tags": ["invalid", "bad_framing", "path", "reserved_hash_size"],
|
|
||||||
"packet_hex": bad_pkt.hex(),
|
|
||||||
"valid": False,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "TXT_MSG",
|
|
||||||
"error": "bad_framing"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Bad path_len: claims 33 hops (2B hash) but only 24 bytes remain
|
|
||||||
bad_pkt2 = bytes([mc_header(0x02, 1), 0x61]) + bytes(24)
|
|
||||||
write_case("bad_path_len", {
|
|
||||||
"description": "TXT_MSG flood, path_len claims 33 hops (2B hash) but data is too short",
|
|
||||||
"tags": ["invalid", "bad_framing", "path"],
|
|
||||||
"packet_hex": bad_pkt2.hex(),
|
|
||||||
"valid": False,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "TXT_MSG",
|
|
||||||
"error": "bad_framing"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Noise: random bytes that don't parse as meshcore
|
|
||||||
write_case("noise_packet", {
|
|
||||||
"description": "Random bytes, not a valid MeshCore packet",
|
|
||||||
"tags": ["invalid", "noise"],
|
|
||||||
"packet_hex": "03d135839fc28d00",
|
|
||||||
"valid": False,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "tdirect",
|
|
||||||
"payload_type": "REQ",
|
|
||||||
"error": "not_meshcore"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Empty packet
|
|
||||||
write_case("empty_packet", {
|
|
||||||
"description": "Empty packet (0 bytes)",
|
|
||||||
"tags": ["invalid", "too_short"],
|
|
||||||
"packet_hex": "",
|
|
||||||
"valid": False,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "REQ",
|
|
||||||
"error": "too_short"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Bad advert: too short for pubkey+timestamp+signature
|
|
||||||
short_advert = advert_packet(b"") # 102 bytes: hdr + path_len + 100 payload
|
|
||||||
truncated = short_advert[:50] # cut in half
|
|
||||||
write_case("advert_truncated", {
|
|
||||||
"description": "ADVERT flood, truncated mid-signature (50 bytes)",
|
|
||||||
"tags": ["invalid", "advert", "truncated"],
|
|
||||||
"packet_hex": truncated.hex(),
|
|
||||||
"valid": True, # parses as ADVERT but payload is bad
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": 1, "hops": []}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
print(f"\nDone! {len(list(OUT.glob('*.json')))} test vectors written to {OUT}/")
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Generate MeshCore test vectors using Hypothesis (property-based testing)."""
|
|
||||||
# /// script
|
|
||||||
# requires-python = ">=3.10"
|
|
||||||
# dependencies = ["hypothesis", "jsonschema"]
|
|
||||||
# ///
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from hypothesis import given, settings, HealthCheck, Phase
|
|
||||||
from hypothesis import strategies as st
|
|
||||||
|
|
||||||
CORPUS_DIR = Path(__file__).parent
|
|
||||||
OUT = CORPUS_DIR / "generated"
|
|
||||||
SCHEMA_PATH = CORPUS_DIR / "schema.json"
|
|
||||||
|
|
||||||
# MeshCore constants
|
|
||||||
MAX_PAYLOAD = 184
|
|
||||||
MAX_PATH_SIZE = 64
|
|
||||||
MAX_ADVERT_DATA_SIZE = 32
|
|
||||||
PUB_KEY_SIZE = 32
|
|
||||||
SIGNATURE_SIZE = 64
|
|
||||||
|
|
||||||
ROUTE_TYPES = {0: "tflood", 1: "flood", 2: "direct", 3: "tdirect"}
|
|
||||||
PAYLOAD_TYPES = {
|
|
||||||
0: "REQ", 1: "RESPONSE", 2: "TXT_MSG", 3: "ACK", 4: "ADVERT",
|
|
||||||
5: "GRP_TXT", 6: "GRP_DATA", 7: "ANON_REQ", 8: "PATH",
|
|
||||||
9: "TRACE", 10: "MULTIPART", 11: "CONTROL", 15: "RAW_CUSTOM",
|
|
||||||
}
|
|
||||||
NODE_TYPES = {1: "chat", 2: "repeater", 3: "room", 4: "sensor"}
|
|
||||||
|
|
||||||
|
|
||||||
def mc_header(payload_type: int, route_type: int, ver: int = 0) -> int:
|
|
||||||
return (ver << 6) | (payload_type << 2) | route_type
|
|
||||||
|
|
||||||
|
|
||||||
def path_len_byte(hop_count: int, hash_size: int) -> int:
|
|
||||||
return ((hash_size - 1) << 6) | (hop_count & 0x3F)
|
|
||||||
|
|
||||||
|
|
||||||
# ── Hypothesis strategies ─────────────────────────────────────────
|
|
||||||
|
|
||||||
hash_sizes = st.sampled_from([1, 2, 3])
|
|
||||||
route_types_no_tc = st.sampled_from([1, 2]) # flood, direct (no transport codes)
|
|
||||||
route_types_all = st.sampled_from([0, 1, 2, 3])
|
|
||||||
payload_vers = st.sampled_from([0]) # only v0 is well-defined
|
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
|
||||||
def path_strategy(draw, hash_size=None):
|
|
||||||
hs = hash_size or draw(hash_sizes)
|
|
||||||
max_hops = min(63, MAX_PATH_SIZE // hs)
|
|
||||||
hop_count = draw(st.integers(min_value=0, max_value=min(max_hops, 10)))
|
|
||||||
hops = [draw(st.binary(min_size=hs, max_size=hs)) for _ in range(hop_count)]
|
|
||||||
return hs, hops
|
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
|
||||||
def advert_appdata(draw):
|
|
||||||
"""Generate valid ADVERT app_data."""
|
|
||||||
node_type = draw(st.sampled_from([1, 2, 3, 4]))
|
|
||||||
has_loc = draw(st.booleans())
|
|
||||||
has_name = draw(st.booleans())
|
|
||||||
flags = node_type
|
|
||||||
data = b""
|
|
||||||
loc = None
|
|
||||||
name = None
|
|
||||||
|
|
||||||
if has_loc:
|
|
||||||
flags |= 0x10
|
|
||||||
lat = draw(st.integers(min_value=-90_000_000, max_value=90_000_000))
|
|
||||||
lon = draw(st.integers(min_value=-180_000_000, max_value=180_000_000))
|
|
||||||
data += struct.pack("<ii", lat, lon)
|
|
||||||
loc = {"lat": round(lat / 1e6, 4), "lon": round(lon / 1e6, 4)}
|
|
||||||
|
|
||||||
if has_name:
|
|
||||||
flags |= 0x80
|
|
||||||
# Generate ASCII names (to avoid UTF-8 scoring ambiguity in generated tests)
|
|
||||||
name = draw(st.text(
|
|
||||||
alphabet=st.characters(whitelist_categories=("L", "N", "P", "Z"),
|
|
||||||
max_codepoint=127),
|
|
||||||
min_size=1, max_size=20
|
|
||||||
))
|
|
||||||
name_bytes = name.encode("ascii")
|
|
||||||
data += name_bytes
|
|
||||||
|
|
||||||
if len(data) + 1 > MAX_ADVERT_DATA_SIZE:
|
|
||||||
data = data[:MAX_ADVERT_DATA_SIZE - 1]
|
|
||||||
if name:
|
|
||||||
name = data[8 if has_loc else 0:].decode("ascii", errors="replace")
|
|
||||||
|
|
||||||
app_data = bytes([flags]) + data
|
|
||||||
node_label = NODE_TYPES.get(node_type, f"0x{node_type:02x}")
|
|
||||||
return app_data, {"flags": flags, "node_type": node_label, "location": loc, "name": name}
|
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
|
||||||
def valid_advert_packet(draw):
|
|
||||||
"""Generate a complete valid ADVERT packet with expected decode."""
|
|
||||||
route_type = draw(route_types_no_tc)
|
|
||||||
ver = draw(payload_vers)
|
|
||||||
hs, hops = draw(path_strategy())
|
|
||||||
pubkey = draw(st.binary(min_size=PUB_KEY_SIZE, max_size=PUB_KEY_SIZE))
|
|
||||||
timestamp = draw(st.integers(min_value=0, max_value=0xFFFFFFFF))
|
|
||||||
signature = draw(st.binary(min_size=SIGNATURE_SIZE, max_size=SIGNATURE_SIZE))
|
|
||||||
app_data, advert_expected = draw(advert_appdata())
|
|
||||||
|
|
||||||
hdr = bytes([mc_header(0x04, route_type, ver)])
|
|
||||||
pl = bytes([path_len_byte(len(hops), hs)])
|
|
||||||
path = b"".join(hops)
|
|
||||||
payload = pubkey + struct.pack("<I", timestamp) + signature + app_data
|
|
||||||
packet = hdr + pl + path + payload
|
|
||||||
|
|
||||||
return packet, {
|
|
||||||
"route_type": ROUTE_TYPES[route_type],
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": ver,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": hs, "hops": [h.hex() for h in hops]},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": pubkey[:4].hex(),
|
|
||||||
"timestamp": timestamp,
|
|
||||||
"advert": advert_expected,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
|
||||||
def valid_ack_packet(draw):
|
|
||||||
route_type = draw(route_types_no_tc)
|
|
||||||
hs, hops = draw(path_strategy())
|
|
||||||
crc = draw(st.integers(min_value=0, max_value=0xFFFFFFFF))
|
|
||||||
|
|
||||||
hdr = bytes([mc_header(0x03, route_type)])
|
|
||||||
pl = bytes([path_len_byte(len(hops), hs)])
|
|
||||||
path = b"".join(hops)
|
|
||||||
packet = hdr + pl + path + struct.pack("<I", crc)
|
|
||||||
|
|
||||||
return packet, {
|
|
||||||
"route_type": ROUTE_TYPES[route_type],
|
|
||||||
"payload_type": "ACK",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": hs, "hops": [h.hex() for h in hops]},
|
|
||||||
"payload": {"ack_crc": crc}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
|
||||||
def valid_encrypted_peer_packet(draw):
|
|
||||||
ptype = draw(st.sampled_from([0, 1, 2, 8])) # REQ/RESPONSE/TXT_MSG/PATH
|
|
||||||
route_type = draw(route_types_no_tc)
|
|
||||||
hs, hops = draw(path_strategy())
|
|
||||||
dst = draw(st.integers(min_value=0, max_value=255))
|
|
||||||
src = draw(st.integers(min_value=0, max_value=255))
|
|
||||||
mac = draw(st.binary(min_size=2, max_size=2))
|
|
||||||
ct_len = draw(st.integers(min_value=16, max_value=64).filter(lambda n: n % 16 == 0))
|
|
||||||
ct = draw(st.binary(min_size=ct_len, max_size=ct_len))
|
|
||||||
|
|
||||||
hdr = bytes([mc_header(ptype, route_type)])
|
|
||||||
pl = bytes([path_len_byte(len(hops), hs)])
|
|
||||||
path = b"".join(hops)
|
|
||||||
packet = hdr + pl + path + bytes([dst, src]) + mac + ct
|
|
||||||
|
|
||||||
return packet, {
|
|
||||||
"route_type": ROUTE_TYPES[route_type],
|
|
||||||
"payload_type": PAYLOAD_TYPES[ptype],
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": hs, "hops": [h.hex() for h in hops]},
|
|
||||||
"payload": {
|
|
||||||
"dst": f"{dst:02x}",
|
|
||||||
"src": f"{src:02x}",
|
|
||||||
"mac": mac.hex(),
|
|
||||||
"ciphertext_len": ct_len
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
|
||||||
def valid_group_packet(draw):
|
|
||||||
ptype = draw(st.sampled_from([5, 6])) # GRP_TXT/GRP_DATA
|
|
||||||
route_type = draw(route_types_no_tc)
|
|
||||||
hs, hops = draw(path_strategy())
|
|
||||||
ch = draw(st.integers(min_value=0, max_value=255))
|
|
||||||
mac = draw(st.binary(min_size=2, max_size=2))
|
|
||||||
ct_len = draw(st.integers(min_value=16, max_value=64).filter(lambda n: n % 16 == 0))
|
|
||||||
ct = draw(st.binary(min_size=ct_len, max_size=ct_len))
|
|
||||||
|
|
||||||
hdr = bytes([mc_header(ptype, route_type)])
|
|
||||||
pl = bytes([path_len_byte(len(hops), hs)])
|
|
||||||
path = b"".join(hops)
|
|
||||||
packet = hdr + pl + path + bytes([ch]) + mac + ct
|
|
||||||
|
|
||||||
return packet, {
|
|
||||||
"route_type": ROUTE_TYPES[route_type],
|
|
||||||
"payload_type": PAYLOAD_TYPES[ptype],
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": None,
|
|
||||||
"path": {"hash_size": hs, "hops": [h.hex() for h in hops]},
|
|
||||||
"payload": {
|
|
||||||
"channel_hash": f"{ch:02x}",
|
|
||||||
"mac": mac.hex(),
|
|
||||||
"ciphertext_len": ct_len
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@st.composite
|
|
||||||
def invalid_packet(draw):
|
|
||||||
"""Generate an invalid MeshCore packet via mutation."""
|
|
||||||
mutation = draw(st.sampled_from(["truncate", "bad_path_len", "random"]))
|
|
||||||
|
|
||||||
if mutation == "truncate":
|
|
||||||
# Valid packet truncated to random length
|
|
||||||
pkt, _ = draw(valid_ack_packet())
|
|
||||||
cut = draw(st.integers(min_value=0, max_value=max(0, len(pkt) - 2)))
|
|
||||||
packet = pkt[:cut]
|
|
||||||
if len(packet) < 3:
|
|
||||||
error = "too_short"
|
|
||||||
else:
|
|
||||||
error = "bad_framing"
|
|
||||||
elif mutation == "bad_path_len":
|
|
||||||
# Header + impossible path_len
|
|
||||||
route_type = draw(route_types_no_tc)
|
|
||||||
ptype = draw(st.sampled_from(list(PAYLOAD_TYPES.keys())))
|
|
||||||
hdr = bytes([mc_header(ptype, route_type)])
|
|
||||||
# path_len claiming huge path
|
|
||||||
# Use hash_size_code 3 (reserved, 0xC0-0xFF) or valid codes with huge counts
|
|
||||||
bad_pl = draw(st.sampled_from([
|
|
||||||
*range(0xC0, 0x100), # reserved hash_size_code 3
|
|
||||||
0x3F, # 63 hops × 1 byte = 63 (valid encoding, will overflow data)
|
|
||||||
0x60, # 32 hops × 2 bytes = 64 (valid encoding, will overflow data)
|
|
||||||
0x95, # 21 hops × 3 bytes = 63 (valid encoding, will overflow data)
|
|
||||||
]))
|
|
||||||
tail = draw(st.binary(min_size=1, max_size=20))
|
|
||||||
packet = hdr + bytes([bad_pl]) + tail
|
|
||||||
error = "bad_framing"
|
|
||||||
else: # random
|
|
||||||
packet = draw(st.binary(min_size=3, max_size=50))
|
|
||||||
error = "not_meshcore"
|
|
||||||
|
|
||||||
header = packet[0] if packet else 0
|
|
||||||
route_type = header & 0x03
|
|
||||||
payload_type = (header >> 2) & 0x0F
|
|
||||||
|
|
||||||
return packet, {
|
|
||||||
"route_type": ROUTE_TYPES.get(route_type, f"rt{route_type}"),
|
|
||||||
"payload_type": PAYLOAD_TYPES.get(payload_type, f"0x{payload_type:02x}"),
|
|
||||||
"error": error if len(packet) < 2 else error
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ── Generation ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_counter = 0
|
|
||||||
|
|
||||||
|
|
||||||
def save_case(packet: bytes, expected: dict, valid: bool, prefix: str):
|
|
||||||
global _counter
|
|
||||||
_counter += 1
|
|
||||||
ptype = expected["payload_type"].lower()
|
|
||||||
name = f"{prefix}_{ptype}_{_counter:04d}"
|
|
||||||
|
|
||||||
tags = [ptype, expected["route_type"]]
|
|
||||||
if not valid:
|
|
||||||
tags.append("invalid")
|
|
||||||
if "error" in expected:
|
|
||||||
tags.append(expected["error"])
|
|
||||||
|
|
||||||
case = {
|
|
||||||
"description": f"Generated {prefix}: {expected['payload_type']} {expected['route_type']}",
|
|
||||||
"tags": tags,
|
|
||||||
"packet_hex": packet.hex(),
|
|
||||||
"valid": valid,
|
|
||||||
"expected": expected,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Validate against schema
|
|
||||||
import jsonschema
|
|
||||||
schema = json.loads(SCHEMA_PATH.read_text())
|
|
||||||
try:
|
|
||||||
jsonschema.validate(case, schema)
|
|
||||||
except jsonschema.ValidationError as e:
|
|
||||||
print(f" SKIP {name}: schema error: {e.message}")
|
|
||||||
return
|
|
||||||
|
|
||||||
(OUT / f"{name}.json").write_text(json.dumps(case, indent=2, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def generate(count: int):
|
|
||||||
OUT.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
# Clear previous generated files
|
|
||||||
for f in OUT.glob("*.json"):
|
|
||||||
f.unlink()
|
|
||||||
|
|
||||||
global _counter
|
|
||||||
_counter = 0
|
|
||||||
|
|
||||||
strategies = [
|
|
||||||
("valid", valid_advert_packet(), True),
|
|
||||||
("valid", valid_ack_packet(), True),
|
|
||||||
("valid", valid_encrypted_peer_packet(), True),
|
|
||||||
("valid", valid_group_packet(), True),
|
|
||||||
("invalid", invalid_packet(), False),
|
|
||||||
]
|
|
||||||
|
|
||||||
per_strategy = max(1, count // len(strategies))
|
|
||||||
|
|
||||||
for prefix, strategy, valid in strategies:
|
|
||||||
for i in range(per_strategy):
|
|
||||||
example = strategy.example()
|
|
||||||
packet, expected = example
|
|
||||||
save_case(packet, expected, valid, prefix)
|
|
||||||
|
|
||||||
total = len(list(OUT.glob("*.json")))
|
|
||||||
print(f"Generated {total} test vectors in {OUT}/")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description="Generate MeshCore test vectors")
|
|
||||||
parser.add_argument("--count", type=int, default=50, help="Total test cases to generate")
|
|
||||||
args = parser.parse_args()
|
|
||||||
generate(args.count)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "ACK flood, simple acknowledgment",
|
|
||||||
"tags": [
|
|
||||||
"ack",
|
|
||||||
"flood"
|
|
||||||
],
|
|
||||||
"packet_hex": "0d00efbeadde",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ACK",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"ack_crc": 3735928559
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "ACK flood, 2 hops with 1-byte hashes",
|
|
||||||
"tags": [
|
|
||||||
"ack",
|
|
||||||
"flood",
|
|
||||||
"path"
|
|
||||||
],
|
|
||||||
"packet_hex": "0d02d1ac9fc28d00",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ACK",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": [
|
|
||||||
"d1",
|
|
||||||
"ac"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"ack_crc": 9290399
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "ADVERT flood, chat node with simple ASCII name",
|
|
||||||
"tags": [
|
|
||||||
"advert",
|
|
||||||
"flood",
|
|
||||||
"name",
|
|
||||||
"chat"
|
|
||||||
],
|
|
||||||
"packet_hex": "1100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f403fc6690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000081546573744e6f6465",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": "00010203",
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 129,
|
|
||||||
"node_type": "chat",
|
|
||||||
"location": null,
|
|
||||||
"name": "TestNode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "ADVERT flood, chat node with emoji in name (multi-byte UTF-8)",
|
|
||||||
"tags": [
|
|
||||||
"advert",
|
|
||||||
"flood",
|
|
||||||
"emoji",
|
|
||||||
"name",
|
|
||||||
"utf8"
|
|
||||||
],
|
|
||||||
"packet_hex": "1100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f403fc6690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000081f09fa68520526170746f72",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": "00010203",
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 129,
|
|
||||||
"node_type": "chat",
|
|
||||||
"location": null,
|
|
||||||
"name": "🦅 Raptor"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "ADVERT flood, feat1+feat2 bits set but bytes are actually the name",
|
|
||||||
"tags": [
|
|
||||||
"advert",
|
|
||||||
"flood",
|
|
||||||
"feat",
|
|
||||||
"scoring",
|
|
||||||
"emoji"
|
|
||||||
],
|
|
||||||
"packet_hex": "1100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f403fc66900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e1f09f8f94efb88f204d746e4e6f6465",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": "00010203",
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 225,
|
|
||||||
"node_type": "chat",
|
|
||||||
"location": null,
|
|
||||||
"name": "🏔️ MtnNode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "ADVERT flood, repeater with valid GPS coords and name",
|
|
||||||
"tags": [
|
|
||||||
"advert",
|
|
||||||
"flood",
|
|
||||||
"location",
|
|
||||||
"repeater"
|
|
||||||
],
|
|
||||||
"packet_hex": "1100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f403fc6690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000092346640023807b4f848696c6c746f7052707472",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": "00010203",
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 146,
|
|
||||||
"node_type": "repeater",
|
|
||||||
"location": {
|
|
||||||
"lat": 37.7749,
|
|
||||||
"lon": -122.4194
|
|
||||||
},
|
|
||||||
"name": "HilltopRptr"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "ADVERT flood, bare minimum (no app_data)",
|
|
||||||
"tags": [
|
|
||||||
"advert",
|
|
||||||
"flood",
|
|
||||||
"minimal"
|
|
||||||
],
|
|
||||||
"packet_hex": "1100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f403fc66900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": "00010203",
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "ADVERT flood, sensor node type",
|
|
||||||
"tags": [
|
|
||||||
"advert",
|
|
||||||
"flood",
|
|
||||||
"sensor"
|
|
||||||
],
|
|
||||||
"packet_hex": "1100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f403fc669000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008454656d7053656e736f72",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": "00010203",
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 132,
|
|
||||||
"node_type": "sensor",
|
|
||||||
"location": null,
|
|
||||||
"name": "TempSensor"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "ADVERT flood, truncated mid-signature (50 bytes)",
|
|
||||||
"tags": [
|
|
||||||
"invalid",
|
|
||||||
"advert",
|
|
||||||
"truncated"
|
|
||||||
],
|
|
||||||
"packet_hex": "1100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f403fc669000000000000000000000000",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "ADVERT flood, 4 hops with 1-byte hashes",
|
|
||||||
"tags": [
|
|
||||||
"advert",
|
|
||||||
"flood",
|
|
||||||
"path"
|
|
||||||
],
|
|
||||||
"packet_hex": "11042a3d35ed000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f403fc669000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008152656c617965644e6f6465",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": [
|
|
||||||
"2a",
|
|
||||||
"3d",
|
|
||||||
"35",
|
|
||||||
"ed"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"pubkey_prefix": "00010203",
|
|
||||||
"timestamp": 1774600000,
|
|
||||||
"advert": {
|
|
||||||
"flags": 129,
|
|
||||||
"node_type": "chat",
|
|
||||||
"location": null,
|
|
||||||
"name": "RelayedNode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "ANON_REQ tdirect header but NO transport codes (older firmware)",
|
|
||||||
"tags": [
|
|
||||||
"anon_req",
|
|
||||||
"tdirect",
|
|
||||||
"tc_fallback"
|
|
||||||
],
|
|
||||||
"packet_hex": "1f03133542d9202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3fb2e2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "tdirect",
|
|
||||||
"payload_type": "ANON_REQ",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": [
|
|
||||||
"13",
|
|
||||||
"35",
|
|
||||||
"42"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"dst": "d9",
|
|
||||||
"ephem_prefix": "20212223",
|
|
||||||
"mac": "b2e2",
|
|
||||||
"ciphertext_len": 48
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "TXT_MSG flood, path_len claims 33 hops (2B hash) but data is too short",
|
|
||||||
"tags": [
|
|
||||||
"invalid",
|
|
||||||
"bad_framing",
|
|
||||||
"path"
|
|
||||||
],
|
|
||||||
"packet_hex": "0961000000000000000000000000000000000000000000000000",
|
|
||||||
"valid": false,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "TXT_MSG",
|
|
||||||
"error": "bad_framing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "TXT_MSG flood, path_len has reserved hash_size_code 3",
|
|
||||||
"tags": [
|
|
||||||
"invalid",
|
|
||||||
"bad_framing",
|
|
||||||
"path",
|
|
||||||
"reserved_hash_size"
|
|
||||||
],
|
|
||||||
"packet_hex": "09c1000000000000000000000000000000000000000000000000",
|
|
||||||
"valid": false,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "TXT_MSG",
|
|
||||||
"error": "bad_framing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "CONTROL flood, DISCOVER_REQ sub-type",
|
|
||||||
"tags": [
|
|
||||||
"control",
|
|
||||||
"flood",
|
|
||||||
"discover"
|
|
||||||
],
|
|
||||||
"packet_hex": "2d0080033412cdab",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "CONTROL",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"control_subtype": "DISCOVER_REQ"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "Empty packet (0 bytes)",
|
|
||||||
"tags": [
|
|
||||||
"invalid",
|
|
||||||
"too_short"
|
|
||||||
],
|
|
||||||
"packet_hex": "",
|
|
||||||
"valid": false,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "REQ",
|
|
||||||
"error": "too_short"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "GRP_TXT flood, group text message",
|
|
||||||
"tags": [
|
|
||||||
"grp_txt",
|
|
||||||
"flood",
|
|
||||||
"group",
|
|
||||||
"encrypted"
|
|
||||||
],
|
|
||||||
"packet_hex": "1500ccabcd0000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "GRP_TXT",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"channel_hash": "cc",
|
|
||||||
"mac": "abcd",
|
|
||||||
"ciphertext_len": 32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "MULTIPART flood, remaining=3, inner type=ACK",
|
|
||||||
"tags": [
|
|
||||||
"multipart",
|
|
||||||
"flood",
|
|
||||||
"ack"
|
|
||||||
],
|
|
||||||
"packet_hex": "290033bebafeca",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "MULTIPART",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"multipart_remaining": 3,
|
|
||||||
"multipart_inner": "ACK"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "Random bytes, not a valid MeshCore packet",
|
|
||||||
"tags": [
|
|
||||||
"invalid",
|
|
||||||
"noise"
|
|
||||||
],
|
|
||||||
"packet_hex": "03d135839fc28d00",
|
|
||||||
"valid": false,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "tdirect",
|
|
||||||
"payload_type": "REQ",
|
|
||||||
"error": "not_meshcore"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "PATH flood, 1 hop with 3-byte hash",
|
|
||||||
"tags": [
|
|
||||||
"path",
|
|
||||||
"flood",
|
|
||||||
"3byte_hash",
|
|
||||||
"encrypted"
|
|
||||||
],
|
|
||||||
"packet_hex": "21811afff3e6e92e4700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "PATH",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 3,
|
|
||||||
"hops": [
|
|
||||||
"1afff3"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"dst": "e6",
|
|
||||||
"src": "e9",
|
|
||||||
"mac": "2e47",
|
|
||||||
"ciphertext_len": 46
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "PATH flood, 4-byte hash (reserved hash_size_code 3, invalid)",
|
|
||||||
"tags": [
|
|
||||||
"invalid",
|
|
||||||
"path",
|
|
||||||
"flood",
|
|
||||||
"4byte_hash",
|
|
||||||
"reserved_hash_size"
|
|
||||||
],
|
|
||||||
"packet_hex": "21c1aabbccdde6e92e4700000000000000000000000000000000",
|
|
||||||
"valid": false,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "PATH",
|
|
||||||
"error": "bad_framing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "REQ transport-direct with transport codes and 1-byte path",
|
|
||||||
"tags": [
|
|
||||||
"req",
|
|
||||||
"tdirect",
|
|
||||||
"transport_codes",
|
|
||||||
"encrypted"
|
|
||||||
],
|
|
||||||
"packet_hex": "033412785602ccdda1b2ef010000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "tdirect",
|
|
||||||
"payload_type": "REQ",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": {
|
|
||||||
"tc1": 4660,
|
|
||||||
"tc2": 22136
|
|
||||||
},
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": [
|
|
||||||
"cc",
|
|
||||||
"dd"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"dst": "a1",
|
|
||||||
"src": "b2",
|
|
||||||
"mac": "ef01",
|
|
||||||
"ciphertext_len": 32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "RESPONSE flood, bit-corrupted path_len (0xc7 should be small), LoRa bit error",
|
|
||||||
"tags": ["invalid", "response", "flood", "corrupted", "real_capture", "bit_error"],
|
|
||||||
"packet_hex": "05c735935ed17863f6ec7bbbd9bf8e5ea7d44b6a0e8f6318",
|
|
||||||
"valid": false,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "RESPONSE",
|
|
||||||
"error": "bad_framing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "RESPONSE flood, bit-corrupted path_len (0x32 should be 0x02 — bits 4,5 flipped), LoRa bit error",
|
|
||||||
"tags": ["invalid", "response", "flood", "corrupted", "real_capture", "bit_error"],
|
|
||||||
"packet_hex": "0532af1a5bd17863f6ec7bbbd9bfbe5ea7d44b6a0e8f6318",
|
|
||||||
"valid": false,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "RESPONSE",
|
|
||||||
"error": "bad_framing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "RESPONSE flood, 1 hop (real capture — same message as corrupted variants)",
|
|
||||||
"tags": ["response", "flood", "path", "real_capture"],
|
|
||||||
"packet_hex": "0501ac5bd17863f6ec7bbbd9bf8e5ea7d44b6a0e8f6318",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "RESPONSE",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {"hash_size": 1, "hops": ["ac"]},
|
|
||||||
"payload": {"dst": "5b", "src": "d1", "mac": "7863", "ciphertext_len": 16}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "RESPONSE flood, 2 hops [ac>ea] (real capture — same message as corrupted variants)",
|
|
||||||
"tags": ["response", "flood", "path", "real_capture"],
|
|
||||||
"packet_hex": "0502acea5bd17863f6ec7bbbd9bf8e5ea7d44b6a0e8f6318",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "RESPONSE",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {"hash_size": 1, "hops": ["ac", "ea"]},
|
|
||||||
"payload": {"dst": "5b", "src": "d1", "mac": "7863", "ciphertext_len": 16}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "Packet too short (1 byte)",
|
|
||||||
"tags": [
|
|
||||||
"invalid",
|
|
||||||
"too_short"
|
|
||||||
],
|
|
||||||
"packet_hex": "11",
|
|
||||||
"valid": false,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"error": "too_short"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "Packet too short (2 bytes, need >= 3)",
|
|
||||||
"tags": [
|
|
||||||
"invalid",
|
|
||||||
"too_short"
|
|
||||||
],
|
|
||||||
"packet_hex": "1100",
|
|
||||||
"valid": false,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "ADVERT",
|
|
||||||
"error": "too_short"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "TRACE flood with 4-byte trace hashes (flags=0x02, hash_size=4)",
|
|
||||||
"tags": [
|
|
||||||
"trace",
|
|
||||||
"flood",
|
|
||||||
"4byte_hash"
|
|
||||||
],
|
|
||||||
"packet_hex": "2500ddccbbaa4433221102deadbeefcafebabe",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "TRACE",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"trace_tag": 2864434397,
|
|
||||||
"trace_hops": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "TRACE flood with tag, auth, and 2 trace hops",
|
|
||||||
"tags": [
|
|
||||||
"trace",
|
|
||||||
"flood"
|
|
||||||
],
|
|
||||||
"packet_hex": "250078563412ddccbbaa001122",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "TRACE",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 1,
|
|
||||||
"hops": []
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"trace_tag": 305419896,
|
|
||||||
"trace_hops": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "TXT_MSG direct, 2-byte hashes, 2 hops",
|
|
||||||
"tags": [
|
|
||||||
"txt_msg",
|
|
||||||
"direct",
|
|
||||||
"2byte_hash",
|
|
||||||
"encrypted"
|
|
||||||
],
|
|
||||||
"packet_hex": "0a42ac98d14d5b43abcd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "direct",
|
|
||||||
"payload_type": "TXT_MSG",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 2,
|
|
||||||
"hops": [
|
|
||||||
"ac98",
|
|
||||||
"d14d"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"dst": "5b",
|
|
||||||
"src": "43",
|
|
||||||
"mac": "abcd",
|
|
||||||
"ciphertext_len": 48
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "TXT_MSG flood with routing loop (ac98 appears 3x)",
|
|
||||||
"tags": [
|
|
||||||
"txt_msg",
|
|
||||||
"flood",
|
|
||||||
"loop",
|
|
||||||
"2byte_hash"
|
|
||||||
],
|
|
||||||
"packet_hex": "0945ac98d14dac98ff01ac985b43abcd0000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
"valid": true,
|
|
||||||
"expected": {
|
|
||||||
"route_type": "flood",
|
|
||||||
"payload_type": "TXT_MSG",
|
|
||||||
"payload_ver": 0,
|
|
||||||
"transport_codes": null,
|
|
||||||
"path": {
|
|
||||||
"hash_size": 2,
|
|
||||||
"hops": [
|
|
||||||
"ac98",
|
|
||||||
"d14d",
|
|
||||||
"ac98",
|
|
||||||
"ff01",
|
|
||||||
"ac98"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"dst": "5b",
|
|
||||||
"src": "43",
|
|
||||||
"mac": "abcd",
|
|
||||||
"ciphertext_len": 32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
||||||
"title": "MeshCore Packet Test Case",
|
|
||||||
"description": "A single test vector for MeshCore packet decoders",
|
|
||||||
"type": "object",
|
|
||||||
"required": ["description", "tags", "packet_hex", "valid", "expected"],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "type": "string" },
|
|
||||||
"minItems": 1
|
|
||||||
},
|
|
||||||
"packet_hex": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[0-9a-f]*$",
|
|
||||||
"description": "Raw MeshCore packet as lowercase hex (no 0x prefix)"
|
|
||||||
},
|
|
||||||
"valid": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether this packet should parse successfully"
|
|
||||||
},
|
|
||||||
"expected": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["route_type", "payload_type"],
|
|
||||||
"properties": {
|
|
||||||
"route_type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["flood", "direct", "tflood", "tdirect"]
|
|
||||||
},
|
|
||||||
"payload_type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"REQ", "RESPONSE", "TXT_MSG", "ACK", "ADVERT",
|
|
||||||
"GRP_TXT", "GRP_DATA", "ANON_REQ", "PATH",
|
|
||||||
"TRACE", "MULTIPART", "CONTROL", "RAW_CUSTOM"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"payload_ver": { "type": "integer", "minimum": 0, "maximum": 3 },
|
|
||||||
"transport_codes": {
|
|
||||||
"oneOf": [
|
|
||||||
{ "type": "null" },
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": ["tc1", "tc2"],
|
|
||||||
"properties": {
|
|
||||||
"tc1": { "type": "integer" },
|
|
||||||
"tc2": { "type": "integer" }
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"path": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["hash_size", "hops"],
|
|
||||||
"properties": {
|
|
||||||
"hash_size": { "type": "integer", "minimum": 1, "maximum": 4 },
|
|
||||||
"hops": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "type": "string", "pattern": "^[0-9a-f]+$" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["too_short", "not_meshcore", "bad_framing"],
|
|
||||||
"description": "Expected error category for invalid packets"
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Payload-type-specific decoded fields",
|
|
||||||
"properties": {
|
|
||||||
"dst": { "type": "string", "pattern": "^[0-9a-f]{2}$" },
|
|
||||||
"src": { "type": "string", "pattern": "^[0-9a-f]{2}$" },
|
|
||||||
"mac": { "type": "string", "pattern": "^[0-9a-f]+$" },
|
|
||||||
"ciphertext_len": { "type": "integer", "minimum": 0 },
|
|
||||||
"channel_hash": { "type": "string", "pattern": "^[0-9a-f]{2}$" },
|
|
||||||
"ack_crc": { "type": "integer" },
|
|
||||||
"pubkey_prefix": { "type": "string", "pattern": "^[0-9a-f]+$" },
|
|
||||||
"timestamp": { "type": "integer" },
|
|
||||||
"advert": {
|
|
||||||
"oneOf": [
|
|
||||||
{ "type": "null" },
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"flags": { "type": "integer", "minimum": 0, "maximum": 255 },
|
|
||||||
"node_type": { "type": "string" },
|
|
||||||
"location": {
|
|
||||||
"oneOf": [
|
|
||||||
{ "type": "null" },
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": ["lat", "lon"],
|
|
||||||
"properties": {
|
|
||||||
"lat": { "type": "number" },
|
|
||||||
"lon": { "type": "number" }
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"name": { "oneOf": [{ "type": "string" }, { "type": "null" }] }
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ephem_prefix": { "type": "string", "pattern": "^[0-9a-f]+$" },
|
|
||||||
"trace_tag": { "type": "integer" },
|
|
||||||
"trace_hops": { "type": "integer" },
|
|
||||||
"multipart_remaining": { "type": "integer" },
|
|
||||||
"multipart_inner": { "type": "string" },
|
|
||||||
"control_subtype": { "type": "string" }
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Validate MeshCore test corpus against schema and decoder."""
|
|
||||||
# /// script
|
|
||||||
# requires-python = ">=3.10"
|
|
||||||
# dependencies = ["jsonschema", "cobs", "pyserial", "pycryptodome"]
|
|
||||||
# ///
|
|
||||||
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import jsonschema
|
|
||||||
|
|
||||||
# Add meshcore/ directory to path so we can import the decoder
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
||||||
from meshcore_rx import decode_meshcore_packet # noqa: E402
|
|
||||||
|
|
||||||
CORPUS_DIR = Path(__file__).parent
|
|
||||||
SCHEMA_PATH = CORPUS_DIR / "schema.json"
|
|
||||||
|
|
||||||
# ANSI escape stripper
|
|
||||||
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
|
||||||
|
|
||||||
|
|
||||||
def strip_ansi(s: str) -> str:
|
|
||||||
return _ANSI_RE.sub("", s)
|
|
||||||
|
|
||||||
|
|
||||||
def load_schema() -> dict:
|
|
||||||
return json.loads(SCHEMA_PATH.read_text())
|
|
||||||
|
|
||||||
|
|
||||||
def find_test_cases() -> list[Path]:
|
|
||||||
cases = []
|
|
||||||
for d in ["hand", "generated"]:
|
|
||||||
p = CORPUS_DIR / d
|
|
||||||
if p.is_dir():
|
|
||||||
cases.extend(sorted(p.glob("*.json")))
|
|
||||||
return cases
|
|
||||||
|
|
||||||
|
|
||||||
def validate_schema(case: dict, schema: dict, path: Path) -> list[str]:
|
|
||||||
"""Validate a test case against the JSON schema. Returns list of errors."""
|
|
||||||
errors = []
|
|
||||||
try:
|
|
||||||
jsonschema.validate(case, schema)
|
|
||||||
except jsonschema.ValidationError as e:
|
|
||||||
errors.append(f"Schema: {e.message}")
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def validate_decode(case: dict, path: Path) -> list[str]:
|
|
||||||
"""Run the decoder and check output against expected fields."""
|
|
||||||
errors = []
|
|
||||||
packet = bytes.fromhex(case["packet_hex"]) if case["packet_hex"] else b""
|
|
||||||
expected = case["expected"]
|
|
||||||
|
|
||||||
# Run decoder — must not crash
|
|
||||||
try:
|
|
||||||
raw_output, _warnings = decode_meshcore_packet(packet)
|
|
||||||
except Exception as e:
|
|
||||||
errors.append(f"Decoder crashed: {e}")
|
|
||||||
return errors
|
|
||||||
|
|
||||||
output = strip_ansi(raw_output)
|
|
||||||
|
|
||||||
if case["valid"]:
|
|
||||||
# Valid packet: should contain the expected payload type name
|
|
||||||
ptype = expected["payload_type"]
|
|
||||||
if ptype not in output:
|
|
||||||
errors.append(f"Expected '{ptype}' in output, got: {output[:120]}")
|
|
||||||
|
|
||||||
# Check route type
|
|
||||||
rtype = expected["route_type"]
|
|
||||||
if rtype not in output:
|
|
||||||
errors.append(f"Expected route '{rtype}' in output, got: {output[:120]}")
|
|
||||||
|
|
||||||
# Check path hops if specified
|
|
||||||
if "path" in expected:
|
|
||||||
exp_path = expected["path"]
|
|
||||||
for hop in exp_path["hops"]:
|
|
||||||
if hop not in output:
|
|
||||||
errors.append(f"Expected hop '{hop}' in output")
|
|
||||||
|
|
||||||
# Check payload-specific fields
|
|
||||||
payload = expected.get("payload")
|
|
||||||
if payload:
|
|
||||||
_check_payload(payload, output, errors)
|
|
||||||
|
|
||||||
# Should NOT contain error markers
|
|
||||||
for marker in ["<too short>", "<not meshcore>", "<bad framing>"]:
|
|
||||||
if marker in output:
|
|
||||||
errors.append(f"Valid packet produced error marker: {marker}")
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Invalid packet: must not crash. Error marker check is best-effort
|
|
||||||
# (random mutations may accidentally produce parseable packets).
|
|
||||||
pass # decoder didn't crash — that's the key requirement
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def _check_payload(payload: dict, output: str, errors: list[str]):
|
|
||||||
"""Check payload-specific fields in the decoder output."""
|
|
||||||
if "ack_crc" in payload:
|
|
||||||
crc_hex = f"0x{payload['ack_crc']:08x}"
|
|
||||||
if crc_hex not in output:
|
|
||||||
errors.append(f"Expected CRC '{crc_hex}' in output")
|
|
||||||
|
|
||||||
if "dst" in payload:
|
|
||||||
if payload["dst"] not in output:
|
|
||||||
errors.append(f"Expected dst='{payload['dst']}' in output")
|
|
||||||
|
|
||||||
if "src" in payload:
|
|
||||||
if payload["src"] not in output:
|
|
||||||
errors.append(f"Expected src='{payload['src']}' in output")
|
|
||||||
|
|
||||||
if "mac" in payload:
|
|
||||||
if payload["mac"] not in output:
|
|
||||||
errors.append(f"Expected mac='{payload['mac']}' in output")
|
|
||||||
|
|
||||||
if "channel_hash" in payload:
|
|
||||||
if payload["channel_hash"] not in output:
|
|
||||||
errors.append(f"Expected ch='{payload['channel_hash']}' in output")
|
|
||||||
|
|
||||||
if "ciphertext_len" in payload:
|
|
||||||
ct_str = f"[{payload['ciphertext_len']}B]"
|
|
||||||
if ct_str not in output:
|
|
||||||
errors.append(f"Expected '{ct_str}' in output")
|
|
||||||
|
|
||||||
if "pubkey_prefix" in payload:
|
|
||||||
pk = payload["pubkey_prefix"]
|
|
||||||
if pk not in output:
|
|
||||||
errors.append(f"Expected pubkey prefix '{pk}' in output")
|
|
||||||
|
|
||||||
if "advert" in payload and payload["advert"] is not None:
|
|
||||||
advert = payload["advert"]
|
|
||||||
if "name" in advert and advert["name"] is not None:
|
|
||||||
if advert["name"] not in output:
|
|
||||||
errors.append(f"Expected name '{advert['name']}' in output")
|
|
||||||
if "node_type" in advert:
|
|
||||||
if advert["node_type"] not in output:
|
|
||||||
errors.append(f"Expected node_type '{advert['node_type']}' in output")
|
|
||||||
if "location" in advert and advert["location"] is not None:
|
|
||||||
loc = advert["location"]
|
|
||||||
lat_str = f"{loc['lat']:.4f}"
|
|
||||||
if lat_str not in output:
|
|
||||||
errors.append(f"Expected lat '{lat_str}' in output")
|
|
||||||
|
|
||||||
if "ephem_prefix" in payload:
|
|
||||||
ep = payload["ephem_prefix"]
|
|
||||||
if ep not in output:
|
|
||||||
errors.append(f"Expected ephemeral prefix '{ep}' in output")
|
|
||||||
|
|
||||||
if "multipart_remaining" in payload:
|
|
||||||
r = payload["multipart_remaining"]
|
|
||||||
if f"remaining={r}" not in output.replace(" ", ""):
|
|
||||||
errors.append(f"Expected remaining={r} in output")
|
|
||||||
|
|
||||||
if "multipart_inner" in payload:
|
|
||||||
inner = payload["multipart_inner"]
|
|
||||||
if inner not in output:
|
|
||||||
errors.append(f"Expected inner type '{inner}' in output")
|
|
||||||
|
|
||||||
if "control_subtype" in payload:
|
|
||||||
sub = payload["control_subtype"]
|
|
||||||
if sub not in output:
|
|
||||||
errors.append(f"Expected control subtype '{sub}' in output")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
schema = load_schema()
|
|
||||||
cases = find_test_cases()
|
|
||||||
|
|
||||||
if not cases:
|
|
||||||
print("No test cases found!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
passed = 0
|
|
||||||
failed = 0
|
|
||||||
total_errors: list[tuple[str, list[str]]] = []
|
|
||||||
|
|
||||||
for path in cases:
|
|
||||||
case = json.loads(path.read_text())
|
|
||||||
name = f"{path.parent.name}/{path.stem}"
|
|
||||||
|
|
||||||
errors = validate_schema(case, schema, path)
|
|
||||||
errors.extend(validate_decode(case, path))
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
failed += 1
|
|
||||||
total_errors.append((name, errors))
|
|
||||||
print(f" \033[1;31mFAIL\033[0m {name}")
|
|
||||||
for e in errors:
|
|
||||||
print(f" {e}")
|
|
||||||
else:
|
|
||||||
passed += 1
|
|
||||||
print(f" \033[32mPASS\033[0m {name}")
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(f"\033[1m{passed + failed} tests: {passed} passed, {failed} failed\033[0m")
|
|
||||||
sys.exit(1 if failed else 0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,931 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""DongLoRa MeshCore receiver: decode and display MeshCore packets."""
|
|
||||||
import base64
|
|
||||||
import csv
|
|
||||||
import donglora as dl
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
import struct
|
|
||||||
import serial
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import time
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
# ── ANSI colors ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
RST = "\033[0m"
|
|
||||||
BOLD = "\033[1m"
|
|
||||||
DIM = "\033[2m"
|
|
||||||
RED = "\033[31m"
|
|
||||||
GRN = "\033[32m"
|
|
||||||
YEL = "\033[33m"
|
|
||||||
BLU = "\033[34m"
|
|
||||||
MAG = "\033[35m"
|
|
||||||
CYN = "\033[36m"
|
|
||||||
WHT = "\033[37m"
|
|
||||||
BRED = "\033[1;31m"
|
|
||||||
BGRN = "\033[1;32m"
|
|
||||||
BYEL = "\033[1;33m"
|
|
||||||
BBLU = "\033[1;34m"
|
|
||||||
BMAG = "\033[1;35m"
|
|
||||||
BCYN = "\033[1;36m"
|
|
||||||
BWHT = "\033[1;37m"
|
|
||||||
|
|
||||||
TYPE_COLORS = {
|
|
||||||
"ADVERT": BGRN,
|
|
||||||
"ACK": DIM,
|
|
||||||
"TXT_MSG": BCYN,
|
|
||||||
"REQ": BYEL,
|
|
||||||
"RESPONSE": BYEL,
|
|
||||||
"GRP_TXT": BMAG,
|
|
||||||
"GRP_DATA": BMAG,
|
|
||||||
"ANON_REQ": YEL,
|
|
||||||
"PATH": BBLU,
|
|
||||||
"TRACE": BLU,
|
|
||||||
"MULTIPART": YEL,
|
|
||||||
"CONTROL": BLU,
|
|
||||||
"RAW_CUSTOM": DIM,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _rssi_color(rssi: int) -> str:
|
|
||||||
if rssi >= -70:
|
|
||||||
return GRN
|
|
||||||
if rssi >= -100:
|
|
||||||
return YEL
|
|
||||||
return RED
|
|
||||||
|
|
||||||
|
|
||||||
def _snr_color(snr: int) -> str:
|
|
||||||
if snr >= 0:
|
|
||||||
return GRN
|
|
||||||
if snr >= -10:
|
|
||||||
return YEL
|
|
||||||
return RED
|
|
||||||
|
|
||||||
|
|
||||||
# ── MeshCore channel crypto ────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def _channel_secret_from_hashtag(name: str) -> bytes:
|
|
||||||
"""Derive 32-byte secret from a hashtag channel name (e.g., '#test')."""
|
|
||||||
h = hashlib.sha256(name.encode()).digest()
|
|
||||||
return h[:16] + b"\x00" * 16
|
|
||||||
|
|
||||||
|
|
||||||
def _channel_secret_from_psk(psk_b64: str) -> bytes:
|
|
||||||
"""Decode a base64 PSK into a 32-byte secret (16-byte key + 16 zero bytes)."""
|
|
||||||
key = base64.b64decode(psk_b64)
|
|
||||||
return key[:16] + b"\x00" * 16
|
|
||||||
|
|
||||||
|
|
||||||
def _channel_hash(secret: bytes) -> int:
|
|
||||||
"""Compute the 1-byte channel hash from a 32-byte secret."""
|
|
||||||
# Hash over the key portion (16 bytes for hashtag/PSK channels)
|
|
||||||
key_len = 16 if secret[16:] == b"\x00" * 16 else 32
|
|
||||||
return hashlib.sha256(secret[:key_len]).digest()[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _grp_verify_and_decrypt(
|
|
||||||
secret: bytes, mac_bytes: bytes, ciphertext: bytes
|
|
||||||
) -> bytes | None:
|
|
||||||
"""Verify MAC then decrypt a GRP_TXT/GRP_DATA payload. Returns plaintext or None."""
|
|
||||||
if not ciphertext or len(ciphertext) % 16 != 0:
|
|
||||||
return None
|
|
||||||
# HMAC-SHA256 over ciphertext, truncated to 2 bytes
|
|
||||||
key_len = 16 if secret[16:] == b"\x00" * 16 else 32
|
|
||||||
computed_mac = hmac.new(secret[:key_len], ciphertext, hashlib.sha256).digest()[:2]
|
|
||||||
if computed_mac != mac_bytes:
|
|
||||||
return None
|
|
||||||
# AES-128-ECB decrypt
|
|
||||||
cipher = AES.new(secret[:16], AES.MODE_ECB)
|
|
||||||
plaintext = b""
|
|
||||||
for i in range(0, len(ciphertext), 16):
|
|
||||||
plaintext += cipher.decrypt(ciphertext[i : i + 16])
|
|
||||||
return plaintext
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_grp_plaintext(plaintext: bytes) -> tuple[int, str] | None:
|
|
||||||
"""Parse decrypted GRP_TXT: timestamp(4) + flags(1) + text. Returns (timestamp, text)."""
|
|
||||||
if len(plaintext) < 5:
|
|
||||||
return None
|
|
||||||
timestamp = struct.unpack_from("<I", plaintext, 0)[0]
|
|
||||||
# flags_and_attempt at byte 4: (attempt & 3) | (txt_type << 2)
|
|
||||||
text = plaintext[5:].split(b"\x00", 1)[0] # null-terminated, strip zero padding
|
|
||||||
return timestamp, text.decode("utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
# Known channels: name → 32-byte secret
|
|
||||||
# Public channel has a hardcoded PSK; hashtag channels derive from name
|
|
||||||
_KNOWN_CHANNELS: dict[str, bytes] = {}
|
|
||||||
|
|
||||||
# channel_hash (1 byte) → list of (name, secret) for matching
|
|
||||||
_CHANNEL_BY_HASH: dict[int, list[tuple[str, bytes]]] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _register_channel(name: str, secret: bytes):
|
|
||||||
_KNOWN_CHANNELS[name] = secret
|
|
||||||
h = _channel_hash(secret)
|
|
||||||
_CHANNEL_BY_HASH.setdefault(h, []).append((name, secret))
|
|
||||||
|
|
||||||
|
|
||||||
_CHANNELS_CSV = Path(__file__).parent / "channels.csv"
|
|
||||||
|
|
||||||
|
|
||||||
def _init_channels():
|
|
||||||
"""Load channels from channels.csv. Hashtag channels use key derivation;
|
|
||||||
PSK channels (hashtag=False) use the raw key_hex."""
|
|
||||||
if not _CHANNELS_CSV.is_file():
|
|
||||||
print(f" {RED}channels.csv not found at {_CHANNELS_CSV}{RST}")
|
|
||||||
return
|
|
||||||
with open(_CHANNELS_CSV, newline="") as f:
|
|
||||||
for row in csv.DictReader(f):
|
|
||||||
name = row["channel_name"]
|
|
||||||
is_hashtag = row["hashtag"].strip().lower() == "true"
|
|
||||||
key_hex = row["key_hex"].strip()
|
|
||||||
if is_hashtag:
|
|
||||||
secret = _channel_secret_from_hashtag(name)
|
|
||||||
else:
|
|
||||||
# PSK channel — raw 16-byte key
|
|
||||||
secret = bytes.fromhex(key_hex) + b"\x00" * 16
|
|
||||||
_register_channel(name, secret)
|
|
||||||
|
|
||||||
|
|
||||||
_init_channels()
|
|
||||||
|
|
||||||
|
|
||||||
# ── GRP_TXT transmit (encrypt + send) ─────────────────────────────
|
|
||||||
|
|
||||||
MAX_GRP_TEXT = 163 # max bytes for "SenderName: message" in a GRP_TXT
|
|
||||||
|
|
||||||
|
|
||||||
def _grp_encrypt(secret: bytes, sender: str, text: str) -> bytes:
|
|
||||||
"""Build an encrypted GRP_TXT payload (channel_hash + mac + ciphertext)."""
|
|
||||||
ch = _channel_hash(secret)
|
|
||||||
# Plaintext: timestamp(4) + flags(1) + "sender: text\0" + zero-pad
|
|
||||||
plaintext = struct.pack("<I", int(time.time())) + b"\x00"
|
|
||||||
plaintext += f"{sender}: {text}\x00".encode("utf-8")[:MAX_GRP_TEXT]
|
|
||||||
pad_len = (16 - len(plaintext) % 16) % 16
|
|
||||||
plaintext += b"\x00" * pad_len
|
|
||||||
# AES-128-ECB encrypt
|
|
||||||
cipher = AES.new(secret[:16], AES.MODE_ECB)
|
|
||||||
ciphertext = b""
|
|
||||||
for i in range(0, len(plaintext), 16):
|
|
||||||
ciphertext += cipher.encrypt(plaintext[i : i + 16])
|
|
||||||
# HMAC-SHA256 over ciphertext, truncated to 2 bytes
|
|
||||||
key_len = 16 if secret[16:] == b"\x00" * 16 else 32
|
|
||||||
mac = hmac.new(secret[:key_len], ciphertext, hashlib.sha256).digest()[:2]
|
|
||||||
return bytes([ch]) + mac + ciphertext
|
|
||||||
|
|
||||||
|
|
||||||
def _grp_build_packet(channel_payload: bytes) -> bytes:
|
|
||||||
"""Wrap encrypted GRP_TXT payload into a full MeshCore packet."""
|
|
||||||
header = bytes([0x15]) # GRP_TXT flood: (5 << 2) | 1
|
|
||||||
path_len = bytes([0x40]) # 0 hops, 2-byte hash mode (hash_size_code=1 << 6)
|
|
||||||
return header + path_len + channel_payload
|
|
||||||
|
|
||||||
|
|
||||||
def _grp_transmit(ser, channel_name: str, sender: str, text: str):
|
|
||||||
"""Encrypt and transmit a GRP_TXT message, then resume RX."""
|
|
||||||
secret = _KNOWN_CHANNELS.get(channel_name)
|
|
||||||
if secret is None:
|
|
||||||
return
|
|
||||||
payload = _grp_encrypt(secret, sender, text)
|
|
||||||
packet = _grp_build_packet(payload)
|
|
||||||
try:
|
|
||||||
send_cmd(ser, "Transmit", quiet=True, payload=packet)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# Always try to resume RX, even if TX failed
|
|
||||||
try:
|
|
||||||
send_cmd(ser, "StartRx", quiet=True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ── Loop/collision aggregator ─────────────────────────────────────
|
|
||||||
|
|
||||||
REPORT_CHANNEL = "#watchman"
|
|
||||||
REPORT_SENDER = "Watchman"
|
|
||||||
REPORT_WINDOW = 60 # seconds — collect events before reporting
|
|
||||||
REPORT_COOLDOWN = 3600 # seconds — suppress repeat reports for same issue
|
|
||||||
|
|
||||||
|
|
||||||
def _nodes_for_hash(hash_hex: str, hash_size: int) -> int:
|
|
||||||
"""Count how many known nodes share this path hash."""
|
|
||||||
return sum(1 for pk in _known_nodes if pk[: hash_size * 2] == hash_hex)
|
|
||||||
|
|
||||||
|
|
||||||
class LoopAggregator:
|
|
||||||
def __init__(self):
|
|
||||||
self._pending: list[dict] = []
|
|
||||||
self._cooldown: dict[str, float] = {}
|
|
||||||
self._window_start = time.monotonic()
|
|
||||||
|
|
||||||
def record(self, dupes: dict[str, int], hash_size: int, all_hops: list[str]):
|
|
||||||
"""Record duplicate hops from one packet."""
|
|
||||||
first_hop = all_hops[0] if all_hops else None
|
|
||||||
self._pending.append(
|
|
||||||
{
|
|
||||||
"dupes": dupes,
|
|
||||||
"hash_size": hash_size,
|
|
||||||
"hops": all_hops,
|
|
||||||
"first_hop": first_hop,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def maybe_report(self, ser) -> bool:
|
|
||||||
if not self._pending:
|
|
||||||
self._window_start = time.monotonic()
|
|
||||||
return False
|
|
||||||
if time.monotonic() - self._window_start < REPORT_WINDOW:
|
|
||||||
return False
|
|
||||||
self._send_report(ser)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _send_report(self, ser):
|
|
||||||
now = time.monotonic()
|
|
||||||
messages: list[str] = []
|
|
||||||
|
|
||||||
# Aggregate: (hash, hash_size) → {packets, relayers, senders}
|
|
||||||
agg: dict[tuple[str, int], dict] = {}
|
|
||||||
for ev in self._pending:
|
|
||||||
for h, count in ev["dupes"].items():
|
|
||||||
key = (h, ev["hash_size"])
|
|
||||||
if key not in agg:
|
|
||||||
agg[key] = {"packets": 0, "relayers": set(), "senders": set()}
|
|
||||||
agg[key]["packets"] += 1
|
|
||||||
if ev["first_hop"]:
|
|
||||||
agg[key]["senders"].add(ev["first_hop"])
|
|
||||||
for hop in ev["hops"]:
|
|
||||||
if hop != h:
|
|
||||||
agg[key]["relayers"].add(hop)
|
|
||||||
|
|
||||||
for (h, hs), info in agg.items():
|
|
||||||
issue_key = f"{h}:{hs}"
|
|
||||||
if (
|
|
||||||
issue_key in self._cooldown
|
|
||||||
and now - self._cooldown[issue_key] < REPORT_COOLDOWN
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
n_known = _nodes_for_hash(h, hs)
|
|
||||||
pkts = info["packets"]
|
|
||||||
senders = sorted(info["senders"])
|
|
||||||
sender_str = ",".join(senders[:3]) if senders else "?"
|
|
||||||
if len(senders) > 3:
|
|
||||||
sender_str += f"+{len(senders) - 3}"
|
|
||||||
relayer_str = ",".join(sorted(info["relayers"])[:3])
|
|
||||||
if len(info["relayers"]) > 3:
|
|
||||||
relayer_str += f"+{len(info['relayers']) - 3}"
|
|
||||||
|
|
||||||
if n_known == 1:
|
|
||||||
# Exactly one node matches → confirmed loop
|
|
||||||
msg = f"Loop: {h} re-sent its own traffic ({pkts} pkt)."
|
|
||||||
if relayer_str:
|
|
||||||
msg += f" Relayed by {relayer_str}."
|
|
||||||
messages.append(msg)
|
|
||||||
elif hs >= 2:
|
|
||||||
if n_known > 1:
|
|
||||||
# 2B+ hash collision — rare, definitely a routing issue
|
|
||||||
msg = (
|
|
||||||
f"Routing conflict: {n_known} nodes share {hs}B "
|
|
||||||
f"hash {h} ({pkts} pkt)."
|
|
||||||
)
|
|
||||||
messages.append(msg)
|
|
||||||
else:
|
|
||||||
# 2B+ duplicate, unknown node → almost certainly a loop
|
|
||||||
msg = f"Probable loop: {h} appeared {pkts}x."
|
|
||||||
if relayer_str:
|
|
||||||
msg += f" Relayed by {relayer_str}."
|
|
||||||
messages.append(msg)
|
|
||||||
else:
|
|
||||||
# 1B hash — can't distinguish loop from collision
|
|
||||||
if n_known > 1:
|
|
||||||
msg = (
|
|
||||||
f"1B hash {h} duplicated in {pkts} pkt "
|
|
||||||
f"({n_known} known nodes share it). "
|
|
||||||
f"Sender {sender_str}: use 2B routing!"
|
|
||||||
)
|
|
||||||
messages.append(msg)
|
|
||||||
else:
|
|
||||||
msg = (
|
|
||||||
f"1B hash {h} duplicated in {pkts} pkt. "
|
|
||||||
f"Sender {sender_str}: use 2B routing to clarify!"
|
|
||||||
)
|
|
||||||
messages.append(msg)
|
|
||||||
|
|
||||||
self._cooldown[issue_key] = now
|
|
||||||
|
|
||||||
if not messages:
|
|
||||||
self._reset()
|
|
||||||
return
|
|
||||||
|
|
||||||
full = " | ".join(messages)
|
|
||||||
max_len = MAX_GRP_TEXT - len(REPORT_SENDER) - 2
|
|
||||||
if len(full) > max_len:
|
|
||||||
full = full[: max_len - 5] + "+more"
|
|
||||||
|
|
||||||
print(f" {DIM}>>>{RST} {BYEL}Report → {REPORT_CHANNEL}:{RST} {full}")
|
|
||||||
_grp_transmit(ser, REPORT_CHANNEL, REPORT_SENDER, full)
|
|
||||||
self._reset()
|
|
||||||
|
|
||||||
def _reset(self):
|
|
||||||
self._pending.clear()
|
|
||||||
self._window_start = time.monotonic()
|
|
||||||
|
|
||||||
|
|
||||||
_aggregator = LoopAggregator()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ── MeshCore packet decoder ──────────────────────────────────────
|
|
||||||
|
|
||||||
ROUTE_NAMES = {0: "tflood", 1: "flood", 2: "direct", 3: "tdirect"}
|
|
||||||
|
|
||||||
PAYLOAD_NAMES = {
|
|
||||||
0x00: "REQ",
|
|
||||||
0x01: "RESPONSE",
|
|
||||||
0x02: "TXT_MSG",
|
|
||||||
0x03: "ACK",
|
|
||||||
0x04: "ADVERT",
|
|
||||||
0x05: "GRP_TXT",
|
|
||||||
0x06: "GRP_DATA",
|
|
||||||
0x07: "ANON_REQ",
|
|
||||||
0x08: "PATH",
|
|
||||||
0x09: "TRACE",
|
|
||||||
0x0A: "MULTIPART",
|
|
||||||
0x0B: "CONTROL",
|
|
||||||
0x0F: "RAW_CUSTOM",
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_TYPES = {0x00: "none", 0x01: "chat", 0x02: "repeater", 0x03: "room", 0x04: "sensor"}
|
|
||||||
|
|
||||||
MAX_PATH_SIZE = 64
|
|
||||||
|
|
||||||
# ── Node registry (persistent, pruned at 12h) ────────────────────
|
|
||||||
|
|
||||||
ADVERT_MAX_AGE = 12 * 3600 # seconds
|
|
||||||
_ADVERT_FILE = Path(tempfile.gettempdir()) / "donglora_adverts.json"
|
|
||||||
|
|
||||||
# In-memory: pubkey_hex → {"name": str, "seen": float (unix timestamp)}
|
|
||||||
_known_nodes: dict[str, dict] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _load_adverts():
|
|
||||||
"""Load persisted adverts from disk, pruning stale entries."""
|
|
||||||
global _known_nodes
|
|
||||||
try:
|
|
||||||
data = json.loads(_ADVERT_FILE.read_text())
|
|
||||||
cutoff = time.time() - ADVERT_MAX_AGE
|
|
||||||
_known_nodes = {k: v for k, v in data.items() if v.get("seen", 0) > cutoff}
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError, ValueError):
|
|
||||||
_known_nodes = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _save_adverts():
|
|
||||||
"""Persist current adverts to disk (atomic write)."""
|
|
||||||
try:
|
|
||||||
tmp = _ADVERT_FILE.with_suffix(".tmp")
|
|
||||||
tmp.write_text(json.dumps(_known_nodes))
|
|
||||||
tmp.rename(_ADVERT_FILE)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _lookup_hash(hash_hex: str, hash_size: int) -> list[str]:
|
|
||||||
"""Find node names whose pubkey prefix matches a path hash."""
|
|
||||||
return [
|
|
||||||
v["name"]
|
|
||||||
for pk_hex, v in _known_nodes.items()
|
|
||||||
if pk_hex[: hash_size * 2] == hash_hex
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _register_node(pubkey: bytes, name: str):
|
|
||||||
"""Register or update a node from an ADVERT."""
|
|
||||||
pk_hex = pubkey.hex()
|
|
||||||
_known_nodes[pk_hex] = {"name": name, "seen": time.time()}
|
|
||||||
_save_adverts()
|
|
||||||
|
|
||||||
|
|
||||||
def _utf8_score(data: bytes) -> int:
|
|
||||||
"""Score how cleanly data decodes as UTF-8. Higher = more replacement chars."""
|
|
||||||
return data.decode("utf-8", errors="replace").count("\ufffd")
|
|
||||||
|
|
||||||
|
|
||||||
def _best_name(candidates: list[bytes]) -> bytes:
|
|
||||||
"""Pick the candidate with the fewest UTF-8 replacement chars; longest wins ties."""
|
|
||||||
return min(candidates, key=lambda d: (_utf8_score(d), -len(d)))
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_advert_appdata(app_data: bytes) -> tuple[str, str, list[str]]:
|
|
||||||
"""Decode advert app_data. Returns (display_string, node_name, quality_warnings)."""
|
|
||||||
if not app_data:
|
|
||||||
return "", "", []
|
|
||||||
flags = app_data[0]
|
|
||||||
node_type = flags & 0x0F
|
|
||||||
node_label = NODE_TYPES.get(node_type, f"0x{node_type:02x}")
|
|
||||||
parts = [f"{DIM}flags={RST}{flags:02x} {DIM}node={RST}{node_label}"]
|
|
||||||
warnings: list[str] = []
|
|
||||||
|
|
||||||
if node_type > 4:
|
|
||||||
warnings.append("bad node_type")
|
|
||||||
|
|
||||||
# Parse location if flagged and coords are valid
|
|
||||||
off = 1
|
|
||||||
loc_str = ""
|
|
||||||
if flags & 0x10 and off + 8 <= len(app_data):
|
|
||||||
lat_raw = struct.unpack_from("<i", app_data, off)[0]
|
|
||||||
lon_raw = struct.unpack_from("<i", app_data, off + 4)[0]
|
|
||||||
lat, lon = lat_raw / 1e6, lon_raw / 1e6
|
|
||||||
if -90 <= lat <= 90 and -180 <= lon <= 180:
|
|
||||||
loc_str = f"{DIM}loc={RST}{lat:.4f},{lon:.4f}"
|
|
||||||
off += 8
|
|
||||||
|
|
||||||
# Build candidate name slices — try with/without feat fields
|
|
||||||
off_with_feat = off
|
|
||||||
if flags & 0x20 and off_with_feat + 2 <= len(app_data):
|
|
||||||
off_with_feat += 2
|
|
||||||
if flags & 0x40 and off_with_feat + 2 <= len(app_data):
|
|
||||||
off_with_feat += 2
|
|
||||||
|
|
||||||
name = ""
|
|
||||||
if flags & 0x80:
|
|
||||||
candidates = []
|
|
||||||
if off_with_feat < len(app_data):
|
|
||||||
candidates.append(app_data[off_with_feat:])
|
|
||||||
if off < len(app_data) and off != off_with_feat:
|
|
||||||
candidates.append(app_data[off:])
|
|
||||||
# Also try raw name-at-byte-1 as a last resort (ignoring all flags)
|
|
||||||
if 1 not in (off, off_with_feat) and 1 < len(app_data):
|
|
||||||
candidates.append(app_data[1:])
|
|
||||||
if candidates:
|
|
||||||
best = _best_name(candidates)
|
|
||||||
name = best.decode("utf-8", errors="replace")
|
|
||||||
if "\ufffd" in name:
|
|
||||||
warnings.append("bad UTF-8")
|
|
||||||
|
|
||||||
if loc_str:
|
|
||||||
parts.append(loc_str)
|
|
||||||
if name:
|
|
||||||
parts.append(f"{BWHT}{name}{RST}")
|
|
||||||
return " ".join(parts), name, warnings
|
|
||||||
|
|
||||||
|
|
||||||
LOOP_INDENT = " " * 38 # align under decoded packet text (after RSSI/SNR/len)
|
|
||||||
|
|
||||||
|
|
||||||
def _detect_loops(hops: list[str], hash_size: int) -> str | None:
|
|
||||||
"""Check for duplicate hops in a path. Returns a warning line or None."""
|
|
||||||
if len(hops) < 2:
|
|
||||||
return None
|
|
||||||
counts: dict[str, int] = {}
|
|
||||||
for h in hops:
|
|
||||||
counts[h] = counts.get(h, 0) + 1
|
|
||||||
dupes = {h: c for h, c in counts.items() if c > 1}
|
|
||||||
if not dupes:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Feed the aggregator for deferred reporting
|
|
||||||
_aggregator.record(dupes, hash_size, hops)
|
|
||||||
|
|
||||||
parts = []
|
|
||||||
for h, c in dupes.items():
|
|
||||||
n_known = _nodes_for_hash(h, hash_size)
|
|
||||||
if n_known == 1:
|
|
||||||
# Exactly one node matches → confirmed loop
|
|
||||||
parts.append(f"{BRED}{h} x{c} LOOP{RST}")
|
|
||||||
elif hash_size >= 2:
|
|
||||||
if n_known > 1:
|
|
||||||
# 2B+ collision is rare and real
|
|
||||||
parts.append(f"{BCYN}{h} x{c} COLLISION {DIM}({n_known} nodes){RST}")
|
|
||||||
else:
|
|
||||||
# 2B+ duplicate, unknown node → almost certainly a loop
|
|
||||||
parts.append(f"{BRED}{h} x{c} LOOP{RST}")
|
|
||||||
else:
|
|
||||||
# 1B hash — ambiguous: could be loop or collision
|
|
||||||
if n_known > 1:
|
|
||||||
parts.append(
|
|
||||||
f"{YEL}{h} x{c} {DIM}(loop or collision? {n_known} nodes share this 1B hash){RST}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
parts.append(f"{YEL}{h} x{c} {DIM}(loop or collision? 1B hash){RST}")
|
|
||||||
|
|
||||||
return f"{LOOP_INDENT}\u26a0 {', '.join(parts)}"
|
|
||||||
|
|
||||||
|
|
||||||
def _try_decode_body(
|
|
||||||
data: bytes,
|
|
||||||
pos: int,
|
|
||||||
type_name: str,
|
|
||||||
type_color: str,
|
|
||||||
payload_type: int,
|
|
||||||
payload_ver: int,
|
|
||||||
route_name: str,
|
|
||||||
tc_str: str,
|
|
||||||
) -> tuple[str | None, list[str]]:
|
|
||||||
"""Try to parse path + payload starting at pos. Returns (decoded_string, warnings) or (None, [])."""
|
|
||||||
if pos >= len(data):
|
|
||||||
return None, []
|
|
||||||
path_len_byte = data[pos]
|
|
||||||
pos += 1
|
|
||||||
hash_size_code = path_len_byte >> 6
|
|
||||||
if hash_size_code == 3:
|
|
||||||
return None, [] # reserved hash size code per spec Section 3
|
|
||||||
hash_size = hash_size_code + 1
|
|
||||||
hop_count = path_len_byte & 0x3F
|
|
||||||
|
|
||||||
path_bytes = hop_count * hash_size
|
|
||||||
if path_bytes > MAX_PATH_SIZE or pos + path_bytes > len(data):
|
|
||||||
return None, []
|
|
||||||
|
|
||||||
hops_str = ""
|
|
||||||
loop_warn = ""
|
|
||||||
hops: list[str] = []
|
|
||||||
if hop_count > 0:
|
|
||||||
for i in range(hop_count):
|
|
||||||
h = data[pos + i * hash_size : pos + (i + 1) * hash_size]
|
|
||||||
hops.append(h.hex())
|
|
||||||
hops_str = f" {DIM}path=[{RST}{'>'.join(hops)}{DIM}]{RST}"
|
|
||||||
warn = _detect_loops(hops, hash_size)
|
|
||||||
if warn:
|
|
||||||
loop_warn = f"\n{warn}"
|
|
||||||
pos += path_bytes
|
|
||||||
|
|
||||||
payload = data[pos:]
|
|
||||||
if not payload or len(payload) > 184:
|
|
||||||
return None, []
|
|
||||||
prefix = f"{type_color}{type_name}{RST} {route_name}{tc_str}{hops_str}"
|
|
||||||
|
|
||||||
warnings: list[str] = []
|
|
||||||
if payload_type == 0x04:
|
|
||||||
result, warnings = _decode_advert(prefix, payload)
|
|
||||||
elif payload_type == 0x03:
|
|
||||||
result = _decode_ack(prefix, payload)
|
|
||||||
elif payload_type in (0x00, 0x01, 0x02, 0x08):
|
|
||||||
result = _decode_encrypted_peer(prefix, type_name, payload)
|
|
||||||
elif payload_type in (0x05, 0x06):
|
|
||||||
result = _decode_encrypted_group(prefix, payload)
|
|
||||||
elif payload_type == 0x07:
|
|
||||||
result = _decode_anon_req(prefix, payload)
|
|
||||||
elif payload_type == 0x09:
|
|
||||||
result = _decode_trace(prefix, payload)
|
|
||||||
elif payload_type == 0x0A:
|
|
||||||
result = _decode_multipart(prefix, payload)
|
|
||||||
elif payload_type == 0x0B:
|
|
||||||
result = _decode_control(prefix, payload)
|
|
||||||
else:
|
|
||||||
result = f"{prefix} {DIM}{payload.hex()}{RST}" if payload else prefix
|
|
||||||
|
|
||||||
return result + loop_warn, warnings
|
|
||||||
|
|
||||||
|
|
||||||
def decode_meshcore_packet(data: bytes) -> tuple[str, list[str]]:
|
|
||||||
"""Decode a MeshCore packet. Returns (display_string, quality_warnings)."""
|
|
||||||
if len(data) < 3:
|
|
||||||
return f"{RED}<too short>{RST} {DIM}{data.hex()}{RST}", []
|
|
||||||
|
|
||||||
header = data[0]
|
|
||||||
if header == 0xFF:
|
|
||||||
return f"{DIM}<no-retransmit marker> {data[1:].hex()}{RST}", []
|
|
||||||
|
|
||||||
route_type = header & 0x03
|
|
||||||
payload_type = (header >> 2) & 0x0F
|
|
||||||
payload_ver = (header >> 6) & 0x03
|
|
||||||
|
|
||||||
# MeshCore only implements PAYLOAD_VER_1 (0x00) — versions 1-3 are reserved
|
|
||||||
# and rejected by real nodes. Packets with ver > 0 are noise or non-MeshCore.
|
|
||||||
if payload_ver != 0:
|
|
||||||
return (
|
|
||||||
f"{RED}<not meshcore>{RST} {DIM}hdr=0x{header:02x} "
|
|
||||||
f"(ver={payload_ver} — only v0 exists) {data[1:].hex()}{RST}",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
route_name = ROUTE_NAMES.get(route_type, f"rt{route_type}")
|
|
||||||
type_name = PAYLOAD_NAMES.get(payload_type, f"0x{payload_type:02x}")
|
|
||||||
type_color = TYPE_COLORS.get(type_name, WHT)
|
|
||||||
|
|
||||||
has_tc = route_type in (0, 3)
|
|
||||||
|
|
||||||
# Try with transport codes first if indicated, fallback without
|
|
||||||
if has_tc and len(data) >= 6:
|
|
||||||
tc1, tc2 = struct.unpack_from("<HH", data, 1)
|
|
||||||
tc_str = f" {DIM}tc={tc1:04x}:{tc2:04x}{RST}"
|
|
||||||
result, warnings = _try_decode_body(
|
|
||||||
data,
|
|
||||||
5,
|
|
||||||
type_name,
|
|
||||||
type_color,
|
|
||||||
payload_type,
|
|
||||||
payload_ver,
|
|
||||||
route_name,
|
|
||||||
tc_str,
|
|
||||||
)
|
|
||||||
if result is not None:
|
|
||||||
return result, warnings
|
|
||||||
# TC parse failed — retry without (older firmware compatibility)
|
|
||||||
result, warnings = _try_decode_body(
|
|
||||||
data, 1, type_name, type_color, payload_type, payload_ver, route_name, ""
|
|
||||||
)
|
|
||||||
if result is not None:
|
|
||||||
return result, warnings
|
|
||||||
else:
|
|
||||||
result, warnings = _try_decode_body(
|
|
||||||
data, 1, type_name, type_color, payload_type, payload_ver, route_name, ""
|
|
||||||
)
|
|
||||||
if result is not None:
|
|
||||||
return result, warnings
|
|
||||||
|
|
||||||
# Both attempts failed — show what we know from the header
|
|
||||||
return (
|
|
||||||
f"{RED}<bad framing>{RST} {type_color}{type_name}{RST} "
|
|
||||||
f"{route_name} {DIM}{data[1:].hex()}{RST}",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_advert(prefix: str, payload: bytes) -> tuple[str, list[str]]:
|
|
||||||
if len(payload) < 100:
|
|
||||||
return f"{prefix} {RED}<bad advert: {len(payload)}B, need >=100>{RST} {DIM}{payload.hex()}{RST}", []
|
|
||||||
pubkey = payload[0:32]
|
|
||||||
timestamp = struct.unpack_from("<I", payload, 32)[0]
|
|
||||||
app_data = payload[100:]
|
|
||||||
warnings: list[str] = []
|
|
||||||
try:
|
|
||||||
ts = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime(
|
|
||||||
"%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
|
||||||
if timestamp > time.time() + 3600:
|
|
||||||
warnings.append("future ts")
|
|
||||||
except (OSError, OverflowError, ValueError):
|
|
||||||
ts = f"epoch={timestamp}"
|
|
||||||
warnings.append("bad ts")
|
|
||||||
app_str, name, app_warnings = _decode_advert_appdata(app_data)
|
|
||||||
warnings.extend(app_warnings)
|
|
||||||
if not warnings:
|
|
||||||
_register_node(pubkey, name)
|
|
||||||
return f"{prefix} {DIM}pub={RST}{pubkey[:4].hex()}.. {DIM}ts={RST}{ts}Z {app_str}", warnings
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_ack(prefix: str, payload: bytes) -> str:
|
|
||||||
if len(payload) < 4:
|
|
||||||
return (
|
|
||||||
f"{prefix} {RED}<bad ack: {len(payload)}B>{RST} {DIM}{payload.hex()}{RST}"
|
|
||||||
)
|
|
||||||
crc = struct.unpack_from("<I", payload, 0)[0]
|
|
||||||
return f"{prefix} {DIM}crc={RST}0x{crc:08x}"
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_encrypted_peer(prefix: str, type_name: str, payload: bytes) -> str:
|
|
||||||
if len(payload) < 20:
|
|
||||||
return f"{prefix} {RED}<bad {type_name}: {len(payload)}B>{RST} {DIM}{payload.hex()}{RST}"
|
|
||||||
dst = payload[0]
|
|
||||||
src = payload[1]
|
|
||||||
mac = payload[2:4]
|
|
||||||
ct_len = len(payload) - 4
|
|
||||||
return f"{prefix} {DIM}dst={RST}{dst:02x} {DIM}src={RST}{src:02x} {DIM}mac={RST}{mac.hex()} {DIM}[{ct_len}B]{RST}"
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_encrypted_group(prefix: str, payload: bytes) -> str:
|
|
||||||
if len(payload) < 19:
|
|
||||||
return (
|
|
||||||
f"{prefix} {RED}<bad group: {len(payload)}B>{RST} {DIM}{payload.hex()}{RST}"
|
|
||||||
)
|
|
||||||
ch = payload[0]
|
|
||||||
mac_bytes = payload[1:3]
|
|
||||||
ciphertext = payload[3:]
|
|
||||||
|
|
||||||
# Try to decrypt with known channels matching this hash
|
|
||||||
candidates = _CHANNEL_BY_HASH.get(ch, [])
|
|
||||||
for chan_name, secret in candidates:
|
|
||||||
plaintext = _grp_verify_and_decrypt(secret, mac_bytes, ciphertext)
|
|
||||||
if plaintext is not None:
|
|
||||||
parsed = _parse_grp_plaintext(plaintext)
|
|
||||||
if parsed:
|
|
||||||
ts, text = parsed
|
|
||||||
try:
|
|
||||||
ts_str = datetime.fromtimestamp(ts, tz=timezone.utc).strftime(
|
|
||||||
"%H:%M:%S"
|
|
||||||
)
|
|
||||||
except (OSError, OverflowError, ValueError):
|
|
||||||
ts_str = f"epoch={ts}"
|
|
||||||
return (
|
|
||||||
f"{prefix} {BGRN}{chan_name}{RST} "
|
|
||||||
f"{DIM}ts={RST}{ts_str}Z {BWHT}{text}{RST}"
|
|
||||||
)
|
|
||||||
# MAC matched but plaintext unparseable
|
|
||||||
return (
|
|
||||||
f"{prefix} {BGRN}{chan_name}{RST} {DIM}[decrypted but unparseable]{RST}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# No known channel matched — show raw info
|
|
||||||
return (
|
|
||||||
f"{prefix} {DIM}ch={RST}{ch:02x} {DIM}mac={RST}{mac_bytes.hex()} "
|
|
||||||
f"{DIM}[{len(ciphertext)}B]{RST}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_anon_req(prefix: str, payload: bytes) -> str:
|
|
||||||
if len(payload) < 51:
|
|
||||||
return f"{prefix} {RED}<bad anon_req: {len(payload)}B>{RST} {DIM}{payload.hex()}{RST}"
|
|
||||||
dst = payload[0]
|
|
||||||
ephem_pub = payload[1:33]
|
|
||||||
mac = payload[33:35]
|
|
||||||
ct_len = len(payload) - 35
|
|
||||||
return f"{prefix} {DIM}dst={RST}{dst:02x} {DIM}ephem={RST}{ephem_pub[:4].hex()}.. {DIM}mac={RST}{mac.hex()} {DIM}[{ct_len}B]{RST}"
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_trace(prefix: str, payload: bytes) -> str:
|
|
||||||
if len(payload) < 9:
|
|
||||||
return (
|
|
||||||
f"{prefix} {RED}<bad trace: {len(payload)}B>{RST} {DIM}{payload.hex()}{RST}"
|
|
||||||
)
|
|
||||||
tag = struct.unpack_from("<I", payload, 0)[0]
|
|
||||||
flags = payload[8]
|
|
||||||
trace_hash_size = 1 << (flags & 0x03)
|
|
||||||
trace_hashes = payload[9:]
|
|
||||||
n_hashes = len(trace_hashes) // trace_hash_size if trace_hash_size else 0
|
|
||||||
hops = []
|
|
||||||
for i in range(n_hashes):
|
|
||||||
h = trace_hashes[i * trace_hash_size : (i + 1) * trace_hash_size]
|
|
||||||
hops.append(h.hex())
|
|
||||||
hops_str = f" {DIM}trace=[{RST}{'>'.join(hops)}{DIM}]{RST}" if hops else ""
|
|
||||||
return f"{prefix} {DIM}tag={RST}0x{tag:08x}{hops_str}"
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_multipart(prefix: str, payload: bytes) -> str:
|
|
||||||
if len(payload) < 2:
|
|
||||||
return f"{prefix} {RED}<bad multipart: {len(payload)}B>{RST}"
|
|
||||||
remaining = (payload[0] >> 4) & 0x0F
|
|
||||||
inner_type = payload[0] & 0x0F
|
|
||||||
inner_name = PAYLOAD_NAMES.get(inner_type, f"0x{inner_type:02x}")
|
|
||||||
return f"{prefix} {DIM}remaining={RST}{remaining} {DIM}inner={RST}{inner_name} {DIM}[{len(payload) - 1}B]{RST}"
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_control(prefix: str, payload: bytes) -> str:
|
|
||||||
if len(payload) < 1:
|
|
||||||
return f"{prefix} {RED}<empty control>{RST}"
|
|
||||||
sub_type = (payload[0] >> 4) & 0x0F
|
|
||||||
sub_names = {0x8: "DISCOVER_REQ", 0x9: "DISCOVER_RESP"}
|
|
||||||
sub_name = sub_names.get(sub_type, f"sub=0x{sub_type:x}")
|
|
||||||
if sub_type == 0x8 and len(payload) >= 6:
|
|
||||||
type_filter = payload[1]
|
|
||||||
tag = struct.unpack_from("<I", payload, 2)[0]
|
|
||||||
return f"{prefix} {sub_name} {DIM}filter={RST}0x{type_filter:02x} {DIM}tag={RST}0x{tag:08x}"
|
|
||||||
elif sub_type == 0x9 and len(payload) >= 6:
|
|
||||||
node_type = payload[0] & 0x0F
|
|
||||||
snr_raw = struct.unpack_from("<b", payload, 1)[0]
|
|
||||||
tag = struct.unpack_from("<I", payload, 2)[0]
|
|
||||||
return f"{prefix} {sub_name} {DIM}node={RST}{NODE_TYPES.get(node_type, f'0x{node_type:02x}')} {DIM}snr={RST}{snr_raw / 4:.1f}dB {DIM}tag={RST}0x{tag:08x}"
|
|
||||||
return f"{prefix} {sub_name} {DIM}{payload[1:].hex()}{RST}"
|
|
||||||
|
|
||||||
|
|
||||||
# ── Main ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def _format_response(resp: dict) -> str:
|
|
||||||
"""Format a response dict for themed display."""
|
|
||||||
t = resp["type"]
|
|
||||||
if t == "Pong":
|
|
||||||
return "Pong"
|
|
||||||
if t == "Ok":
|
|
||||||
return "Ok"
|
|
||||||
if t == "TxDone":
|
|
||||||
return "TxDone"
|
|
||||||
if t == "Config":
|
|
||||||
return f"Config {DIM}{resp.get('raw', '')}{RST}"
|
|
||||||
if t == "Error":
|
|
||||||
return f"{RED}Error (code {resp.get('code', '?')}){RST}"
|
|
||||||
return t
|
|
||||||
|
|
||||||
|
|
||||||
def send_cmd(ser, cmd_name: str, label: str = "", quiet: bool = False, **kwargs) -> dict | None:
|
|
||||||
frame = dl.cobs_encode(dl.encode_command(cmd_name, **kwargs))
|
|
||||||
if not quiet:
|
|
||||||
print(f" {DIM}>>>{RST} {label}")
|
|
||||||
ser.write(frame)
|
|
||||||
ser.flush()
|
|
||||||
resp_data = dl.read_frame(ser)
|
|
||||||
if resp_data is None:
|
|
||||||
if not quiet:
|
|
||||||
print(f" {DIM}<<<{RST} {YEL}timeout{RST}")
|
|
||||||
return None
|
|
||||||
resp = dl.decode_response(resp_data)
|
|
||||||
if not quiet:
|
|
||||||
print(f" {DIM}<<<{RST} {_format_response(resp)}")
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
RADIO_CONFIG = {
|
|
||||||
"freq_hz": 910_525_000,
|
|
||||||
"bw": 6, # Khz62 = variant index 6
|
|
||||||
"sf": 7,
|
|
||||||
"cr": 5, # CR 4/5 — denominator value
|
|
||||||
"sync_word": 0x3444,
|
|
||||||
"tx_power_dbm": -128, # TX_POWER_MAX: use board's maximum
|
|
||||||
"preamble_len": 0, # firmware default (16)
|
|
||||||
"cad": 1, # CAD on
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _format_rssi_snr(rssi: int, snr: int) -> str:
|
|
||||||
rc = _rssi_color(rssi)
|
|
||||||
sc = _snr_color(snr)
|
|
||||||
line = f"{rc}RSSI:{rssi:4d}dBm{RST} {sc}SNR:{snr:3d}dB{RST}"
|
|
||||||
# Flag bogus dongle responses (SNR outside SX126x range -32..+32)
|
|
||||||
if not (-32 <= snr <= 32):
|
|
||||||
line += f" {BRED}[bad SNR]{RST}"
|
|
||||||
return line
|
|
||||||
|
|
||||||
|
|
||||||
DISPLAY_IDLE_TIMEOUT = 90 # seconds — turn display off when no activity
|
|
||||||
|
|
||||||
|
|
||||||
def configure_and_listen(ser):
|
|
||||||
send_cmd(ser, "Ping", "Ping")
|
|
||||||
send_cmd(ser, "SetConfig", "SetConfig 910.525/62.5k/SF7/CR4/5", config=RADIO_CONFIG)
|
|
||||||
send_cmd(ser, "StartRx", "StartRx")
|
|
||||||
|
|
||||||
print(f"\n {BWHT}Listening{RST} {DIM}(Ctrl+C to stop){RST}\n")
|
|
||||||
ser.timeout = 1
|
|
||||||
|
|
||||||
last_activity = time.monotonic()
|
|
||||||
display_off = False
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
_aggregator.maybe_report(ser)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Auto-off display after idle timeout
|
|
||||||
now = time.monotonic()
|
|
||||||
if not display_off and now - last_activity > DISPLAY_IDLE_TIMEOUT:
|
|
||||||
send_cmd(ser, "DisplayOff", quiet=True)
|
|
||||||
display_off = True
|
|
||||||
|
|
||||||
data = dl.read_frame(ser)
|
|
||||||
if data is None:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
resp = dl.decode_response(data)
|
|
||||||
if resp["type"] == "RxPacket":
|
|
||||||
# Wake display on activity
|
|
||||||
if display_off:
|
|
||||||
send_cmd(ser, "DisplayOn", quiet=True)
|
|
||||||
display_off = False
|
|
||||||
last_activity = time.monotonic()
|
|
||||||
|
|
||||||
payload = resp["payload"]
|
|
||||||
decoded, warnings = decode_meshcore_packet(payload)
|
|
||||||
snr = resp["snr"]
|
|
||||||
# SNR below LoRa demodulation floor is one quality signal
|
|
||||||
sf = RADIO_CONFIG["sf"]
|
|
||||||
if -32 <= snr <= 32 and snr < -2.5 * (sf - 4):
|
|
||||||
warnings.append("low SNR")
|
|
||||||
warn_tag = ""
|
|
||||||
if warnings:
|
|
||||||
warn_tag = f" {BYEL}[{', '.join(warnings)}]{RST}"
|
|
||||||
rssi_snr = _format_rssi_snr(resp["rssi"], snr)
|
|
||||||
print(f" {rssi_snr} {DIM}len:{len(payload):3d}{RST} {decoded}{warn_tag}")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
_load_adverts()
|
|
||||||
n = len(_known_nodes)
|
|
||||||
if n:
|
|
||||||
print(f" {DIM}Loaded {n} known node(s){RST}")
|
|
||||||
|
|
||||||
port = sys.argv[1] if len(sys.argv) > 1 else None
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
ser = dl.connect(port)
|
|
||||||
configure_and_listen(ser)
|
|
||||||
except (serial.SerialException, ConnectionError, OSError) as e:
|
|
||||||
print(f"\n {RED}Disconnected: {e}{RST}")
|
|
||||||
print(f" {DIM}Reconnecting when device reappears...{RST}")
|
|
||||||
time.sleep(1)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print()
|
|
||||||
try:
|
|
||||||
ser.timeout = 2
|
|
||||||
send_cmd(ser, "StopRx", "StopRx")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +0,0 @@
|
|||||||
[tools]
|
|
||||||
python = "3.14"
|
|
||||||
uv = "latest"
|
|
||||||
just = "latest"
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Two-dongle demo: one transmits, one receives.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python examples/ping_pong.py --role tx [PORT]
|
|
||||||
python examples/ping_pong.py --role rx [PORT]
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
import serial
|
|
||||||
|
|
||||||
import donglora as dl
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="DongLoRa ping-pong demo")
|
|
||||||
parser.add_argument("--role", choices=["tx", "rx"], required=True)
|
|
||||||
parser.add_argument("port", nargs="?", help="Serial port (auto-detect if omitted)")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
ser = dl.connect(args.port)
|
|
||||||
print(dl.send(ser, "SetConfig", config=dl.DEFAULT_CONFIG))
|
|
||||||
|
|
||||||
if args.role == "tx":
|
|
||||||
print("Transmitting every 2 seconds (Ctrl+C to stop)...\n")
|
|
||||||
seq = 0
|
|
||||||
while True:
|
|
||||||
msg = f"ping #{seq}"
|
|
||||||
resp = dl.send(ser, "Transmit", payload=msg.encode())
|
|
||||||
print(f" TX: {msg!r} → {resp['type']}")
|
|
||||||
seq += 1
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
else:
|
|
||||||
print(dl.send(ser, "StartRx"))
|
|
||||||
print("Receiving (Ctrl+C to stop)...\n")
|
|
||||||
ser.timeout = 1
|
|
||||||
while True:
|
|
||||||
data = dl.read_frame(ser)
|
|
||||||
if data is None:
|
|
||||||
continue
|
|
||||||
resp = dl.decode_response(data)
|
|
||||||
if resp["type"] == "RxPacket":
|
|
||||||
p = resp["payload"]
|
|
||||||
text = p.decode("utf-8", errors="replace")
|
|
||||||
print(f" RX: {text!r} RSSI:{resp['rssi']}dBm SNR:{resp['snr']}dB")
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
try:
|
|
||||||
dl.send(ser, "StopRx")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
print("\nDone.")
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"\nSerial error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "donglora-examples"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "DongLoRa example scripts"
|
|
||||||
license = "MIT"
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
dependencies = [
|
|
||||||
"donglora",
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
meshcore = ["donglora[meshcore]"]
|
|
||||||
orac = ["donglora[orac]"]
|
|
||||||
|
|
||||||
[tool.uv]
|
|
||||||
package = false
|
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
donglora = { path = "../clients/python", editable = true }
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Receive LoRa packets and print them.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python examples/simple_rx.py [PORT]
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import serial
|
|
||||||
|
|
||||||
import donglora as dl
|
|
||||||
|
|
||||||
try:
|
|
||||||
ser = dl.connect(sys.argv[1] if len(sys.argv) > 1 else None)
|
|
||||||
print(dl.send(ser, "SetConfig", config=dl.DEFAULT_CONFIG))
|
|
||||||
print(dl.send(ser, "StartRx"))
|
|
||||||
|
|
||||||
print("\nListening (Ctrl+C to stop)...\n")
|
|
||||||
ser.timeout = 1
|
|
||||||
|
|
||||||
while True:
|
|
||||||
data = dl.read_frame(ser)
|
|
||||||
if data is None:
|
|
||||||
continue
|
|
||||||
resp = dl.decode_response(data)
|
|
||||||
if resp["type"] == "RxPacket":
|
|
||||||
p = resp["payload"]
|
|
||||||
print(f" RSSI:{resp['rssi']:4d}dBm SNR:{resp['snr']:3d}dB len:{len(p):3d} {p.hex()}")
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
try:
|
|
||||||
dl.send(ser, "StopRx")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
print("\nDone.")
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"\nSerial error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Transmit a single LoRa packet.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python examples/simple_tx.py [PORT] [MESSAGE]
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import serial
|
|
||||||
|
|
||||||
import donglora as dl
|
|
||||||
|
|
||||||
port = None
|
|
||||||
message = "Hello from DongLoRa!"
|
|
||||||
|
|
||||||
args = sys.argv[1:]
|
|
||||||
if args and not args[0].startswith("/dev"):
|
|
||||||
message = " ".join(args)
|
|
||||||
elif args:
|
|
||||||
port = args[0]
|
|
||||||
if len(args) > 1:
|
|
||||||
message = " ".join(args[1:])
|
|
||||||
|
|
||||||
try:
|
|
||||||
ser = dl.connect(port)
|
|
||||||
print(dl.send(ser, "SetConfig", config=dl.DEFAULT_CONFIG))
|
|
||||||
print(dl.send(ser, "Transmit", payload=message.encode()))
|
|
||||||
print(f"Sent: {message!r}")
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"\nSerial error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
Generated
-630
@@ -1,630 +0,0 @@
|
|||||||
version = 1
|
|
||||||
revision = 3
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "annotated-types"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anthropic"
|
|
||||||
version = "0.88.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "anyio" },
|
|
||||||
{ name = "distro" },
|
|
||||||
{ name = "docstring-parser" },
|
|
||||||
{ name = "httpx" },
|
|
||||||
{ name = "jiter" },
|
|
||||||
{ name = "pydantic" },
|
|
||||||
{ name = "sniffio" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/86/68/565f13059c0a6a6fd5f96f306f2a0fb478a0e1174ec18a4df16b5fac9379/anthropic-0.88.0.tar.gz", hash = "sha256:f4c7f6863d08c869913516f08d658fe53caaf8bcc4fbea3218df343d2a876c58", size = 596654, upload-time = "2026-04-01T19:59:05.287Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/ac/68f646998160c9f2e6f9353a31dd87292ef02b915b455aaf70a52a059a75/anthropic-0.88.0-py3-none-any.whl", hash = "sha256:71898b32332bc75d9739bc10095288d40a29605da6d00da2fe832b1aa036552f", size = 478338, upload-time = "2026-04-01T19:59:03.832Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anyio"
|
|
||||||
version = "4.13.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "idna" },
|
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "certifi"
|
|
||||||
version = "2026.2.25"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cffi"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cobs"
|
|
||||||
version = "1.2.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/34/ef/ea149311227a4fc3160cc885fce06da7c7d76782a308ef070b8065c69953/cobs-1.2.2.tar.gz", hash = "sha256:dbdd5e32111d72786f83d0c269215dcd6ac629b1ac1962c6878221f3b2ca98da", size = 14582, upload-time = "2025-07-20T01:08:35.434Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/e0/9574590639a624a9a84c89a569f36343523a306fff2ffddc9468a2b9b05d/cobs-1.2.2-cp312-cp312-win32.whl", hash = "sha256:3afe0cec5d91164f84dcf35b471949aa36285664d21ee984350e39d485707d11", size = 26400, upload-time = "2025-07-20T01:08:30.288Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/4f/5418d9decdc4cf27a814fc189bc4fa44d216d37b871d633654f37ed331f8/cobs-1.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:4da6048f6e16cbd6c8ec5e95e8520e226248ffb8d31e9d6fc5e0888b3b65c54b", size = 27776, upload-time = "2025-07-20T01:08:31.377Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/df/7f07559954d53d09680aa4f5c72db999e7643253e2ca4e431023fe5c1c40/cobs-1.2.2-cp313-cp313-win32.whl", hash = "sha256:a8fdf6ca70247e7271caea6f2f6f9c0343f5e872e2fd47e21f71831fa9ababa8", size = 26401, upload-time = "2025-07-20T01:08:32.508Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/00/5b063105ff567bb7fb72c3cd3e96116a3889381fa2c0e7747497391dbecc/cobs-1.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe3f7194d5fbbc02e8fb03d26ad626a33fa0d420b50d66ed60ac4bf516322d8e", size = 27774, upload-time = "2025-07-20T01:08:33.692Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/35/de1e889c26fcfc9ae6852b8f99e744e6690a3baae99f63e2b9fffdd21710/cobs-1.2.2-cp314-cp314-win32.whl", hash = "sha256:bcf08cb0b68c1824a87cfa89e0684d5a3eaee9b9e08b02ce4a54d5589536f01c", size = 26779, upload-time = "2026-03-09T02:18:48.711Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/64/390e226843cc83066acc60257e0e71ebdbce6dd915ec4f3d34101b9ac31b/cobs-1.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:1f27b7a553d809487287e4ed88540b2c30ed9c9bb596173bfcf549e705af7bf0", size = 28205, upload-time = "2026-03-09T02:18:20.193Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "distro"
|
|
||||||
version = "1.9.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "docstring-parser"
|
|
||||||
version = "0.17.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "donglora"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { editable = "../clients/python" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "cobs" },
|
|
||||||
{ name = "pyserial" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.optional-dependencies]
|
|
||||||
meshcore = [
|
|
||||||
{ name = "pycryptodome" },
|
|
||||||
{ name = "pynacl" },
|
|
||||||
]
|
|
||||||
orac = [
|
|
||||||
{ name = "anthropic" },
|
|
||||||
{ name = "pycryptodome" },
|
|
||||||
{ name = "pynacl" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "anthropic", marker = "extra == 'orac'" },
|
|
||||||
{ name = "cobs" },
|
|
||||||
{ name = "pycryptodome", marker = "extra == 'meshcore'" },
|
|
||||||
{ name = "pycryptodome", marker = "extra == 'orac'" },
|
|
||||||
{ name = "pynacl", marker = "extra == 'meshcore'" },
|
|
||||||
{ name = "pynacl", marker = "extra == 'orac'" },
|
|
||||||
{ name = "pyserial" },
|
|
||||||
]
|
|
||||||
provides-extras = ["meshcore", "orac"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "donglora-examples"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { virtual = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "donglora" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.optional-dependencies]
|
|
||||||
meshcore = [
|
|
||||||
{ name = "donglora", extra = ["meshcore"] },
|
|
||||||
]
|
|
||||||
orac = [
|
|
||||||
{ name = "donglora", extra = ["orac"] },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "donglora", editable = "../clients/python" },
|
|
||||||
{ name = "donglora", extras = ["meshcore"], marker = "extra == 'meshcore'", editable = "../clients/python" },
|
|
||||||
{ name = "donglora", extras = ["orac"], marker = "extra == 'orac'", editable = "../clients/python" },
|
|
||||||
]
|
|
||||||
provides-extras = ["meshcore", "orac"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "exceptiongroup"
|
|
||||||
version = "1.3.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "h11"
|
|
||||||
version = "0.16.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "httpcore"
|
|
||||||
version = "1.0.9"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "h11" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "httpx"
|
|
||||||
version = "0.28.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "anyio" },
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "httpcore" },
|
|
||||||
{ name = "idna" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idna"
|
|
||||||
version = "3.11"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiter"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[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]]
|
|
||||||
name = "pycryptodome"
|
|
||||||
version = "3.23.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pydantic"
|
|
||||||
version = "2.12.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "annotated-types" },
|
|
||||||
{ name = "pydantic-core" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
{ name = "typing-inspection" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pydantic-core"
|
|
||||||
version = "2.41.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pynacl"
|
|
||||||
version = "1.6.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyserial"
|
|
||||||
version = "3.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sniffio"
|
|
||||||
version = "1.3.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typing-extensions"
|
|
||||||
version = "4.15.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typing-inspection"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
|
||||||
]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[target.'cfg(target_arch = "xtensa")']
|
|
||||||
rustflags = [
|
|
||||||
"-C", "link-arg=-Wl,-Tlinkall.x",
|
|
||||||
"-C", "link-arg=-Wl,-Tdefmt.x",
|
|
||||||
"-C", "link-arg=-nostartfiles",
|
|
||||||
]
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/target
|
|
||||||
/vendor
|
|
||||||
/builds
|
|
||||||
/.hypothesis
|
|
||||||
src/board/mod.rs
|
|
||||||
Generated
-2003
File diff suppressed because it is too large.
Load diff
@@ -1,105 +0,0 @@
|
|||||||
[workspace]
|
|
||||||
members = ["."]
|
|
||||||
|
|
||||||
[package]
|
|
||||||
name = "donglora"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
build = "build.rs"
|
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
|
||||||
license = "MIT"
|
|
||||||
description = "Transparent LoRa radio over USB — plug in a board, talk LoRa from your host"
|
|
||||||
repository = "https://github.com/swaits/donglora"
|
|
||||||
keywords = ["lora", "radio", "embedded", "usb", "firmware"]
|
|
||||||
categories = ["embedded", "hardware-support", "no-std"]
|
|
||||||
|
|
||||||
# UPGRADE BLOCKED: embassy-executor 0.10 and ucobs 0.3 need Rust >= 1.93
|
|
||||||
# stable. The ESP Xtensa toolchain (espup) is a nightly fork that may not
|
|
||||||
# support the required language features or pass MSRV gating. Revisit when
|
|
||||||
# espup catches up. Check: rustup run esp rustc --version
|
|
||||||
[dependencies]
|
|
||||||
cfg-if = "1.0"
|
|
||||||
critical-section = "1.2"
|
|
||||||
defmt = "1.0"
|
|
||||||
embedded-hal-async = "1.0"
|
|
||||||
embassy-embedded-hal = "0.6"
|
|
||||||
embassy-executor = "0.9"
|
|
||||||
embassy-sync = "0.8"
|
|
||||||
embassy-time = "0.5"
|
|
||||||
embassy-usb = { version = "0.6", features = ["defmt"] }
|
|
||||||
embassy-usb-driver = "0.2"
|
|
||||||
ucobs = { version = "0.3", features = ["legacy-msrv"] }
|
|
||||||
heapless = "0.9"
|
|
||||||
lora-phy = "3.0"
|
|
||||||
static_cell = "2.1"
|
|
||||||
|
|
||||||
# Display rendering
|
|
||||||
display-interface = "0.5"
|
|
||||||
embedded-graphics = "0.8"
|
|
||||||
ssd1306 = { version = "0.10", features = ["async"] }
|
|
||||||
|
|
||||||
# Async IO traits and combinators
|
|
||||||
embedded-io-async = "0.7"
|
|
||||||
embassy-futures = "0.1"
|
|
||||||
|
|
||||||
# ESP32-S3 (Heltec V3) — requires esp toolchain (espup)
|
|
||||||
esp-backtrace = { version = "0.18", optional = true, features = [
|
|
||||||
"esp32s3",
|
|
||||||
"panic-handler",
|
|
||||||
"defmt",
|
|
||||||
] }
|
|
||||||
esp-hal = { version = "1.0", optional = true, features = [
|
|
||||||
"esp32s3",
|
|
||||||
"unstable",
|
|
||||||
] }
|
|
||||||
esp-println = { version = "0.16", optional = true, features = [
|
|
||||||
"esp32s3",
|
|
||||||
"defmt-espflash",
|
|
||||||
] }
|
|
||||||
esp-rtos = { version = "0.2", optional = true, features = [
|
|
||||||
"esp32s3",
|
|
||||||
"embassy",
|
|
||||||
] }
|
|
||||||
|
|
||||||
# nRF52840 (RAK 4631)
|
|
||||||
cortex-m-rt = { version = "0.7", optional = true }
|
|
||||||
embassy-nrf = { version = "0.10", optional = true, features = [
|
|
||||||
"nrf52840",
|
|
||||||
"gpiote",
|
|
||||||
"time-driver-rtc1",
|
|
||||||
"defmt",
|
|
||||||
] }
|
|
||||||
panic-probe = { version = "1.0", optional = true, features = ["print-defmt"] }
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
minijinja = "2.18"
|
|
||||||
owo-colors = "4.3"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
heltec_v3 = [
|
|
||||||
"dep:esp-backtrace",
|
|
||||||
"dep:esp-hal",
|
|
||||||
"dep:esp-println",
|
|
||||||
"dep:esp-rtos",
|
|
||||||
]
|
|
||||||
heltec_v4 = [
|
|
||||||
"dep:esp-backtrace",
|
|
||||||
"dep:esp-hal",
|
|
||||||
"dep:esp-println",
|
|
||||||
"dep:esp-rtos",
|
|
||||||
]
|
|
||||||
rak_wisblock_4631 = [
|
|
||||||
"dep:cortex-m-rt",
|
|
||||||
"dep:embassy-nrf",
|
|
||||||
"dep:panic-probe",
|
|
||||||
"embassy-executor/arch-cortex-m",
|
|
||||||
"embassy-executor/executor-thread",
|
|
||||||
]
|
|
||||||
|
|
||||||
[lints.clippy]
|
|
||||||
undocumented_unsafe_blocks = "deny"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
codegen-units = 1
|
|
||||||
lto = "fat"
|
|
||||||
opt-level = 3 # "s"/"z" break Xtensa (l32r literal range exceeded)
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
# DongLoRa USB Protocol
|
|
||||||
|
|
||||||
DongLoRa communicates over USB CDC-ACM (virtual serial port).
|
|
||||||
No baud rate configuration needed.
|
|
||||||
|
|
||||||
**USB identifiers:** VID `1209`, PID `5741`
|
|
||||||
|
|
||||||
## Framing
|
|
||||||
|
|
||||||
Each message is [COBS](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing)-encoded
|
|
||||||
and terminated with a `0x00` sentinel byte:
|
|
||||||
|
|
||||||
```
|
|
||||||
[COBS-encoded bytes] [0x00]
|
|
||||||
```
|
|
||||||
|
|
||||||
COBS guarantees `0x00` never appears in the encoded data, so the
|
|
||||||
sentinel unambiguously marks frame boundaries. USB packets are
|
|
||||||
chunked at 64 bytes.
|
|
||||||
|
|
||||||
## Encoding
|
|
||||||
|
|
||||||
All integers are **fixed-size little-endian**. No variable-length
|
|
||||||
encoding. Every field has a known size at a known offset.
|
|
||||||
|
|
||||||
| Type | Size | Encoding |
|
|
||||||
|------|------|----------|
|
|
||||||
| `u8` | 1 | Raw byte |
|
|
||||||
| `i8` | 1 | Raw byte (two's complement) |
|
|
||||||
| `u16` | 2 | Little-endian |
|
|
||||||
| `i16` | 2 | Little-endian (two's complement) |
|
|
||||||
| `u32` | 4 | Little-endian |
|
|
||||||
|
|
||||||
## RadioConfig (13 bytes)
|
|
||||||
|
|
||||||
| Offset | Size | Field | Type | Valid range |
|
|
||||||
|--------|------|-------|------|-------------|
|
|
||||||
| 0 | 4 | `freq_hz` | u32 LE | 150,000,000 – 960,000,000 |
|
|
||||||
| 4 | 1 | `bw` | u8 | 0–9 (see bandwidth table) |
|
|
||||||
| 5 | 1 | `sf` | u8 | 5–12 |
|
|
||||||
| 6 | 1 | `cr` | u8 | 5–8 (coding rate denominator) |
|
|
||||||
| 7 | 2 | `sync_word` | u16 LE | any |
|
|
||||||
| 9 | 1 | `tx_power_dbm` | i8 | board-dependent, or `-128` for max |
|
|
||||||
| 10 | 2 | `preamble_len` | u16 LE | 6–65535, or `0` for default (16) |
|
|
||||||
| 12 | 1 | `cad` | u8 | 0 = disabled, non-zero = enabled |
|
|
||||||
|
|
||||||
Set `tx_power_dbm` to `-128` (0x80) to use the board's maximum
|
|
||||||
TX power. The firmware resolves this to the actual hardware max.
|
|
||||||
|
|
||||||
Set `preamble_len` to `0` to use the firmware default of 16 symbols.
|
|
||||||
Longer preambles improve reliability in mesh/noisy environments at
|
|
||||||
the cost of slightly longer air time.
|
|
||||||
|
|
||||||
Set `cad` to `1` (or any non-zero value) to enable Channel Activity
|
|
||||||
Detection (listen-before-talk) before each transmission. The radio
|
|
||||||
checks for channel activity and waits for a clear channel before
|
|
||||||
transmitting. Set to `0` to disable. Default: enabled.
|
|
||||||
|
|
||||||
## Commands (Host → Firmware)
|
|
||||||
|
|
||||||
Each command is a tag byte followed by fixed-size fields.
|
|
||||||
|
|
||||||
| Tag | Command | Payload |
|
|
||||||
|-----|---------|---------|
|
|
||||||
| 0 | Ping | — |
|
|
||||||
| 1 | GetConfig | — |
|
|
||||||
| 2 | SetConfig | RadioConfig (10 bytes) |
|
|
||||||
| 3 | StartRx | — |
|
|
||||||
| 4 | StopRx | — |
|
|
||||||
| 5 | Transmit | has_config (1) + [RadioConfig if 1] + len (u16 LE) + payload |
|
|
||||||
| 6 | DisplayOn | — |
|
|
||||||
| 7 | DisplayOff | — |
|
|
||||||
| 8 | GetMac | — |
|
|
||||||
|
|
||||||
## Responses (Firmware → Host)
|
|
||||||
|
|
||||||
| Tag | Response | Payload |
|
|
||||||
|-----|----------|---------|
|
|
||||||
| 0 | Pong | — |
|
|
||||||
| 1 | Config | RadioConfig (10 bytes) |
|
|
||||||
| 2 | RxPacket | rssi (i16 LE) + snr (i16 LE) + len (u16 LE) + payload |
|
|
||||||
| 3 | TxDone | — |
|
|
||||||
| 4 | Ok | — |
|
|
||||||
| 5 | Error | code (u8) |
|
|
||||||
| 6 | MacAddress | 6 raw bytes (board's MAC/device address) |
|
|
||||||
|
|
||||||
## Bandwidth
|
|
||||||
|
|
||||||
| Value | Bandwidth |
|
|
||||||
|-------|-----------|
|
|
||||||
| 0 | 7.8 kHz |
|
|
||||||
| 1 | 10.4 kHz |
|
|
||||||
| 2 | 15.6 kHz |
|
|
||||||
| 3 | 20.8 kHz |
|
|
||||||
| 4 | 31.25 kHz |
|
|
||||||
| 5 | 41.7 kHz |
|
|
||||||
| 6 | 62.5 kHz |
|
|
||||||
| 7 | 125 kHz |
|
|
||||||
| 8 | 250 kHz |
|
|
||||||
| 9 | 500 kHz |
|
|
||||||
|
|
||||||
## Error Codes
|
|
||||||
|
|
||||||
| Value | Error | Meaning |
|
|
||||||
|-------|-------|---------|
|
|
||||||
| 0 | InvalidConfig | Radio config validation failed |
|
|
||||||
| 1 | RadioBusy | Radio busy (e.g. RX restart failed) |
|
|
||||||
| 2 | TxTimeout | Transmission timed out |
|
|
||||||
| 3 | CrcError | CRC error on received packet |
|
|
||||||
| 4 | NotConfigured | SetConfig required first |
|
|
||||||
| 5 | NoDisplay | No display attached |
|
|
||||||
|
|
||||||
## Worked Example
|
|
||||||
|
|
||||||
Configure for 915 MHz, 125 kHz BW, SF7, CR 4/5, max TX power, default preamble, CAD on:
|
|
||||||
|
|
||||||
### 1. Build the bytes
|
|
||||||
|
|
||||||
```python
|
|
||||||
import struct
|
|
||||||
config = struct.pack("<IBBBHBHB",
|
|
||||||
915_000_000, # freq_hz
|
|
||||||
7, # bw (125 kHz)
|
|
||||||
7, # sf
|
|
||||||
5, # cr (4/5)
|
|
||||||
0x1424, # sync_word
|
|
||||||
0x80, # tx_power_dbm (-128 = max)
|
|
||||||
0, # preamble_len (0 = default 16)
|
|
||||||
1, # cad (1 = enabled)
|
|
||||||
)
|
|
||||||
command = b"\x02" + config # tag 2 = SetConfig
|
|
||||||
```
|
|
||||||
|
|
||||||
Raw bytes: `02 C0 CA 89 36 07 07 05 24 14 80 00 00 01`
|
|
||||||
|
|
||||||
### 2. COBS-encode and send
|
|
||||||
|
|
||||||
COBS-encode the bytes, append `0x00`, write to USB serial port.
|
|
||||||
Read back the response, COBS-decode it. First byte `0x04` = Ok.
|
|
||||||
|
|
||||||
## Command/Response Discipline
|
|
||||||
|
|
||||||
### Solicited vs. unsolicited responses
|
|
||||||
|
|
||||||
Every response is either **solicited** (exactly one per command) or
|
|
||||||
**unsolicited** (arrives at any time):
|
|
||||||
|
|
||||||
| Response | Category |
|
|
||||||
|----------|----------|
|
|
||||||
| `RxPacket` | **Unsolicited** — arrives whenever a LoRa packet is received during an active RX session |
|
|
||||||
| All others | **Solicited** — exactly one per command, in the order commands were sent |
|
|
||||||
|
|
||||||
### Command → response mapping
|
|
||||||
|
|
||||||
Each command produces exactly one solicited response:
|
|
||||||
|
|
||||||
| Command | Success | Possible errors |
|
|
||||||
|---------|---------|-----------------|
|
|
||||||
| Ping | Pong | — |
|
|
||||||
| GetConfig | Config | Error(NotConfigured) |
|
|
||||||
| SetConfig | Ok | Error(InvalidConfig) |
|
|
||||||
| StartRx | Ok | Error(NotConfigured), Error(InvalidConfig) |
|
|
||||||
| StopRx | Ok | — |
|
|
||||||
| Transmit | TxDone | Error(NotConfigured), Error(InvalidConfig), Error(TxTimeout) |
|
|
||||||
| DisplayOn | Ok | Error(NoDisplay) |
|
|
||||||
| DisplayOff | Ok | Error(NoDisplay) |
|
|
||||||
| GetMac | MacAddress | — |
|
|
||||||
|
|
||||||
### One outstanding command
|
|
||||||
|
|
||||||
The host **must** wait for the solicited response to the current
|
|
||||||
command before sending the next command. Pipelining multiple commands
|
|
||||||
without waiting for responses produces **undefined response ordering**.
|
|
||||||
|
|
||||||
### Processing order
|
|
||||||
|
|
||||||
Commands are processed in FIFO order. The firmware never reorders
|
|
||||||
commands or drops them silently.
|
|
||||||
|
|
||||||
### Interleaving with RxPacket
|
|
||||||
|
|
||||||
Between sending a command and receiving its solicited response, the
|
|
||||||
host may receive zero or more unsolicited `RxPacket` frames.
|
|
||||||
Hosts **must skip** `RxPacket` frames (tag 2) when waiting for a
|
|
||||||
solicited response. Skipped `RxPacket` frames should be buffered,
|
|
||||||
not discarded, if the application needs received LoRa data.
|
|
||||||
|
|
||||||
### Locally-handled commands
|
|
||||||
|
|
||||||
`DisplayOn`, `DisplayOff`, and `GetMac` are handled immediately by
|
|
||||||
the USB task and respond without involving the radio. All other
|
|
||||||
commands are routed to the radio task and respond after radio
|
|
||||||
processing. The one-outstanding-command rule ensures these two
|
|
||||||
response paths never conflict.
|
|
||||||
|
|
||||||
## Received Packet Quality
|
|
||||||
|
|
||||||
The SX126x radio will demodulate and deliver packets that are
|
|
||||||
technically intact (LoRa CRC passed) but were received so close to the
|
|
||||||
noise floor that the payload may be corrupt. This is inherent to LoRa:
|
|
||||||
the radio's internal CRC is only 16 bits, and at very low SNR the false-
|
|
||||||
positive rate is non-trivial. The firmware delivers every packet the
|
|
||||||
radio accepts — it is the host's responsibility to assess quality.
|
|
||||||
|
|
||||||
### SNR demodulation floor
|
|
||||||
|
|
||||||
Each spreading factor has a theoretical minimum SNR for reliable
|
|
||||||
demodulation. Packets received below this floor are statistically
|
|
||||||
likely to contain errors even though the radio reported a valid CRC:
|
|
||||||
|
|
||||||
| SF | Min SNR (dB) |
|
|
||||||
|----|--------------|
|
|
||||||
| 5 | -2.5 |
|
|
||||||
| 6 | -5.0 |
|
|
||||||
| 7 | -7.5 |
|
|
||||||
| 8 | -10.0 |
|
|
||||||
| 9 | -12.5 |
|
|
||||||
| 10 | -15.0 |
|
|
||||||
| 11 | -17.5 |
|
|
||||||
| 12 | -20.0 |
|
|
||||||
|
|
||||||
The pattern is: **min_snr = -2.5 × (SF - 4)**.
|
|
||||||
|
|
||||||
### SNR validity range
|
|
||||||
|
|
||||||
The SX126x reports SNR as a signed value in the range **-32 to +32 dB**.
|
|
||||||
Values outside this range indicate a firmware or decoding error — treat
|
|
||||||
the packet as unreliable.
|
|
||||||
|
|
||||||
### Recommended quality grading
|
|
||||||
|
|
||||||
Hosts should classify each `RxPacket` before trusting the payload:
|
|
||||||
|
|
||||||
```
|
|
||||||
function grade_packet(snr, sf):
|
|
||||||
# Step 1: reject impossible SNR values
|
|
||||||
if snr < -32 or snr > 32:
|
|
||||||
return INVALID # bad metadata, discard or flag
|
|
||||||
|
|
||||||
# Step 2: check against demodulation floor
|
|
||||||
min_snr = -2.5 * (sf - 4)
|
|
||||||
margin = snr - min_snr # dB above the floor
|
|
||||||
|
|
||||||
if margin < 0:
|
|
||||||
return UNRELIABLE # below floor, likely corrupt
|
|
||||||
if margin < 3:
|
|
||||||
return MARGINAL # near floor, may contain errors
|
|
||||||
return GOOD # comfortable margin
|
|
||||||
```
|
|
||||||
|
|
||||||
**What to do with each grade:**
|
|
||||||
|
|
||||||
- **GOOD** — process normally.
|
|
||||||
- **MARGINAL** — process, but prefer application-layer integrity
|
|
||||||
checks (checksums, sequence numbers, known-format validation)
|
|
||||||
before acting on the payload. Log for diagnostics.
|
|
||||||
- **UNRELIABLE** — log for diagnostics but do not trust the payload
|
|
||||||
for routing decisions, command execution, or retransmission.
|
|
||||||
Applications may still display the data with a warning.
|
|
||||||
- **INVALID** — discard. The SNR metadata itself is corrupt.
|
|
||||||
|
|
||||||
### Application-layer defenses
|
|
||||||
|
|
||||||
For safety-critical or command-and-control applications, do not rely
|
|
||||||
solely on the radio CRC. Add your own integrity checks:
|
|
||||||
|
|
||||||
- An application-layer CRC or hash over the payload
|
|
||||||
- Sequence numbers to detect duplicates and missing packets
|
|
||||||
- Known-format validation (magic bytes, expected lengths)
|
|
||||||
- Require acknowledgement before acting on commands
|
|
||||||
|
|
||||||
### Radio config impact
|
|
||||||
|
|
||||||
Wider bandwidth and lower spreading factor increase throughput but
|
|
||||||
reduce sensitivity, making low-SNR packets more common in marginal
|
|
||||||
links. If your application sees frequent MARGINAL/UNRELIABLE packets,
|
|
||||||
consider increasing SF or decreasing BW to improve link margin at the
|
|
||||||
cost of air time.
|
|
||||||
|
|
||||||
## Host Implementation Checklist
|
|
||||||
|
|
||||||
1. Open USB serial port (find by VID:PID `1209:5741`)
|
|
||||||
2. Implement COBS encode/decode (libraries exist for every language)
|
|
||||||
3. Pack commands with `struct.pack` or equivalent — all fields are fixed-size LE
|
|
||||||
4. Optionally send `GetMac`, read back `MacAddress` (useful with multiple dongles)
|
|
||||||
5. Send `SetConfig`, read back `Ok`
|
|
||||||
6. Send `StartRx`, read back `Ok`
|
|
||||||
7. Loop: read frames, decode `RxPacket` (rssi, snr, payload at known offsets)
|
|
||||||
8. To transmit: send `Transmit`, read back `TxDone`
|
|
||||||
|
|
||||||
See `examples/` for working Python implementations.
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# DongLoRa Firmware
|
|
||||||
|
|
||||||
Embedded Rust firmware for DongLoRa boards. Exposes the LoRa radio over
|
|
||||||
USB CDC-ACM using a COBS-framed binary protocol.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
```sh
|
|
||||||
just setup # one-time toolchain install
|
|
||||||
just build heltec_v4 # build for a specific board
|
|
||||||
just flash heltec_v4 # build + flash
|
|
||||||
just check-all # compile-check all boards
|
|
||||||
just clippy heltec_v4 # lint
|
|
||||||
just test # host-side protocol unit tests
|
|
||||||
```
|
|
||||||
|
|
||||||
All commands are run from the `firmware/` directory.
|
|
||||||
|
|
||||||
## Supported Boards
|
|
||||||
|
|
||||||
| Board | MCU | Radio | Target |
|
|
||||||
|-------|-----|-------|--------|
|
|
||||||
| Heltec V3 | ESP32-S3 | SX1262 | xtensa-esp32s3-none-elf |
|
|
||||||
| Heltec V4 | ESP32-S3 | SX1262 | xtensa-esp32s3-none-elf |
|
|
||||||
| RAK WisBlock 4631 | nRF52840 | SX1262 | thumbv7em-none-eabihf |
|
|
||||||
|
|
||||||
## Adding a Board
|
|
||||||
|
|
||||||
See [src/board/PORTING.md](src/board/PORTING.md).
|
|
||||||
|
|
||||||
## Protocol
|
|
||||||
|
|
||||||
See [PROTOCOL.md](PROTOCOL.md) for the complete wire format specification.
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
use minijinja::{context, Environment};
|
|
||||||
use owo_colors::OwoColorize;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// Protocol tests run on the host without a board feature. Cargo doesn't
|
|
||||||
// expose `cfg(test)` to build scripts, so we rely on an env var set by
|
|
||||||
// the justfile `test` recipe. See: `just test`
|
|
||||||
if std::env::var_os("DONGLORA_HOST_TEST").is_some() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let board_dir = "src/board";
|
|
||||||
let boards: Vec<String> = fs::read_dir(board_dir)
|
|
||||||
.expect("failed to read src/board directory")
|
|
||||||
.filter_map(|e| {
|
|
||||||
let p = e.ok()?.path();
|
|
||||||
let stem = p.file_stem()?.to_str()?;
|
|
||||||
// Exclude non-board files: mod.rs, traits.rs, and platform helpers (esp32s3.rs).
|
|
||||||
if p.is_file()
|
|
||||||
&& p.extension()? == "rs"
|
|
||||||
&& !matches!(stem, "mod" | "traits" | "esp32s3")
|
|
||||||
{
|
|
||||||
return Some(p.file_stem()?.to_str()?.to_string());
|
|
||||||
}
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let enabled: Vec<_> = std::env::vars()
|
|
||||||
.filter(|(k, _)| k.starts_with("CARGO_FEATURE_"))
|
|
||||||
.map(|(k, _)| k.replace("CARGO_FEATURE_", "").to_lowercase())
|
|
||||||
.filter(|f| boards.contains(f))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
match enabled.len() {
|
|
||||||
0 => panic!(
|
|
||||||
"\n\n{}\n",
|
|
||||||
format!("No board selected! Found: {:?}", boards)
|
|
||||||
.red()
|
|
||||||
.bold()
|
|
||||||
),
|
|
||||||
1 => println!("cargo:rustc-cfg=board_selected"),
|
|
||||||
_ => panic!(
|
|
||||||
"\n\n{}\n",
|
|
||||||
format!("Conflict: Multiple boards enabled {:?}", enabled)
|
|
||||||
.yellow()
|
|
||||||
.bold()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut env = Environment::new();
|
|
||||||
let template =
|
|
||||||
fs::read_to_string("src/board/mod.rs.j2").expect("missing src/board/mod.rs.j2");
|
|
||||||
env.add_template("mod", &template)
|
|
||||||
.expect("failed to parse board template");
|
|
||||||
let rendered = env
|
|
||||||
.get_template("mod")
|
|
||||||
.expect("template not found")
|
|
||||||
.render(context!(boards => boards))
|
|
||||||
.expect("failed to render board module");
|
|
||||||
fs::write("src/board/mod.rs", rendered).expect("failed to write src/board/mod.rs");
|
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=src/board/mod.rs.j2");
|
|
||||||
println!("cargo:rerun-if-changed={}", board_dir);
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
set shell := ["bash", "-c"]
|
|
||||||
|
|
||||||
# Board definitions: feature target chip
|
|
||||||
# Xtensa targets use nightly cargo + esp rustc + -Zbuild-std=core.
|
|
||||||
# Tool versions are pinned in mise.toml; run `just setup` to install everything.
|
|
||||||
heltec_v3 := "heltec_v3 xtensa-esp32s3-none-elf esp32s3"
|
|
||||||
heltec_v4 := "heltec_v4 xtensa-esp32s3-none-elf esp32s3"
|
|
||||||
rak_wisblock_4631 := "rak_wisblock_4631 thumbv7em-none-eabihf nRF52840_xxAA"
|
|
||||||
|
|
||||||
builds_dir := "builds"
|
|
||||||
|
|
||||||
# All known boards
|
|
||||||
boards := "heltec_v3 heltec_v4 rak_wisblock_4631"
|
|
||||||
|
|
||||||
# Install all required tools and toolchains
|
|
||||||
setup:
|
|
||||||
mise trust --yes
|
|
||||||
mise install
|
|
||||||
@# ESP Xtensa toolchain (not managed by mise)
|
|
||||||
@if ! rustup toolchain list | grep -q "^esp"; then \
|
|
||||||
echo "ESP toolchain not found, installing via espup (this may take a while)..."; \
|
|
||||||
espup install || { echo "error: espup install failed" >&2; exit 1; }; \
|
|
||||||
fi
|
|
||||||
@# nightly rust-src for -Zbuild-std
|
|
||||||
@if ! rustup component list --toolchain nightly --installed 2>/dev/null | grep -q "^rust-src"; then \
|
|
||||||
rustup component add rust-src --toolchain nightly; \
|
|
||||||
fi
|
|
||||||
@# ARM target
|
|
||||||
@rustup target list --installed | grep -q "^thumbv7em-none-eabihf$" || rustup target add thumbv7em-none-eabihf
|
|
||||||
|
|
||||||
# Build release firmware for all boards, installing toolchains as needed
|
|
||||||
build-all:
|
|
||||||
@trap 'exit 130' INT; \
|
|
||||||
for board in {{boards}}; do \
|
|
||||||
if just _can_build $board; then \
|
|
||||||
echo "── building $board ──"; \
|
|
||||||
just build $board || exit $?; \
|
|
||||||
else \
|
|
||||||
echo "── skipping $board (toolchain install failed) ──"; \
|
|
||||||
fi; \
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check all boards compile, installing toolchains as needed
|
|
||||||
check-all:
|
|
||||||
@trap 'exit 130' INT; \
|
|
||||||
for board in {{boards}}; do \
|
|
||||||
if just _can_build $board; then \
|
|
||||||
echo "── checking $board ──"; \
|
|
||||||
just check $board || exit $?; \
|
|
||||||
else \
|
|
||||||
echo "── skipping $board (toolchain install failed) ──"; \
|
|
||||||
fi; \
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check a single board compiles
|
|
||||||
check board:
|
|
||||||
@just _cargo {{board}} check
|
|
||||||
|
|
||||||
# Run clippy on a single board
|
|
||||||
clippy board:
|
|
||||||
@just _cargo {{board}} "clippy" "-- -D warnings"
|
|
||||||
|
|
||||||
# Build release firmware and copy to builds/ with a readable name
|
|
||||||
build board profile="release":
|
|
||||||
@just _cargo {{board}} "build --{{profile}}"
|
|
||||||
@just _copy_firmware {{board}} {{profile}}
|
|
||||||
|
|
||||||
# Build and flash a board (espflash for Xtensa, probe-rs for ARM)
|
|
||||||
flash board:
|
|
||||||
@just _ensure_tools
|
|
||||||
@just build {{board}} release
|
|
||||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
|
||||||
case "$target" in \
|
|
||||||
xtensa-*) \
|
|
||||||
port=$(just _find_port "303a:1001" "1209:5741"); \
|
|
||||||
if [ -n "$port" ]; then \
|
|
||||||
echo "Using $port"; \
|
|
||||||
espflash flash -p "$port" "{{builds_dir}}/donglora-{{board}}-release.elf"; \
|
|
||||||
else \
|
|
||||||
echo "No port found, falling back to espflash auto-detection..." >&2; \
|
|
||||||
espflash flash "{{builds_dir}}/donglora-{{board}}-release.elf"; \
|
|
||||||
fi ;; \
|
|
||||||
*) probe-rs run --chip $chip "{{builds_dir}}/donglora-{{board}}-release.elf" ;; \
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Show binary size for a release build
|
|
||||||
size board:
|
|
||||||
@just _cargo {{board}} "size --release"
|
|
||||||
|
|
||||||
# Run host-side protocol unit tests
|
|
||||||
test:
|
|
||||||
DONGLORA_HOST_TEST=1 cargo test
|
|
||||||
|
|
||||||
# Check for outdated dependencies
|
|
||||||
outdated:
|
|
||||||
@# embassy-executor 0.10 renamed arch-* → platform-* features.
|
|
||||||
@# esp-rtos 0.2 still pins embassy-executor ^0.9, blocking the upgrade.
|
|
||||||
@# cargo-outdated crashes trying to resolve 0.10 with the old feature name.
|
|
||||||
cargo outdated --exclude embassy-executor
|
|
||||||
|
|
||||||
# ── Private helpers ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Install mise-managed tools if any are missing (fast no-op when current)
|
|
||||||
[private]
|
|
||||||
_ensure_tools:
|
|
||||||
@mise trust --yes . 2>/dev/null; mise install --quiet
|
|
||||||
|
|
||||||
# Run a cargo command for a board.
|
|
||||||
# Xtensa: nightly cargo + esp rustc (via RUSTC override) + -Zbuild-std=core
|
|
||||||
[private]
|
|
||||||
_cargo board cmd extra_args="":
|
|
||||||
@just _ensure_tools
|
|
||||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
|
||||||
env=""; extra=""; \
|
|
||||||
case "$target" in xtensa-*) \
|
|
||||||
just _require_esp_toolchain; \
|
|
||||||
[ -f "$HOME/export-esp.sh" ] && . "$HOME/export-esp.sh"; \
|
|
||||||
env="RUSTC=$(rustup which rustc --toolchain esp)"; extra="+nightly"; buildstd="-Zbuild-std=core,alloc";; \
|
|
||||||
esac; \
|
|
||||||
eval $env cargo $extra {{cmd}} --target "$target" --features "$feat" $buildstd {{extra_args}}
|
|
||||||
|
|
||||||
# Ensure a board's toolchain is available, auto-installing if needed
|
|
||||||
[private]
|
|
||||||
_can_build board:
|
|
||||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
|
||||||
case "$target" in \
|
|
||||||
xtensa-*) just _require_esp_toolchain ;; \
|
|
||||||
*) rustup target list --installed | grep -q "^$target$" || rustup target add "$target" ;; \
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Ensure the ESP Xtensa toolchain is installed (espup installed via mise)
|
|
||||||
[private]
|
|
||||||
_require_esp_toolchain:
|
|
||||||
@just _ensure_tools
|
|
||||||
@if ! rustup toolchain list | grep -q "^esp"; then \
|
|
||||||
echo "ESP toolchain not found, installing via espup (this may take a while)..." >&2; \
|
|
||||||
espup install || { echo "error: espup install failed" >&2; exit 1; }; \
|
|
||||||
fi; \
|
|
||||||
if ! rustup component list --toolchain nightly --installed 2>/dev/null | grep -q "^rust-src"; then \
|
|
||||||
echo "Installing nightly rust-src (needed for -Zbuild-std)..." >&2; \
|
|
||||||
rustup component add rust-src --toolchain nightly || { echo "error: failed to install rust-src" >&2; exit 1; }; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Find a serial port matching any of the given VID:PID pairs (checked in order)
|
|
||||||
[private]
|
|
||||||
_find_port +vid_pids:
|
|
||||||
@for vidpid in {{vid_pids}}; do \
|
|
||||||
vid="${vidpid%%:*}"; pid="${vidpid##*:}"; \
|
|
||||||
for dev in /dev/ttyACM* /dev/ttyUSB*; do \
|
|
||||||
[ -e "$dev" ] || continue; \
|
|
||||||
info=$(udevadm info --query=property --name="$dev" 2>/dev/null) || continue; \
|
|
||||||
dev_vid=$(echo "$info" | sed -n 's/^ID_VENDOR_ID=//p' | tr '[:upper:]' '[:lower:]'); \
|
|
||||||
dev_pid=$(echo "$info" | sed -n 's/^ID_MODEL_ID=//p' | tr '[:upper:]' '[:lower:]'); \
|
|
||||||
if [ "$dev_vid" = "$vid" ] && [ "$dev_pid" = "$pid" ]; then \
|
|
||||||
echo "$dev"; exit 0; \
|
|
||||||
fi; \
|
|
||||||
done; \
|
|
||||||
done
|
|
||||||
|
|
||||||
[private]
|
|
||||||
_copy_firmware board profile:
|
|
||||||
@mkdir -p {{builds_dir}}
|
|
||||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
|
||||||
src="target/$target/{{profile}}/donglora"; \
|
|
||||||
dst="{{builds_dir}}/donglora-{{board}}-{{profile}}"; \
|
|
||||||
if [ -f "$src" ]; then cp "$src" "$dst.elf"; echo "→ $dst.elf"; fi
|
|
||||||
|
|
||||||
[private]
|
|
||||||
_info name:
|
|
||||||
@if [ "{{name}}" == "heltec_v3" ]; then echo "{{heltec_v3}}"; \
|
|
||||||
elif [ "{{name}}" == "heltec_v4" ]; then echo "{{heltec_v4}}"; \
|
|
||||||
elif [ "{{name}}" == "rak_wisblock_4631" ]; then echo "{{rak_wisblock_4631}}"; \
|
|
||||||
else echo "Unknown board: {{name}}" >&2; exit 1; fi
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[tools]
|
|
||||||
rust = "1.93"
|
|
||||||
just = "latest"
|
|
||||||
|
|
||||||
"cargo:cargo-binutils" = "latest" # `cargo size`, `cargo objdump`, etc.
|
|
||||||
"cargo:espup" = "0.16"
|
|
||||||
"cargo:espflash" = "3" # 4.x has broken nix dependency on current stable Rust
|
|
||||||
"cargo:probe-rs-tools" = "0.31"
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
# Porting DongLoRa to a New Board
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
### 1. Create the board file
|
|
||||||
|
|
||||||
Create `src/board/your_board.rs`. Use an existing board as a template:
|
|
||||||
- ESP32-S3 boards: copy `heltec_v4.rs` (uses shared helpers from `esp32s3.rs`)
|
|
||||||
- nRF52840 boards: copy `rak_wisblock_4631.rs`
|
|
||||||
|
|
||||||
ESP32-S3 boards share common type aliases, peripheral bundles, and init helpers
|
|
||||||
via `src/board/esp32s3.rs`. Your board file only needs the trait impl and
|
|
||||||
board-specific init (Vext GPIO, display reset). If you add a new ESP32-S3 board,
|
|
||||||
add its feature to the `#[cfg(any(...))]` gate on `mod esp32s3` in
|
|
||||||
`src/board/mod.rs.j2`.
|
|
||||||
|
|
||||||
### 2. Define concrete types
|
|
||||||
|
|
||||||
Your board must export these type aliases:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub type RadioDriver = ...; // Must work with lora_phy::LoRa<RadioDriver, Delay>
|
|
||||||
pub type UsbDriver = ...; // Must implement embassy_usb_driver::Driver<'static>
|
|
||||||
pub type DisplayI2c = ...; // Must implement embedded_hal_async::i2c::I2c
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Define peripheral bundles
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct RadioParts {
|
|
||||||
pub driver: RadioDriver,
|
|
||||||
pub delay: embassy_time::Delay,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UsbParts {
|
|
||||||
pub driver: UsbDriver,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DisplayParts {
|
|
||||||
pub i2c: DisplayI2c,
|
|
||||||
pub mac: [u8; 6],
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Implement the LoRaBoard trait
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use super::traits::LoRaBoard;
|
|
||||||
|
|
||||||
impl LoRaBoard for Board {
|
|
||||||
const NAME: &'static str = "Your Board Name";
|
|
||||||
const TX_POWER_RANGE: (i8, i8) = (-9, 22); // check your radio + PA
|
|
||||||
fn init() -> Self { ... }
|
|
||||||
fn mac_address() -> [u8; 6] { ... } // read from efuse, FICR, etc.
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The compiler will refuse to build if any of these are missing.
|
|
||||||
|
|
||||||
### 5. Implement `Board::into_parts()`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl Board {
|
|
||||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
|
||||||
// Initialize SPI, DMA, radio, USB, I2C
|
|
||||||
// Return peripheral bundles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Add Cargo feature
|
|
||||||
|
|
||||||
In `firmware/Cargo.toml`, add a feature with your board's HAL dependencies:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[features]
|
|
||||||
your_board = ["dep:your-hal", ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Add to justfile
|
|
||||||
|
|
||||||
Add your board's feature/target/chip to the `boards` list at the top of `justfile`.
|
|
||||||
|
|
||||||
### 8. Build and test
|
|
||||||
|
|
||||||
```sh
|
|
||||||
just check your_board # Must compile
|
|
||||||
just clippy your_board # Must be clean
|
|
||||||
just build your_board # Produces firmware
|
|
||||||
```
|
|
||||||
|
|
||||||
`build.rs` auto-discovers your `.rs` file — no template editing needed.
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
//! Shared types and initialization helpers for ESP32-S3 boards.
|
|
||||||
//!
|
|
||||||
//! All ESP32-S3 boards in this project share the same SPI/DMA, USB OTG,
|
|
||||||
//! and I2C peripheral wiring. This module provides the common type aliases,
|
|
||||||
//! peripheral bundle structs, and init functions. Each board file re-exports
|
|
||||||
//! these types and calls the helpers from its own `Board::into_parts()`.
|
|
||||||
|
|
||||||
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
|
||||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
|
||||||
use embassy_time::Delay;
|
|
||||||
use esp_hal::dma::{DmaRxBuf, DmaTxBuf};
|
|
||||||
use esp_hal::gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull};
|
|
||||||
use esp_hal::i2c::master::{Config as I2cConfig, I2c};
|
|
||||||
use esp_hal::spi::master::{Config as SpiConfig, Spi, SpiDmaBus};
|
|
||||||
use esp_hal::spi::Mode as SpiMode;
|
|
||||||
use esp_hal::time::Rate;
|
|
||||||
use esp_hal::timer::timg::TimerGroup;
|
|
||||||
use static_cell::StaticCell;
|
|
||||||
|
|
||||||
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
|
||||||
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
|
||||||
|
|
||||||
// ── Concrete peripheral types ────────────────────────────────────────
|
|
||||||
|
|
||||||
type SpiBus = SpiDmaBus<'static, esp_hal::Async>;
|
|
||||||
type Nss = Output<'static>;
|
|
||||||
type Iv = GenericSx126xInterfaceVariant<Output<'static>, Input<'static>>;
|
|
||||||
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>;
|
|
||||||
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
|
||||||
|
|
||||||
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
|
||||||
|
|
||||||
pub type DisplayI2c = I2c<'static, esp_hal::Async>;
|
|
||||||
|
|
||||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub struct RadioParts {
|
|
||||||
pub driver: RadioDriver,
|
|
||||||
pub delay: Delay,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UsbParts {
|
|
||||||
pub driver: UsbDriver,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DisplayParts {
|
|
||||||
pub i2c: DisplayI2c,
|
|
||||||
pub mac: [u8; 6],
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Shared initialization helpers ───────────────────────────────────
|
|
||||||
|
|
||||||
/// Start the ESP-RTOS scheduler and Embassy time driver.
|
|
||||||
pub fn start_timer(timg0: esp_hal::peripherals::TIMG0<'static>) {
|
|
||||||
let timg0 = TimerGroup::new(timg0);
|
|
||||||
esp_rtos::start(timg0.timer0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize SPI + DMA + SX1262 radio.
|
|
||||||
///
|
|
||||||
/// All current ESP32-S3 boards use the same SPI/radio pin mapping:
|
|
||||||
/// SCK=GPIO9, MOSI=GPIO10, MISO=GPIO11, NSS=GPIO8,
|
|
||||||
/// Reset=GPIO12, DIO1=GPIO14, BUSY=GPIO13.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn init_radio(
|
|
||||||
spi2: esp_hal::peripherals::SPI2<'static>,
|
|
||||||
dma_ch0: esp_hal::peripherals::DMA_CH0<'static>,
|
|
||||||
sck: esp_hal::peripherals::GPIO9<'static>,
|
|
||||||
mosi: esp_hal::peripherals::GPIO10<'static>,
|
|
||||||
miso: esp_hal::peripherals::GPIO11<'static>,
|
|
||||||
nss_pin: esp_hal::peripherals::GPIO8<'static>,
|
|
||||||
reset_pin: esp_hal::peripherals::GPIO12<'static>,
|
|
||||||
dio1_pin: esp_hal::peripherals::GPIO14<'static>,
|
|
||||||
busy_pin: esp_hal::peripherals::GPIO13<'static>,
|
|
||||||
) -> RadioParts {
|
|
||||||
let spi = Spi::new(
|
|
||||||
spi2,
|
|
||||||
SpiConfig::default()
|
|
||||||
.with_frequency(Rate::from_mhz(1))
|
|
||||||
.with_mode(SpiMode::_0),
|
|
||||||
)
|
|
||||||
.expect("SPI init")
|
|
||||||
.with_sck(sck)
|
|
||||||
.with_mosi(mosi)
|
|
||||||
.with_miso(miso)
|
|
||||||
.with_dma(dma_ch0);
|
|
||||||
|
|
||||||
let (rx_buf, rx_desc, tx_buf, tx_desc) = esp_hal::dma_buffers!(256);
|
|
||||||
let dma_rx = DmaRxBuf::new(rx_desc, rx_buf).expect("DMA RX buf");
|
|
||||||
let dma_tx = DmaTxBuf::new(tx_desc, tx_buf).expect("DMA TX buf");
|
|
||||||
|
|
||||||
let spi = spi.with_buffers(dma_rx, dma_tx).into_async();
|
|
||||||
|
|
||||||
static SPI_BUS: StaticCell<embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus>> =
|
|
||||||
StaticCell::new();
|
|
||||||
let spi_bus = SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi));
|
|
||||||
|
|
||||||
let nss = Output::new(nss_pin, Level::High, OutputConfig::default());
|
|
||||||
let spi_device = SpiDevice::new(spi_bus, nss);
|
|
||||||
|
|
||||||
let reset = Output::new(reset_pin, Level::High, OutputConfig::default());
|
|
||||||
let dio1 = Input::new(dio1_pin, InputConfig::default().with_pull(Pull::Down));
|
|
||||||
let busy = Input::new(busy_pin, InputConfig::default().with_pull(Pull::Down));
|
|
||||||
|
|
||||||
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None)
|
|
||||||
.expect("SX1262 interface init");
|
|
||||||
|
|
||||||
let sx_config = sx126x::Config {
|
|
||||||
chip: Sx1262,
|
|
||||||
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V7),
|
|
||||||
use_dcdc: true,
|
|
||||||
rx_boost: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
RadioParts {
|
|
||||||
driver: Sx126x::new(spi_device, iv, sx_config),
|
|
||||||
delay: Delay,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize USB OTG CDC-ACM driver.
|
|
||||||
pub fn init_usb(
|
|
||||||
usb0: esp_hal::peripherals::USB0<'static>,
|
|
||||||
dp: esp_hal::peripherals::GPIO20<'static>,
|
|
||||||
dm: esp_hal::peripherals::GPIO19<'static>,
|
|
||||||
) -> UsbParts {
|
|
||||||
let usb_inst = esp_hal::otg_fs::Usb::new(usb0, dp, dm);
|
|
||||||
static EP_OUT_BUF: StaticCell<[u8; 1024]> = StaticCell::new();
|
|
||||||
let ep_out_buf = EP_OUT_BUF.init([0u8; 1024]);
|
|
||||||
UsbParts {
|
|
||||||
driver: esp_hal::otg_fs::asynch::Driver::new(
|
|
||||||
usb_inst,
|
|
||||||
ep_out_buf,
|
|
||||||
esp_hal::otg_fs::asynch::Config::default(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize I2C bus for display.
|
|
||||||
pub fn init_display_i2c(
|
|
||||||
i2c0: esp_hal::peripherals::I2C0<'static>,
|
|
||||||
sda: esp_hal::peripherals::GPIO17<'static>,
|
|
||||||
scl: esp_hal::peripherals::GPIO18<'static>,
|
|
||||||
) -> DisplayI2c {
|
|
||||||
I2c::new(i2c0, I2cConfig::default())
|
|
||||||
.expect("I2C init")
|
|
||||||
.with_sda(sda)
|
|
||||||
.with_scl(scl)
|
|
||||||
.into_async()
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use esp_hal::gpio::{Level, Output, OutputConfig};
|
|
||||||
|
|
||||||
use super::esp32s3;
|
|
||||||
use super::traits::LoRaBoard;
|
|
||||||
|
|
||||||
#[allow(unused_imports)] // Re-exported for other modules (display, radio, usb tasks)
|
|
||||||
pub use super::esp32s3::{DisplayI2c, DisplayParts, RadioDriver, RadioParts, UsbDriver, UsbParts};
|
|
||||||
|
|
||||||
// ── Board init ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub struct Board {
|
|
||||||
p: esp_hal::peripherals::Peripherals,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoRaBoard for Board {
|
|
||||||
const NAME: &'static str = "Heltec V3";
|
|
||||||
const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
|
||||||
|
|
||||||
fn init() -> Self {
|
|
||||||
let p = esp_hal::init(esp_hal::Config::default());
|
|
||||||
Self { p }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mac_address() -> [u8; 6] {
|
|
||||||
esp_hal::efuse::Efuse::mac_address()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Board {
|
|
||||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
|
||||||
let p = self.p;
|
|
||||||
|
|
||||||
esp32s3::start_timer(p.TIMG0);
|
|
||||||
|
|
||||||
// Vext power: GPIO21, active LOW to enable OLED
|
|
||||||
let vext = Output::new(p.GPIO21, Level::Low, OutputConfig::default());
|
|
||||||
core::mem::forget(vext); // hold pin low permanently; drop would reset it
|
|
||||||
|
|
||||||
let radio = esp32s3::init_radio(
|
|
||||||
p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11, p.GPIO8, p.GPIO12, p.GPIO14,
|
|
||||||
p.GPIO13,
|
|
||||||
);
|
|
||||||
let usb = esp32s3::init_usb(p.USB0, p.GPIO20, p.GPIO19);
|
|
||||||
let i2c = esp32s3::init_display_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
|
||||||
let mac = Self::mac_address();
|
|
||||||
let display = Some(DisplayParts { i2c, mac });
|
|
||||||
|
|
||||||
(radio, usb, display)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
use esp_hal::gpio::{Level, Output, OutputConfig};
|
|
||||||
|
|
||||||
use super::esp32s3;
|
|
||||||
use super::traits::LoRaBoard;
|
|
||||||
|
|
||||||
#[allow(unused_imports)] // Re-exported for other modules (display, radio, usb tasks)
|
|
||||||
pub use super::esp32s3::{DisplayI2c, DisplayParts, RadioDriver, RadioParts, UsbDriver, UsbParts};
|
|
||||||
|
|
||||||
// ── Board implementation ────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub struct Board {
|
|
||||||
p: esp_hal::peripherals::Peripherals,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoRaBoard for Board {
|
|
||||||
const NAME: &'static str = "Heltec V4";
|
|
||||||
const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
|
||||||
|
|
||||||
fn init() -> Self {
|
|
||||||
let p = esp_hal::init(esp_hal::Config::default());
|
|
||||||
Self { p }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mac_address() -> [u8; 6] {
|
|
||||||
esp_hal::efuse::Efuse::mac_address()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Board {
|
|
||||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
|
||||||
let p = self.p;
|
|
||||||
|
|
||||||
esp32s3::start_timer(p.TIMG0);
|
|
||||||
|
|
||||||
// Vext power: GPIO36, active LOW to enable peripherals
|
|
||||||
let vext = Output::new(p.GPIO36, Level::Low, OutputConfig::default());
|
|
||||||
core::mem::forget(vext); // hold pin low permanently; drop would reset it
|
|
||||||
|
|
||||||
let radio = esp32s3::init_radio(
|
|
||||||
p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11, p.GPIO8, p.GPIO12, p.GPIO14,
|
|
||||||
p.GPIO13,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note: switches internal USB PHY from Serial-JTAG to OTG.
|
|
||||||
// espflash --monitor will stop working after this point.
|
|
||||||
let usb = esp32s3::init_usb(p.USB0, p.GPIO20, p.GPIO19);
|
|
||||||
|
|
||||||
// SSD1315 display reset: pulse GPIO21 low->high before I2C init
|
|
||||||
let mut display_rst = Output::new(p.GPIO21, Level::Low, OutputConfig::default());
|
|
||||||
esp_hal::delay::Delay::new().delay_millis(10);
|
|
||||||
display_rst.set_high();
|
|
||||||
esp_hal::delay::Delay::new().delay_millis(10);
|
|
||||||
core::mem::forget(display_rst); // hold reset high permanently
|
|
||||||
|
|
||||||
let i2c = esp32s3::init_display_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
|
||||||
let mac = Self::mac_address();
|
|
||||||
let display = Some(DisplayParts { i2c, mac });
|
|
||||||
|
|
||||||
(radio, usb, display)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// @generated by build.rs — DO NOT EDIT
|
|
||||||
pub mod traits;
|
|
||||||
pub use traits::*;
|
|
||||||
|
|
||||||
#[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))]
|
|
||||||
mod esp32s3;
|
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
|
||||||
{%- for board in boards %}
|
|
||||||
{% if loop.first %}if{% else %}else if{% endif %} #[cfg(feature = "{{ board }}")] {
|
|
||||||
mod {{ board }};
|
|
||||||
pub use {{ board }}::*;
|
|
||||||
}
|
|
||||||
{%- endfor %}
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
|
||||||
use embassy_nrf::gpio::{Input, Level, Output, OutputDrive, Pull};
|
|
||||||
use embassy_nrf::spim::{self, Spim};
|
|
||||||
use embassy_nrf::twim::{self, Twim};
|
|
||||||
use embassy_nrf::usb::Driver;
|
|
||||||
use embassy_nrf::{bind_interrupts, Peripherals};
|
|
||||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
|
||||||
use embassy_time::Delay;
|
|
||||||
use static_cell::StaticCell;
|
|
||||||
|
|
||||||
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
|
||||||
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
|
||||||
|
|
||||||
use super::traits::LoRaBoard;
|
|
||||||
|
|
||||||
bind_interrupts!(struct Irqs {
|
|
||||||
USBD => embassy_nrf::usb::InterruptHandler<embassy_nrf::peripherals::USBD>;
|
|
||||||
SPIM3 => embassy_nrf::spim::InterruptHandler<embassy_nrf::peripherals::SPI3>;
|
|
||||||
TWISPI0 => embassy_nrf::twim::InterruptHandler<embassy_nrf::peripherals::TWISPI0>;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Concrete peripheral types ────────────────────────────────────────
|
|
||||||
|
|
||||||
type SpiBus = Spim<'static>;
|
|
||||||
type Nss = Output<'static>;
|
|
||||||
type Iv = GenericSx126xInterfaceVariant<Output<'static>, Input<'static>>;
|
|
||||||
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>;
|
|
||||||
/// SX126x driver (pre-LoRa construction — LoRa::new is async).
|
|
||||||
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
|
||||||
|
|
||||||
/// USB driver for CDC-ACM.
|
|
||||||
pub type UsbDriver = Driver<'static, &'static embassy_nrf::usb::vbus_detect::SoftwareVbusDetect>;
|
|
||||||
|
|
||||||
/// I2C bus for an optional SSD1306 OLED.
|
|
||||||
pub type DisplayI2c = Twim<'static>;
|
|
||||||
|
|
||||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub struct RadioParts {
|
|
||||||
pub driver: RadioDriver,
|
|
||||||
pub delay: Delay,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UsbParts {
|
|
||||||
pub driver: UsbDriver,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DisplayParts {
|
|
||||||
pub i2c: DisplayI2c,
|
|
||||||
pub mac: [u8; 6],
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Board init ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub struct Board {
|
|
||||||
p: Peripherals,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl super::traits::LoRaBoard for Board {
|
|
||||||
const NAME: &'static str = "RAK WisBlock 4631";
|
|
||||||
const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
|
||||||
|
|
||||||
fn init() -> Self {
|
|
||||||
let p = embassy_nrf::init(Default::default());
|
|
||||||
Self { p }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mac_address() -> [u8; 6] {
|
|
||||||
// SAFETY: 0x10000000 is the nRF52840 FICR (Factory Information Configuration
|
|
||||||
// Registers) base address. Offsets 0x0A4 and 0x0A8 are the DEVICEADDR[0] and
|
|
||||||
// DEVICEADDR[1] registers containing the factory-programmed device address.
|
|
||||||
// read_volatile is correct for memory-mapped hardware registers.
|
|
||||||
unsafe {
|
|
||||||
let ficr = 0x10000000 as *const u32;
|
|
||||||
let addr0 = core::ptr::read_volatile(ficr.byte_add(0x0A4));
|
|
||||||
let addr1 = core::ptr::read_volatile(ficr.byte_add(0x0A8));
|
|
||||||
[
|
|
||||||
addr0 as u8,
|
|
||||||
(addr0 >> 8) as u8,
|
|
||||||
(addr0 >> 16) as u8,
|
|
||||||
(addr0 >> 24) as u8,
|
|
||||||
addr1 as u8,
|
|
||||||
(addr1 >> 8) as u8,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Board {
|
|
||||||
|
|
||||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
|
||||||
let p = self.p;
|
|
||||||
|
|
||||||
// ── SPI bus for SX1262 ───────────────────────────────────
|
|
||||||
let mut spi_cfg = spim::Config::default();
|
|
||||||
spi_cfg.frequency = spim::Frequency::M1;
|
|
||||||
let spi = Spim::new(p.SPI3, Irqs, p.P1_11, p.P1_13, p.P1_12, spi_cfg);
|
|
||||||
|
|
||||||
static SPI_BUS: StaticCell<embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus>> =
|
|
||||||
StaticCell::new();
|
|
||||||
let spi_bus = SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi));
|
|
||||||
|
|
||||||
let nss = Output::new(p.P1_10, Level::High, OutputDrive::Standard);
|
|
||||||
let spi_device = SpiDevice::new(spi_bus, nss);
|
|
||||||
|
|
||||||
// ── SX1262 control pins ──────────────────────────────────
|
|
||||||
let reset = Output::new(p.P1_06, Level::High, OutputDrive::Standard);
|
|
||||||
let dio1 = Input::new(p.P1_15, Pull::Down);
|
|
||||||
let busy = Input::new(p.P1_14, Pull::Down);
|
|
||||||
|
|
||||||
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None).expect("SX1262 interface init");
|
|
||||||
|
|
||||||
let sx_config = sx126x::Config {
|
|
||||||
chip: Sx1262,
|
|
||||||
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V7),
|
|
||||||
use_dcdc: true,
|
|
||||||
rx_boost: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let radio = RadioParts {
|
|
||||||
driver: Sx126x::new(spi_device, iv, sx_config),
|
|
||||||
delay: Delay,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── USB ──────────────────────────────────────────────────
|
|
||||||
static VBUS: StaticCell<embassy_nrf::usb::vbus_detect::SoftwareVbusDetect> =
|
|
||||||
StaticCell::new();
|
|
||||||
let vbus = VBUS.init(embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new(true, false));
|
|
||||||
let usb = UsbParts {
|
|
||||||
driver: Driver::new(p.USBD, Irqs, vbus),
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Display (optional RAK1921 SSD1306 OLED on I2C) ──────
|
|
||||||
// Always provide the I2C bus; display_task detects presence
|
|
||||||
// via SSD1306 init (fails gracefully if no display attached).
|
|
||||||
static TWIM_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
|
||||||
let twim_buf = TWIM_BUF.init([0u8; 256]);
|
|
||||||
let i2c_cfg = twim::Config::default();
|
|
||||||
let i2c = Twim::new(p.TWISPI0, Irqs, p.P0_13, p.P0_14, i2c_cfg, twim_buf);
|
|
||||||
let mac = Self::mac_address();
|
|
||||||
let display = Some(DisplayParts { i2c, mac });
|
|
||||||
|
|
||||||
(radio, usb, display)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
//! Board abstraction trait.
|
|
||||||
//!
|
|
||||||
//! Every board must implement [`LoRaBoard`]. The compiler enforces completeness —
|
|
||||||
//! missing constants or methods are compile errors, not runtime surprises.
|
|
||||||
//!
|
|
||||||
//! Board modules also export concrete types (`RadioParts`, `UsbParts`,
|
|
||||||
//! `DisplayParts`, `RadioDriver`, `UsbDriver`, `DisplayI2c`) that the tasks
|
|
||||||
//! use directly. The trait verifies metadata and initialization; the types
|
|
||||||
//! verify hardware compatibility.
|
|
||||||
//!
|
|
||||||
//! See `src/board/PORTING.md` for a step-by-step guide to adding a new board.
|
|
||||||
|
|
||||||
/// Compile-time contract for a DongLoRa board.
|
|
||||||
///
|
|
||||||
/// Every board's `Board` struct must implement this trait. It verifies that
|
|
||||||
/// the board provides all required metadata and initialization methods.
|
|
||||||
/// The tasks use the board's concrete types directly (not the trait's
|
|
||||||
/// associated types) because Embassy tasks must be concrete.
|
|
||||||
pub trait LoRaBoard {
|
|
||||||
/// Human-readable board name (shown on display splash screen).
|
|
||||||
const NAME: &'static str;
|
|
||||||
|
|
||||||
/// TX power range in dBm (min, max) for this board's radio + PA.
|
|
||||||
const TX_POWER_RANGE: (i8, i8);
|
|
||||||
|
|
||||||
/// Initialize the board hardware.
|
|
||||||
fn init() -> Self;
|
|
||||||
|
|
||||||
/// Read the board's unique hardware address (MAC, device ID, etc.).
|
|
||||||
fn mac_address() -> [u8; 6];
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
//! Inter-task communication channels.
|
|
||||||
//!
|
|
||||||
//! Each channel connects exactly two tasks:
|
|
||||||
//! - [`CommandChannel`]: USB → Radio (host commands)
|
|
||||||
//! - [`ResponseChannel`]: Radio → USB (firmware responses)
|
|
||||||
//! - [`DisplayCommandChannel`]: USB → Display (on/off/reset)
|
|
||||||
//! - [`StatusWatch`]: Radio → Display (observable radio state)
|
|
||||||
|
|
||||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
|
||||||
use embassy_sync::channel::Channel;
|
|
||||||
use embassy_sync::watch::Watch;
|
|
||||||
|
|
||||||
use crate::protocol::{Command, RadioConfig, Response};
|
|
||||||
|
|
||||||
/// Display commands routed from usb_task to display_task.
|
|
||||||
#[derive(Debug, Clone, Copy, defmt::Format)]
|
|
||||||
pub enum DisplayCommand {
|
|
||||||
On,
|
|
||||||
Off,
|
|
||||||
Reset,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// USB-to-display command channel (depth 4: display commands are rare events).
|
|
||||||
pub type DisplayCommandChannel = Channel<CriticalSectionRawMutex, DisplayCommand, 4>;
|
|
||||||
|
|
||||||
/// Host-to-radio command channel (depth 16: buffer bursty host command sequences).
|
|
||||||
pub type CommandChannel = Channel<CriticalSectionRawMutex, Command, 16>;
|
|
||||||
|
|
||||||
/// Radio-to-host response channel (depth 32: buffer RX packets while USB writes).
|
|
||||||
pub type ResponseChannel = Channel<CriticalSectionRawMutex, Response, 32>;
|
|
||||||
|
|
||||||
/// Observable radio status for the display task (2 receivers max).
|
|
||||||
/// Display always sees the latest state — intermediate updates may be
|
|
||||||
/// skipped, which is fine for a status display.
|
|
||||||
pub type StatusWatch = Watch<CriticalSectionRawMutex, RadioStatus, 2>;
|
|
||||||
|
|
||||||
/// Current radio state exposed to observers (e.g. display).
|
|
||||||
#[derive(Debug, Clone, defmt::Format)]
|
|
||||||
pub struct RadioStatus {
|
|
||||||
pub state: RadioState,
|
|
||||||
pub config: Option<RadioConfig>,
|
|
||||||
pub rx_count: u32,
|
|
||||||
pub tx_count: u32,
|
|
||||||
pub last_rssi: Option<i16>,
|
|
||||||
pub last_snr: Option<i16>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Radio state machine states.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
|
|
||||||
pub enum RadioState {
|
|
||||||
Idle,
|
|
||||||
Receiving,
|
|
||||||
Transmitting,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RadioStatus {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
state: RadioState::Idle,
|
|
||||||
config: None,
|
|
||||||
rx_count: 0,
|
|
||||||
tx_count: 0,
|
|
||||||
last_rssi: None,
|
|
||||||
last_snr: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
//! OLED display driver with radio status dashboard.
|
|
||||||
|
|
||||||
mod render;
|
|
||||||
mod task;
|
|
||||||
pub use task::display_task;
|
|
||||||
@@ -1,398 +0,0 @@
|
|||||||
use core::fmt::Write;
|
|
||||||
|
|
||||||
use embedded_graphics::mono_font::ascii::{FONT_6X10, FONT_9X15_BOLD};
|
|
||||||
use embedded_graphics::mono_font::MonoTextStyle;
|
|
||||||
use embedded_graphics::pixelcolor::BinaryColor;
|
|
||||||
use embedded_graphics::prelude::*;
|
|
||||||
use embedded_graphics::primitives::{Line, PrimitiveStyle, Rectangle};
|
|
||||||
use embedded_graphics::text::renderer::CharacterStyle;
|
|
||||||
use embedded_graphics::text::{Alignment, Text};
|
|
||||||
use heapless::String;
|
|
||||||
|
|
||||||
use crate::channel::{RadioState, RadioStatus};
|
|
||||||
use crate::protocol::Bandwidth;
|
|
||||||
|
|
||||||
/// RSSI ring buffer length — supports displays up to 256px wide (2px per bar).
|
|
||||||
pub const RSSI_HISTORY_LEN: usize = 128;
|
|
||||||
|
|
||||||
// Font metrics (FONT_6X10)
|
|
||||||
const CHAR_W: i32 = 6;
|
|
||||||
const FONT_H: i32 = 10;
|
|
||||||
|
|
||||||
// Mode box: 2 chars + 1px padding each side
|
|
||||||
const MODE_BOX_W: i32 = 2 * CHAR_W + 2; // 14px
|
|
||||||
|
|
||||||
// RSSI mapping range (dBm)
|
|
||||||
const RSSI_MIN: i16 = -120;
|
|
||||||
const RSSI_MAX: i16 = 0;
|
|
||||||
|
|
||||||
/// Static board identity info for display rendering.
|
|
||||||
pub struct BoardInfo<'a> {
|
|
||||||
pub name: &'a str,
|
|
||||||
pub version: &'a str,
|
|
||||||
pub mac: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the active dashboard (shown when radio is in RX or TX mode).
|
|
||||||
pub fn dashboard(
|
|
||||||
target: &mut impl DrawTarget<Color = BinaryColor>,
|
|
||||||
status: &RadioStatus,
|
|
||||||
rssi_history: &[i16; RSSI_HISTORY_LEN],
|
|
||||||
tx_history: &[bool; RSSI_HISTORY_LEN],
|
|
||||||
rssi_count: usize,
|
|
||||||
current_slot_rssi: i16,
|
|
||||||
current_slot_tx: bool,
|
|
||||||
board: &BoardInfo,
|
|
||||||
) {
|
|
||||||
let bb = target.bounding_box();
|
|
||||||
let w = bb.size.width as i32;
|
|
||||||
let h = bb.size.height as i32;
|
|
||||||
let title_x = MODE_BOX_W + 1;
|
|
||||||
let title_w = w - title_x;
|
|
||||||
let header_h = 2 * FONT_H;
|
|
||||||
let sep1_y = header_h + 3; // 3px gap below header text (clears descenders)
|
|
||||||
let info_y = sep1_y + 1; // 1px line, info text tight below separator
|
|
||||||
let sep2_y = info_y + 2 * FONT_H + 3; // 3px gap below info text (clears descenders)
|
|
||||||
let spark_top = sep2_y + 2; // 1px line + 1px gap above graph
|
|
||||||
let spark_h = h - spark_top;
|
|
||||||
let visible_bars = ((w / 2) as usize).min(RSSI_HISTORY_LEN);
|
|
||||||
|
|
||||||
let _ = target.clear(BinaryColor::Off);
|
|
||||||
let style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
|
|
||||||
let mut buf: String<32> = String::new();
|
|
||||||
|
|
||||||
// ── Mode box (left column, 2 rows tall) ─────────────────────────
|
|
||||||
let fill = PrimitiveStyle::with_fill(BinaryColor::On);
|
|
||||||
let mut inv_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::Off);
|
|
||||||
inv_style.set_background_color(Some(BinaryColor::On));
|
|
||||||
|
|
||||||
match status.state {
|
|
||||||
RadioState::Receiving => {
|
|
||||||
// "RX" inverted on row 0
|
|
||||||
Rectangle::new(Point::new(0, 0), Size::new(MODE_BOX_W as u32, FONT_H as u32))
|
|
||||||
.into_styled(fill)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
Text::new("RX", Point::new(1, FONT_H - 1), inv_style)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
RadioState::Transmitting => {
|
|
||||||
// "TX" inverted on row 1
|
|
||||||
Rectangle::new(
|
|
||||||
Point::new(0, FONT_H),
|
|
||||||
Size::new(MODE_BOX_W as u32, FONT_H as u32),
|
|
||||||
)
|
|
||||||
.into_styled(fill)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
Text::new("TX", Point::new(1, 2 * FONT_H - 1), inv_style)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
RadioState::Idle => {} // Not shown on active screen
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertical separator between mode box and title box
|
|
||||||
Line::new(Point::new(MODE_BOX_W, 0), Point::new(MODE_BOX_W, sep1_y))
|
|
||||||
.into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// ── Title box (right of mode box) ───────────────────────────────
|
|
||||||
let title_center_x = title_x + title_w / 2;
|
|
||||||
|
|
||||||
// Row 0: "DongLoRa v{version}"
|
|
||||||
buf.clear();
|
|
||||||
let _ = write!(buf, "DongLoRa v{}", board.version);
|
|
||||||
Text::with_alignment(
|
|
||||||
&buf,
|
|
||||||
Point::new(title_center_x, FONT_H - 1),
|
|
||||||
style,
|
|
||||||
Alignment::Center,
|
|
||||||
)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Row 1: radio settings "{freq}/{bw}/{sf}/{cr}"
|
|
||||||
if let Some(cfg) = status.config {
|
|
||||||
buf.clear();
|
|
||||||
let freq_mhz = cfg.freq_hz / 1_000_000;
|
|
||||||
let freq_khz = (cfg.freq_hz % 1_000_000) / 1_000;
|
|
||||||
let bw_str = match cfg.bw {
|
|
||||||
Bandwidth::Khz7 => "7.8",
|
|
||||||
Bandwidth::Khz10 => "10.4",
|
|
||||||
Bandwidth::Khz15 => "15.6",
|
|
||||||
Bandwidth::Khz20 => "20.8",
|
|
||||||
Bandwidth::Khz31 => "31.2",
|
|
||||||
Bandwidth::Khz41 => "41.7",
|
|
||||||
Bandwidth::Khz62 => "62.5",
|
|
||||||
Bandwidth::Khz125 => "125",
|
|
||||||
Bandwidth::Khz250 => "250",
|
|
||||||
Bandwidth::Khz500 => "500",
|
|
||||||
};
|
|
||||||
let _ = write!(
|
|
||||||
buf,
|
|
||||||
"{}.{:03}/{}/{}/{}",
|
|
||||||
freq_mhz, freq_khz, bw_str, cfg.sf, cfg.cr
|
|
||||||
);
|
|
||||||
Text::with_alignment(
|
|
||||||
&buf,
|
|
||||||
Point::new(title_center_x, 2 * FONT_H - 1),
|
|
||||||
style,
|
|
||||||
Alignment::Center,
|
|
||||||
)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── First separator line ────────────────────────────────────────
|
|
||||||
Line::new(Point::new(0, sep1_y), Point::new(w - 1, sep1_y))
|
|
||||||
.into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// ── Info rows (centered) ────────────────────────────────────────
|
|
||||||
let center_x = w / 2;
|
|
||||||
|
|
||||||
// Row 2: packet counters
|
|
||||||
buf.clear();
|
|
||||||
let _ = write!(
|
|
||||||
buf,
|
|
||||||
"RX:{} TX:{}",
|
|
||||||
compact_count(status.rx_count),
|
|
||||||
compact_count(status.tx_count)
|
|
||||||
);
|
|
||||||
Text::with_alignment(
|
|
||||||
&buf,
|
|
||||||
Point::new(center_x, info_y + FONT_H - 1),
|
|
||||||
style,
|
|
||||||
Alignment::Center,
|
|
||||||
)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Row 3: RSSI + SNR
|
|
||||||
buf.clear();
|
|
||||||
match (status.last_rssi, status.last_snr) {
|
|
||||||
(Some(rssi), Some(snr)) => {
|
|
||||||
let _ = write!(buf, "RSSI:{}dBm SNR:{}dB", rssi, snr);
|
|
||||||
}
|
|
||||||
(Some(rssi), None) => {
|
|
||||||
let _ = write!(buf, "RSSI:{}dBm", rssi);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let _ = write!(buf, "No signal");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text::with_alignment(
|
|
||||||
&buf,
|
|
||||||
Point::new(center_x, info_y + 2 * FONT_H - 1),
|
|
||||||
style,
|
|
||||||
Alignment::Center,
|
|
||||||
)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// ── Second separator line ───────────────────────────────────────
|
|
||||||
Line::new(Point::new(0, sep2_y), Point::new(w - 1, sep2_y))
|
|
||||||
.into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// ── Sparkline bar graph ─────────────────────────────────────────
|
|
||||||
if spark_h > 0 {
|
|
||||||
rssi_sparkline(
|
|
||||||
target,
|
|
||||||
rssi_history,
|
|
||||||
tx_history,
|
|
||||||
rssi_count,
|
|
||||||
current_slot_rssi,
|
|
||||||
current_slot_tx,
|
|
||||||
spark_top,
|
|
||||||
spark_h,
|
|
||||||
visible_bars,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the splash/waiting screen (shown when idle or no config).
|
|
||||||
pub fn splash(
|
|
||||||
target: &mut impl DrawTarget<Color = BinaryColor>,
|
|
||||||
board: &BoardInfo,
|
|
||||||
) {
|
|
||||||
let bb = target.bounding_box();
|
|
||||||
let w = bb.size.width as i32;
|
|
||||||
|
|
||||||
let _ = target.clear(BinaryColor::Off);
|
|
||||||
let style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
|
|
||||||
let title_style = MonoTextStyle::new(&FONT_9X15_BOLD, BinaryColor::On);
|
|
||||||
let mut buf: String<32> = String::new();
|
|
||||||
|
|
||||||
let center_x = w / 2;
|
|
||||||
|
|
||||||
// Row 1: "DongLoRa" bold left, version small right
|
|
||||||
Text::new("DongLoRa", Point::new(4, 15), title_style)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
buf.clear();
|
|
||||||
let _ = write!(buf, "v{}", board.version);
|
|
||||||
Text::with_alignment(&buf, Point::new(w - 2, 15), style, Alignment::Right)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Row 2: board name
|
|
||||||
Text::with_alignment(
|
|
||||||
board.name,
|
|
||||||
Point::new(center_x, 28),
|
|
||||||
style,
|
|
||||||
Alignment::Center,
|
|
||||||
)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Row 3: MAC address
|
|
||||||
Text::with_alignment(
|
|
||||||
board.mac,
|
|
||||||
Point::new(center_x, 41),
|
|
||||||
style,
|
|
||||||
Alignment::Center,
|
|
||||||
)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Row 4: status
|
|
||||||
Text::with_alignment(
|
|
||||||
"Waiting for host...",
|
|
||||||
Point::new(center_x, 54),
|
|
||||||
style,
|
|
||||||
Alignment::Center,
|
|
||||||
)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format a u32 count compactly for display.
|
|
||||||
fn compact_count(n: u32) -> String<10> {
|
|
||||||
let mut s: String<10> = String::new();
|
|
||||||
if n > 9_999_999 {
|
|
||||||
let _ = write!(s, "{}M", n / 1_000_000);
|
|
||||||
} else if n > 999_999 {
|
|
||||||
let _ = write!(s, "{}k", n / 1_000);
|
|
||||||
} else {
|
|
||||||
let _ = write!(s, "{}", n);
|
|
||||||
}
|
|
||||||
s
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the RSSI history as a bar-chart sparkline.
|
|
||||||
///
|
|
||||||
/// The current (uncommitted) slot is rendered at the rightmost position
|
|
||||||
/// so that incoming packets appear on the graph immediately.
|
|
||||||
fn rssi_sparkline(
|
|
||||||
target: &mut impl DrawTarget<Color = BinaryColor>,
|
|
||||||
history: &[i16; RSSI_HISTORY_LEN],
|
|
||||||
tx_history: &[bool; RSSI_HISTORY_LEN],
|
|
||||||
count: usize,
|
|
||||||
current_rssi: i16,
|
|
||||||
current_tx: bool,
|
|
||||||
spark_top: i32,
|
|
||||||
spark_h: i32,
|
|
||||||
visible_bars: usize,
|
|
||||||
) {
|
|
||||||
let live = current_rssi > RSSI_MIN || current_tx;
|
|
||||||
let committed = count.min(RSSI_HISTORY_LEN);
|
|
||||||
let hist_slots = if live {
|
|
||||||
committed.min(visible_bars.saturating_sub(1))
|
|
||||||
} else {
|
|
||||||
committed.min(visible_bars)
|
|
||||||
};
|
|
||||||
let total = hist_slots + if live { 1 } else { 0 };
|
|
||||||
|
|
||||||
if total == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fill = PrimitiveStyle::with_fill(BinaryColor::On);
|
|
||||||
|
|
||||||
// Draw committed history
|
|
||||||
for i in 0..hist_slots {
|
|
||||||
let idx = if count <= RSSI_HISTORY_LEN {
|
|
||||||
// Buffer hasn't wrapped: slots occupy indices 0..committed-1.
|
|
||||||
// Skip oldest entries that don't fit on screen.
|
|
||||||
let effective_bars = if live { visible_bars - 1 } else { visible_bars };
|
|
||||||
i + committed.saturating_sub(effective_bars)
|
|
||||||
} else {
|
|
||||||
let start = count - RSSI_HISTORY_LEN;
|
|
||||||
let skip = RSSI_HISTORY_LEN.saturating_sub(if live {
|
|
||||||
visible_bars - 1
|
|
||||||
} else {
|
|
||||||
visible_bars
|
|
||||||
});
|
|
||||||
(start + skip + i) % RSSI_HISTORY_LEN
|
|
||||||
};
|
|
||||||
let is_tx = tx_history[idx];
|
|
||||||
let rssi = history[idx];
|
|
||||||
|
|
||||||
if let Some(bar_h) = bar_height(rssi, is_tx, spark_h) {
|
|
||||||
let x = (visible_bars - total + i) as i32 * 2;
|
|
||||||
draw_bar(target, x, bar_h, is_tx, spark_top, spark_h, &fill);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw live (current) bar at the rightmost position
|
|
||||||
if live {
|
|
||||||
if let Some(bar_h) = bar_height(current_rssi, current_tx, spark_h) {
|
|
||||||
let x = (visible_bars - 1) as i32 * 2;
|
|
||||||
draw_bar(target, x, bar_h, current_tx, spark_top, spark_h, &fill);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute the pixel height for a sparkline bar, or None to skip.
|
|
||||||
fn bar_height(rssi: i16, is_tx: bool, spark_h: i32) -> Option<i32> {
|
|
||||||
let h = if rssi <= RSSI_MIN {
|
|
||||||
if is_tx {
|
|
||||||
spark_h / 3
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let clamped = rssi.clamp(RSSI_MIN, RSSI_MAX);
|
|
||||||
((clamped - RSSI_MIN) as i32 * spark_h) / (RSSI_MAX - RSSI_MIN) as i32
|
|
||||||
};
|
|
||||||
if h == 0 { None } else { Some(h) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw a single sparkline bar (solid for RX, dotted for TX).
|
|
||||||
fn draw_bar(
|
|
||||||
target: &mut impl DrawTarget<Color = BinaryColor>,
|
|
||||||
x: i32,
|
|
||||||
bar_h: i32,
|
|
||||||
is_tx: bool,
|
|
||||||
spark_top: i32,
|
|
||||||
spark_h: i32,
|
|
||||||
fill: &PrimitiveStyle<BinaryColor>,
|
|
||||||
) {
|
|
||||||
let y = spark_top + spark_h - bar_h;
|
|
||||||
if is_tx {
|
|
||||||
for row in 0..bar_h {
|
|
||||||
if row % 2 == 0 {
|
|
||||||
Rectangle::new(Point::new(x, y + row), Size::new(2, 1))
|
|
||||||
.into_styled(*fill)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Rectangle::new(Point::new(x, y), Size::new(2, bar_h as u32))
|
|
||||||
.into_styled(*fill)
|
|
||||||
.draw(target)
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the display (display-off state).
|
|
||||||
pub fn blank(target: &mut impl DrawTarget<Color = BinaryColor>) {
|
|
||||||
let _ = target.clear(BinaryColor::Off);
|
|
||||||
}
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
use embassy_executor::task;
|
|
||||||
use embassy_futures::select::{select3, Either3};
|
|
||||||
use embassy_time::Timer;
|
|
||||||
use ssd1306::mode::DisplayConfigAsync;
|
|
||||||
use ssd1306::prelude::DisplayRotation;
|
|
||||||
use ssd1306::size::DisplaySize128x64;
|
|
||||||
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
|
||||||
|
|
||||||
use crate::board::DisplayParts;
|
|
||||||
use crate::channel::{
|
|
||||||
DisplayCommand, DisplayCommandChannel, RadioState, RadioStatus, StatusWatch,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::board::{Board, LoRaBoard};
|
|
||||||
|
|
||||||
use super::render::{self, RSSI_HISTORY_LEN};
|
|
||||||
|
|
||||||
const BOARD_NAME: &str = Board::NAME;
|
|
||||||
|
|
||||||
/// Duration per sparkline slot. 128 slots * 1s = ~2 minutes of history.
|
|
||||||
const SPARK_SLOT_MS: u64 = 1000;
|
|
||||||
|
|
||||||
/// Sentinel: no packet received in this slot. Below SX1262 sensitivity
|
|
||||||
/// floor (-120 dBm), so it cannot be confused with a real RSSI value.
|
|
||||||
const NO_SIGNAL: i16 = -121;
|
|
||||||
|
|
||||||
struct DisplayState {
|
|
||||||
rssi_history: [i16; RSSI_HISTORY_LEN],
|
|
||||||
tx_history: [bool; RSSI_HISTORY_LEN],
|
|
||||||
rssi_count: usize,
|
|
||||||
current_slot_rssi: i16,
|
|
||||||
current_slot_tx: bool,
|
|
||||||
display_on: bool,
|
|
||||||
disconnected: bool,
|
|
||||||
last_status: RadioStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayState {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
rssi_history: [NO_SIGNAL; RSSI_HISTORY_LEN],
|
|
||||||
tx_history: [false; RSSI_HISTORY_LEN],
|
|
||||||
rssi_count: 0,
|
|
||||||
current_slot_rssi: NO_SIGNAL,
|
|
||||||
current_slot_tx: false,
|
|
||||||
display_on: true,
|
|
||||||
disconnected: true,
|
|
||||||
last_status: RadioStatus::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record an RSSI sample in the current time slot (keep best).
|
|
||||||
fn record_rssi(&mut self, rssi: i16) {
|
|
||||||
if self.current_slot_rssi == NO_SIGNAL || rssi > self.current_slot_rssi {
|
|
||||||
self.current_slot_rssi = rssi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark the current time slot as having a transmit.
|
|
||||||
fn record_tx(&mut self) {
|
|
||||||
self.current_slot_tx = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Advance to the next time slot, committing the current slot's data.
|
|
||||||
fn advance_slot(&mut self) {
|
|
||||||
let idx = self.rssi_count % RSSI_HISTORY_LEN;
|
|
||||||
self.rssi_history[idx] = self.current_slot_rssi;
|
|
||||||
self.tx_history[idx] = self.current_slot_tx;
|
|
||||||
self.rssi_count += 1;
|
|
||||||
self.current_slot_rssi = NO_SIGNAL;
|
|
||||||
self.current_slot_tx = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the active dashboard should be shown (RX or TX mode).
|
|
||||||
fn is_active(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
self.last_status.state,
|
|
||||||
RadioState::Receiving | RadioState::Transmitting
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[task]
|
|
||||||
pub async fn display_task(
|
|
||||||
parts: DisplayParts,
|
|
||||||
status: &'static StatusWatch,
|
|
||||||
display_commands: &'static DisplayCommandChannel,
|
|
||||||
) {
|
|
||||||
// Format MAC address as "XX:XX:XX:XX:XX:XX"
|
|
||||||
let mut mac_str: heapless::String<18> = heapless::String::new();
|
|
||||||
let m = parts.mac;
|
|
||||||
let _ = core::fmt::Write::write_fmt(
|
|
||||||
&mut mac_str,
|
|
||||||
format_args!(
|
|
||||||
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
|
||||||
m[0], m[1], m[2], m[3], m[4], m[5]
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let board_info = render::BoardInfo {
|
|
||||||
name: BOARD_NAME,
|
|
||||||
version: env!("CARGO_PKG_VERSION"),
|
|
||||||
mac: &mac_str,
|
|
||||||
};
|
|
||||||
|
|
||||||
let interface = I2CDisplayInterface::new(parts.i2c);
|
|
||||||
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
|
||||||
.into_buffered_graphics_mode();
|
|
||||||
|
|
||||||
if display.init().await.is_err() {
|
|
||||||
defmt::error!("SSD1306 init failed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if display
|
|
||||||
.set_brightness(ssd1306::prelude::Brightness::BRIGHTEST)
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
defmt::warn!("display brightness set failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut state = DisplayState::new();
|
|
||||||
let Some(mut receiver) = status.receiver() else {
|
|
||||||
defmt::error!("no watch receiver available for display");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show splash/waiting screen
|
|
||||||
render_splash(&mut display, &board_info).await;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match select3(
|
|
||||||
receiver.changed(),
|
|
||||||
display_commands.receive(),
|
|
||||||
Timer::after_millis(SPARK_SLOT_MS),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Either3::First(radio_status) => {
|
|
||||||
if state.disconnected {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(rssi) = radio_status.last_rssi {
|
|
||||||
if radio_status.rx_count != state.last_status.rx_count {
|
|
||||||
state.record_rssi(rssi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if radio_status.tx_count != state.last_status.tx_count {
|
|
||||||
state.record_tx();
|
|
||||||
}
|
|
||||||
state.last_status = radio_status;
|
|
||||||
|
|
||||||
if state.display_on {
|
|
||||||
render_current(&mut display, &state, &board_info).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Either3::Second(cmd) => match cmd {
|
|
||||||
DisplayCommand::Off => {
|
|
||||||
state.disconnected = false;
|
|
||||||
state.display_on = false;
|
|
||||||
render::blank(&mut display);
|
|
||||||
let _ = display.flush().await;
|
|
||||||
}
|
|
||||||
DisplayCommand::On => {
|
|
||||||
state.disconnected = false;
|
|
||||||
state.display_on = true;
|
|
||||||
if let Some(s) = receiver.try_get() {
|
|
||||||
state.last_status = s;
|
|
||||||
}
|
|
||||||
render_current(&mut display, &state, &board_info).await;
|
|
||||||
}
|
|
||||||
DisplayCommand::Reset => {
|
|
||||||
state = DisplayState::new();
|
|
||||||
render_splash(&mut display, &board_info).await;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Either3::Third(()) => {
|
|
||||||
// Timer tick: advance sparkline slot
|
|
||||||
state.advance_slot();
|
|
||||||
if state.display_on && state.is_active() {
|
|
||||||
render_dashboard(&mut display, &state, &board_info).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The concrete display type is verbose but avoids making this generic
|
|
||||||
// over both DrawTarget and the SSD1306-specific flush method.
|
|
||||||
type Display<I> = ssd1306::Ssd1306Async<
|
|
||||||
I,
|
|
||||||
DisplaySize128x64,
|
|
||||||
ssd1306::mode::BufferedGraphicsModeAsync<DisplaySize128x64>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
/// Render the appropriate screen (active dashboard or splash) and flush.
|
|
||||||
async fn render_current<I>(
|
|
||||||
display: &mut Display<I>,
|
|
||||||
state: &DisplayState,
|
|
||||||
board: &render::BoardInfo<'_>,
|
|
||||||
) where
|
|
||||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
|
||||||
{
|
|
||||||
if state.is_active() {
|
|
||||||
render_dashboard(display, state, board).await;
|
|
||||||
} else {
|
|
||||||
render_splash(display, board).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the active dashboard and flush.
|
|
||||||
async fn render_dashboard<I>(
|
|
||||||
display: &mut Display<I>,
|
|
||||||
state: &DisplayState,
|
|
||||||
board: &render::BoardInfo<'_>,
|
|
||||||
) where
|
|
||||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
|
||||||
{
|
|
||||||
render::dashboard(
|
|
||||||
display,
|
|
||||||
&state.last_status,
|
|
||||||
&state.rssi_history,
|
|
||||||
&state.tx_history,
|
|
||||||
state.rssi_count,
|
|
||||||
state.current_slot_rssi,
|
|
||||||
state.current_slot_tx,
|
|
||||||
board,
|
|
||||||
);
|
|
||||||
let _ = display.flush().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the splash/waiting screen and flush.
|
|
||||||
async fn render_splash<I>(display: &mut Display<I>, board: &render::BoardInfo<'_>)
|
|
||||||
where
|
|
||||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
|
||||||
{
|
|
||||||
render::splash(display, board);
|
|
||||||
let _ = display.flush().await;
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
//! DongLoRa firmware — transparent LoRa radio over USB.
|
|
||||||
//!
|
|
||||||
//! Three async tasks communicate via static channels:
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! usb_task ──Command──► radio_task ──► SX1262
|
|
||||||
//! ◄──Response── │
|
|
||||||
//! StatusWatch
|
|
||||||
//! ▼
|
|
||||||
//! display_task (optional)
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! The host drives everything. The radio idles until commanded.
|
|
||||||
|
|
||||||
#![cfg_attr(not(test), no_std)]
|
|
||||||
#![cfg_attr(not(test), no_main)]
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
mod board;
|
|
||||||
#[cfg(not(test))]
|
|
||||||
mod channel;
|
|
||||||
#[cfg(not(test))]
|
|
||||||
mod display;
|
|
||||||
mod protocol;
|
|
||||||
#[cfg(not(test))]
|
|
||||||
mod radio;
|
|
||||||
#[cfg(not(test))]
|
|
||||||
mod usb;
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
use embassy_executor::Spawner;
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
use crate::board::LoRaBoard;
|
|
||||||
#[cfg(not(test))]
|
|
||||||
use crate::channel::{CommandChannel, DisplayCommandChannel, ResponseChannel, StatusWatch};
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
cfg_if::cfg_if! {
|
|
||||||
if #[cfg(feature = "rak_wisblock_4631")] {
|
|
||||||
use panic_probe as _;
|
|
||||||
} else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] {
|
|
||||||
use esp_backtrace as _;
|
|
||||||
use esp_println as _;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
static COMMANDS: CommandChannel = CommandChannel::new();
|
|
||||||
#[cfg(not(test))]
|
|
||||||
static RESPONSES: ResponseChannel = ResponseChannel::new();
|
|
||||||
#[cfg(not(test))]
|
|
||||||
static STATUS: StatusWatch = StatusWatch::new();
|
|
||||||
#[cfg(not(test))]
|
|
||||||
static DISPLAY_COMMANDS: DisplayCommandChannel = DisplayCommandChannel::new();
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
cfg_if::cfg_if! {
|
|
||||||
if #[cfg(feature = "rak_wisblock_4631")] {
|
|
||||||
#[embassy_executor::main]
|
|
||||||
async fn main(spawner: Spawner) {
|
|
||||||
run(spawner).await;
|
|
||||||
}
|
|
||||||
} else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] {
|
|
||||||
#[esp_rtos::main]
|
|
||||||
async fn main(spawner: Spawner) {
|
|
||||||
run(spawner).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
async fn run(spawner: Spawner) {
|
|
||||||
let board = <board::Board as LoRaBoard>::init();
|
|
||||||
let mac = board::Board::mac_address();
|
|
||||||
let (radio, usb, display) = board.into_parts();
|
|
||||||
|
|
||||||
let has_display = display.is_some();
|
|
||||||
|
|
||||||
spawner
|
|
||||||
.spawn(radio::radio_task(radio, &COMMANDS, &RESPONSES, &STATUS))
|
|
||||||
.expect("spawn radio_task");
|
|
||||||
spawner
|
|
||||||
.spawn(usb::usb_task(
|
|
||||||
usb,
|
|
||||||
&COMMANDS,
|
|
||||||
&RESPONSES,
|
|
||||||
&DISPLAY_COMMANDS,
|
|
||||||
has_display,
|
|
||||||
mac,
|
|
||||||
))
|
|
||||||
.expect("spawn usb_task");
|
|
||||||
|
|
||||||
if let Some(dp) = display {
|
|
||||||
spawner
|
|
||||||
.spawn(display::display_task(dp, &STATUS, &DISPLAY_COMMANDS))
|
|
||||||
.expect("spawn display_task");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,661 +0,0 @@
|
|||||||
//! Wire protocol types and fixed-size little-endian serialization.
|
|
||||||
//!
|
|
||||||
//! Every integer is fixed-width LE. No varints, no zigzag.
|
|
||||||
//! See `docs/PROTOCOL.md` for the complete specification.
|
|
||||||
|
|
||||||
use heapless::Vec;
|
|
||||||
|
|
||||||
/// Maximum LoRa payload size in bytes.
|
|
||||||
pub const MAX_PAYLOAD: usize = 256;
|
|
||||||
|
|
||||||
/// RadioConfig wire size (fixed).
|
|
||||||
pub const RADIO_CONFIG_SIZE: usize = 13;
|
|
||||||
|
|
||||||
/// Sentinel value for `tx_power_dbm`: use the board's maximum TX power.
|
|
||||||
pub const TX_POWER_MAX: i8 = i8::MIN; // -128 on the wire
|
|
||||||
|
|
||||||
/// Sentinel value for `preamble_len`: use the firmware default (16 symbols).
|
|
||||||
pub const PREAMBLE_DEFAULT: u16 = 0;
|
|
||||||
|
|
||||||
|
|
||||||
/// LoRa signal bandwidth.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[cfg_attr(not(test), derive(defmt::Format))]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum Bandwidth {
|
|
||||||
Khz7 = 0,
|
|
||||||
Khz10 = 1,
|
|
||||||
Khz15 = 2,
|
|
||||||
Khz20 = 3,
|
|
||||||
Khz31 = 4,
|
|
||||||
Khz41 = 5,
|
|
||||||
Khz62 = 6,
|
|
||||||
Khz125 = 7,
|
|
||||||
Khz250 = 8,
|
|
||||||
Khz500 = 9,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bandwidth {
|
|
||||||
fn from_u8(v: u8) -> Option<Self> {
|
|
||||||
match v {
|
|
||||||
0 => Some(Self::Khz7),
|
|
||||||
1 => Some(Self::Khz10),
|
|
||||||
2 => Some(Self::Khz15),
|
|
||||||
3 => Some(Self::Khz20),
|
|
||||||
4 => Some(Self::Khz31),
|
|
||||||
5 => Some(Self::Khz41),
|
|
||||||
6 => Some(Self::Khz62),
|
|
||||||
7 => Some(Self::Khz125),
|
|
||||||
8 => Some(Self::Khz250),
|
|
||||||
9 => Some(Self::Khz500),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Complete LoRa radio configuration.
|
|
||||||
///
|
|
||||||
/// Wire layout (13 bytes, all little-endian):
|
|
||||||
/// ```text
|
|
||||||
/// [freq_hz:4] [bw:1] [sf:1] [cr:1] [sync_word:2] [tx_power_dbm:1] [preamble_len:2] [cad:1]
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[cfg_attr(not(test), derive(defmt::Format))]
|
|
||||||
pub struct RadioConfig {
|
|
||||||
/// Frequency in Hz (150_000_000 - 960_000_000 for SX1262).
|
|
||||||
pub freq_hz: u32,
|
|
||||||
pub bw: Bandwidth,
|
|
||||||
/// Spreading factor (5-12).
|
|
||||||
pub sf: u8,
|
|
||||||
/// Coding rate denominator (5-8). E.g. 5 = CR 4/5, 8 = CR 4/8.
|
|
||||||
pub cr: u8,
|
|
||||||
pub sync_word: u16,
|
|
||||||
/// Transmit power in dBm. Set to [`TX_POWER_MAX`] (-128) for the
|
|
||||||
/// board's maximum.
|
|
||||||
pub tx_power_dbm: i8,
|
|
||||||
/// Preamble length in symbols. Set to [`PREAMBLE_DEFAULT`] (0) for
|
|
||||||
/// the firmware default (16 symbols). Valid range: 6–65535.
|
|
||||||
pub preamble_len: u16,
|
|
||||||
/// Channel Activity Detection (listen-before-talk) before TX.
|
|
||||||
/// 0 = disabled, non-zero = enabled. Default: 1 (enabled).
|
|
||||||
pub cad: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RadioConfig {
|
|
||||||
/// Validate fields against hardware limits.
|
|
||||||
pub fn validate(&self, power_range: (i8, i8)) -> Result<(), &'static str> {
|
|
||||||
if !(150_000_000..=960_000_000).contains(&self.freq_hz) {
|
|
||||||
return Err("frequency out of range (150-960 MHz)");
|
|
||||||
}
|
|
||||||
if !(5..=12).contains(&self.sf) {
|
|
||||||
return Err("spreading factor out of range (5-12)");
|
|
||||||
}
|
|
||||||
if !(5..=8).contains(&self.cr) {
|
|
||||||
return Err("coding rate out of range (5-8)");
|
|
||||||
}
|
|
||||||
if self.tx_power_dbm != TX_POWER_MAX
|
|
||||||
&& !(power_range.0..=power_range.1).contains(&self.tx_power_dbm)
|
|
||||||
{
|
|
||||||
return Err("TX power out of range for this board");
|
|
||||||
}
|
|
||||||
if self.preamble_len != PREAMBLE_DEFAULT && self.preamble_len < 6 {
|
|
||||||
return Err("preamble length too short (min 6)");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve sentinel values to concrete defaults.
|
|
||||||
pub fn resolve(mut self, power_range: (i8, i8)) -> Self {
|
|
||||||
if self.tx_power_dbm == TX_POWER_MAX {
|
|
||||||
self.tx_power_dbm = power_range.1;
|
|
||||||
}
|
|
||||||
if self.preamble_len == PREAMBLE_DEFAULT {
|
|
||||||
self.preamble_len = 16;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether CAD (listen-before-talk) is enabled.
|
|
||||||
pub fn cad_enabled(&self) -> bool {
|
|
||||||
self.cad != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize to fixed-size LE bytes. Returns number of bytes written (always 13).
|
|
||||||
pub fn write_to(self, buf: &mut [u8]) -> usize {
|
|
||||||
buf[0..4].copy_from_slice(&self.freq_hz.to_le_bytes());
|
|
||||||
buf[4] = self.bw as u8;
|
|
||||||
buf[5] = self.sf;
|
|
||||||
buf[6] = self.cr;
|
|
||||||
buf[7..9].copy_from_slice(&self.sync_word.to_le_bytes());
|
|
||||||
buf[9] = self.tx_power_dbm as u8;
|
|
||||||
buf[10..12].copy_from_slice(&self.preamble_len.to_le_bytes());
|
|
||||||
buf[12] = self.cad;
|
|
||||||
RADIO_CONFIG_SIZE
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deserialize from fixed-size LE bytes.
|
|
||||||
pub fn from_bytes(buf: &[u8]) -> Option<Self> {
|
|
||||||
if buf.len() < RADIO_CONFIG_SIZE {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(Self {
|
|
||||||
freq_hz: u32::from_le_bytes(buf[0..4].try_into().ok()?),
|
|
||||||
bw: Bandwidth::from_u8(buf[4])?,
|
|
||||||
sf: buf[5],
|
|
||||||
cr: buf[6],
|
|
||||||
sync_word: u16::from_le_bytes(buf[7..9].try_into().ok()?),
|
|
||||||
tx_power_dbm: buf[9] as i8,
|
|
||||||
preamble_len: u16::from_le_bytes(buf[10..12].try_into().ok()?),
|
|
||||||
cad: buf[12],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Host → firmware commands.
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub enum Command {
|
|
||||||
Ping,
|
|
||||||
GetConfig,
|
|
||||||
SetConfig(RadioConfig),
|
|
||||||
StartRx,
|
|
||||||
StopRx,
|
|
||||||
Transmit {
|
|
||||||
config: Option<RadioConfig>,
|
|
||||||
payload: Vec<u8, MAX_PAYLOAD>,
|
|
||||||
},
|
|
||||||
DisplayOn,
|
|
||||||
DisplayOff,
|
|
||||||
GetMac,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command {
|
|
||||||
/// Deserialize from a COBS-decoded frame.
|
|
||||||
pub fn from_bytes(buf: &[u8]) -> Option<Self> {
|
|
||||||
let tag = *buf.first()?;
|
|
||||||
let rest = &buf[1..];
|
|
||||||
match tag {
|
|
||||||
0 => Some(Self::Ping),
|
|
||||||
1 => Some(Self::GetConfig),
|
|
||||||
2 => Some(Self::SetConfig(RadioConfig::from_bytes(rest)?)),
|
|
||||||
3 => Some(Self::StartRx),
|
|
||||||
4 => Some(Self::StopRx),
|
|
||||||
5 => {
|
|
||||||
// Transmit: has_config:u8 + [RadioConfig if 1] + len:u16 LE + payload
|
|
||||||
if rest.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (config, pos) = if rest[0] == 0 {
|
|
||||||
(None, 1)
|
|
||||||
} else if rest[0] == 1 && rest.len() > RADIO_CONFIG_SIZE {
|
|
||||||
(Some(RadioConfig::from_bytes(&rest[1..])?), 1 + RADIO_CONFIG_SIZE)
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
if rest.len() < pos + 2 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let len = u16::from_le_bytes(rest[pos..pos + 2].try_into().ok()?) as usize;
|
|
||||||
let data_start = pos + 2;
|
|
||||||
if rest.len() < data_start + len {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut payload = Vec::new();
|
|
||||||
let _ = payload.extend_from_slice(&rest[data_start..data_start + len]);
|
|
||||||
Some(Self::Transmit { config, payload })
|
|
||||||
}
|
|
||||||
6 => Some(Self::DisplayOn),
|
|
||||||
7 => Some(Self::DisplayOff),
|
|
||||||
8 => Some(Self::GetMac),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Firmware → host responses.
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub enum Response {
|
|
||||||
Pong,
|
|
||||||
Config(RadioConfig),
|
|
||||||
RxPacket {
|
|
||||||
rssi: i16,
|
|
||||||
snr: i16,
|
|
||||||
payload: Vec<u8, MAX_PAYLOAD>,
|
|
||||||
},
|
|
||||||
TxDone,
|
|
||||||
Ok,
|
|
||||||
Error(ErrorCode),
|
|
||||||
MacAddress([u8; 6]),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
/// Serialize to fixed-size LE bytes. Returns number of bytes written.
|
|
||||||
pub fn write_to(self, buf: &mut [u8]) -> usize {
|
|
||||||
match self {
|
|
||||||
Self::Pong => {
|
|
||||||
buf[0] = 0;
|
|
||||||
1
|
|
||||||
}
|
|
||||||
Self::Config(cfg) => {
|
|
||||||
buf[0] = 1;
|
|
||||||
1 + cfg.write_to(&mut buf[1..])
|
|
||||||
}
|
|
||||||
Self::RxPacket { rssi, snr, payload } => {
|
|
||||||
buf[0] = 2;
|
|
||||||
buf[1..3].copy_from_slice(&rssi.to_le_bytes());
|
|
||||||
buf[3..5].copy_from_slice(&snr.to_le_bytes());
|
|
||||||
buf[5..7].copy_from_slice(&(payload.len() as u16).to_le_bytes());
|
|
||||||
buf[7..7 + payload.len()].copy_from_slice(&payload);
|
|
||||||
7 + payload.len()
|
|
||||||
}
|
|
||||||
Self::TxDone => {
|
|
||||||
buf[0] = 3;
|
|
||||||
1
|
|
||||||
}
|
|
||||||
Self::Ok => {
|
|
||||||
buf[0] = 4;
|
|
||||||
1
|
|
||||||
}
|
|
||||||
Self::Error(code) => {
|
|
||||||
buf[0] = 5;
|
|
||||||
buf[1] = code as u8;
|
|
||||||
2
|
|
||||||
}
|
|
||||||
Self::MacAddress(mac) => {
|
|
||||||
buf[0] = 6;
|
|
||||||
buf[1..7].copy_from_slice(&mac);
|
|
||||||
7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error codes reported to the host.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
#[cfg_attr(not(test), derive(defmt::Format))]
|
|
||||||
#[repr(u8)]
|
|
||||||
#[allow(dead_code)] // CrcError reserved per PROTOCOL.md (error code 3)
|
|
||||||
pub enum ErrorCode {
|
|
||||||
InvalidConfig = 0,
|
|
||||||
RadioBusy = 1,
|
|
||||||
TxTimeout = 2,
|
|
||||||
CrcError = 3,
|
|
||||||
NotConfigured = 4,
|
|
||||||
NoDisplay = 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tests ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn make_config() -> RadioConfig {
|
|
||||||
RadioConfig {
|
|
||||||
freq_hz: 915_000_000,
|
|
||||||
bw: Bandwidth::Khz125,
|
|
||||||
sf: 7,
|
|
||||||
cr: 5,
|
|
||||||
sync_word: 0x3444,
|
|
||||||
tx_power_dbm: 22,
|
|
||||||
preamble_len: 16,
|
|
||||||
cad: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── RadioConfig roundtrip ───────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_roundtrip() {
|
|
||||||
let cfg = make_config();
|
|
||||||
let mut buf = [0u8; RADIO_CONFIG_SIZE];
|
|
||||||
let n = cfg.write_to(&mut buf);
|
|
||||||
assert_eq!(n, RADIO_CONFIG_SIZE);
|
|
||||||
assert_eq!(RadioConfig::from_bytes(&buf), Some(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_roundtrip_all_bandwidths() {
|
|
||||||
for bw_val in 0u8..=9 {
|
|
||||||
let bw = Bandwidth::from_u8(bw_val).unwrap();
|
|
||||||
let cfg = RadioConfig {
|
|
||||||
freq_hz: 433_000_000,
|
|
||||||
bw,
|
|
||||||
sf: 12,
|
|
||||||
cr: 8,
|
|
||||||
sync_word: 0x1234,
|
|
||||||
tx_power_dbm: -9,
|
|
||||||
};
|
|
||||||
let mut buf = [0u8; RADIO_CONFIG_SIZE];
|
|
||||||
cfg.write_to(&mut buf);
|
|
||||||
assert_eq!(RadioConfig::from_bytes(&buf), Some(cfg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_roundtrip_negative_power() {
|
|
||||||
let cfg = RadioConfig {
|
|
||||||
tx_power_dbm: TX_POWER_MAX,
|
|
||||||
..make_config()
|
|
||||||
};
|
|
||||||
let mut buf = [0u8; RADIO_CONFIG_SIZE];
|
|
||||||
cfg.write_to(&mut buf);
|
|
||||||
assert_eq!(RadioConfig::from_bytes(&buf), Some(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_from_short_buffer() {
|
|
||||||
let buf = [0u8; RADIO_CONFIG_SIZE - 1];
|
|
||||||
assert!(RadioConfig::from_bytes(&buf).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_from_empty_buffer() {
|
|
||||||
assert!(RadioConfig::from_bytes(&[]).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn radio_config_invalid_bandwidth() {
|
|
||||||
let mut buf = [0u8; RADIO_CONFIG_SIZE];
|
|
||||||
make_config().write_to(&mut buf);
|
|
||||||
buf[4] = 255; // invalid bandwidth
|
|
||||||
assert!(RadioConfig::from_bytes(&buf).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── RadioConfig::validate ───────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validate_freq_boundaries() {
|
|
||||||
let power_range = (-9, 22);
|
|
||||||
let base = make_config();
|
|
||||||
|
|
||||||
let mut cfg = RadioConfig { freq_hz: 150_000_000, ..base };
|
|
||||||
assert!(cfg.validate(power_range).is_ok());
|
|
||||||
|
|
||||||
cfg.freq_hz = 960_000_000;
|
|
||||||
assert!(cfg.validate(power_range).is_ok());
|
|
||||||
|
|
||||||
cfg.freq_hz = 149_999_999;
|
|
||||||
assert!(cfg.validate(power_range).is_err());
|
|
||||||
|
|
||||||
cfg.freq_hz = 960_000_001;
|
|
||||||
assert!(cfg.validate(power_range).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validate_sf_boundaries() {
|
|
||||||
let power_range = (-9, 22);
|
|
||||||
let base = make_config();
|
|
||||||
|
|
||||||
for sf in 5..=12 {
|
|
||||||
assert!(RadioConfig { sf, ..base }.validate(power_range).is_ok());
|
|
||||||
}
|
|
||||||
assert!(RadioConfig { sf: 4, ..base }.validate(power_range).is_err());
|
|
||||||
assert!(RadioConfig { sf: 13, ..base }.validate(power_range).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validate_cr_boundaries() {
|
|
||||||
let power_range = (-9, 22);
|
|
||||||
let base = make_config();
|
|
||||||
|
|
||||||
for cr in 5..=8 {
|
|
||||||
assert!(RadioConfig { cr, ..base }.validate(power_range).is_ok());
|
|
||||||
}
|
|
||||||
assert!(RadioConfig { cr: 4, ..base }.validate(power_range).is_err());
|
|
||||||
assert!(RadioConfig { cr: 9, ..base }.validate(power_range).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validate_tx_power_max_sentinel() {
|
|
||||||
let cfg = RadioConfig { tx_power_dbm: TX_POWER_MAX, ..make_config() };
|
|
||||||
assert!(cfg.validate((-9, 22)).is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validate_tx_power_out_of_range() {
|
|
||||||
let cfg = RadioConfig { tx_power_dbm: 23, ..make_config() };
|
|
||||||
assert!(cfg.validate((-9, 22)).is_err());
|
|
||||||
|
|
||||||
let cfg = RadioConfig { tx_power_dbm: -10, ..make_config() };
|
|
||||||
assert!(cfg.validate((-9, 22)).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validate_preamble_default_sentinel() {
|
|
||||||
let cfg = RadioConfig { preamble_len: PREAMBLE_DEFAULT, ..make_config() };
|
|
||||||
assert!(cfg.validate((-9, 22)).is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validate_preamble_boundaries() {
|
|
||||||
let power_range = (-9, 22);
|
|
||||||
let base = make_config();
|
|
||||||
|
|
||||||
assert!(RadioConfig { preamble_len: 6, ..base }.validate(power_range).is_ok());
|
|
||||||
assert!(RadioConfig { preamble_len: 5, ..base }.validate(power_range).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── RadioConfig::resolve ──────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_power_max_sentinel() {
|
|
||||||
let cfg = RadioConfig { tx_power_dbm: TX_POWER_MAX, ..make_config() };
|
|
||||||
assert_eq!(cfg.resolve((-9, 22)).tx_power_dbm, 22);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_power_explicit_unchanged() {
|
|
||||||
let cfg = RadioConfig { tx_power_dbm: 10, ..make_config() };
|
|
||||||
assert_eq!(cfg.resolve((-9, 22)).tx_power_dbm, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_preamble_default() {
|
|
||||||
let cfg = RadioConfig { preamble_len: PREAMBLE_DEFAULT, ..make_config() };
|
|
||||||
assert_eq!(cfg.resolve((-9, 22)).preamble_len, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_preamble_explicit_unchanged() {
|
|
||||||
let cfg = RadioConfig { preamble_len: 32, ..make_config() };
|
|
||||||
assert_eq!(cfg.resolve((-9, 22)).preamble_len, 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Command::from_bytes ─────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_ping() {
|
|
||||||
assert_eq!(Command::from_bytes(&[0]), Some(Command::Ping));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_get_config() {
|
|
||||||
assert_eq!(Command::from_bytes(&[1]), Some(Command::GetConfig));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_set_config() {
|
|
||||||
let cfg = make_config();
|
|
||||||
let mut buf = [0u8; 1 + RADIO_CONFIG_SIZE];
|
|
||||||
buf[0] = 2;
|
|
||||||
cfg.write_to(&mut buf[1..]);
|
|
||||||
assert_eq!(Command::from_bytes(&buf), Some(Command::SetConfig(cfg)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_start_stop_rx() {
|
|
||||||
assert_eq!(Command::from_bytes(&[3]), Some(Command::StartRx));
|
|
||||||
assert_eq!(Command::from_bytes(&[4]), Some(Command::StopRx));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_transmit_no_config() {
|
|
||||||
let payload = b"hello";
|
|
||||||
let mut buf = [0u8; 64];
|
|
||||||
buf[0] = 5; // tag
|
|
||||||
buf[1] = 0; // has_config = false
|
|
||||||
buf[2..4].copy_from_slice(&(payload.len() as u16).to_le_bytes());
|
|
||||||
buf[4..9].copy_from_slice(payload);
|
|
||||||
|
|
||||||
match Command::from_bytes(&buf[..9]).unwrap() {
|
|
||||||
Command::Transmit { config, payload: p } => {
|
|
||||||
assert!(config.is_none());
|
|
||||||
assert_eq!(p.as_slice(), b"hello");
|
|
||||||
}
|
|
||||||
_ => panic!("expected Transmit"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_transmit_with_config() {
|
|
||||||
let cfg = make_config();
|
|
||||||
let mut buf = [0u8; 64];
|
|
||||||
buf[0] = 5; // tag
|
|
||||||
buf[1] = 1; // has_config = true
|
|
||||||
cfg.write_to(&mut buf[2..]);
|
|
||||||
let payload = b"test";
|
|
||||||
let pos = 2 + RADIO_CONFIG_SIZE;
|
|
||||||
buf[pos..pos + 2].copy_from_slice(&(payload.len() as u16).to_le_bytes());
|
|
||||||
buf[pos + 2..pos + 6].copy_from_slice(payload);
|
|
||||||
|
|
||||||
match Command::from_bytes(&buf[..pos + 6]).unwrap() {
|
|
||||||
Command::Transmit { config, payload: p } => {
|
|
||||||
assert_eq!(config, Some(cfg));
|
|
||||||
assert_eq!(p.as_slice(), b"test");
|
|
||||||
}
|
|
||||||
_ => panic!("expected Transmit"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_transmit_empty_payload() {
|
|
||||||
let mut buf = [0u8; 4];
|
|
||||||
buf[0] = 5; // tag
|
|
||||||
buf[1] = 0; // has_config = false
|
|
||||||
buf[2..4].copy_from_slice(&0u16.to_le_bytes());
|
|
||||||
|
|
||||||
match Command::from_bytes(&buf).unwrap() {
|
|
||||||
Command::Transmit { config, payload } => {
|
|
||||||
assert!(config.is_none());
|
|
||||||
assert!(payload.is_empty());
|
|
||||||
}
|
|
||||||
_ => panic!("expected Transmit"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_transmit_truncated() {
|
|
||||||
// Tag only — missing has_config byte
|
|
||||||
assert!(Command::from_bytes(&[5]).is_none());
|
|
||||||
|
|
||||||
// has_config=1 but no config bytes
|
|
||||||
assert!(Command::from_bytes(&[5, 1]).is_none());
|
|
||||||
|
|
||||||
// has_config=0 but no length bytes
|
|
||||||
assert!(Command::from_bytes(&[5, 0]).is_none());
|
|
||||||
|
|
||||||
// has_config=0, length says 5 but only 2 bytes of payload
|
|
||||||
let mut buf = [0u8; 6];
|
|
||||||
buf[0] = 5;
|
|
||||||
buf[1] = 0;
|
|
||||||
buf[2..4].copy_from_slice(&5u16.to_le_bytes());
|
|
||||||
buf[4] = 0xAA;
|
|
||||||
buf[5] = 0xBB;
|
|
||||||
assert!(Command::from_bytes(&buf).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_display_and_mac() {
|
|
||||||
assert_eq!(Command::from_bytes(&[6]), Some(Command::DisplayOn));
|
|
||||||
assert_eq!(Command::from_bytes(&[7]), Some(Command::DisplayOff));
|
|
||||||
assert_eq!(Command::from_bytes(&[8]), Some(Command::GetMac));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_invalid_tag() {
|
|
||||||
assert!(Command::from_bytes(&[9]).is_none());
|
|
||||||
assert!(Command::from_bytes(&[255]).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn command_empty_buffer() {
|
|
||||||
assert!(Command::from_bytes(&[]).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Response::write_to ──────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_pong() {
|
|
||||||
let mut buf = [0u8; 1];
|
|
||||||
assert_eq!(Response::Pong.write_to(&mut buf), 1);
|
|
||||||
assert_eq!(buf[0], 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_config() {
|
|
||||||
let cfg = make_config();
|
|
||||||
let mut buf = [0u8; 1 + RADIO_CONFIG_SIZE];
|
|
||||||
let n = Response::Config(cfg).write_to(&mut buf);
|
|
||||||
assert_eq!(n, 1 + RADIO_CONFIG_SIZE);
|
|
||||||
assert_eq!(buf[0], 1);
|
|
||||||
assert_eq!(RadioConfig::from_bytes(&buf[1..]), Some(cfg));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_rx_packet() {
|
|
||||||
let mut payload = Vec::new();
|
|
||||||
let _ = payload.extend_from_slice(b"data");
|
|
||||||
let mut buf = [0u8; 64];
|
|
||||||
let n = Response::RxPacket { rssi: -80, snr: 10, payload }.write_to(&mut buf);
|
|
||||||
assert_eq!(n, 7 + 4); // tag(1) + rssi(2) + snr(2) + len(2) + "data"(4)
|
|
||||||
assert_eq!(buf[0], 2);
|
|
||||||
assert_eq!(i16::from_le_bytes([buf[1], buf[2]]), -80);
|
|
||||||
assert_eq!(i16::from_le_bytes([buf[3], buf[4]]), 10);
|
|
||||||
assert_eq!(u16::from_le_bytes([buf[5], buf[6]]), 4);
|
|
||||||
assert_eq!(&buf[7..11], b"data");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_tx_done() {
|
|
||||||
let mut buf = [0u8; 1];
|
|
||||||
assert_eq!(Response::TxDone.write_to(&mut buf), 1);
|
|
||||||
assert_eq!(buf[0], 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_ok() {
|
|
||||||
let mut buf = [0u8; 1];
|
|
||||||
assert_eq!(Response::Ok.write_to(&mut buf), 1);
|
|
||||||
assert_eq!(buf[0], 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_error_codes() {
|
|
||||||
let mut buf = [0u8; 2];
|
|
||||||
for (code, val) in [
|
|
||||||
(ErrorCode::InvalidConfig, 0),
|
|
||||||
(ErrorCode::RadioBusy, 1),
|
|
||||||
(ErrorCode::TxTimeout, 2),
|
|
||||||
(ErrorCode::CrcError, 3),
|
|
||||||
(ErrorCode::NotConfigured, 4),
|
|
||||||
(ErrorCode::NoDisplay, 5),
|
|
||||||
] {
|
|
||||||
let n = Response::Error(code).write_to(&mut buf);
|
|
||||||
assert_eq!(n, 2);
|
|
||||||
assert_eq!(buf[0], 5);
|
|
||||||
assert_eq!(buf[1], val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn response_mac_address() {
|
|
||||||
let mac = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
|
|
||||||
let mut buf = [0u8; 7];
|
|
||||||
let n = Response::MacAddress(mac).write_to(&mut buf);
|
|
||||||
assert_eq!(n, 7);
|
|
||||||
assert_eq!(buf[0], 6);
|
|
||||||
assert_eq!(&buf[1..7], &mac);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user