From 32f8173df5b5fe112f5235c576cf0cb6e321294b Mon Sep 17 00:00:00 2001 From: devkyato Date: Wed, 29 Jul 2026 09:23:08 +0800 Subject: [PATCH 1/4] docs: establish the OpenNet project foundation --- .gitignore | 15 + .vscode/extensions.json | 6 + .vscode/settings.json | 5 + CHANGELOG.md | 16 + CODE_OF_CONDUCT.md | 28 + CONTRIBUTING.md | 47 ++ LICENSE | 21 + README.md | 83 ++- SECURITY.md | 27 + docs/architecture.md | 44 ++ docs/assets/opennet-cover.png | Bin 0 -> 161237 bytes docs/compatibility.md | 31 + docs/getting-started.md | 72 +++ docs/protocol.md | 101 +++ docs/release.md | 14 + keywords.txt | 10 + library.json | 15 + library.properties | 10 + proxies.txt.txt | 1099 --------------------------------- 19 files changed, 541 insertions(+), 1103 deletions(-) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 docs/architecture.md create mode 100644 docs/assets/opennet-cover.png create mode 100644 docs/compatibility.md create mode 100644 docs/getting-started.md create mode 100644 docs/protocol.md create mode 100644 docs/release.md create mode 100644 keywords.txt create mode 100644 library.json create mode 100644 library.properties delete mode 100644 proxies.txt.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83032db --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.pio/ +.venv/ +__pycache__/ +*.py[cod] +*.egg-info/ +build/ +dist/ +.coverage +.pytest_cache/ +.mypy_cache/ +.DS_Store +.idea/ +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..1647c3d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "platformio.platformio-ide", + "ms-python.python" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..eb41b61 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/Scripts/python.exe", + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": ["python/tests"] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d52aabb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes are documented here. OpenNet follows +[Semantic Versioning](https://semver.org/). + +## [Unreleased] + +### Added + +- ONP/1 protocol specification and typed binary framing. +- Python asyncio client and server. +- ESP32 Arduino client. +- Arduino IDE, PlatformIO, and Python examples. +- Cross-platform tests, CI, packaging, and contributor documentation. + +[Unreleased]: https://github.com/devkyato/OpenNet/compare/v0.1.0...HEAD diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8127c16 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,28 @@ +# Code of Conduct + +## Our pledge + +We pledge to make participation in OpenNet a respectful, harassment-free +experience for everyone, regardless of background, identity, experience, or +education level. + +## Expected behavior + +- Be patient with learners and specific when giving feedback. +- Discuss technical decisions, not personal traits. +- Credit other people's work. +- Protect private information and report security issues privately. + +## Unacceptable behavior + +Harassment, threats, discriminatory language, sexualized attention, deliberate +disruption, and publishing another person's private information are not accepted. + +## Enforcement + +Report conduct concerns privately to the repository owner through the security +advisory form. Maintainers may edit or remove content and temporarily or +permanently restrict participation. Reports will be handled as confidentially as +practical. + +This policy is adapted from the Contributor Covenant 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fdbdef3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing to OpenNet + +Thank you for helping. Contributions from students and first-time open-source +developers are especially welcome. + +## Choose a task + +Comment on an existing issue before starting substantial work. Issues labeled +`good first issue` are scoped for newcomers; `help wanted` items may require +protocol or embedded experience. For a new idea, open an issue and explain the +problem before proposing an API. + +## Development setup + +Python: + +```bash +python -m venv .venv +.venv/Scripts/python -m pip install -e "./python[dev]" +.venv/Scripts/python -m pytest +``` + +On Linux/macOS, activate with `source .venv/bin/activate`. For embedded work, +install PlatformIO and run: + +```bash +pio test -e native +pio run -e esp32dev +``` + +## Pull requests + +1. Branch from `main`. +2. Make one coherent change with tests and documentation. +3. Use a Conventional Commit subject such as `feat: add discovery packet`. +4. Run the relevant checks locally. +5. Complete the pull-request checklist and link the issue. + +Do not add generated build output or credentials. Protocol changes must update +`docs/protocol.md` and include cross-language test vectors. Compatibility claims +must name the exact board/OS, dependency versions, and test procedure. + +## Review + +Maintainers review correctness, compatibility, tests, security, and clarity. +Authors cannot approve their own pull requests; another reviewer should approve +before merge when one is available. CI must pass. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f24b1a7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 devkyato + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5485b14..07b4887 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,80 @@ -# Open-Proxies -Proxies, dont need to ask where I got em'. +# OpenNet -Can be used for our personal or public projects. -@九六七 / Developer +![OpenNet cover](docs/assets/opennet-cover.png) + +[![CI](https://github.com/devkyato/OpenNet/actions/workflows/ci.yml/badge.svg)](https://github.com/devkyato/OpenNet/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-4a638f.svg)](LICENSE) +[![Protocol](https://img.shields.io/badge/protocol-ONP%2F1-4a638f.svg)](docs/protocol.md) + +OpenNet is a small, typed messaging protocol and library for sending JSON, text, +numbers, and arbitrary binary data between ESP32 devices, Raspberry Pi computers, +and backend services over Wi-Fi or Ethernet. + +The first release deliberately targets one dependable path: an ESP32 client and a +Python 3.9+ client/server communicating over TCP. The wire format is documented and +language-neutral, so additional transports and language bindings can be added +without inventing incompatible message formats. + +## What it provides + +- A compact binary frame with versioning, message IDs, type information, CRC-32, + and a 16 MiB defensive payload limit. +- JSON, UTF-8 text, signed integers, IEEE-754 doubles, booleans, null, and raw bytes. +- Async Python client/server APIs for Raspberry Pi and backend systems. +- An Arduino-compatible ESP32 client with reconnect, heartbeat, and delivery ACKs. +- Optional TLS at the transport layer. +- Arduino IDE, PlatformIO, Python, and VS Code examples. +- Protocol conformance tests and cross-platform CI. + +## Five-minute start + +Run the Python server: + +```bash +python -m pip install -e "./python[dev]" +opennet-server --host 0.0.0.0 --port 8765 +``` + +Install this repository as an Arduino library, open +`File > Examples > OpenNet > TelemetryClient`, set the Wi-Fi credentials and +server address, then upload it to an ESP32. + +Python clients are equally small: + +```python +import asyncio +from opennet import OpenNetClient + +async def main(): + async with OpenNetClient("192.168.1.50", 8765) as client: + await client.send("temperature", {"celsius": 24.7}) + +asyncio.run(main()) +``` + +See the [getting-started guide](docs/getting-started.md), [protocol +specification](docs/protocol.md), and [compatibility policy](docs/compatibility.md). + +## Project status + +OpenNet is pre-1.0 software. ONP/1 framing is stable for the v0.x series, while +higher-level APIs may improve based on real hardware feedback. Do not represent +untested boards or operating systems as supported; add a compatibility report when +you test one. + +## Contributing + +Student contributions are welcome. Good first tasks are labeled in the issue +tracker, and every feature should include tests or a reproducible hardware test +report. Read [CONTRIBUTING.md](CONTRIBUTING.md) and the +[code of conduct](CODE_OF_CONDUCT.md) before opening a pull request. + +## Security + +CRC detects accidental corruption; it is not encryption or authentication. Use TLS +on untrusted networks. See [SECURITY.md](SECURITY.md) for the threat model and +private reporting instructions. + +## License + +OpenNet is available under the [MIT License](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5f602d6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security Policy + +## Supported versions + +Security fixes are provided for the latest released minor version. OpenNet is +currently pre-1.0 and should not be used as the only safety mechanism in critical +systems. + +## Reporting a vulnerability + +Use **Security > Report a vulnerability** in this GitHub repository. Do not open a +public issue containing exploit details, credentials, private addresses, or device +identifiers. Include affected versions, reproduction steps, impact, and a proposed +fix if available. + +The maintainer aims to acknowledge a complete report within seven days and will +coordinate disclosure after a fix is available. + +## Threat model + +- ONP/1 CRC-32 detects transmission errors, not malicious modification. +- Plain TCP provides no confidentiality, peer identity, or replay protection. +- Use TLS with certificate validation across untrusted networks. +- Do not use `setInsecure()` in production. +- Applications must authorize topics and validate payloads. +- The 16 MiB frame limit reduces memory-exhaustion risk; deployments should choose + a smaller application limit appropriate for their hardware. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..e121881 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,44 @@ +# Architecture + +OpenNet separates the parts that must interoperate from platform-specific I/O. + +```text +Application values and topics + | + OpenNet API + | + ONP/1 frame codec <- language-neutral and tested with vectors + | + TCP or TLS byte stream <- reliable ordered transport + | + Wi-Fi / Ethernet / OS networking +``` + +The Python package provides the reference codec plus asyncio client and server. The +Arduino library uses the ESP32 networking clients and a bounded receive state +machine. Both implementations use the same 24-byte header. + +## Reliability + +TCP already retransmits lost packets and preserves order. ONP/1 acknowledgements +add application-level evidence that a complete frame was parsed and accepted. +Applications needing durable delivery must persist outgoing messages and decide +how to handle duplicates after reconnecting; OpenNet does not pretend RAM queues +survive power loss. + +## Latency + +OpenNet disables Nagle's algorithm where supported, uses a compact header, and +does not require a broker round trip. Actual latency still depends on radio +conditions, access-point load, power-saving mode, operating-system scheduling, and +the application. Low-signal or disconnected areas cannot be made reliable by a +software library alone. + +## Planned extensions + +- Authenticated session handshake and topic authorization helpers. +- Optional service discovery for trusted LANs. +- Persistent outbound queues. +- TypeScript and portable C bindings. + +Extensions will be proposed in issues and specified before implementation. diff --git a/docs/assets/opennet-cover.png b/docs/assets/opennet-cover.png new file mode 100644 index 0000000000000000000000000000000000000000..24f89ab439b5c38307dca997b83082dde255f396 GIT binary patch literal 161237 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—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

_sGhHP^2q6F%js=&0 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 Include Library > Add .ZIP Library**. +3. Install Espressif's `esp32` board package. +4. Open **File > Examples > OpenNet > TelemetryClient**. +5. Set Wi-Fi name, password, and the server's LAN IP address. +6. Select your exact ESP32 board and upload. + +Call `client.poll()` frequently from `loop()`. OpenNet avoids blocking delays after +the TCP connection is established. + +## PlatformIO and VS Code + +Clone the repository and open the folder in VS Code with PlatformIO installed. +Build the included example: + +```bash +pio run -e esp32dev +``` + +For another project: + +```ini +lib_deps = + https://github.com/devkyato/OpenNet.git#v0.1.0 +``` + +## Sending values + +Topics describe what a value means: + +```cpp +client.sendText("lab/status", "online"); +client.sendJson("lab/telemetry", "{\"temperature\":24.7}"); +client.sendBinary("camera/chunk", bytes, byteCount); +client.sendInt("counter", 42); +client.sendDouble("voltage", 3.3); +client.sendBool("door/open", false); +``` + +Validate incoming JSON against an application schema and bound binary payload sizes +for the receiving device. diff --git a/docs/protocol.md b/docs/protocol.md new file mode 100644 index 0000000..981c4ca --- /dev/null +++ b/docs/protocol.md @@ -0,0 +1,101 @@ +# OpenNet Protocol 1 (ONP/1) + +Status: stable for the OpenNet 0.x series. + +ONP/1 is a framed, topic-based messaging protocol. It is intentionally smaller +than a broker protocol: peers establish a reliable byte stream (normally TCP or +TLS), exchange frames, and decide at the application layer which topics are +allowed. + +## Byte order and limits + +All multibyte integers and floating-point values use network byte order (big +endian). A receiver must reject frames with a payload length greater than +16,777,216 bytes before allocating the payload. Embedded applications should set a +smaller limit. + +## Frame + +The fixed header is 24 bytes: + +| Offset | Size | Field | Meaning | +|---:|---:|---|---| +| 0 | 2 | magic | ASCII `ON` (`0x4f 0x4e`) | +| 2 | 1 | version | `0x01` | +| 3 | 1 | kind | frame kind | +| 4 | 1 | flags | bit field | +| 5 | 1 | value type | application payload type | +| 6 | 2 | topic length | UTF-8 topic byte length | +| 8 | 4 | message ID | unsigned, non-zero for DATA | +| 12 | 4 | payload length | application payload bytes | +| 16 | 4 | CRC-32 | CRC of topic bytes followed by payload bytes | +| 20 | 4 | reserved | must be zero; ignore when receiving | + +The header is followed by `topic length` bytes and then `payload length` bytes. +Topics must be valid UTF-8, between 1 and 1024 bytes for DATA frames, and use `/` +as an optional hierarchy separator. + +## Frame kinds + +| Value | Name | Topic/payload | +|---:|---|---| +| 1 | DATA | typed application message | +| 2 | ACK | empty; message ID identifies accepted DATA | +| 3 | PING | empty | +| 4 | PONG | empty | +| 5 | CLOSE | optional UTF-8 reason | +| 6 | ERROR | UTF-8 diagnostic; must not contain secrets | + +Unknown frame kinds must produce ERROR and close the connection. A peer receiving +PING should promptly return PONG. + +## Flags + +- Bit 0 (`0x01`), `ACK_REQUIRED`: receiver sends ACK after accepting a DATA frame. +- Bit 1 (`0x02`), `DUPLICATE`: retransmission of the same message ID. +- Bits 2–7 are reserved and must be zero when sending. + +ACK confirms that the receiver parsed and accepted the frame, not that application +side effects completed. Message IDs are scoped to one connection. Receivers should +deduplicate repeated IDs for the duration of the connection. + +## Value types + +| Value | Type | Encoding | +|---:|---|---| +| 0 | BYTES | uninterpreted bytes | +| 1 | UTF8 | valid UTF-8 | +| 2 | JSON | UTF-8 JSON value | +| 3 | INT64 | signed 64-bit integer | +| 4 | FLOAT64 | IEEE-754 binary64 | +| 5 | BOOL | exactly one byte: `0x00` or `0x01` | +| 6 | NULL | empty payload | + +Kind-specific control payloads use the encoding described in the frame-kind table, +regardless of the value-type byte. + +## CRC + +CRC-32 is the IEEE polynomial used by zlib. Initialize to zero through the public +zlib-style API and calculate over the exact topic bytes, then continue over the +payload bytes. The expected value for topic `sensor/temp` and payload `24.5` is +defined by the conformance tests. + +CRC protects against accidental corruption only. TLS is required for +confidentiality, peer authentication, and tamper resistance. + +## Connection lifecycle + +1. Connect a reliable byte stream. +2. Either peer may send DATA or PING. +3. Send PING after an application-configured idle interval; require PONG before + the liveness timeout. +4. Send CLOSE when practical before ending a healthy connection. +5. Reconnect with exponential backoff and jitter. Application code decides whether + to replay unacknowledged messages. + +## Compatibility + +Senders must use version 1. A version-1 receiver must ignore non-zero reserved +header bytes for forward compatibility but reject reserved flag bits. New value +types or frame kinds require a protocol revision or an extension specification. diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..db8a89c --- /dev/null +++ b/docs/release.md @@ -0,0 +1,14 @@ +# Release process + +1. Confirm CI passes on `main`. +2. Update `CHANGELOG.md` and versions in `library.properties`, `library.json`, and + `python/pyproject.toml`. +3. Create and push an annotated `vX.Y.Z` tag. +4. The release workflow verifies that all versions match, runs tests, builds the + Python wheel/source distribution and Arduino ZIP, then creates a GitHub release. +5. A maintainer checks installation from the published artifacts and edits the + generated release notes if needed. + +PyPI publication is intentionally not automatic until the owner configures trusted +publishing. Release assets remain installable and reproducible without a registry +credential. diff --git a/keywords.txt b/keywords.txt new file mode 100644 index 0000000..e3bcd8d --- /dev/null +++ b/keywords.txt @@ -0,0 +1,10 @@ +OpenNetClient KEYWORD1 +OpenNetMessage KEYWORD1 +OpenNetValueType KEYWORD1 +connect KEYWORD2 +poll KEYWORD2 +send KEYWORD2 +sendText KEYWORD2 +sendJson KEYWORD2 +sendBinary KEYWORD2 +connected KEYWORD2 diff --git a/library.json b/library.json new file mode 100644 index 0000000..1d7dbb1 --- /dev/null +++ b/library.json @@ -0,0 +1,15 @@ +{ + "name": "OpenNet", + "version": "0.1.0", + "description": "Typed, reliable messaging between ESP32, Raspberry Pi, and backend services.", + "keywords": ["esp32", "raspberry-pi", "iot", "tcp", "messaging"], + "repository": { + "type": "git", + "url": "https://github.com/devkyato/OpenNet.git" + }, + "authors": [{"name": "devkyato", "maintainer": true}], + "license": "MIT", + "frameworks": ["arduino"], + "platforms": ["espressif32"], + "headers": "OpenNet.h" +} diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..ad3910b --- /dev/null +++ b/library.properties @@ -0,0 +1,10 @@ +name=OpenNet +version=0.1.0 +author=devkyato +maintainer=devkyato +sentence=Typed, reliable messaging between ESP32, Raspberry Pi, and backend services. +paragraph=OpenNet sends JSON, text, numeric values, and binary data using the documented ONP/1 framing protocol over TCP or TLS. +category=Communication +url=https://github.com/devkyato/OpenNet +architectures=esp32 +includes=OpenNet.h diff --git a/proxies.txt.txt b/proxies.txt.txt deleted file mode 100644 index bf0243f..0000000 --- a/proxies.txt.txt +++ /dev/null @@ -1,1099 +0,0 @@ -122.6.245.14:8090 -123.184.6.251:8088 -123.236.215.131:6588 -172.163.146.56:6588 -189.37.28.147:6588 -190.53.89.103:6588 -200.104.104.91:6588 -200.126.98.135:6588 -200.252.201.144:80 -201.42.59.201:6588 -202.134.202.226:80 -211.140.151.214:8080 -212.12.114.252:3128 -218.252.37.227:808 -59.95.1.229:6588 -59.95.73.173:6588 -61.174.122.2:8088 -61.54.82.130:808 -66.167.100.59:6649 -68.175.47.31:9090 -69.36.7.253:4004 -70.86.138.210:8131 -71.61.176.210:3128 -78.138.131.150:8080 -79.172.81.79:44933 -81.164.167.69:9090 -82.228.101.113:14841 -82.23.123.51:9090 -83.167.122.41:3128 -85.11.150.211:6588 -85.24.89.199:6588 -98.175.97.7:8080 -125.137.151.104:3128 -193.19.82.24:3128 -195.89.143.211:8090 -200.112.83.72:6588 -200.140.83.108:6588 -200.150.139.211:6588 -201.17.188.5:6588 -201.229.208.2:80 -201.229.208.3:80 -207.38.251.111:9090 -212.17.86.109:8080 -218.83.248.32:8088 -222.88.211.181:8089 -24.1.166.12:9090 -24.10.243.231:7212 -24.10.84.226:9090 -24.116.25.150:9090 -24.118.207.118:9090 -24.125.84.2:9090 -24.126.147.186:9090 -24.127.113.227:9090 -24.137.215.227:9090 -24.139.68.242:9090 -24.14.112.139:9090 -24.15.52.62:9090 -24.185.121.80:9090 -24.189.131.189:9090 -24.189.29.189:9090 -24.217.194.73:9090 -24.222.220.140:9090 -24.230.182.225:9090 -24.3.105.116:9090 -24.4.239.144:9090 -24.98.81.111:9090 -58.253.111.2:8089 -59.94.189.230:6588 -61.153.140.106:808 -64.179.172.134:9090 -64.30.123.252:9090 -65.75.189.33:9090 -67.149.165.201:9090 -67.149.215.109:9090 -67.161.239.245:9090 -67.166.66.244:9090 -67.191.220.137:9090 -67.9.3.63:9090 -68.10.87.155:9090 -68.180.117.215:9090 -68.194.177.78:9090 -68.198.252.19:9090 -68.198.72.147:9090 -68.59.217.62:9090 -68.63.27.57:9090 -68.84.47.147:9090 -69.113.232.218:9090 -69.120.173.183:9090 -69.123.44.118:9090 -69.127.115.255:9090 -69.136.136.125:9090 -69.138.47.42:9090 -69.180.245.32:9090 -69.180.8.201:9090 -69.22.123.154:9090 -69.242.176.42:9090 -69.246.117.136:9090 -69.254.246.123:9090 -71.194.216.20:7212 -71.195.130.227:9090 -71.197.189.88:9090 -71.199.134.177:9090 -71.205.113.223:9090 -71.205.37.198:9090 -71.224.87.71:9090 -71.228.231.241:9090 -71.236.126.53:9090 -71.237.41.13:9090 -71.237.98.13:9090 -72.198.36.75:9090 -75.64.232.63:9090 -75.64.35.123:9090 -75.66.1.212:9090 -75.85.136.141:9090 -76.106.127.211:9090 -76.116.82.97:9090 -76.117.245.191:9090 -76.183.112.143:9090 -76.28.0.206:9090 -76.28.250.36:9090 -76.29.10.61:11055 -81.192.16.53:6588 -82.238.120.88:8080 -82.32.122.223:9090 -82.34.88.122:9090 -82.35.73.55:9090 -86.4.25.128:9090 -86.6.122.30:9090 -87.116.164.85:6588 -87.120.67.39:6588 -88.165.169.130:9090 -88.171.218.44:9090 -88.172.20.212:11033 -96.21.139.56:9090 -96.28.160.240:9090 -98.163.204.145:9090 -98.181.63.127:9090 -98.192.95.181:9090 -98.216.3.48:9090 -98.223.204.15:9090 -98.240.186.255:9090 -99.199.237.158:9090 -125.128.25.151:11011 -157.182.52.224:9090 -160.7.251.98:9090 -189.111.166.103:6588 -189.18.103.16:6588 -200.150.47.167:6588 -200.223.197.176:6588 -201.40.178.226:6588 -202.105.31.68:443 -204.111.219.182:9090 -208.53.199.48:9090 -208.53.199.75:9090 -211.90.22.106:8088 -216.51.236.24:9090 -218.206.194.247:8800 -218.75.23.110:3128 -218.75.76.74:8088 -218.97.194.94:80 -219.121.151.29:80 -24.125.217.129:9090 -24.13.108.167:9090 -24.164.253.14:9090 -24.175.116.55:9090 -24.186.57.77:9090 -24.188.121.167:9090 -24.19.124.152:9090 -24.190.104.34:9090 -24.190.65.208:9090 -24.20.45.101:9090 -24.201.119.10:9090 -24.209.62.190:9090 -24.211.49.0:9090 -24.23.182.99:9090 -24.231.250.241:7212 -24.254.113.238:9090 -24.255.219.247:9090 -24.5.195.141:9090 -24.8.191.246:9090 -58.20.51.207:808 -59.92.40.60:6588 -61.234.254.69:8088 -63.245.152.3:9090 -64.136.197.96:9090 -64.179.170.189:9090 -66.165.197.37:9090 -66.175.164.59:9090 -66.177.219.202:9090 -66.214.17.189:9090 -66.229.205.251:9090 -66.38.121.88:9090 -66.91.101.52:9090 -67.162.165.118:9090 -67.165.189.3:9090 -67.188.156.177:9090 -67.48.22.73:9090 -67.84.148.144:9090 -67.84.35.131:9090 -67.87.24.194:9090 -68.104.55.221:9090 -68.11.237.184:9090 -68.199.153.104:9090 -68.201.24.46:9090 -68.225.96.18:9090 -68.8.224.217:9090 -68.81.191.233:9090 -68.83.91.141:9090 -69.116.42.119:9090 -69.122.222.90:9090 -69.124.132.102:9090 -69.127.175.231:9090 -69.136.70.21:9090 -69.161.78.160:9090 -69.246.61.14:9090 -69.249.151.19:9090 -69.47.165.83:9090 -69.47.174.178:9090 -70.177.47.119:9090 -70.177.53.179:9090 -70.180.206.70:9090 -70.180.62.153:9090 -70.186.174.186:9090 -70.225.83.202:9090 -70.64.225.85:9090 -71.192.234.31:9090 -71.204.73.210:9090 -71.205.238.140:9090 -71.205.238.236:9090 -71.207.56.148:9090 -71.224.107.188:9090 -71.226.155.186:9090 -71.64.8.82:9090 -72.134.20.99:9090 -72.178.207.48:9090 -72.188.68.197:9090 -72.227.236.241:9090 -72.227.36.24:9090 -74.15.86.86:9090 -75.108.122.150:9090 -75.181.46.254:9090 -75.64.211.234:9090 -75.87.189.110:9090 -76.102.161.57:9090 -76.102.95.54:9090 -76.110.211.162:9090 -76.114.240.59:9090 -76.115.37.7:9090 -76.182.53.239:9090 -76.26.216.31:9090 -76.89.23.238:9090 -80.193.168.196:9090 -80.25.151.126:15353 -80.4.60.88:9090 -82.2.234.184:9090 -82.38.36.40:9090 -82.4.69.11:9090 -89.241.213.95:9090 -92.9.76.236:9090 -96.18.67.70:9090 -96.3.152.82:9090 -96.3.172.29:9090 -97.85.152.126:9090 -97.91.188.113:9090 -98.121.129.95:9090 -98.193.185.111:9090 -98.194.172.64:9090 -98.247.53.216:9090 -99.139.195.186:9090 -99.228.98.56:9090 -99.242.140.117:9090 -123.127.110.243:80 -189.19.233.32:6588 -194.117.157.72:27076 -201.17.130.27:6588 -201.228.130.234:6588 -201.251.34.65:6588 -204.112.136.25:9090 -210.52.15.210:808 -211.167.248.228:8080 -216.164.170.134:9090 -218.194.80.220:808 -220.174.209.178:808 -24.10.186.31:9090 -24.118.147.89:9090 -24.151.126.249:9090 -24.175.139.203:9090 -24.2.4.204:9090 -24.22.86.147:9090 -24.77.22.225:9090 -24.90.177.18:9090 -64.111.32.54:9090 -64.83.209.110:9090 -65.30.92.48:9090 -66.57.1.142:9090 -66.99.71.247:9090 -67.9.255.2:9090 -67.9.28.224:9090 -68.105.0.173:9090 -68.144.70.254:9090 -68.62.176.8:9090 -69.151.73.128:9090 -69.246.218.125:9090 -69.246.38.184:9090 -70.127.205.107:9090 -70.186.168.130:9090 -70.26.57.239:9090 -71.178.155.167:9090 -71.192.233.196:9090 -71.200.233.55:9090 -71.89.55.232:9090 -72.190.121.79:9090 -72.24.212.232:9090 -72.24.213.96:9090 -72.9.72.199:9090 -74.131.139.186:9090 -74.192.92.162:9090 -74.79.165.21:9090 -75.83.57.219:9090 -76.170.85.232:9090 -76.68.66.51:9090 -76.84.237.206:9090 -77.97.26.167:9090 -82.33.117.189:9090 -82.35.165.44:9090 -82.40.28.187:9090 -82.44.239.26:9090 -82.6.69.14:9090 -86.42.180.157:9090 -98.206.20.88:9090 -98.210.111.207:9090 -99.253.188.98:9090 -142.59.52.201:9090 -173.32.150.159:9090 -189.79.63.28:6588 -200.161.6.214:6588 -200.58.206.145:6588 -201.21.210.84:6588 -201.254.56.35:6588 -207.181.207.36:9090 -217.119.20.82:3128 -24.166.174.56:7212 -24.170.82.144:9090 -24.2.69.26:9090 -24.208.37.143:9090 -24.30.90.20:9090 -60.28.209.8:80 -65.185.5.17:9090 -66.186.237.190:9090 -66.57.75.68:9090 -66.67.106.227:9090 -68.104.19.90:9090 -68.113.102.37:9090 -68.117.211.122:9090 -68.118.245.35:9090 -68.207.186.253:9090 -69.245.52.76:9090 -70.74.213.38:9090 -72.141.35.81:9090 -72.141.53.106:9090 -72.174.161.51:9090 -75.18.209.8:7212 -75.183.7.150:9090 -76.107.137.6:9090 -80.192.65.203:9090 -82.6.184.38:9090 -83.220.195.232:9090 -92.233.226.34:9090 -92.234.58.166:9090 -97.87.101.89:9090 -98.161.24.110:9090 -99.199.229.95:9090 -99.21.134.25:9090 -201.3.53.102:6588 -204.85.72.128:9090 -216.117.225.240:9090 -24.168.35.163:9090 -24.179.152.2:9090 -59.56.174.199:808 -65.28.107.26:9090 -65.28.8.13:9090 -67.177.158.94:9090 -67.68.46.169:9090 -68.188.91.22:9090 -68.4.217.7:9090 -70.82.140.29:9090 -71.90.230.116:9090 -72.136.252.159:9090 -76.27.54.31:9090 -77.99.183.136:9090 -78.43.175.129:9090 -80.6.34.116:9090 -86.2.160.241:9090 -88.109.194.140:9090 -201.86.70.162:80 -208.53.196.161:9090 -66.253.168.169:9090 -66.26.117.254:9090 -67.61.171.202:9090 -70.125.110.220:9090 -71.200.118.56:9090 -75.152.23.65:9090 -76.107.94.147:9090 -81.106.225.136:9090 -92.238.40.83:9090 -200.220.222.215:6588 -201.26.133.204:8080 -209.159.241.112:9090 -24.78.155.155:9090 -66.57.230.14:9090 -70.76.83.81:9090 -75.68.212.175:9090 -24.11.124.76:9090 -66.253.232.57:9090 -91.188.161.235:3128 -86.12.223.22:9090 -116.7.255.88:8080 -124.107.149.63:3128 -20.132.16.22:80 -201.245.152.230:80 -202.129.181.242:8080 -202.83.174.14:8080 -202.99.29.27:80 -207.248.228.166:80 -212.95.178.130:80 -219.102.226.100:8080 -62.69.178.8:80 -70.86.138.210:8125 -114.30.47.10:80 -117.102.54.110:8080 -12.130.107.115:80 -130.225.245.156:8080 -131.247.155.3:9090 -142.33.100.41:8080 -165.166.201.241:8080 -190.144.112.42:8080 -192.115.90.150:8080 -210.51.22.185:80 -212.119.85.164:80 -212.191.130.227:8080 -212.24.237.49:8080 -213.130.111.206:8080 -219.198.208.140:8080 -219.53.216.132:8080 -221.249.144.93:8088 -222.188.91.85:8080 -24.72.74.152:9090 -38.103.164.174:9090 -58.22.101.251:80 -61.19.222.7:80 -62.99.163.242:8080 -66.171.189.82:9090 -66.187.205.130:80 -71.227.225.115:8000 -75.148.238.58:8080 -75.68.4.225:9090 -76.110.43.10:7212 -82.76.17.46:8080 -83.141.17.20:80 -85.105.144.28:8088 -87.245.155.179:80 -89.189.84.1:80 -89.189.84.118:80 -89.189.85.20:80 -89.189.85.67:80 -91.203.132.59:3128 -119.147.23.118:8080 -211.100.4.71:80 -218.4.65.118:8080 -219.106.246.6:80 -219.117.209.83:80 -219.133.180.67:80 -219.142.121.37:80 -220.157.98.136:80 -221.11.27.110:8080 -24.126.135.71:9090 -58.42.242.10:8089 -60.213.25.170:3128 -62.150.81.35:80 -78.109.149.162:3128 -120.28.64.69:8080 -160.97.36.70:8000 -193.173.119.83:8080 -195.209.224.91:3128 -200.163.66.116:80 -203.67.172.25:8080 -207.157.9.179:9090 -212.14.233.47:3128 -213.97.52.28:8080 -219.239.90.83:80 -221.91.128.132:8080 -67.162.88.110:9090 -67.85.66.187:9090 -82.233.131.115:7212 -121.73.79.171:9090 -189.19.231.136:8000 -194.176.176.82:8080 -202.106.121.134:80 -206.64.92.16:8000 -208.77.219.76:80 -63.162.106.70:80 -64.184.84.246:80 -66.253.186.239:9090 -70.181.122.211:9090 -75.148.238.57:8080 -75.185.79.63:8080 -80.126.22.246:80 -195.248.239.142:80 -200.243.51.252:80 -203.94.229.227:8080 -210.86.181.202:80 -24.189.79.154:8000 -62.150.126.228:80 -200.104.250.92:80 -66.146.139.235:8000 -85.252.212.138:9090 -118.98.232.202:8080 -151.11.232.92:80 -203.70.96.9:80 -216.195.37.250:82 -74.54.156.73:80 -195.226.16.132:80 -200.55.208.203:80 -59.61.88.193:80 -211.138.198.6:10917 -213.132.44.241:80 -217.172.187.98:80 -220.224.224.66:3128 -92.64.178.98:8080 -62.149.95.43:8080 -208.81.7.18:80 -91.121.84.92:80 -125.89.73.159:80 -116.90.129.134:80 -159.148.82.3:3128 -189.51.255.19:8080 -192.203.138.63:80 -193.45.143.12:80 -193.45.143.41:80 -194.44.221.137:3128 -200.165.90.114:8080 -200.171.58.137:3128 -200.211.233.132:3128 -200.213.117.2:8080 -200.49.141.56:8080 -201.133.171.213:8080 -201.14.116.3:3128 -202.134.202.144:80 -202.40.182.82:8080 -202.71.103.109:3128 -206.227.34.10:3128 -208.45.143.105:80 -208.67.195.32:80 -208.74.50.197:3128 -209.124.34.27:80 -209.20.82.228:80 -209.235.218.83:80 -209.237.236.110:80 -209.34.241.44:80 -216.14.98.225:80 -216.173.2.205:8080 -217.12.51.38:8080 -218.107.24.3:3128 -222.92.51.221:808 -59.93.43.186:3128 -60.217.80.36:8080 -60.6.205.20:808 -63.146.109.190:80 -65.199.160.6:80 -65.243.68.184:80 -65.57.240.117:80 -65.57.240.121:80 -65.57.240.174:80 -65.57.240.176:80 -65.57.240.177:80 -65.57.240.234:80 -66.77.122.132:80 -66.77.122.135:80 -66.94.172.129:8080 -67.15.104.28:80 -77.42.159.251:80 -79.188.118.70:8080 -80.51.221.94:3128 -80.81.52.225:8080 -81.177.3.10:3128 -83.170.109.8:8080 -87.204.246.122:8080 -87.204.246.192:8080 -87.204.246.68:8080 -87.204.246.78:8080 -88.191.44.184:8080 -89.208.43.141:80 -91.121.61.20:80 -119.70.40.101:8080 -121.14.211.71:80 -121.15.164.9:3128 -121.207.252.139:80 -121.80.179.213:80 -159.148.82.4:3128 -189.108.155.120:3128 -189.21.136.84:3128 -190.24.8.82:3128 -190.65.211.154:8080 -190.65.224.10:80 -195.248.240.146:8080 -195.251.38.71:8888 -200.207.9.168:3128 -200.223.102.82:80 -200.223.19.101:3128 -200.29.106.240:3128 -200.43.141.173:8080 -201.24.240.130:3128 -201.65.121.102:3128 -202.107.231.157:8080 -202.181.211.237:80 -202.29.22.12:3128 -202.29.22.3:3128 -202.29.22.4:3128 -203.153.214.146:80 -203.160.1.103:80 -203.160.1.112:80 -203.160.1.121:80 -203.160.1.130:554 -203.160.1.162:554 -203.160.1.66:80 -203.77.192.92:80 -203.89.30.142:80 -208.45.143.104:80 -210.238.184.150:3128 -212.109.155.130:8080 -212.117.162.228:3128 -212.93.193.72:443 -212.93.193.74:443 -212.93.193.78:443 -212.93.193.82:443 -212.93.193.83:443 -213.207.35.43:80 -218.17.246.197:3128 -218.75.100.114:8080 -219.101.35.83:3128 -219.132.142.10:8080 -219.134.242.204:3128 -219.159.77.144:3128 -220.227.31.179:8080 -220.227.47.6:8080 -220.227.47.7:8080 -221.215.143.18:808 -221.224.53.83:3128 -221.230.139.83:3128 -222.124.5.92:8080 -222.223.127.130:808 -222.73.227.233:80 -222.73.86.75:80 -58.242.42.91:3128 -58.254.201.237:3128 -60.2.93.11:80 -66.104.77.20:3128 -66.197.252.182:80 -66.198.41.11:3128 -67.19.148.234:3128 -67.228.42.208:80 -67.69.254.243:80 -67.69.254.246:80 -67.69.254.248:80 -67.69.254.252:80 -67.69.254.254:80 -67.69.254.255:80 -72.249.149.144:80 -75.126.176.161:80 -77.240.82.6:80 -80.91.112.202:8080 -82.192.83.7:3128 -85.214.59.79:8080 -87.224.243.113:3128 -87.237.228.18:8080 -89.144.96.79:808 -89.210.114.220:8080 -89.31.143.2:80 -95.82.194.72:3128 -117.102.15.218:8080 -117.102.60.205:8080 -117.102.93.131:3128 -118.144.187.156:80 -119.10.182.20:80 -119.146.73.114:8080 -12.191.44.111:80 -12.228.215.52:9090 -12.228.215.61:9090 -12.228.215.62:9090 -121.22.29.180:80 -121.22.29.181:80 -121.22.29.182:80 -121.22.29.183:80 -122.224.97.85:3128 -123.49.42.81:8080 -124.106.77.200:3128 -124.164.247.43:3128 -124.30.18.245:3128 -124.81.45.233:80 -125.242.128.78:8080 -125.93.187.234:3128 -187.4.61.2:3128 -189.21.246.135:3128 -190.136.215.70:3128 -196.212.252.194:8080 -200.174.31.29:3128 -200.254.12.2:8080 -200.254.221.146:3128 -201.26.37.196:3128 -201.75.105.179:8080 -203.160.1.75:80 -203.160.1.85:80 -203.160.1.94:80 -203.162.183.222:80 -203.162.7.82:8080 -203.172.138.187:80 -203.177.152.162:3128 -203.179.87.186:3128 -211.95.176.6:808 -218.0.1.30:8080 -218.14.227.197:3128 -219.136.252.120:3128 -219.239.90.85:3128 -219.239.90.96:3128 -221.203.154.26:8080 -222.122.19.72:80 -222.122.19.76:80 -222.124.9.122:80 -222.135.178.62:8080 -222.208.242.30:3128 -222.239.222.56:8080 -41.242.150.184:3128 -41.244.100.108:3128 -58.222.254.13:3128 -58.61.143.137:3128 -59.18.13.75:80 -60.188.118.172:8088 -60.28.252.206:3128 -61.139.73.6:8080 -61.144.24.106:3128 -64.66.192.62:80 -67.69.254.245:80 -69.90.107.184:80 -78.138.4.34:80 -89.211.49.84:3128 -89.31.143.3:80 -118.98.172.34:3128 -196.29.205.45:80 -201.26.74.58:3128 -201.6.125.234:8080 -201.9.131.84:80 -203.151.233.143:80 -212.116.208.242:3128 -220.128.169.153:3128 -222.215.152.139:8080 -59.120.47.230:3128 -60.249.176.80:3128 -60.49.177.228:3128 -67.69.254.240:80 -67.69.254.244:80 -67.69.254.250:80 -77.71.0.246:3128 -82.206.129.160:3128 -89.207.240.235:3128 -12.228.215.55:9090 -122.224.222.134:3128 -125.22.252.134:80 -196.202.28.38:80 -200.199.22.92:3128 -200.201.187.250:3128 -200.41.60.135:3128 -201.251.159.7:3128 -201.48.46.3:3128 -60.12.190.109:8088 -60.251.140.10:3128 -67.69.254.253:80 -86.105.181.238:3128 -87.120.60.3:8080 -89.31.143.1:80 -120.50.176.58:8080 -122.224.171.91:3128 -187.0.92.17:3128 -189.108.124.155:3128 -189.85.162.190:3128 -190.18.106.88:80 -194.44.170.81:3128 -195.234.182.13:3128 -200.242.232.8:8080 -219.88.253.41:80 -222.187.221.91:80 -87.120.58.65:8080 -89.28.20.132:3128 -170.210.18.245:8080 -187.5.67.55:80 -196.29.205.49:80 -201.82.32.56:8080 -203.73.180.16:3128 -59.120.77.147:3128 -60.190.151.77:8088 -118.98.169.66:80 -164.100.42.68:8080 -189.114.225.170:80 -189.74.11.18:8080 -201.17.130.41:3128 -210.51.14.197:80 -213.169.51.151:8080 -213.185.116.152:8080 -213.215.7.108:3128 -58.213.152.15:3128 -60.30.83.220:8080 -60.50.80.111:3128 -61.19.213.91:80 -67.69.254.247:80 -67.69.254.251:80 -81.85.253.74:80 -89.42.17.147:3128 -91.121.14.70:3128 -219.93.178.162:3128 -68.100.4.165:3128 -85.114.131.54:8081 -87.120.57.8:8080 -187.2.151.207:3128 -201.75.54.245:3128 -202.101.8.184:80 -220.194.221.29:3128 -222.124.172.220:8080 -123.255.250.5:3128 -217.10.246.2:8080 -217.133.80.15:3128 -61.105.175.113:88 -61.247.7.8:3128 -221.2.145.28:3128 -200.243.243.150:3128 -212.0.143.5:3128 -123.127.110.244:80 -200.109.72.53:6588 -201.15.184.136:6588 -201.44.24.98:80 -201.53.164.111:3128 -201.53.73.44:6588 -24.23.29.41:8080 -58.34.239.226:8088 -59.95.161.172:6588 -59.99.129.193:6588 -66.199.247.42:6649 -72.55.191.6:3128 -86.122.132.27:8080 -92.48.127.2:8080 -121.204.86.172:8088 -121.206.106.110:8090 -195.135.236.204:3128 -200.118.158.45:6588 -201.13.183.108:6588 -201.43.215.59:6588 -201.88.248.243:6588 -202.134.202.251:80 -203.86.31.92:3128 -216.183.95.239:80 -24.125.134.220:9090 -24.125.71.100:9090 -24.156.135.87:9090 -24.188.125.225:9090 -24.239.46.244:9090 -24.61.52.46:9090 -65.182.107.212:9090 -65.30.216.140:9090 -67.191.141.209:9090 -67.84.241.17:9090 -67.86.138.122:9090 -68.11.182.166:9090 -68.11.249.230:9090 -68.42.122.142:9090 -68.43.246.42:9090 -68.45.42.160:9090 -68.52.135.28:9090 -68.55.132.134:9090 -68.55.225.102:9090 -69.119.243.192:9090 -69.127.102.247:9090 -69.136.140.1:9090 -69.136.58.38:9090 -69.251.251.153:9090 -69.253.188.82:9090 -71.57.210.123:9090 -72.196.135.11:9090 -74.131.165.152:9090 -74.141.111.159:9090 -76.107.38.217:9090 -76.107.44.181:9090 -76.127.22.84:9090 -76.22.0.234:9090 -76.30.187.68:9090 -80.143.226.158:8080 -82.41.96.73:9090 -82.43.58.68:9090 -82.5.189.202:7212 -98.141.23.139:9090 -98.192.124.144:9090 -98.27.180.252:9090 -99.163.104.229:9090 -99.233.94.169:9090 -12.208.190.71:9090 -12.51.72.38:9090 -123.6.23.77:8090 -142.59.90.148:9090 -200.3.252.170:3128 -201.75.9.88:6588 -210.245.63.218:80 -216.80.118.13:9090 -220.72.71.220:8080 -222.179.57.225:808 -24.12.214.237:9090 -24.127.136.205:9090 -24.161.131.67:9090 -24.189.5.235:9090 -24.192.240.240:9090 -24.228.49.186:9090 -24.59.34.24:9090 -58.33.52.65:8088 -61.191.53.137:808 -61.238.104.200:808 -65.29.85.76:9090 -66.167.228.62:9090 -67.163.161.226:9090 -67.217.17.182:9090 -67.84.196.173:9090 -67.87.64.23:9090 -68.11.145.150:9090 -68.194.169.106:7212 -68.49.227.31:9090 -69.142.108.83:9090 -69.142.114.143:9090 -69.181.224.191:9090 -69.71.85.202:9090 -70.172.242.76:9090 -70.95.110.195:9090 -71.101.126.174:9090 -71.12.195.20:9090 -71.82.77.13:9090 -71.86.150.78:9090 -72.174.123.174:9090 -74.211.5.196:9090 -75.34.25.118:7212 -75.87.150.14:9090 -76.104.70.144:9090 -76.105.105.96:9090 -76.123.18.157:9090 -76.176.208.180:9090 -76.228.201.224:9090 -76.9.38.239:9090 -76.98.163.114:9090 -77.99.40.240:9090 -80.195.186.31:9090 -81.105.32.186:9090 -86.17.234.113:9090 -87.232.56.70:9090 -92.236.137.151:9090 -98.169.171.231:9090 -98.181.60.131:9090 -98.202.107.151:9090 -98.208.46.176:9090 -98.210.147.111:9090 -99.155.153.203:9090 -12.167.39.75:9090 -12.240.37.195:9090 -156.34.176.45:9090 -171.66.223.221:9090 -174.0.50.242:9090 -200.89.39.210:6588 -207.50.148.37:9090 -210.34.14.166:81 -218.6.15.70:8080 -221.11.11.202:30781 -24.125.158.161:9090 -59.37.166.56:3128 -64.17.66.234:9090 -65.25.158.119:7212 -65.31.98.112:9090 -66.184.78.194:9090 -68.11.226.141:9090 -68.118.147.60:9090 -69.246.123.26:9090 -70.101.148.107:9090 -70.176.119.94:9090 -70.179.177.238:9090 -71.14.93.226:9090 -71.14.94.189:9090 -71.229.16.100:9090 -71.86.181.6:9090 -71.89.7.99:28848 -72.128.40.214:9090 -72.203.130.111:9090 -74.222.221.250:9090 -74.77.117.65:9090 -76.22.128.2:9090 -77.96.218.150:9090 -82.22.138.43:9090 -82.36.17.30:9090 -99.254.218.6:9090 -202.98.141.200:808 -203.129.53.177:9090 -222.242.221.156:8080 -24.230.163.136:9090 -65.190.207.153:9090 -66.211.127.78:9090 -68.111.231.178:9090 -69.250.8.55:9090 -69.46.16.232:34311 -70.24.40.159:9090 -71.17.227.187:9090 -71.71.69.209:9090 -74.60.191.215:9090 -75.81.22.134:9090 -75.94.87.64:9090 -76.102.189.58:9090 -76.107.111.45:9090 -81.103.66.124:9090 -82.39.25.203:9090 -82.41.10.6:9090 -92.233.176.50:9090 -99.252.209.25:9090 -202.149.113.148:8080 -206.174.3.131:9090 -207.192.207.240:9090 -209.124.242.193:9090 -209.145.114.173:9090 -24.108.35.246:7212 -24.174.246.62:9090 -24.197.130.9:9090 -24.70.39.70:9090 -65.50.35.144:9090 -65.50.35.157:9090 -68.13.235.68:9090 -71.205.51.156:9090 -74.197.219.75:9090 -75.119.93.168:9090 -76.107.42.95:9090 -76.107.93.40:9090 -81.100.109.24:9090 -82.29.59.18:9090 -82.33.114.102:9090 -82.38.0.167:9090 -92.233.166.55:9090 -194.117.157.72:14624 -208.59.142.26:9090 -68.183.54.233:7212 -81.106.241.109:9090 -82.33.168.194:9090 -86.61.76.7:8088 -99.225.136.21:9090 -24.68.44.46:7212 -68.98.0.233:9090 -24.14.107.77:9090 -208.149.67.192:9090 -72.222.172.142:9090 -76.9.42.163:9090 -77.103.136.52:9090 -99.253.240.184:9090 -193.69.186.83:80 -70.111.46.122:7212 -81.98.109.201:9090 -71.109.3.95:7212 -82.46.144.165:9090 -163.29.250.4:3128 -190.67.73.194:8080 -193.2.8.8:80 -212.193.5.226:80 -217.12.254.154:80 -220.15.224.200:8080 -221.25.244.167:8080 -67.84.8.11:7212 -80.88.242.32:9090 -84.113.7.221:8080 -124.35.158.72:80 -203.82.52.210:8080 -219.191.64.95:8080 -220.22.196.3:80 -24.44.64.205:8000 -67.208.3.220:8080 -68.196.110.175:8000 -68.43.151.18:9090 -72.191.26.100:7212 -72.214.91.6:8008 -74.210.245.5:8008 -84.23.101.224:8080 -91.203.68.222:80 -92.239.116.56:9090 -98.165.169.136:8000 -124.29.140.84:80 -124.5.165.8:8080 -195.34.253.66:3128 -200.253.135.236:80 -212.11.179.159:80 -212.57.189.242:80 -213.166.145.170:80 -216.102.26.104:8000 -41.240.178.30:80 -76.31.231.123:8000 -220.53.245.8:8080 \ No newline at end of file From 94a241f5c0a88f3c69d3de06deea2ab07798c724 Mon Sep 17 00:00:00 2001 From: devkyato Date: Wed, 29 Jul 2026 09:23:15 +0800 Subject: [PATCH 2/4] feat(protocol): add the ONP/1 Python reference stack --- python/README.md | 23 +++ python/examples/echo_server.py | 25 +++ python/examples/sensor_client.py | 28 ++++ python/pyproject.toml | 57 +++++++ python/src/opennet/__init__.py | 28 ++++ python/src/opennet/cli.py | 50 ++++++ python/src/opennet/client.py | 168 ++++++++++++++++++++ python/src/opennet/protocol.py | 256 +++++++++++++++++++++++++++++++ python/src/opennet/server.py | 149 ++++++++++++++++++ python/src/opennet/stream.py | 22 +++ python/tests/test_integration.py | 61 ++++++++ python/tests/test_protocol.py | 97 ++++++++++++ 12 files changed, 964 insertions(+) create mode 100644 python/README.md create mode 100644 python/examples/echo_server.py create mode 100644 python/examples/sensor_client.py create mode 100644 python/pyproject.toml create mode 100644 python/src/opennet/__init__.py create mode 100644 python/src/opennet/cli.py create mode 100644 python/src/opennet/client.py create mode 100644 python/src/opennet/protocol.py create mode 100644 python/src/opennet/server.py create mode 100644 python/src/opennet/stream.py create mode 100644 python/tests/test_integration.py create mode 100644 python/tests/test_protocol.py diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..fa19f1f --- /dev/null +++ b/python/README.md @@ -0,0 +1,23 @@ +# OpenNet for Python + +The reference ONP/1 client and server for Raspberry Pi and backend systems. + +```bash +python -m pip install opennet-protocol +opennet-server --host 0.0.0.0 --port 8765 +``` + +```python +import asyncio +from opennet import OpenNetClient + +async def main(): + async with OpenNetClient("127.0.0.1") as client: + await client.send("demo/message", {"hello": "OpenNet"}) + +asyncio.run(main()) +``` + +The complete documentation, ESP32 Arduino library, examples, protocol +specification, and security guidance are in the +[OpenNet repository](https://github.com/devkyato/OpenNet). diff --git a/python/examples/echo_server.py b/python/examples/echo_server.py new file mode 100644 index 0000000..6f28459 --- /dev/null +++ b/python/examples/echo_server.py @@ -0,0 +1,25 @@ +"""Reply to each OpenNet DATA frame.""" + +import asyncio + +from opennet import Frame, OpenNetServer, Peer + + +async def echo(peer: Peer, frame: Frame) -> None: + print(peer.address, frame.topic, len(frame.payload)) + await peer.send_frame( + Frame( + frame.kind, + frame.value_type, + f"echo/{frame.topic}", + frame.payload, + frame.message_id, + ) + ) + + +async def main() -> None: + await OpenNetServer(echo, host="0.0.0.0").serve_forever() + + +asyncio.run(main()) diff --git a/python/examples/sensor_client.py b/python/examples/sensor_client.py new file mode 100644 index 0000000..91ff79f --- /dev/null +++ b/python/examples/sensor_client.py @@ -0,0 +1,28 @@ +"""Send sample telemetry to an OpenNet server.""" + +import argparse +import asyncio +import random + +from opennet import OpenNetClient + + +async def run(host: str, port: int) -> None: + async with OpenNetClient(host, port) as client: + while True: + await client.send( + "lab/telemetry", + { + "temperature_c": round(random.uniform(22, 28), 2), + "source": "python-example", + }, + ) + await asyncio.sleep(2) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("host") + parser.add_argument("--port", type=int, default=8765) + args = parser.parse_args() + asyncio.run(run(args.host, args.port)) diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..ed60ecd --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,57 @@ +[build-system] +requires = ["hatchling>=1.25"] +build-backend = "hatchling.build" + +[project] +name = "opennet-protocol" +version = "0.1.0" +description = "Typed ONP/1 messaging for Raspberry Pi and backend systems" +readme = "README.md" +requires-python = ">=3.9" +license = "MIT" +authors = [{name = "devkyato"}] +keywords = ["iot", "esp32", "raspberry-pi", "messaging", "tcp"] +classifiers = [ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Topic :: System :: Networking" +] +dependencies = [] + +[project.optional-dependencies] +dev = [ + "mypy>=1.10", + "pytest>=8", + "pytest-asyncio>=0.23", + "ruff>=0.5" +] + +[project.scripts] +opennet-server = "opennet.cli:main" + +[project.urls] +Homepage = "https://github.com/devkyato/OpenNet" +Documentation = "https://github.com/devkyato/OpenNet/tree/main/docs" +Issues = "https://github.com/devkyato/OpenNet/issues" + +[tool.hatch.build.targets.wheel] +packages = ["src/opennet"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] + +[tool.ruff] +target-version = "py39" +line-length = 100 + +[tool.ruff.lint] +select = ["E", "F", "I", "B", "SIM"] + +[tool.mypy] +python_version = "3.9" +strict = true +packages = ["opennet"] +mypy_path = "src" diff --git a/python/src/opennet/__init__.py b/python/src/opennet/__init__.py new file mode 100644 index 0000000..e87c99d --- /dev/null +++ b/python/src/opennet/__init__.py @@ -0,0 +1,28 @@ +"""OpenNet Protocol 1 reference implementation.""" + +from .client import OpenNetClient +from .protocol import ( + Flags, + Frame, + FrameKind, + ProtocolError, + ValueType, + decode_value, + encode_value, +) +from .server import OpenNetServer, Peer + +__all__ = [ + "Flags", + "Frame", + "FrameKind", + "OpenNetClient", + "OpenNetServer", + "Peer", + "ProtocolError", + "ValueType", + "decode_value", + "encode_value", +] + +__version__ = "0.1.0" diff --git a/python/src/opennet/cli.py b/python/src/opennet/cli.py new file mode 100644 index 0000000..cc6eeab --- /dev/null +++ b/python/src/opennet/cli.py @@ -0,0 +1,50 @@ +"""Small observable OpenNet development server.""" + +from __future__ import annotations + +import argparse +import asyncio +import contextlib +import logging + +from .protocol import Frame, decode_value +from .server import OpenNetServer, Peer + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run an ONP/1 development server") + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=8765) + parser.add_argument("--max-payload", type=int, default=1024 * 1024) + return parser.parse_args() + + +async def run(args: argparse.Namespace) -> None: + async def log_message(peer: Peer, frame: Frame) -> None: + value = decode_value(frame.value_type, frame.payload) + summary = ( + f"<{len(value)} bytes>" if isinstance(value, bytes) else repr(value) + ) + logging.info( + "%s id=%d topic=%s value=%s", + peer.address, + frame.message_id, + frame.topic, + summary, + ) + + server = OpenNetServer( + log_message, host=args.host, port=args.port, max_payload=args.max_payload + ) + logging.info("listening on %s:%d", args.host, args.port) + await server.serve_forever() + + +def main() -> None: + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") + with contextlib.suppress(KeyboardInterrupt): + asyncio.run(run(parse_args())) + + +if __name__ == "__main__": + main() diff --git a/python/src/opennet/client.py b/python/src/opennet/client.py new file mode 100644 index 0000000..d65d5bb --- /dev/null +++ b/python/src/opennet/client.py @@ -0,0 +1,168 @@ +"""Async OpenNet client.""" + +from __future__ import annotations + +import asyncio +import contextlib +import ssl +from collections import deque +from types import TracebackType +from typing import Any, AsyncIterator, Deque, Optional, Type + +from .protocol import ( + DEFAULT_MAX_PAYLOAD, + Flags, + Frame, + FrameKind, + ProtocolError, + encode_value, +) +from .stream import read_frame, write_frame + + +class OpenNetClient: + """A single ONP/1 connection with typed send/receive operations.""" + + def __init__( + self, + host: str, + port: int = 8765, + *, + ssl_context: Optional[ssl.SSLContext] = None, + max_payload: int = DEFAULT_MAX_PAYLOAD, + ack_timeout: float = 5.0, + ) -> None: + self.host = host + self.port = port + self.ssl_context = ssl_context + self.max_payload = max_payload + self.ack_timeout = ack_timeout + self._reader: Optional[asyncio.StreamReader] = None + self._writer: Optional[asyncio.StreamWriter] = None + self._next_id = 1 + self._pending: dict[int, asyncio.Future[None]] = {} + self._messages: asyncio.Queue[Frame] = asyncio.Queue() + self._backlog: Deque[Frame] = deque() + self._reader_task: Optional[asyncio.Task[None]] = None + + @property + def connected(self) -> bool: + return self._writer is not None and not self._writer.is_closing() + + async def connect(self) -> None: + if self.connected: + return + self._reader, self._writer = await asyncio.open_connection( + self.host, self.port, ssl=self.ssl_context + ) + sock = self._writer.get_extra_info("socket") + if sock is not None: + sock.setsockopt(6, 1, 1) # IPPROTO_TCP, TCP_NODELAY + self._reader_task = asyncio.create_task(self._read_loop(), name="opennet-reader") + + async def close(self) -> None: + writer = self._writer + if writer is None: + return + with contextlib.suppress(Exception): + await write_frame(writer, Frame(FrameKind.CLOSE), self.max_payload) + writer.close() + with contextlib.suppress(Exception): + await writer.wait_closed() + if self._reader_task is not None: + self._reader_task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await self._reader_task + self._reader = None + self._writer = None + self._fail_pending(ConnectionError("OpenNet connection closed")) + + async def send( + self, + topic: str, + value: Any, + *, + require_ack: bool = True, + ) -> int: + writer = self._require_writer() + value_type, payload = encode_value(value) + message_id = self._next_message_id() + flags = Flags.ACK_REQUIRED if require_ack else Flags.NONE + frame = Frame(FrameKind.DATA, value_type, topic, payload, message_id, flags) + future: Optional[asyncio.Future[None]] = None + if require_ack: + future = asyncio.get_running_loop().create_future() + self._pending[message_id] = future + try: + await write_frame(writer, frame, self.max_payload) + if future is not None: + await asyncio.wait_for(future, self.ack_timeout) + finally: + self._pending.pop(message_id, None) + return message_id + + async def receive(self) -> Frame: + if self._backlog: + return self._backlog.popleft() + return await self._messages.get() + + async def messages(self) -> AsyncIterator[Frame]: + while self.connected: + yield await self.receive() + + async def ping(self, timeout: float = 5.0) -> None: + writer = self._require_writer() + await write_frame(writer, Frame(FrameKind.PING), self.max_payload) + while True: + frame = await asyncio.wait_for(self._messages.get(), timeout) + if frame.kind is FrameKind.PONG: + return + self._backlog.append(frame) + + async def __aenter__(self) -> "OpenNetClient": + await self.connect() + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + await self.close() + + def _require_writer(self) -> asyncio.StreamWriter: + if not self.connected or self._writer is None: + raise ConnectionError("OpenNet client is not connected") + return self._writer + + def _next_message_id(self) -> int: + message_id = self._next_id + self._next_id = 1 if message_id == 0xFFFFFFFF else message_id + 1 + return message_id + + async def _read_loop(self) -> None: + assert self._reader is not None and self._writer is not None + try: + while True: + frame = await read_frame(self._reader, self.max_payload) + if frame.kind is FrameKind.ACK: + future = self._pending.get(frame.message_id) + if future is not None and not future.done(): + future.set_result(None) + elif frame.kind is FrameKind.PING: + await write_frame(self._writer, Frame(FrameKind.PONG), self.max_payload) + elif frame.kind is FrameKind.CLOSE: + return + else: + await self._messages.put(frame) + except (asyncio.IncompleteReadError, ConnectionError, ProtocolError) as exc: + self._fail_pending(exc) + finally: + if self._writer is not None: + self._writer.close() + + def _fail_pending(self, exc: BaseException) -> None: + for future in self._pending.values(): + if not future.done(): + future.set_exception(exc) diff --git a/python/src/opennet/protocol.py b/python/src/opennet/protocol.py new file mode 100644 index 0000000..b5fd0d0 --- /dev/null +++ b/python/src/opennet/protocol.py @@ -0,0 +1,256 @@ +"""ONP/1 framing and typed-value codec.""" + +from __future__ import annotations + +import json +import math +import struct +import zlib +from dataclasses import dataclass +from enum import IntEnum, IntFlag +from typing import Any, Final, Tuple + +MAGIC: Final = b"ON" +VERSION: Final = 1 +HEADER: Final = struct.Struct("!2sBBBBHIIII") +HEADER_SIZE: Final = HEADER.size +MAX_TOPIC_SIZE: Final = 1024 +DEFAULT_MAX_PAYLOAD: Final = 16 * 1024 * 1024 + + +class ProtocolError(ValueError): + """Raised when bytes violate ONP/1.""" + + +class FrameKind(IntEnum): + DATA = 1 + ACK = 2 + PING = 3 + PONG = 4 + CLOSE = 5 + ERROR = 6 + + +class Flags(IntFlag): + NONE = 0 + ACK_REQUIRED = 1 + DUPLICATE = 2 + + +class ValueType(IntEnum): + BYTES = 0 + UTF8 = 1 + JSON = 2 + INT64 = 3 + FLOAT64 = 4 + BOOL = 5 + NULL = 6 + + +@dataclass(frozen=True) +class Frame: + kind: FrameKind + value_type: ValueType = ValueType.NULL + topic: str = "" + payload: bytes = b"" + message_id: int = 0 + flags: Flags = Flags.NONE + + def __post_init__(self) -> None: + topic_bytes = self.topic.encode("utf-8") + if len(topic_bytes) > MAX_TOPIC_SIZE: + raise ProtocolError("topic is longer than 1024 UTF-8 bytes") + if self.kind is FrameKind.DATA: + if not topic_bytes: + raise ProtocolError("DATA frames require a topic") + if not 1 <= self.message_id <= 0xFFFFFFFF: + raise ProtocolError("DATA message_id must be between 1 and 2^32-1") + if not 0 <= self.message_id <= 0xFFFFFFFF: + raise ProtocolError("message_id is outside uint32 range") + if int(self.flags) & ~int(Flags.ACK_REQUIRED | Flags.DUPLICATE): + raise ProtocolError("reserved flag bits are set") + + def to_bytes(self, max_payload: int = DEFAULT_MAX_PAYLOAD) -> bytes: + topic = self.topic.encode("utf-8") + if len(self.payload) > max_payload: + raise ProtocolError(f"payload exceeds configured {max_payload}-byte limit") + checksum = zlib.crc32(topic) + checksum = zlib.crc32(self.payload, checksum) & 0xFFFFFFFF + return b"".join( + ( + HEADER.pack( + MAGIC, + VERSION, + int(self.kind), + int(self.flags), + int(self.value_type), + len(topic), + self.message_id, + len(self.payload), + checksum, + 0, + ), + topic, + self.payload, + ) + ) + + @classmethod + def from_parts( + cls, + header: bytes, + body: bytes, + max_payload: int = DEFAULT_MAX_PAYLOAD, + ) -> "Frame": + if len(header) != HEADER_SIZE: + raise ProtocolError(f"header must be exactly {HEADER_SIZE} bytes") + ( + magic, + version, + raw_kind, + raw_flags, + raw_type, + topic_length, + message_id, + payload_length, + expected_crc, + _reserved, + ) = HEADER.unpack(header) + if magic != MAGIC: + raise ProtocolError("invalid frame magic") + if version != VERSION: + raise ProtocolError(f"unsupported protocol version {version}") + if raw_flags & ~int(Flags.ACK_REQUIRED | Flags.DUPLICATE): + raise ProtocolError("reserved flag bits are set") + if topic_length > MAX_TOPIC_SIZE: + raise ProtocolError("topic exceeds protocol limit") + if payload_length > max_payload: + raise ProtocolError("payload exceeds configured limit") + if len(body) != topic_length + payload_length: + raise ProtocolError("body length does not match header") + topic_bytes = body[:topic_length] + payload = body[topic_length:] + checksum = zlib.crc32(topic_bytes) + checksum = zlib.crc32(payload, checksum) & 0xFFFFFFFF + if checksum != expected_crc: + raise ProtocolError("CRC-32 mismatch") + try: + topic = topic_bytes.decode("utf-8") + kind = FrameKind(raw_kind) + value_type = ValueType(raw_type) + except (UnicodeDecodeError, ValueError) as exc: + raise ProtocolError(str(exc)) from exc + return cls(kind, value_type, topic, payload, message_id, Flags(raw_flags)) + + +def inspect_header(header: bytes, max_payload: int = DEFAULT_MAX_PAYLOAD) -> Tuple[int, int]: + """Validate allocation-sensitive header fields and return body lengths.""" + if len(header) != HEADER_SIZE: + raise ProtocolError(f"header must be exactly {HEADER_SIZE} bytes") + ( + magic, + version, + _kind, + flags, + _type, + topic_length, + _message_id, + payload_length, + _crc, + _reserved, + ) = HEADER.unpack(header) + if magic != MAGIC or version != VERSION: + raise ProtocolError("invalid magic or unsupported version") + if flags & ~int(Flags.ACK_REQUIRED | Flags.DUPLICATE): + raise ProtocolError("reserved flag bits are set") + if topic_length > MAX_TOPIC_SIZE or payload_length > max_payload: + raise ProtocolError("declared frame size exceeds configured limit") + return topic_length, payload_length + + +def encode_value(value: Any, value_type: ValueType | None = None) -> tuple[ValueType, bytes]: + """Encode a Python value, inferring the ONP type when omitted.""" + if value_type is None: + if value is None: + value_type = ValueType.NULL + elif isinstance(value, bool): + value_type = ValueType.BOOL + elif isinstance(value, bytes): + value_type = ValueType.BYTES + elif isinstance(value, str): + value_type = ValueType.UTF8 + elif isinstance(value, int): + value_type = ValueType.INT64 + elif isinstance(value, float): + value_type = ValueType.FLOAT64 + else: + value_type = ValueType.JSON + + if value_type is ValueType.NULL: + if value is not None: + raise TypeError("NULL requires None") + return value_type, b"" + if value_type is ValueType.BOOL: + if not isinstance(value, bool): + raise TypeError("BOOL requires bool") + return value_type, bytes((int(value),)) + if value_type is ValueType.BYTES: + if not isinstance(value, bytes): + raise TypeError("BYTES requires bytes") + return value_type, value + if value_type is ValueType.UTF8: + if not isinstance(value, str): + raise TypeError("UTF8 requires str") + return value_type, value.encode("utf-8") + if value_type is ValueType.JSON: + return value_type, json.dumps( + value, ensure_ascii=False, separators=(",", ":"), allow_nan=False + ).encode("utf-8") + if value_type is ValueType.INT64: + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError("INT64 requires int") + try: + return value_type, struct.pack("!q", value) + except struct.error as exc: + raise OverflowError("integer is outside int64 range") from exc + if value_type is ValueType.FLOAT64: + if not isinstance(value, (int, float)) or isinstance(value, bool): + raise TypeError("FLOAT64 requires a number") + number = float(value) + if not math.isfinite(number): + raise ValueError("FLOAT64 must be finite") + return value_type, struct.pack("!d", number) + raise TypeError(f"unsupported value type {value_type!r}") + + +def decode_value(value_type: ValueType, payload: bytes) -> Any: + """Decode and strictly validate an ONP typed payload.""" + try: + if value_type is ValueType.NULL: + if payload: + raise ProtocolError("NULL payload must be empty") + return None + if value_type is ValueType.BOOL: + if payload not in (b"\x00", b"\x01"): + raise ProtocolError("BOOL payload must be one 0 or 1 byte") + return payload == b"\x01" + if value_type is ValueType.BYTES: + return payload + if value_type is ValueType.UTF8: + return payload.decode("utf-8") + if value_type is ValueType.JSON: + return json.loads(payload.decode("utf-8")) + if value_type is ValueType.INT64: + if len(payload) != 8: + raise ProtocolError("INT64 payload must be 8 bytes") + return struct.unpack("!q", payload)[0] + if value_type is ValueType.FLOAT64: + if len(payload) != 8: + raise ProtocolError("FLOAT64 payload must be 8 bytes") + value = struct.unpack("!d", payload)[0] + if not math.isfinite(value): + raise ProtocolError("FLOAT64 must be finite") + return value + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ProtocolError(str(exc)) from exc + raise ProtocolError(f"unsupported value type {value_type!r}") diff --git a/python/src/opennet/server.py b/python/src/opennet/server.py new file mode 100644 index 0000000..cbf61c0 --- /dev/null +++ b/python/src/opennet/server.py @@ -0,0 +1,149 @@ +"""Async OpenNet server.""" + +from __future__ import annotations + +import asyncio +import contextlib +import inspect +import ssl +from dataclasses import dataclass +from typing import Awaitable, Callable, Optional, Union + +from .protocol import ( + DEFAULT_MAX_PAYLOAD, + Flags, + Frame, + FrameKind, + ProtocolError, + ValueType, +) +from .stream import read_frame, write_frame + +Handler = Callable[["Peer", Frame], Union[None, Awaitable[None]]] + + +@dataclass +class Peer: + """A connected peer passed to server handlers.""" + + reader: asyncio.StreamReader + writer: asyncio.StreamWriter + max_payload: int + + @property + def address(self) -> object: + return self.writer.get_extra_info("peername") + + async def send_frame(self, frame: Frame) -> None: + await write_frame(self.writer, frame, self.max_payload) + + async def send( + self, topic: str, payload: bytes, message_id: int, value_type: ValueType + ) -> None: + await self.send_frame( + Frame(FrameKind.DATA, value_type, topic, payload, message_id) + ) + + async def close(self) -> None: + if not self.writer.is_closing(): + with contextlib.suppress(Exception): + await self.send_frame(Frame(FrameKind.CLOSE)) + self.writer.close() + with contextlib.suppress(Exception): + await self.writer.wait_closed() + + +class OpenNetServer: + """ONP/1 TCP/TLS server that acknowledges accepted DATA frames.""" + + def __init__( + self, + handler: Handler, + *, + host: str = "127.0.0.1", + port: int = 8765, + ssl_context: Optional[ssl.SSLContext] = None, + max_payload: int = DEFAULT_MAX_PAYLOAD, + ) -> None: + self.handler = handler + self.host = host + self.port = port + self.ssl_context = ssl_context + self.max_payload = max_payload + self._server: Optional[asyncio.AbstractServer] = None + self._peers: set[asyncio.StreamWriter] = set() + + @property + def sockets(self) -> list[object]: + sockets = getattr(self._server, "sockets", None) + return list(sockets or ()) + + async def start(self) -> None: + if self._server is not None: + return + self._server = await asyncio.start_server( + self._accept, self.host, self.port, ssl=self.ssl_context + ) + + async def serve_forever(self) -> None: + await self.start() + assert self._server is not None + async with self._server: + await self._server.serve_forever() + + async def close(self) -> None: + if self._server is not None: + self._server.close() + await self._server.wait_closed() + self._server = None + writers = list(self._peers) + for writer in writers: + writer.close() + await asyncio.gather( + *(writer.wait_closed() for writer in writers), return_exceptions=True + ) + + async def __aenter__(self) -> "OpenNetServer": + await self.start() + return self + + async def __aexit__(self, *_args: object) -> None: + await self.close() + + async def _accept( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ) -> None: + self._peers.add(writer) + peer = Peer(reader, writer, self.max_payload) + seen: set[int] = set() + try: + while True: + frame = await read_frame(reader, self.max_payload) + if frame.kind is FrameKind.CLOSE: + break + if frame.kind is FrameKind.PING: + await peer.send_frame(Frame(FrameKind.PONG)) + continue + if frame.kind is not FrameKind.DATA: + continue + duplicate = frame.message_id in seen + seen.add(frame.message_id) + if not duplicate: + result = self.handler(peer, frame) + if inspect.isawaitable(result): + await result + if frame.flags & Flags.ACK_REQUIRED: + await peer.send_frame( + Frame( + FrameKind.ACK, + message_id=frame.message_id, + flags=Flags.DUPLICATE if duplicate else Flags.NONE, + ) + ) + except (asyncio.IncompleteReadError, ConnectionError, ProtocolError): + pass + finally: + self._peers.discard(writer) + writer.close() + with contextlib.suppress(Exception): + await writer.wait_closed() diff --git a/python/src/opennet/stream.py b/python/src/opennet/stream.py new file mode 100644 index 0000000..31261b8 --- /dev/null +++ b/python/src/opennet/stream.py @@ -0,0 +1,22 @@ +"""Async stream helpers shared by client and server.""" + +from __future__ import annotations + +import asyncio + +from .protocol import DEFAULT_MAX_PAYLOAD, HEADER_SIZE, Frame, inspect_header + + +async def read_frame( + reader: asyncio.StreamReader, + max_payload: int = DEFAULT_MAX_PAYLOAD, +) -> Frame: + header = await reader.readexactly(HEADER_SIZE) + topic_length, payload_length = inspect_header(header, max_payload) + body = await reader.readexactly(topic_length + payload_length) + return Frame.from_parts(header, body, max_payload) + + +async def write_frame(writer: asyncio.StreamWriter, frame: Frame, max_payload: int) -> None: + writer.write(frame.to_bytes(max_payload)) + await writer.drain() diff --git a/python/tests/test_integration.py b/python/tests/test_integration.py new file mode 100644 index 0000000..6fe98eb --- /dev/null +++ b/python/tests/test_integration.py @@ -0,0 +1,61 @@ +import asyncio + +from opennet import Frame, FrameKind, OpenNetClient, OpenNetServer, decode_value + + +async def test_client_server_ack_and_values(): + received: asyncio.Queue[tuple[str, object]] = asyncio.Queue() + + async def handler(_peer, frame: Frame): + await received.put((frame.topic, decode_value(frame.value_type, frame.payload))) + + server = OpenNetServer(handler, host="127.0.0.1", port=0) + await server.start() + port = server.sockets[0].getsockname()[1] # type: ignore[union-attr] + try: + async with OpenNetClient("127.0.0.1", port) as client: + message_id = await client.send("lab/temperature", {"celsius": 24.5}) + assert message_id == 1 + assert await asyncio.wait_for(received.get(), 1) == ( + "lab/temperature", + {"celsius": 24.5}, + ) + await client.ping(timeout=1) + finally: + await server.close() + + +async def test_duplicate_data_is_delivered_once_and_acknowledged_twice(): + calls = 0 + ack_count = 0 + + async def handler(_peer, _frame): + nonlocal calls + calls += 1 + + server = OpenNetServer(handler, host="127.0.0.1", port=0) + await server.start() + port = server.sockets[0].getsockname()[1] # type: ignore[union-attr] + reader, writer = await asyncio.open_connection("127.0.0.1", port) + frame = Frame( + FrameKind.DATA, + topic="event", + payload=b"", + message_id=7, + flags=1, + ) + try: + writer.write(frame.to_bytes() + frame.to_bytes()) + await writer.drain() + from opennet.stream import read_frame + + for _ in range(2): + ack = await asyncio.wait_for(read_frame(reader), 1) + assert ack.kind is FrameKind.ACK + ack_count += 1 + assert calls == 1 + assert ack_count == 2 + finally: + writer.close() + await writer.wait_closed() + await server.close() diff --git a/python/tests/test_protocol.py b/python/tests/test_protocol.py new file mode 100644 index 0000000..5d20bb8 --- /dev/null +++ b/python/tests/test_protocol.py @@ -0,0 +1,97 @@ +import struct + +import pytest + +from opennet.protocol import ( + HEADER, + Flags, + Frame, + FrameKind, + ProtocolError, + ValueType, + decode_value, + encode_value, + inspect_header, +) + + +@pytest.mark.parametrize( + ("value", "expected_type"), + [ + (None, ValueType.NULL), + (True, ValueType.BOOL), + (b"\x00\xff", ValueType.BYTES), + ("hello", ValueType.UTF8), + (42, ValueType.INT64), + (3.5, ValueType.FLOAT64), + ({"ok": True}, ValueType.JSON), + ], +) +def test_typed_value_round_trip(value, expected_type): + value_type, payload = encode_value(value) + assert value_type is expected_type + assert decode_value(value_type, payload) == value + + +def test_frame_round_trip_and_known_vector(): + value_type, payload = encode_value({"celsius": 24.5}, ValueType.JSON) + frame = Frame( + FrameKind.DATA, + value_type, + "sensor/temp", + payload, + 0x01020304, + Flags.ACK_REQUIRED, + ) + encoded = frame.to_bytes() + # Fixed vector consumed by implementations in other languages. + assert encoded.hex() == ( + "4f4e01010102000b010203040000001049a1dc1c00000000" + "73656e736f722f74656d707b2263656c73697573223a32342e357d" + ) + topic_length, payload_length = inspect_header(encoded[: HEADER.size]) + decoded = Frame.from_parts( + encoded[: HEADER.size], + encoded[HEADER.size : HEADER.size + topic_length + payload_length], + ) + assert decoded == frame + + +def test_bad_crc_is_rejected(): + encoded = bytearray( + Frame(FrameKind.DATA, ValueType.UTF8, "topic", b"value", 1).to_bytes() + ) + encoded[-1] ^= 0x01 + with pytest.raises(ProtocolError, match="CRC"): + Frame.from_parts(bytes(encoded[: HEADER.size]), bytes(encoded[HEADER.size :])) + + +def test_oversize_is_rejected_before_body_allocation(): + header = HEADER.pack( + b"ON", + 1, + FrameKind.DATA, + 0, + ValueType.BYTES, + 1, + 1, + 4097, + 0, + 0, + ) + with pytest.raises(ProtocolError, match="size"): + inspect_header(header, max_payload=4096) + + +@pytest.mark.parametrize( + ("value_type", "payload"), + [ + (ValueType.BOOL, b"\x02"), + (ValueType.INT64, b"\x00"), + (ValueType.FLOAT64, struct.pack("!d", float("nan"))), + (ValueType.NULL, b"x"), + ], +) +def test_invalid_typed_payloads(value_type, payload): + with pytest.raises(ProtocolError): + decode_value(value_type, payload) From c34645063af578eda940973a56e26d1d52e405f2 Mon Sep 17 00:00:00 2001 From: devkyato Date: Wed, 29 Jul 2026 09:23:15 +0800 Subject: [PATCH 3/4] feat(esp32): add the Arduino client and examples --- examples/BinaryClient/BinaryClient.ino | 29 ++ examples/TelemetryClient/TelemetryClient.ino | 59 ++++ platformio.ini | 15 + src/OpenNet.cpp | 349 +++++++++++++++++++ src/OpenNet.h | 117 +++++++ 5 files changed, 569 insertions(+) create mode 100644 examples/BinaryClient/BinaryClient.ino create mode 100644 examples/TelemetryClient/TelemetryClient.ino create mode 100644 platformio.ini create mode 100644 src/OpenNet.cpp create mode 100644 src/OpenNet.h diff --git a/examples/BinaryClient/BinaryClient.ino b/examples/BinaryClient/BinaryClient.ino new file mode 100644 index 0000000..232520d --- /dev/null +++ b/examples/BinaryClient/BinaryClient.ino @@ -0,0 +1,29 @@ +#include +#include + +const char* WIFI_NAME = "YOUR_WIFI_NAME"; +const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; +const char* OPENNET_HOST = "192.168.1.50"; + +WiFiClient transport; +OpenNetClient client(transport); + +void setup() { + Serial.begin(115200); + WiFi.begin(WIFI_NAME, WIFI_PASSWORD); + while (WiFi.status() != WL_CONNECTED) { + delay(250); + } + if (!client.connect(OPENNET_HOST, 8765)) { + Serial.println("OpenNet connection failed"); + return; + } + + const uint8_t sample[] = {0x00, 0x01, 0x7f, 0x80, 0xfe, 0xff}; + client.sendBinary("device/sample", sample, sizeof(sample)); +} + +void loop() { + client.poll(); + delay(1); +} diff --git a/examples/TelemetryClient/TelemetryClient.ino b/examples/TelemetryClient/TelemetryClient.ino new file mode 100644 index 0000000..203996e --- /dev/null +++ b/examples/TelemetryClient/TelemetryClient.ino @@ -0,0 +1,59 @@ +#include +#include + +const char* WIFI_NAME = "YOUR_WIFI_NAME"; +const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; +const char* OPENNET_HOST = "192.168.1.50"; +const uint16_t OPENNET_PORT = 8765; + +WiFiClient transport; +OpenNetClient client(transport); + +unsigned long nextSend = 0; +unsigned long nextReconnect = 0; + +void connectNetwork() { + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_NAME, WIFI_PASSWORD); + Serial.print("Connecting to Wi-Fi"); + while (WiFi.status() != WL_CONNECTED) { + delay(250); + Serial.print("."); + } + Serial.println(); + Serial.println(WiFi.localIP()); +} + +void setup() { + Serial.begin(115200); + connectNetwork(); + client.onMessage([](const OpenNetMessage& message) { + Serial.printf("received id=%lu topic=%s bytes=%u\n", + static_cast(message.messageId), + message.topic.c_str(), + static_cast(message.payload.size())); + }); +} + +void loop() { + client.poll(); + + const unsigned long now = millis(); + if (!client.connected() && now >= nextReconnect) { + Serial.println("Connecting to OpenNet..."); + if (client.connect(OPENNET_HOST, OPENNET_PORT)) { + Serial.println("OpenNet connected"); + } + nextReconnect = now + 2000; + } + + if (client.connected() && now >= nextSend) { + const float temperature = 20.0f + static_cast(esp_random() % 1000) / 100.0f; + const String json = + "{\"device\":\"esp32-example\",\"temperature_c\":" + + String(temperature, 2) + "}"; + const uint32_t id = client.sendJson("lab/telemetry", json); + Serial.printf("sent message %lu\n", static_cast(id)); + nextSend = now + 2000; + } +} diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..8a2cbb6 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,15 @@ +[platformio] +default_envs = esp32dev +src_dir = examples/TelemetryClient + +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +lib_extra_dirs = . +build_flags = -Wall -Wextra + +[env:native] +platform = native +test_framework = unity +build_flags = -std=c++17 diff --git a/src/OpenNet.cpp b/src/OpenNet.cpp new file mode 100644 index 0000000..7c00ad6 --- /dev/null +++ b/src/OpenNet.cpp @@ -0,0 +1,349 @@ +#include "OpenNet.h" + +#include +#include + +String OpenNetMessage::text() const { + if (valueType != OpenNetValueType::Utf8 && + valueType != OpenNetValueType::Json) { + return String(); + } + String result; + result.reserve(payload.size()); + for (uint8_t byte : payload) { + result += static_cast(byte); + } + return result; +} + +OpenNetClient::OpenNetClient(Client& transport, uint32_t maxPayload) + : transport_(transport), + maxPayload_(maxPayload), + nextMessageId_(1), + lastAcknowledged_(0), + lastError_(OpenNetError::None), + headerReceived_(0), + bodyReceived_(0), + expectedTopicLength_(0), + expectedPayloadLength_(0) {} + +bool OpenNetClient::connect(const char* host, uint16_t port) { + if (host == nullptr || host[0] == '\0' || port == 0) { + lastError_ = OpenNetError::InvalidArgument; + return false; + } + resetReceiver(); + if (!transport_.connect(host, port)) { + lastError_ = OpenNetError::NotConnected; + return false; + } + lastError_ = OpenNetError::None; + return true; +} + +void OpenNetClient::disconnect(const char* reason) { + if (transport_.connected()) { + const uint8_t* bytes = reinterpret_cast(reason); + const size_t length = reason == nullptr ? 0 : strlen(reason); + sendFrame(OpenNetFrameKind::Close, OpenNetValueType::Utf8, "", bytes, + length, 0, 0); + } + transport_.stop(); + resetReceiver(); +} + +bool OpenNetClient::connected() const { return transport_.connected(); } + +void OpenNetClient::poll() { + if (!transport_.connected()) { + return; + } + + while (transport_.available() > 0) { + if (headerReceived_ < kHeaderSize) { + const int byte = transport_.read(); + if (byte < 0) { + return; + } + header_[headerReceived_++] = static_cast(byte); + if (headerReceived_ == kHeaderSize && !prepareBody()) { + transport_.stop(); + resetReceiver(); + return; + } + } else { + const size_t total = + static_cast(expectedTopicLength_) + expectedPayloadLength_; + while (bodyReceived_ < total && transport_.available() > 0) { + const int byte = transport_.read(); + if (byte < 0) { + return; + } + body_[bodyReceived_++] = static_cast(byte); + } + if (bodyReceived_ == total) { + handleFrame(); + resetReceiver(); + } + } + } +} + +void OpenNetClient::onMessage(MessageHandler handler) { + handler_ = std::move(handler); +} + +OpenNetError OpenNetClient::lastError() const { return lastError_; } + +uint32_t OpenNetClient::lastAcknowledgedMessage() const { + return lastAcknowledged_; +} + +uint32_t OpenNetClient::send(const char* topic, OpenNetValueType type, + const uint8_t* payload, size_t length, + bool requireAck) { + if (!transport_.connected()) { + lastError_ = OpenNetError::NotConnected; + return 0; + } + if (topic == nullptr || topic[0] == '\0' || + (payload == nullptr && length != 0)) { + lastError_ = OpenNetError::InvalidArgument; + return 0; + } + const uint32_t id = nextMessageId_; + nextMessageId_ = id == UINT32_MAX ? 1 : id + 1; + if (!sendFrame(OpenNetFrameKind::Data, type, topic, payload, length, id, + requireAck ? kAckRequired : 0)) { + return 0; + } + return id; +} + +uint32_t OpenNetClient::sendText(const char* topic, const String& value, + bool requireAck) { + return send(topic, OpenNetValueType::Utf8, + reinterpret_cast(value.c_str()), value.length(), + requireAck); +} + +uint32_t OpenNetClient::sendJson(const char* topic, const String& json, + bool requireAck) { + return send(topic, OpenNetValueType::Json, + reinterpret_cast(json.c_str()), json.length(), + requireAck); +} + +uint32_t OpenNetClient::sendBinary(const char* topic, const uint8_t* value, + size_t length, bool requireAck) { + return send(topic, OpenNetValueType::Bytes, value, length, requireAck); +} + +uint32_t OpenNetClient::sendInt(const char* topic, int64_t value, + bool requireAck) { + uint8_t bytes[8]; + const uint64_t bits = static_cast(value); + for (size_t i = 0; i < sizeof(bytes); ++i) { + bytes[i] = static_cast(bits >> (56 - 8 * i)); + } + return send(topic, OpenNetValueType::Int64, bytes, sizeof(bytes), requireAck); +} + +uint32_t OpenNetClient::sendDouble(const char* topic, double value, + bool requireAck) { + static_assert(sizeof(double) == sizeof(uint64_t), + "OpenNet requires an IEEE-754 64-bit double"); + uint64_t bits; + memcpy(&bits, &value, sizeof(bits)); + uint8_t bytes[8]; + for (size_t i = 0; i < sizeof(bytes); ++i) { + bytes[i] = static_cast(bits >> (56 - 8 * i)); + } + return send(topic, OpenNetValueType::Float64, bytes, sizeof(bytes), + requireAck); +} + +uint32_t OpenNetClient::sendBool(const char* topic, bool value, + bool requireAck) { + const uint8_t byte = value ? 1 : 0; + return send(topic, OpenNetValueType::Bool, &byte, 1, requireAck); +} + +uint32_t OpenNetClient::sendNull(const char* topic, bool requireAck) { + return send(topic, OpenNetValueType::Null, nullptr, 0, requireAck); +} + +bool OpenNetClient::ping() { + return sendFrame(OpenNetFrameKind::Ping, OpenNetValueType::Null, "", nullptr, + 0, 0, 0); +} + +bool OpenNetClient::sendFrame(OpenNetFrameKind kind, OpenNetValueType type, + const char* topic, const uint8_t* payload, + size_t length, uint32_t messageId, + uint8_t flags) { + if (!transport_.connected()) { + lastError_ = OpenNetError::NotConnected; + return false; + } + const char* safeTopic = topic == nullptr ? "" : topic; + const size_t topicLength = strlen(safeTopic); + if (topicLength > kMaxTopicLength || topicLength > UINT16_MAX) { + lastError_ = OpenNetError::TopicTooLong; + return false; + } + if (length > maxPayload_ || length > UINT32_MAX) { + lastError_ = OpenNetError::PayloadTooLarge; + return false; + } + + uint8_t header[kHeaderSize] = {}; + header[0] = 'O'; + header[1] = 'N'; + header[2] = 1; + header[3] = static_cast(kind); + header[4] = flags; + header[5] = static_cast(type); + writeU16(header + 6, static_cast(topicLength)); + writeU32(header + 8, messageId); + writeU32(header + 12, static_cast(length)); + uint32_t checksum = + crc32(reinterpret_cast(safeTopic), topicLength); + checksum = crc32(payload, length, checksum); + writeU32(header + 16, checksum); + + const size_t expected = kHeaderSize + topicLength + length; + size_t written = transport_.write(header, kHeaderSize); + written += transport_.write(reinterpret_cast(safeTopic), + topicLength); + if (length > 0) { + written += transport_.write(payload, length); + } + if (written != expected) { + lastError_ = OpenNetError::TransportWrite; + return false; + } + lastError_ = OpenNetError::None; + return true; +} + +void OpenNetClient::resetReceiver() { + headerReceived_ = 0; + body_.clear(); + bodyReceived_ = 0; + expectedTopicLength_ = 0; + expectedPayloadLength_ = 0; +} + +bool OpenNetClient::prepareBody() { + if (header_[0] != 'O' || header_[1] != 'N' || header_[2] != 1 || + header_[3] < static_cast(OpenNetFrameKind::Data) || + header_[3] > static_cast(OpenNetFrameKind::Error) || + header_[5] > static_cast(OpenNetValueType::Null) || + (header_[4] & ~kAllowedFlags) != 0) { + lastError_ = OpenNetError::InvalidFrame; + return false; + } + expectedTopicLength_ = readU16(header_ + 6); + expectedPayloadLength_ = readU32(header_ + 12); + if (header_[3] == static_cast(OpenNetFrameKind::Data) && + (expectedTopicLength_ == 0 || readU32(header_ + 8) == 0)) { + lastError_ = OpenNetError::InvalidFrame; + return false; + } + if (expectedTopicLength_ > kMaxTopicLength || + expectedPayloadLength_ > maxPayload_) { + lastError_ = OpenNetError::PayloadTooLarge; + return false; + } + body_.resize(static_cast(expectedTopicLength_) + + expectedPayloadLength_); + if (body_.empty()) { + handleFrame(); + resetReceiver(); + } + return true; +} + +void OpenNetClient::handleFrame() { + const uint32_t expectedChecksum = readU32(header_ + 16); + const uint8_t* topicBytes = body_.empty() ? nullptr : body_.data(); + const uint8_t* payload = + body_.empty() ? nullptr : body_.data() + expectedTopicLength_; + uint32_t actualChecksum = crc32(topicBytes, expectedTopicLength_); + actualChecksum = crc32(payload, expectedPayloadLength_, actualChecksum); + if (actualChecksum != expectedChecksum) { + lastError_ = OpenNetError::ChecksumMismatch; + transport_.stop(); + return; + } + + const auto kind = static_cast(header_[3]); + const uint32_t messageId = readU32(header_ + 8); + if (kind == OpenNetFrameKind::Ack) { + lastAcknowledged_ = messageId; + return; + } + if (kind == OpenNetFrameKind::Ping) { + sendFrame(OpenNetFrameKind::Pong, OpenNetValueType::Null, "", nullptr, 0, + 0, 0); + return; + } + if (kind != OpenNetFrameKind::Data) { + return; + } + + OpenNetMessage message; + message.kind = kind; + message.valueType = static_cast(header_[5]); + message.messageId = messageId; + message.flags = header_[4]; + message.topic.reserve(expectedTopicLength_); + for (size_t i = 0; i < expectedTopicLength_; ++i) { + message.topic += static_cast(body_[i]); + } + message.payload.assign(payload, payload + expectedPayloadLength_); + if (handler_) { + handler_(message); + } + if ((message.flags & kAckRequired) != 0) { + sendFrame(OpenNetFrameKind::Ack, OpenNetValueType::Null, "", nullptr, 0, + message.messageId, 0); + } +} + +uint16_t OpenNetClient::readU16(const uint8_t* bytes) { + return static_cast((static_cast(bytes[0]) << 8) | + bytes[1]); +} + +uint32_t OpenNetClient::readU32(const uint8_t* bytes) { + return (static_cast(bytes[0]) << 24) | + (static_cast(bytes[1]) << 16) | + (static_cast(bytes[2]) << 8) | bytes[3]; +} + +void OpenNetClient::writeU16(uint8_t* bytes, uint16_t value) { + bytes[0] = static_cast(value >> 8); + bytes[1] = static_cast(value); +} + +void OpenNetClient::writeU32(uint8_t* bytes, uint32_t value) { + bytes[0] = static_cast(value >> 24); + bytes[1] = static_cast(value >> 16); + bytes[2] = static_cast(value >> 8); + bytes[3] = static_cast(value); +} + +uint32_t OpenNetClient::crc32(const uint8_t* data, size_t length, + uint32_t previous) { + uint32_t crc = previous ^ 0xFFFFFFFFu; + for (size_t i = 0; i < length; ++i) { + crc ^= data[i]; + for (uint8_t bit = 0; bit < 8; ++bit) { + const uint32_t mask = -(crc & 1u); + crc = (crc >> 1) ^ (0xEDB88320u & mask); + } + } + return crc ^ 0xFFFFFFFFu; +} diff --git a/src/OpenNet.h b/src/OpenNet.h new file mode 100644 index 0000000..2b2c92a --- /dev/null +++ b/src/OpenNet.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include + +#include +#include + +enum class OpenNetFrameKind : uint8_t { + Data = 1, + Ack = 2, + Ping = 3, + Pong = 4, + Close = 5, + Error = 6, +}; + +enum class OpenNetValueType : uint8_t { + Bytes = 0, + Utf8 = 1, + Json = 2, + Int64 = 3, + Float64 = 4, + Bool = 5, + Null = 6, +}; + +struct OpenNetMessage { + OpenNetFrameKind kind; + OpenNetValueType valueType; + String topic; + std::vector payload; + uint32_t messageId; + uint8_t flags; + + String text() const; +}; + +enum class OpenNetError : uint8_t { + None, + NotConnected, + InvalidArgument, + TopicTooLong, + PayloadTooLarge, + TransportWrite, + InvalidFrame, + ChecksumMismatch, +}; + +class OpenNetClient { + public: + using MessageHandler = std::function; + + static constexpr uint32_t kDefaultMaxPayload = 4096; + static constexpr uint16_t kMaxTopicLength = 1024; + + explicit OpenNetClient(Client& transport, + uint32_t maxPayload = kDefaultMaxPayload); + + bool connect(const char* host, uint16_t port); + void disconnect(const char* reason = nullptr); + bool connected() const; + void poll(); + + void onMessage(MessageHandler handler); + OpenNetError lastError() const; + uint32_t lastAcknowledgedMessage() const; + + uint32_t send(const char* topic, OpenNetValueType type, + const uint8_t* payload, size_t length, + bool requireAck = true); + uint32_t sendText(const char* topic, const String& value, + bool requireAck = true); + uint32_t sendJson(const char* topic, const String& json, + bool requireAck = true); + uint32_t sendBinary(const char* topic, const uint8_t* value, size_t length, + bool requireAck = true); + uint32_t sendInt(const char* topic, int64_t value, bool requireAck = true); + uint32_t sendDouble(const char* topic, double value, + bool requireAck = true); + uint32_t sendBool(const char* topic, bool value, bool requireAck = true); + uint32_t sendNull(const char* topic, bool requireAck = true); + bool ping(); + + private: + static constexpr size_t kHeaderSize = 24; + static constexpr uint8_t kAckRequired = 0x01; + static constexpr uint8_t kAllowedFlags = 0x03; + + Client& transport_; + uint32_t maxPayload_; + uint32_t nextMessageId_; + uint32_t lastAcknowledged_; + OpenNetError lastError_; + MessageHandler handler_; + + uint8_t header_[kHeaderSize]; + size_t headerReceived_; + std::vector body_; + size_t bodyReceived_; + uint16_t expectedTopicLength_; + uint32_t expectedPayloadLength_; + + bool sendFrame(OpenNetFrameKind kind, OpenNetValueType type, + const char* topic, const uint8_t* payload, size_t length, + uint32_t messageId, uint8_t flags); + void resetReceiver(); + bool prepareBody(); + void handleFrame(); + + static uint16_t readU16(const uint8_t* bytes); + static uint32_t readU32(const uint8_t* bytes); + static void writeU16(uint8_t* bytes, uint16_t value); + static void writeU32(uint8_t* bytes, uint32_t value); + static uint32_t crc32(const uint8_t* data, size_t length, + uint32_t previous = 0); +}; From 68d446e59614cd493ddb0012d1ee6f17ecbff9e8 Mon Sep 17 00:00:00 2001 From: devkyato Date: Wed, 29 Jul 2026 09:23:15 +0800 Subject: [PATCH 4/4] ci: add verification and release automation --- .github/ISSUE_TEMPLATE/bug.yml | 39 ++++++++++++ .github/ISSUE_TEMPLATE/compatibility.yml | 25 ++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature.yml | 35 +++++++++++ .github/dependabot.yml | 12 ++++ .github/pull_request_template.md | 19 ++++++ .github/workflows/ci.yml | 68 +++++++++++++++++++++ .github/workflows/codeql.yml | 33 ++++++++++ .github/workflows/release.yml | 38 ++++++++++++ scripts/build_release.py | 78 ++++++++++++++++++++++++ 10 files changed, 352 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/compatibility.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/release.yml create mode 100644 scripts/build_release.py diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..099f193 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,39 @@ +name: Bug report +description: Report a reproducible OpenNet problem +title: "[Bug]: " +labels: ["bug", "needs-triage"] +body: + - type: markdown + attributes: + value: Do not include credentials or security vulnerabilities. Use a private security advisory for vulnerabilities. + - type: textarea + id: summary + attributes: + label: What happened? + description: Include the expected and actual behavior. + validations: + required: true + - type: input + id: version + attributes: + label: OpenNet version or commit + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: Device, OS, Python/Arduino core, compiler, and network. + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Minimal reproduction + validations: + required: true + - type: textarea + id: logs + attributes: + label: Sanitized logs + render: text diff --git a/.github/ISSUE_TEMPLATE/compatibility.yml b/.github/ISSUE_TEMPLATE/compatibility.yml new file mode 100644 index 0000000..dc2445b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/compatibility.yml @@ -0,0 +1,25 @@ +name: Hardware compatibility report +description: Contribute a reproducible device test result +title: "[Compatibility]: " +labels: ["compatibility"] +body: + - type: input + id: device + attributes: + label: Exact device and revision + validations: + required: true + - type: textarea + id: software + attributes: + label: Software versions + description: OS, Python, Arduino core, PlatformIO/IDE, and OpenNet commit. + validations: + required: true + - type: textarea + id: procedure + attributes: + label: Procedure and result + description: Network, example, duration, messages, sizes, and failures. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..474a4f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/devkyato/OpenNet/security/advisories/new + about: Report vulnerabilities privately. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..e4225c3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,35 @@ +name: Feature proposal +description: Propose a compatible, testable improvement +title: "[Proposal]: " +labels: ["enhancement", "needs-discussion"] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What real use case cannot be handled today? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed behavior + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives and compatibility impact + validations: + required: true + - type: dropdown + id: help + attributes: + label: Can you help implement or test it? + options: + - Implement and test + - Hardware-test + - Review or document + - Not currently + validations: + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f8f137e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: /python + schedule: + interval: monthly + labels: [dependencies, python] + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + labels: [dependencies, ci] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..cd8b386 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +## Summary + +Explain the problem and the smallest useful solution. + +Closes # + +## Verification + +- [ ] Python tests pass, if applicable. +- [ ] ESP32 example compiles, if applicable. +- [ ] Protocol changes include cross-language vectors. +- [ ] Documentation and changelog are updated. +- [ ] No credentials, personal data, or generated build output are included. + +## Hardware report + +For embedded changes, list the exact board, Arduino core/toolchain versions, network +setup, payload sizes, message count, and test duration. Write `Not hardware-tested` +when that is the honest status. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..83fb48f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + python: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: python/pyproject.toml + - run: python -m pip install -e "./python[dev]" + - run: python -m pytest python + - if: matrix.python-version == '3.14' + run: | + python -m ruff check python + python -m mypy python/src + + esp32: + name: ESP32 Arduino compile + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.14" + cache: pip + - run: python -m pip install platformio + - run: platformio run -e esp32dev + + package: + name: Reproducible packages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.14" + cache: pip + - run: python -m pip install build + - run: python scripts/build_release.py + - run: python -m build python + - uses: actions/upload-artifact@v6 + with: + name: packages + path: | + dist/OpenNet-*.zip + python/dist/* + if-no-files-found: error diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..abad022 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,33 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + schedule: + - cron: "17 3 * * 2" + +permissions: + contents: read + security-events: write + +jobs: + analyze: + runs-on: ubuntu-latest + strategy: + matrix: + language: ["python", "cpp"] + steps: + - uses: actions/checkout@v7 + - uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + - if: matrix.language == 'cpp' + uses: actions/setup-python@v7 + with: + python-version: "3.14" + - if: matrix.language == 'cpp' + run: | + python -m pip install platformio + platformio run -e esp32dev + - uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..43e9184 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Release + +on: + push: + tags: ["v*.*.*"] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: actions/setup-python@v7 + with: + python-version: "3.14" + cache: pip + - name: Verify, test, and build + run: | + python -m pip install -e "./python[dev]" build + python -m pytest python + python -m ruff check python + python -m mypy python/src + python scripts/build_release.py --expected-version "${GITHUB_REF_NAME#v}" + python -m build python + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: >- + gh release create "$GITHUB_REF_NAME" + dist/OpenNet-*.zip + python/dist/* + --generate-notes + --verify-tag + --title "OpenNet $GITHUB_REF_NAME" diff --git a/scripts/build_release.py b/scripts/build_release.py new file mode 100644 index 0000000..052f7d4 --- /dev/null +++ b/scripts/build_release.py @@ -0,0 +1,78 @@ +"""Build and validate the Arduino release ZIP with only installable files.""" + +from __future__ import annotations + +import argparse +import json +import re +import tempfile +import zipfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +TEXT_FILES = [ + "README.md", + "LICENSE", + "CHANGELOG.md", + "library.properties", + "library.json", + "keywords.txt", +] +DIRECTORIES = ["src", "examples", "docs"] + + +def versions() -> dict[str, str]: + properties = (ROOT / "library.properties").read_text(encoding="utf-8") + arduino = re.search(r"^version=(.+)$", properties, re.MULTILINE) + metadata = json.loads((ROOT / "library.json").read_text(encoding="utf-8")) + pyproject = (ROOT / "python" / "pyproject.toml").read_text(encoding="utf-8") + python = re.search(r'^version = "([^"]+)"$', pyproject, re.MULTILINE) + init = (ROOT / "python" / "src" / "opennet" / "__init__.py").read_text( + encoding="utf-8" + ) + package = re.search(r'^__version__ = "([^"]+)"$', init, re.MULTILINE) + if not arduino or not python or not package: + raise SystemExit("Could not read every project version") + return { + "arduino": arduino.group(1), + "platformio": str(metadata["version"]), + "python": python.group(1), + "package": package.group(1), + } + + +def build(expected: str | None) -> Path: + project_versions = versions() + unique = set(project_versions.values()) + if len(unique) != 1: + raise SystemExit(f"Version mismatch: {project_versions}") + version = unique.pop() + if expected is not None and expected != version: + raise SystemExit(f"Tag version {expected} does not match project {version}") + + output = ROOT / "dist" / f"OpenNet-{version}.zip" + output.parent.mkdir(exist_ok=True) + with tempfile.TemporaryDirectory() as temporary: + staging = Path(temporary) / "OpenNet" + staging.mkdir() + for relative in TEXT_FILES: + target = staging / relative + target.write_bytes((ROOT / relative).read_bytes()) + for directory in DIRECTORIES: + for source in (ROOT / directory).rglob("*"): + if source.is_file(): + target = staging / source.relative_to(ROOT) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(source.read_bytes()) + with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive: + for source in sorted(staging.rglob("*")): + if source.is_file(): + archive.write(source, source.relative_to(staging.parent)) + return output + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--expected-version") + result = build(parser.parse_args().expected_version) + print(result)