Compare commits

..

186 Commits

Author SHA1 Message Date
Mark Qvist 708f666787 Updated changelog 2025-11-22 15:00:17 +01:00
Mark Qvist 4f03302ae2 Cleanup 2025-11-22 14:57:50 +01:00
Mark Qvist d8f6ab206b Updated docs 2025-11-22 12:06:58 +01:00
Mark Qvist 472e69fe9a Updated docs 2025-11-22 11:21:33 +01:00
Mark Qvist aeed5279f8 Fixed formatting 2025-11-22 11:21:05 +01:00
Mark Qvist f3b8965fa6 Fixed formatting 2025-11-22 11:16:29 +01:00
Mark Qvist 1bbaab1db9 Updated version 2025-11-21 17:10:13 +01:00
Mark Qvist bf2fcbba37 Added interference detection status and history to rnstatus output for RNode interfaces 2025-11-21 15:56:17 +01:00
Mark Qvist a63dd67a07 Updated changelog 2025-11-19 15:40:03 +01:00
Mark Qvist b27f9836ae Updated docs and manual 2025-11-19 15:39:30 +01:00
Mark Qvist 9504c5b863 Updated docs 2025-11-19 15:37:24 +01:00
Mark Qvist 9767b3453e Updated docs 2025-11-19 15:23:35 +01:00
Mark Qvist 643fbbbc84 Fixed broken links. Thanks @symbioquine in #999. 2025-11-19 15:21:55 +01:00
Mark Qvist 2a5bcd5f52 Updated docs 2025-11-19 15:16:46 +01:00
Mark Qvist 237c3160eb Updated RNode TCP read timeouts 2025-11-19 14:37:47 +01:00
Mark Qvist fcdcf1a2a8 RNode TCP connection on Android 2025-11-18 12:27:36 +01:00
Mark Qvist 7c99aca1d0 Improved reconnect/hotplug reliability and responsiveness for RNodes connected over WiFi 2025-11-18 03:12:42 +01:00
Mark Qvist 309f1999e7 Cleanup 2025-11-17 21:13:21 +01:00
Mark Qvist fa6de7ff79 Updated docs 2025-11-17 19:00:13 +01:00
Mark Qvist 47dfcab170 Added ability to configure RNode IP settings to rnodeconf 2025-11-17 18:46:36 +01:00
Mark Qvist 8abd19800f Updated version 2025-11-17 17:16:29 +01:00
Mark Qvist b2d6ed733d Added support for configuring RNode WiFi settings to rnodeconf 2025-11-17 17:16:06 +01:00
Mark Qvist 1179757893 Added support for connecting RNode devices over TCP connections 2025-11-17 00:31:37 +01:00
Mark Qvist d328ef5ce0 Cleanup 2025-11-15 14:20:43 +01:00
Mark Qvist f577d3018f Updated manual 2025-11-15 13:43:36 +01:00
Mark Qvist e6db629915 Updated manual 2025-11-15 13:42:14 +01:00
Mark Qvist acaab30b91 Updated manual 2025-11-15 13:35:00 +01:00
Mark Qvist 76cedeed07 Updated BLE connection read timeouts on Android 2025-11-15 12:11:45 +01:00
Mark Qvist 5beea74eb3 Handle serial port never being opened due to failure on interface detach for RNodeInterface 2025-11-11 10:29:06 +01:00
Mark Qvist 1f91a8f6f2 Updated manual 2025-11-10 19:05:36 +01:00
Mark Qvist 080216bd55 Updated version 2025-11-10 18:58:27 +01:00
Mark Qvist aa37172293 Added support for CPU temperature reporting from RNode devices 2025-11-10 18:44:57 +01:00
Mark Qvist 5836d7f8ba Added support for RNodes with PAs up to 37 dBm. Corrected Heltec V4 board parameters in rnodeconf. 2025-11-07 19:34:16 +01:00
Mark Qvist a699d7c110 Run github workflow tests on Python 3.11 2025-11-02 22:43:12 +01:00
Mark Qvist 8eedbb9d91 Run github workflow tests on Python 3.11 2025-11-02 22:41:25 +01:00
Mark Qvist 2afa85db60 Updated manual 2025-11-02 22:37:34 +01:00
Mark Qvist a5672e7afe Updated manual 2025-11-02 22:36:40 +01:00
Mark Qvist 77d40215c8 Updated documentation 2025-11-02 22:27:27 +01:00
Mark Qvist 0896df05b6 Workaround for https://github.com/python/cpython/issues/138720 2025-11-02 11:50:17 +01:00
Mark Qvist cf0b1c6237 AutoInterface peering timing 2025-11-01 17:29:53 +01:00
Mark Qvist 08b129c8e0 Cleanup 2025-11-01 17:23:40 +01:00
Mark Qvist e2ea397715 Added Heltec v4 to rnodeconf 2025-11-01 15:23:31 +01:00
Mark Qvist 25a73e6ef9 Added detection and logging of multicast echoes never arriving on AutoInterface system interfaces. Implements #905. 2025-11-01 14:51:50 +01:00
markqvist f1c4bba3f2 Merge pull request #599 from faragher/master
Ported rncp allow files to rnx
2025-11-01 14:18:03 +01:00
Mark Qvist 704019ded8 Fixed typo 2025-11-01 14:13:17 +01:00
markqvist 79f5b92bae Merge pull request #911 from SCWhite/patch-1
minor error in hardware.html
2025-11-01 14:12:51 +01:00
Mark Qvist 5aaa743ef8 Updated rnid output 2025-11-01 14:11:35 +01:00
markqvist 9721c0bf85 Merge pull request #901 from jacobeva/cmd_interfaces_fix
Change CMD_INTERFACES value to be 0x71, fix #900
2025-11-01 14:10:34 +01:00
Mark Qvist 56848cdb63 Ensure default destination app data can be generated and sent even on first system-internal discovery announce 2025-10-31 21:25:28 +01:00
Mark Qvist 41ad089ff7 Added path response status signalling to announce handlers 2025-10-30 15:00:39 +01:00
Mark Qvist 2df355d7b4 Updated version 2025-10-30 14:11:06 +01:00
Mark Qvist 39a63b0643 WeaveInterface compatibility on Android 2025-10-29 15:44:47 +01:00
Mark Qvist ddf14e5636 Updated WeaveInterface. Added support for Weave devices to rnstatus. 2025-10-29 12:43:20 +01:00
Mark Qvist 7138749307 Fixed RNodeInterface BLE reconnection hang on Android 2025-10-28 12:31:52 +01:00
Mark Qvist af7697f223 Fixed string formatting for Android 2025-10-28 12:31:30 +01:00
Mark Qvist 0bcb4b8573 Cleanup 2025-10-28 10:59:58 +01:00
Mark Qvist 6d47b59b1e Added WeaveInterface 2025-10-28 02:31:33 +01:00
Mark Qvist 3d8eaffe9a Added WeaveInterface 2025-10-28 02:20:22 +01:00
Mark Qvist d8039aca17 Changed CMD_INTERFACES to 0x71. Fixes #900. 2025-10-28 02:10:02 +01:00
Mark Qvist 4e4d379486 Fixed Android BLE MTU for RNodeInterface 2025-10-28 02:05:59 +01:00
SCWhite 87faffa785 minor error in hardware.html
manual/hareware.html: correct T114 device platform "ESP32" → "nRF52"
2025-09-04 02:59:53 +08:00
jacob.eva adef3f80f0 Change CMD_INTERFACES value to be 0x71, fix #900 2025-08-16 16:10:13 +01:00
Mark Qvist 319c798f78 Updated readme 2025-07-15 02:28:54 +02:00
Mark Qvist 8579a7f2a5 Updated changelog 2025-07-14 16:37:41 +02:00
Mark Qvist ffbbba7395 Updated manual 2025-07-14 16:37:06 +02:00
Mark Qvist e66745c9ef Updated documentation 2025-07-14 16:07:07 +02:00
Mark Qvist 45fc9338a7 Improved BLE MTU on Android 2025-07-13 23:45:50 +02:00
Mark Qvist f2969bd1b0 Improved BLE device discovery on Android 2025-07-13 22:07:47 +02:00
Mark Qvist e0f1f3f947 Merge branch 'master' of github.com:markqvist/Reticulum 2025-07-13 19:17:13 +02:00
Mark Qvist e3827f2e25 Updated manual 2025-07-13 19:17:00 +02:00
Mark Qvist fad1d4972c Fixed potential EPOLL hang on interface failure 2025-07-13 19:16:51 +02:00
markqvist 2c33ce6c98 Merge pull request #839 from wincentbalin/master
Fixed typo
2025-07-13 15:02:45 +02:00
Mark Qvist c0d7f42f17 Cleanup 2025-07-13 14:51:24 +02:00
Mark Qvist d5a8e4b056 Trace exception on public key load failure 2025-07-13 13:58:09 +02:00
Mark Qvist 76dd50a060 Fixed potential AutoInterface peer discovery add before final init complete 2025-07-13 13:14:18 +02:00
Mark Qvist 6f9a9a7ad9 Fixed link request handling with invalid link mode 2025-07-13 13:05:37 +02:00
Mark Qvist eaec9a493b Updated readme 2025-07-13 12:28:50 +02:00
Mark Qvist d3c8555b39 Updated documentation 2025-07-13 12:26:24 +02:00
Mark Qvist 446f5c0989 Merge branch 'master' of github.com:markqvist/Reticulum 2025-07-13 12:05:59 +02:00
markqvist f3b72a8a3c Merge pull request #844 from Aareon/patch-2
fix(rnstatus): Add validation for missing -i flag when using -R
2025-07-13 12:06:20 +02:00
markqvist d2c5a1f34b Merge pull request #856 from jacobeva/multi_transport_fix
Fix announce cap crash when transport mode enabled
2025-07-13 11:56:59 +02:00
markqvist 182b49cc04 Merge pull request #843 from Aareon/patch-1
docs: Document -R and -i flag dependency in rnstatus usage
2025-07-13 11:54:59 +02:00
Mark Qvist cc8bd34cd4 Merge branch 'master' of github.com:markqvist/Reticulum 2025-07-13 11:54:54 +02:00
Mark Qvist 957ece7394 Fixed typo 2025-07-13 11:53:25 +02:00
Mark Qvist 762343adf9 Updated version 2025-07-13 11:49:44 +02:00
Mark Qvist 8d32b378d9 Fixed log statements 2025-07-13 11:49:12 +02:00
jacob.eva 41e816d299 Fix announce cap crash when transport mode enabled 2025-07-01 18:16:24 +01:00
Aareon Sullivan 4226a62f23 fix(rnstatus): Add validation for missing -i flag when using -R
Add check to ensure `management_identity` is provided when using remote
query flag (`-R`). Prevents `TypeError` and provides clear error message
when user forgets to specify identity file with `-i` flag.

Before: `expected str, bytes or os.PathLike object, not NoneType`
After: `Remote management requires an identity file. Use -i to specify the path to a management identity.`

Fixes #792
2025-06-08 19:52:46 -05:00
Aareon Sullivan 5dda28559b docs: Document -R and -i flag dependency in rnstatus usage
Add usage example and clarify that -R requires -i flag for remote
transport instance status queries. Fixes confusion where users get
"expected str, bytes or os.PathLike object, not NoneType" error
when using -R without -i.

- Add remote status query example to Usage Examples section
- Update -R flag description to indicate -i requirement
- Add explanatory note about management identity authorization

Addresses user reported issue where documentation was unclear about
mandatory flag combination for remote management functionality.
2025-06-08 19:33:42 -05:00
Wincent Balin d055ca50d6 Fixed typo 2025-05-31 01:12:25 +02:00
Mark Qvist 799bcfc7aa Updated version 2025-05-26 19:08:03 +02:00
Mark Qvist 045cb662ef Removed legacy AES-128 handlers 2025-05-26 19:04:30 +02:00
Mark Qvist 51e3983bf8 Updated readme 2025-05-17 10:39:52 +02:00
Mark Qvist 95fdc41845 Updated readme 2025-05-17 10:39:30 +02:00
Mark Qvist d795fbeaf3 Updated documentation 2025-05-17 10:37:11 +02:00
Mark Qvist 13037d68ed Updated documentation 2025-05-17 10:35:49 +02:00
Mark Qvist 6da5df9f21 Merge branch 'master' of github.com:markqvist/Reticulum 2025-05-17 10:25:24 +02:00
Mark Qvist 8128f573ef Updated manual 2025-05-17 10:25:09 +02:00
markqvist accf104553 Update FUNDING.yml 2025-05-17 10:24:38 +02:00
Mark Qvist 5387264dcb Updated changelog 2025-05-15 22:24:33 +02:00
Mark Qvist 308a6906db Updated manual 2025-05-15 15:32:31 +02:00
Mark Qvist 96ce7e3f47 Updated changelog 2025-05-15 15:32:20 +02:00
Mark Qvist f186b6266b Implemented dynamic keepalive and link timeout calculation 2025-05-15 12:50:16 +02:00
Mark Qvist 756029e5af Added option to specify shared instance type 2025-05-15 01:14:55 +02:00
Mark Qvist c1673f39b6 Updated changelog 2025-05-13 19:46:18 +02:00
Mark Qvist 30a08c4192 Updated manual 2025-05-13 18:01:13 +02:00
Mark Qvist d680f4d411 Cleanup 2025-05-13 17:59:26 +02:00
Mark Qvist 29a52e19cf Cleanup 2025-05-13 17:25:00 +02:00
Mark Qvist 11511168dc Cleanup 2025-05-13 13:32:35 +02:00
Mark Qvist d4ea698236 Cleanup 2025-05-13 13:29:20 +02:00
Mark Qvist 11e06b477e Cleanup 2025-05-13 13:26:26 +02:00
Mark Qvist 4e4c68071f Removed legacy encryption modes. Default to AES-256 for links and packets. 2025-05-13 13:18:44 +02:00
Mark Qvist 5f502746a4 Updated tests 2025-05-13 13:16:37 +02:00
Mark Qvist 17bbb9c0b4 Updated docs 2025-05-12 20:20:29 +02:00
Mark Qvist 8b13d6e08b Fixed announce handlers not triggering after shared instance disappearance/reappearance 2025-05-12 11:41:06 +02:00
Mark Qvist efa512be32 Cleanup 2025-05-11 16:40:14 +02:00
Mark Qvist 594f5fba1e Added ability to return file resources for request responses. Added option to specify request response auto-compression limits. 2025-05-11 16:37:57 +02:00
Mark Qvist 2912fb2184 Added option to specify resource auto-compression limits 2025-05-11 16:37:19 +02:00
Mark Qvist 02496f39f7 Removed completed tasks from roadmap 2025-05-11 11:30:06 +02:00
Mark Qvist 4e31f113c6 Optimised hardware MTU autoconfig 2025-05-10 23:15:43 +02:00
Mark Qvist 9aded3e1da Updated readme 2025-05-10 23:09:21 +02:00
Mark Qvist 3337d18e9a Added allow overwrite option to rncp 2025-05-10 21:44:42 +02:00
Mark Qvist 2cb6d019f9 Improved rncp memory utilisation and performance 2025-05-10 21:19:57 +02:00
Mark Qvist 3dc260a300 Updated link tests 2025-05-10 20:59:23 +02:00
Mark Qvist 4d7f5b8ca6 Let shared instance handle packet hashlist 2025-05-10 20:58:54 +02:00
Mark Qvist 48be5f65d8 Faster link cleanup on close 2025-05-10 20:58:01 +02:00
Mark Qvist b5d854a55c Resource performance and memory optimisations 2025-05-10 20:57:32 +02:00
Mark Qvist 552663c625 Fixed offset 2025-05-10 17:00:27 +02:00
Mark Qvist e6f0b92464 Updated example 2025-05-10 17:00:02 +02:00
Mark Qvist 08a6820aa0 Updated descriptions 2025-05-10 15:42:22 +02:00
Mark Qvist cc1faa55be Updated readme 2025-05-10 15:39:51 +02:00
Mark Qvist 840966f3e6 Updated version 2025-05-10 15:38:28 +02:00
Mark Qvist 763078a1ae Added ability to include metadata on resource transfers 2025-05-10 15:38:06 +02:00
Mark Qvist 5fb6abd019 Added resource example 2025-05-10 15:32:06 +02:00
Mark Qvist 7065856229 Added resource with metadata tests 2025-05-10 15:31:35 +02:00
Mark Qvist 668ef9253a Cleanup 2025-05-09 12:07:00 +02:00
Mark Qvist 6f333b8234 Cleanup 2025-05-09 12:04:55 +02:00
Mark Qvist 32c839f497 Updated changelog 2025-05-09 11:31:54 +02:00
Mark Qvist cbdef1d538 Updated manual 2025-05-09 11:21:13 +02:00
Mark Qvist c398b34dd8 Fixed potential unhandled exception on fast-flapping connections 2025-05-08 10:57:34 +02:00
Mark Qvist 9a1884cfec Updated manual 2025-05-07 12:10:53 +02:00
Mark Qvist 378dc1e931 Added link mode get method to Link API 2025-05-06 19:09:40 +02:00
Mark Qvist be821b6927 Added instance_name option and description to default config file 2025-05-06 19:09:20 +02:00
Mark Qvist af46e98865 Improved ratchet persist reliability 2025-05-06 18:18:05 +02:00
Mark Qvist 65b1667ae7 Updated readme 2025-05-06 17:57:08 +02:00
Mark Qvist 5bc1fc2bde Updated readme 2025-05-06 17:55:45 +02:00
Mark Qvist 4ae0f28aa0 Cleanup 2025-05-06 17:48:38 +02:00
Mark Qvist 62ecc0549d Updated readme 2025-05-06 17:25:53 +02:00
Mark Qvist cbf4c71a73 Added pure-python AES-256 implementation 2025-05-06 17:20:55 +02:00
Mark Qvist 1d27fae370 Updated github test workflow 2025-05-06 16:45:16 +02:00
Mark Qvist 05b9a80a07 Path MTU clamping handling with link mode signalling 2025-05-06 16:37:04 +02:00
Mark Qvist 38241452d3 Dynamic link mode establishment 2025-05-06 16:31:36 +02:00
Mark Qvist 40e040807a Link mode signalling generators 2025-05-06 16:12:54 +02:00
Mark Qvist 437da99d63 Handle AES-256 compatibility in Identity 2025-05-06 16:12:15 +02:00
Mark Qvist 3cbcbec942 Updated tests for link modes 2025-05-06 14:23:42 +02:00
Mark Qvist bc7a8cd09f Updated documentation 2025-05-06 14:14:42 +02:00
Mark Qvist ab0ac46d5a Enabled AES_256_CBC link mode 2025-05-06 12:10:34 +02:00
Mark Qvist d7791c60e2 Implemented compatibility handling for AES-256 migration 2025-05-06 12:08:17 +02:00
Mark Qvist 5dc8cdc6dc Merge branch 'master' of github.com:markqvist/Reticulum 2025-04-29 11:42:23 +02:00
Mark Qvist cdc33a25c5 Updated readme 2025-04-29 11:42:07 +02:00
markqvist 2b6766f68a Merge pull request #801 from LinuxinaBit/master
Generative AI Policy
2025-04-28 15:29:32 +02:00
Linux in a Bit e871bbdc07 Add Generative AI Policy reminder to 🐛-bug-report.md 2025-04-20 15:04:55 -05:00
Linux in a Bit 6a98158ba6 [Contributing.md] Add LLM Policy; Emphasize CLA 2025-04-20 14:59:23 -05:00
Mark Qvist ef8d44c257 Updated changelog 2025-04-20 21:23:08 +02:00
Mark Qvist 6a48a4d1c0 Updated version 2025-04-18 12:25:47 +02:00
Mark Qvist 4d2ba28934 Docs build 2025-04-17 23:04:00 +02:00
Mark Qvist 98d4f1c69e Fixed instance name from config being overwritten if option was not last in section 2025-04-17 15:02:08 +02:00
Mark Qvist a0f0d73204 Improved ratchet persist 2025-04-17 14:25:24 +02:00
Mark Qvist 1dbb1a6a35 Merge branch 'linkmodes' 2025-04-16 14:11:14 +02:00
Mark Qvist cc50ca82b8 Added todo note 2025-04-16 14:09:43 +02:00
Mark Qvist 373790c890 Added AES-256 support to Link 2025-04-16 14:05:34 +02:00
Mark Qvist ef30d21b58 Added AES_256_CBC support to Token 2025-04-16 14:03:40 +02:00
Mark Qvist c4cafed6aa Added AES_128_CBC and AES_256_CBC mode proxies 2025-04-16 14:01:26 +02:00
Mark Qvist 828eec5e0d Cleanup 2025-04-16 01:30:11 +02:00
Mark Qvist a8c50fe7d4 Link mode signalling fields 2025-04-16 00:44:30 +02:00
Mark Qvist ab9fc7b370 Updated docs 2025-04-16 00:43:35 +02:00
Mark Qvist 0dc972f7c9 Updated docs 2025-04-16 00:41:50 +02:00
Mark Qvist 796cffe29d Updated docs 2025-04-16 00:40:29 +02:00
Mark Qvist a0f6c99fb5 Updated docs 2025-04-16 00:37:11 +02:00
Mark Qvist eff0c91ed0 Updated docs 2025-04-16 00:35:56 +02:00
Mark Qvist dba6cd8393 Updated license 2025-04-15 20:48:12 +02:00
Mark Qvist e7daceec82 Updated license 2025-04-15 20:19:33 +02:00
Mark Qvist a65473f6ab Updated docs 2025-04-15 18:57:12 +02:00
Mark Qvist 1851fda9e0 Fixed interface string representation 2025-04-15 18:51:52 +02:00
Mark Qvist 80eec131f8 Updated docs 2025-04-15 18:25:12 +02:00
faragher 95cea24527 Ported rncp allow files to rnx 2024-11-02 23:06:43 -05:00
127 changed files with 8011 additions and 16647 deletions
@@ -12,6 +12,7 @@ Before creating a bug report on this issue tracker, you **must** read the [Contr
- The issue tracker is used by developers of this project. **Do not use it to ask general questions, or for support requests**.
- Ideas and feature requests can be made on the [Discussions](https://github.com/markqvist/Reticulum/discussions). **Only** feature requests accepted by maintainers and developers are tracked and included on the issue tracker. **Do not post feature requests here**.
- Do not submit code written using large language models (LLMs) or other generative 'AI' programs (see the [Generative AI Policy](/Contributing.md#generative-ai-policy) for details).
- After reading the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md), **delete this section only** (*"Read the Contribution Guidelines"*) from your bug report, **and fill in all the other sections**.
**Describe the Bug**
+5 -3
View File
@@ -28,8 +28,10 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- run: make test
python-version: 3.11
- run: |
python -m pip install -q cryptography
make test
package:
needs: test
@@ -41,7 +43,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
python-version: 3.11
- run: |
python -m pip install -q build wheel setuptools
make remove_symlinks
+180
View File
@@ -1,3 +1,183 @@
### 2025-11-19: RNS 1.0.4
This release includes updates to RNode BLE reliability, and adds support for connecting RNodes to a host over WiFi and Ethernet.
**Changes**
- Improved handling for RNodes with PA/LNA combo
- Added interference detection stats to `rnstatus` output for RNode interfaces
- Updated documentation
**Release Hashes**
```
7a2b7893410833b42c0fa7f9a9e3369cebb085cdd26bd83f3031fa6c1051653c rns-1.0.4-py3-none-any.whl
ee647e7b3b94abdf1fab618a861390531a4aacc93eecce12c9e97280195c0e2d rnspure-1.0.4-py3-none-any.whl
```
### 2025-11-19: RNS 1.0.3
This release includes updates to RNode BLE reliability, and adds support for connecting RNodes to a host over WiFi and Ethernet.
**Changes**
- Added support for connecting RNode devices over WiFi and Ethernet
- Added support for configuring RNode WiFi and IP settings to `rnodeconf`
- Updated BLE connection read timeouts on Android, fixes intermittent BLE connection resets in areas with high 2.4GHz spectrum utilization
- Added handling for edge case where RNode serial port was never opened due to failure on interface detach
- Fixed broken links in documentation
**Release Hashes**
```
6bafde4c838ad778bf6878967e84c798e34d6ca621b255f59a60f38cb04ac138 rns-1.0.3-py3-none-any.whl
f277899f95c1189c6bf3beb40ac656c8b36dfd3d7e4cfb2bc3b4a1e6dc3484fa rnspure-1.0.3-py3-none-any.whl
```
### 2025-11-10: RNS 1.0.2
This maintenance release adds support for high-power RNodes with a LoRa PA and/or LNA.
**Changes**
- Added support for RNodes with a PA and/or LNA
- Added support for monitoring RNode CPU temperature via `rnodeconf`
**Release Hashes**
```
723bcf0a839025060ff680c4202b09fa766b35093a4a08506bb85485b8a1f154 rns-1.0.2-py3-none-any.whl
b02de8aeb1381ed2610f27f78799bab031367ed7bf500951fb8d5c2542d4a409 rnspure-1.0.2-py3-none-any.whl
```
### 2025-11-02: RNS 1.0.1
This release brings a number of bugfixes, as well as stability and reliability improvements. It also adds support for using Weave devices as Reticulum interfaces, fixes long-standing Bluetooth Low Energy connection issues on Android, and includes several API and usability improvements.
**Changes**
- Added path response signalling to announce handler API
- Added interface module for Weave devices
- Added support for connecting to Weave devices over serial/USB on Android
- Added support for allow files to `rnx`
- Added detection and logging of multicast echoes never arriving on AutoInterface system devices.
- Added Heltec v4 support to `rnodeconf`
- Implemented handler for ensuring dynamic destination app data can be generated and sent even on first system-internal discovery announce
- Updated documentation and manual
- Improved `AutoInterface` peering timing
- Fixed RNodeInterface Bluetooth Low Energy connection hangs on Android
- Fixed RNodeInterface Bluetooth Low Energy MTU not being configured correctly on Android
- Fixed command byte collision in RNodeInterface and RNodeMultiInterface
- Fixed string formatting for Android log output
- Updated output formatting for `rnid`
**Release Hashes**
```
aa77b4c8e1b6899117666e1e55b05b3250416ab5fea2826254358ae320e8b3ed rns-1.0.1-py3-none-any.whl
b3ddfa0b533631d9f1213043a0282952ae6e9f72c3072bbca053ac48e0483f7e rnspure-1.0.1-py3-none-any.whl
```
### 2025-07-14: RNS 1.0.0
We're out of beta. Thanks to **everyone** who helped make it this far.
This release brings a number of bugfixes, as well as stability and reliability improvements.
**Changes**
- Improved BLE device discovery on Android
- Improved BLE MTU configuration on Android
- Fixed a bug in handling of link requests with invalid link mode bytes
- Fixed potential AutoInterface peer discovery add before final init complete
- Fixed a potential EPOLL backend hang on interface failure
- Fixed various log statements
- Fixed announce cap crash for `RNodeMultiInterface` with transport mode enabled
- Updated documentation
- Removed legacy AES-128 handlers
**Release Hashes**
```
5a9f18840510b69f89c6706d130177e2843c9e19c774707ae2661030d693dfc1 rns-1.0.0-py3-none-any.whl
acfd52af9bf41f78be017579ca06c0abe748d0b98dbdc9baacf140a0606599ec rnspure-1.0.0-py3-none-any.whl
```
### 2025-05-15: RNS β 0.9.6
This release activates AES-256 as the default encryption mode for all communication. It is the last release that will support the old AES-128 based modes, which will be entirely phased out in the next release.
This release also includes a number of API and resource consumption improvements, and fixes a bug.
**Changes**
- Enabled AES-256 as default encryption mode for all traffic
- Added dynamic link keepalive and timeout calculation
- Added ability to efficiently transfer files as responses in the `Request` API
- Added ability to include metadata on `Resource` transfers
- Added option to specify `Resource` auto-compression limits
- Added option to specify `Request` response auto-compression limits
- Added `Resource` transfer example
- Added allow overwrite option to `rncp`
- Improved hardware MTU auto-configuration
- Improved handling of file transfers using the `Resource` API
- Improved `Resource` transfer memory consumption
- Improved memory consumption of applications connected to a shared instance
- Improved `rncp` memory consumption for large files
- Fixed announce handlers not triggering after shared instance disappearance
**Release Hashes**
```
a23c64a04c1e83fd0ab449f564ac904da7fd4f61c0faf68a063f486cc48b44bd rns-0.9.6-py3-none-any.whl
4544882dea902b18b00d8a04c9ab93201974573b7b63c3db06cb310b0acec240 rnspure-0.9.6-py3-none-any.whl
```
### 2025-05-09: RNS β 0.9.5
This release initiates migration of Reticulum from AES-128 to AES-256 as the default link and packet cipher mode. It is a compatibility/migration release, that while supporting AES-256 doesn't use it by default. It will work with both the old AES-128 based modes, and the new AES-256 based modes. There's a very slight penalty in performance to support both the old and new modes at the same time, but only for single packet APIs (not links), and it really shouldn't be noticeable in any everyday use.
In the next release, version `0.9.6`, Reticulum will transition fully to AES-256 and use it by default for all communications. That means that both single packets and links will use AES-256 by default. The old AES-128 link mode may or may not be available for a few releases, but will ultimately be phased out entirely.
The update requires no intervention, configuration changes or anything similar from a users or developers perspective. Everything should simply work. This goes both for the `0.9.5` update, and the next `0.9.6` update that transitions fully to AES-256.
**Changes**
- Added support for AES-256 mode to links and packets
- Added dynamic link mode support
- Added temporary backwards compatibility for AES-128 link and packet modes
- Added `get_mode()` method to link API
- Added tests for all enabled link modes
- Added `instance_name` option and description to default config file
- Improved ratchet persist reliability if Reticulum is force killed while persisting ratchets
- Fixed interface string representation for some interfaces
- Fixed instance name config option being overwritten if option was not last in section
- Fixed unhandled potential exception on fast-flapping `BackboneInterface` connections
**Release Hashes**
```
ae6587c86c98cae0df73567af093cc92fe204e71bb01f2506da9aec626a27e97 rns-0.9.5-py3-none-any.whl
96208c1d1234e3e4b1c18ca986bad5d4693aeb431453efd7ade33b87f35600e1 rnspure-0.9.5-py3-none-any.whl
```
### 2025-04-15: RNS β 0.9.4
This release significantly improves memory utilisation and performance. It also includes a few new features and general improvements to the included utilities and programs.
**Changes**
- Significantly improved memory utilisation, thread count and performance on nodes with many interfaces or clients
- Switched local instance communication to run over abstract domain sockets on Linux and Android
- Switched instance IPC to run over abstract domain sockets on Linux and Android
- Added kernel event based I/O backend on Linux and Android
- Added fast `BackboneInterface` type
- Added support for XIAO-ESP32S3 to `rnodeconf`
- Added interactive shell option to `rnsd`
- Added API option to search for identity by identity hash
- Added option to run TCP and Backbone interfaces in AP mode
- Improved `RNodeMultiInterface` host communications specification
- Improved `rncp` statistics output
- Improved link and reverse-table culling
- Fixed an occasional I/O thread hang on instance shutdown, that would result in an error printed to the console
- Fixed various minor interface logging inconsistencies
- Fixed various minor interface checking inconsistencies
- Updated internal `configobj` implementation
- Refactored various parts of the transport core code
- Swicthed to using internal `netinfo` implementation instead of including full `ifaddr` library
- Cleaned out unneeded dependencies
**Release Hashes**
```
737294f29e013f9fa9c8c1326006d0547497607156828fee3dc2a0d3ddd754e7 rns-0.9.4-py3-none-any.whl
0bd8a908af115c27733484853d779574d6383ebc1d78160e5a72c14ed9692a13 rnspure-0.9.4-py3-none-any.whl
```
### 2025-03-13: RNS β 0.9.3
This maintenance release improves performance and fixes a number of bugs.
+9 -1
View File
@@ -40,4 +40,12 @@ Pull requests have a high chance of being accepted if they are:
Even new ideas and proposals that have not been approved by a maintainer, or fall outside the established roadmap, are *occasionally* accepted - if they possess the remaining of the above qualities. If not, they will be closed and removed without comments or explanation.
By contributing code to this project, you agree that copyright for the code is transferred to the Reticulum maintainers and that the code is irrevocably placed under the [MIT license](./LICENSE).
## Generative AI Policy
Contributions written using large language models (LLMs) or other generative 'AI' programs are prohibited. LLMs produce errors so frequently and in a way that is so unlike human error that issues will regularly remain undetected and slip through, even with stringent review. This is not a worthwhile tradeoff for Reticulum, especially considering the limited time maintainers have to correct these issues, and we ask that you refrain from using any such output in your contributions.
This applies to all official Reticlulm projects and documentation as well as all submitted issues and discussion in official channels, except in cases where language translation and/or speech recogntion technologies are required for communication. We also ask that you avoid using LLMs for troubleshooting, as results can be misleading, and instead request help in one of our [various communities](https://reticulum.network/start.html).
## Contributor License Agreement
By contributing code to this project, you agree that copyright for the code is transferred to the Reticulum maintainers and that the code is irrevocably placed under the [Reticulum License](./LICENSE).
-2
View File
@@ -1,5 +1,3 @@
# MIT License - Copyright (c) 2024 Mark Qvist / unsigned.io
# This example illustrates creating a custom interface
# definition, that can be loaded and used by Reticulum at
# runtime. Any number of custom interfaces can be created
+2 -2
View File
@@ -1,6 +1,6 @@
##########################################################
# This RNS example demonstrates how to set perform #
# requests and receive responses over a link. #
# This RNS example demonstrates how to perform requests #
# and receive responses over a link. #
##########################################################
import os
+294
View File
@@ -0,0 +1,294 @@
##########################################################
# This RNS example demonstrates how to transfer a #
# resource over an established link #
##########################################################
import os
import sys
import time
import random
import argparse
import RNS
# Let's define an app name. We'll use this for all
# destinations we create. Since this echo example
# is part of a range of example utilities, we'll put
# them all within the app namespace "example_utilities"
APP_NAME = "example_utilities"
##########################################################
#### Server Part #########################################
##########################################################
# A reference to the latest client link that connected
latest_client_link = None
# This initialisation is executed when the users chooses
# to run as a server
def server(configpath):
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our link example
server_identity = RNS.Identity()
# We create a destination that clients can connect to. We
# want clients to create links to this destination, so we
# need to create a "single" destination type.
server_destination = RNS.Destination(
server_identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
APP_NAME,
"resourceexample"
)
# We configure a function that will get called every time
# a new client creates a link to this destination.
server_destination.set_link_established_callback(client_connected)
# Everything's ready!
# Let's Wait for client resources or user input
server_loop(server_destination)
def server_loop(destination):
# Let the user know that everything is ready
RNS.log(
"Resource example "+
RNS.prettyhexrep(destination.hash)+
" running, waiting for a connection."
)
RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
# We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server
# destination on the network, which will let clients
# know how to create messages directed towards it.
while True:
entered = input()
destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
# When a client establishes a link to our server
# destination, this function will be called with
# a reference to the link.
def client_connected(link):
global latest_client_link
RNS.log("Client connected")
# We configure the link to accept all resources
# and set a callback for completed resources
link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
link.set_resource_concluded_callback(resource_concluded)
link.set_link_closed_callback(client_disconnected)
latest_client_link = link
def client_disconnected(link):
RNS.log("Client disconnected")
def resource_concluded(resource):
if resource.status == RNS.Resource.COMPLETE:
RNS.log(f"Resource {resource} received")
RNS.log(f"Metadata: {resource.metadata}")
RNS.log(f"Data length: {os.stat(resource.data.name).st_size}")
RNS.log(f"Data can be read directly from: {resource.data}")
RNS.log(f"Data can be moved or copied from: {resource.data.name}")
RNS.log(f"First 32 bytes of data: {RNS.hexrep(resource.data.read(32))}")
else:
RNS.log(f"Receiving resource {resource} failed")
##########################################################
#### Client Part #########################################
##########################################################
# A reference to the server link
server_link = None
def random_text_generator():
texts = ["They looked up", "On each full moon", "Becky was upset", "Ill stay away from it", "The pet shop stocks everything"]
return texts[random.randint(0, len(texts)-1)]
# This initialisation is executed when the users chooses
# to run as a client
def client(destination_hexhash, configpath):
# We need a binary representation of the destination
# hash that was entered on the command line
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError(
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
)
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
sys.exit(0)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Check if we know a path to the destination
if not RNS.Transport.has_path(destination_hash):
RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
RNS.Transport.request_path(destination_hash)
while not RNS.Transport.has_path(destination_hash):
time.sleep(0.1)
# Recall the server identity
server_identity = RNS.Identity.recall(destination_hash)
# Inform the user that we'll begin connecting
RNS.log("Establishing link with server...")
# When the server identity is known, we set
# up a destination
server_destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
APP_NAME,
"resourceexample"
)
# And create a link
link = RNS.Link(server_destination)
# We'll set up functions to inform the
# user when the link is established or closed
link.set_link_established_callback(link_established)
link.set_link_closed_callback(link_closed)
# Everything is set up, so let's enter a loop
# for the user to interact with the example
client_loop()
def client_loop():
global server_link
# Wait for the link to become active
while not server_link:
time.sleep(0.1)
should_quit = False
while not should_quit:
try:
print("> ", end=" ")
text = input()
# Check if we should quit the example
if text == "quit" or text == "q" or text == "exit":
should_quit = True
server_link.teardown()
else:
# Generate 32 megabytes of random data
data = os.urandom(32*1024*1024)
RNS.log(f"Data length: {len(data)}")
RNS.log(f"First 32 bytes of data: {RNS.hexrep(data[:32])}")
# Generate some metadata
metadata = {"text": random_text_generator(), "numbers": [1,2,3,4], "blob": os.urandom(16)}
# Send the resource
resource = RNS.Resource(data, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False)
# Alternatively, you can stream data
# directly from an open file descriptor
# with open("/path/to/file", "rb") as data_file:
# resource = RNS.Resource(data_file, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False)
except Exception as e:
RNS.log("Error while sending resource over the link: "+str(e))
should_quit = True
server_link.teardown()
def resource_concluded_sending(resource):
if resource.status == RNS.Resource.COMPLETE: RNS.log(f"The resource {resource} was sent successfully")
else: RNS.log(f"Sending the resource {resource} failed")
# This function is called when a link
# has been established with the server
def link_established(link):
# We store a reference to the link
# instance for later use
global server_link
server_link = link
# Inform the user that the server is
# connected
RNS.log("Link established with server, hit enter to sand a resource, or type in \"quit\" to quit")
# When a link is closed, we'll inform the
# user, and exit the program
def link_closed(link):
if link.teardown_reason == RNS.Link.TIMEOUT:
RNS.log("The link timed out, exiting now")
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
RNS.log("The link was closed by the server, exiting now")
else:
RNS.log("Link closed, exiting now")
time.sleep(1.5)
sys.exit(0)
##########################################################
#### Program Startup #####################################
##########################################################
# This part of the program runs at startup,
# and parses input of from the user, and then
# starts up the desired program mode.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="Simple resource example")
parser.add_argument(
"-s",
"--server",
action="store_true",
help="wait for incoming resources from clients"
)
parser.add_argument(
"--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
parser.add_argument(
"destination",
nargs="?",
default=None,
help="hexadecimal hash of the server destination",
type=str
)
args = parser.parse_args()
if args.config:
configarg = args.config
else:
configarg = None
if args.server:
server(configarg)
else:
if (args.destination == None):
print("")
parser.print_help()
print("")
else:
client(args.destination, configarg)
except KeyboardInterrupt:
print("")
sys.exit(0)
+2 -1
View File
@@ -1,2 +1,3 @@
liberapay: Reticulum
ko_fi: markqvist
custom: "https://unsigned.io/donate"
custom: "https://unsigned.io/donate"
+12 -4
View File
@@ -1,6 +1,6 @@
MIT License, unless otherwise noted
Reticulum License
Copyright (c) 2016-2024 Mark Qvist / unsigned.io
Copyright (c) 2016-2025 Mark Qvist
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@ 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 shall not be used in any kind of system which includes amongst
its functions the ability to purposefully do harm to human beings.
- The Software shall not be used, directly or indirectly, in the creation of
an artificial intelligence, machine learning or language model training
dataset, including but not limited to any use that contributes to the
training or development of such a model or algorithm.
- 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,
+39 -25
View File
@@ -1,4 +1,4 @@
Reticulum Network Stack β <img align="right" src="https://static.pepy.tech/personalized-badge/rns?period=month&units=international_system&left_color=grey&right_color=blue&left_text=Installs/month" style="padding-left:10px"/><a href="https://github.com/markqvist/Reticulum/actions/workflows/build.yml"><img align="right" src="https://github.com/markqvist/Reticulum/actions/workflows/build.yml/badge.svg"/></a>
Reticulum Network Stack <img align="right" src="https://static.pepy.tech/personalized-badge/rns?period=month&units=international_system&left_color=grey&right_color=blue&left_text=Installs/month" style="padding-left:10px"/><a href="https://github.com/markqvist/Reticulum/actions/workflows/build.yml"><img align="right" src="https://github.com/markqvist/Reticulum/actions/workflows/build.yml/badge.svg"/></a>
==========
<p align="center"><img width="200" src="https://raw.githubusercontent.com/markqvist/Reticulum/master/docs/source/graphics/rns_logo_512.png"></p>
@@ -52,7 +52,7 @@ For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ
- Forward Secrecy is available for all communication types, both for single packets and over links
- Reticulum uses the following format for encrypted tokens:
- Ephemeral per-packet and link keys and derived from an ECDH key exchange on Curve25519
- AES-128 in CBC mode with PKCS7 padding
- AES-256 in CBC mode with PKCS7 padding
- HMAC using SHA256 for authentication
- IVs are generated through os.urandom()
- Unforgeable packet delivery confirmations
@@ -62,7 +62,7 @@ For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ
- Easily create your own custom interfaces for communicating over anything
- Authentication and virtual network segmentation on all supported interface types
- An intuitive and easy-to-use API
- Simpler and easier to use than sockets APIs and simpler, but more powerful
- Simpler and easier to use than sockets APIs, but more powerful
- Makes building distributed and decentralised applications much simpler
- Reliable and efficient transfer of arbitrary amounts of data
- Reticulum can handle a few bytes of data or files of many gigabytes
@@ -86,9 +86,10 @@ following resources.
- You can use the [rnsh](https://github.com/acehoss/rnsh) program to establish remote shell sessions over Reticulum.
- [LXMF](https://github.com/markqvist/lxmf) is a distributed, delay and disruption tolerant message transfer protocol built on Reticulum
- The [LXST](https://github.com/markqvist/lxst) protocol and framework provides real-time audio and signals transport over Reticulum. It includes primitives and utilities for building voice-based applications and hardware devices, such as the `rnphone` program, that can be used to build hardware telephones.
- For an off-grid, encrypted and resilient mesh communications platform, see [Nomad Network](https://github.com/markqvist/NomadNet)
- The Android, Linux, macOS and Windows app [Sideband](https://github.com/markqvist/Sideband) has a graphical interface and focuses on ease of use.
- [MeshChat](https://github.com/liamcottle/reticulum-meshchat) is a user-friendly LXMF client, that also supports voice calls.
- The Android, Linux, macOS and Windows app [Sideband](https://github.com/markqvist/Sideband) has a graphical interface and many advanced features, such as file transfers, image and voice messages, real-time voice calls, a distributed telemetry system, mapping capabilities and full plugin extensibility.
- [MeshChat](https://github.com/liamcottle/reticulum-meshchat) is a user-friendly LXMF client with a web-based interface, that also supports image and voice messages, as well as file transfers. It also includes a built-in page browser for browsing Nomad Network nodes.
## Where can Reticulum be used?
Over practically any medium that can support at least a half-duplex channel
@@ -205,15 +206,14 @@ provide a dynamic performance envelope from 250 bits per second, to 1 gigabit
per second on normal hardware.
Currently, the usable performance envelope is approximately 150 bits per second
to 40 megabits per second, with physical mediums faster than that not being
to 500 megabits per second, with physical mediums faster than that not being
saturated. Performance beyond the current level is intended for future
upgrades, but not highly prioritised at this point in time.
## Current Status
Reticulum should currently be considered beta software. All core protocol
features are implemented and functioning, but additions will probably occur as
real-world use is explored. There will be bugs. The API and wire-format can be
considered relatively stable at the moment, but could change if warranted.
All core protocol features are implemented and functioning, but additions will
probably occur as real-world use is explored and understood. The API and wire-format
can be considered stable.
## Dependencies
The installation of the default `rns` package requires the dependencies listed
@@ -296,23 +296,30 @@ You can help support the continued development of open, free and private communi
```
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
```
- Ethereum
```
0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73
```
- Bitcoin
```
35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH
bc1p4a6axuvl7n9hpapfj8sv5reqj8kz6uxa67d5en70vzrttj0fmcusgxsfk5
```
- Ko-Fi: https://ko-fi.com/markqvist
- Ethereum
```
0xae89F3B94fC4AD6563F0864a55F9a697a90261ff
```
- Liberapay: https://liberapay.com/Reticulum/
Are certain features in the development roadmap are important to you or your
organisation? Make them a reality quickly by sponsoring their implementation.
- Ko-Fi: https://ko-fi.com/markqvist
## Cryptographic Primitives
Reticulum uses a simple suite of efficient, strong and well-tested cryptographic
primitives, with widely available implementations that can be used both on
general-purpose CPUs and on microcontrollers. The utilised primitives are:
general-purpose CPUs and on microcontrollers.
One of the primary considerations for choosing this particular set of primitives is
that they can be implemented *safely* with relatively few pitfalls, on practically
all current computing platforms.
The primitives listed here **are authoritative**. Anything claiming to be Reticulum,
but not using these exact primitives **is not** Reticulum, and possibly an
intentionally compromised or weakened clone. The utilised primitives are:
- Reticulum Identity Keys are 512-bit Curve25519 keysets
- A 256-bit Ed25519 key for signatures
@@ -320,15 +327,15 @@ general-purpose CPUs and on microcontrollers. The utilised primitives are:
- HKDF for key derivation
- Encrypted tokens are based on the [Fernet spec](https://github.com/fernet/spec/)
- Ephemeral keys derived from an ECDH key exchange on Curve25519
- AES-128 in CBC mode with PKCS7 padding
- HMAC using SHA256 for message authentication
- IVs are generated through os.urandom()
- IVs must be generated through `os.urandom()` or better
- AES-256 in CBC mode with PKCS7 padding
- No Fernet version and timestamp metadata fields
- SHA-256
- SHA-512
In the default installation configuration, the `X25519`, `Ed25519` and
`AES-128-CBC` primitives are provided by [OpenSSL](https://www.openssl.org/)
In the default installation configuration, the `X25519`, `Ed25519`,
and `AES-256-CBC` primitives are provided by [OpenSSL](https://www.openssl.org/)
(via the [PyCA/cryptography](https://github.com/pyca/cryptography) package).
The hashing functions `SHA-256` and `SHA-512` are provided by the standard
Python [hashlib](https://docs.python.org/3/library/hashlib.html). The `HKDF`,
@@ -342,13 +349,19 @@ provided by the following internal implementations:
Reticulum also includes a complete implementation of all necessary primitives
in pure Python. If OpenSSL & PyCA are not available on the system when
in pure Python. If OpenSSL and PyCA are not available on the system when
Reticulum is started, Reticulum will instead use the internal pure-python
primitives. A trivial consequence of this is performance, with the OpenSSL
backend being *much* faster. The most important consequence however, is the
potential loss of security by using primitives that has not seen the same
amount of scrutiny, testing and review as those from OpenSSL.
Please note that by default, installing Reticulum will **require** OpenSSL and
PyCA to also be automatically installed if not already available. It is only
possible to use the pure-python primitives if this requirement is specifically
overridden by the user, for example by installing the `rnspure` package instead
of the normal `rns` package, or by running directly from local source-code.
If you want to use the internal pure-python primitives, it is **highly
advisable** that you have a good understanding of the risks that this pose, and
make an informed decision on whether those risks are acceptable to you.
@@ -372,7 +385,8 @@ projects:
- [PyCA/cryptography](https://github.com/pyca/cryptography), *BSD License*
- [Pure-25519](https://github.com/warner/python-pure25519) by [Brian Warner](https://github.com/warner), *MIT License*
- [Pysha2](https://github.com/thomdixon/pysha2) by [Thom Dixon](https://github.com/thomdixon), *MIT License*
- [Python-AES](https://github.com/orgurar/python-aes) by [Or Gur Arie](https://github.com/orgurar), *MIT License*
- [Python AES-128](https://github.com/orgurar/python-aes) by [Or Gur Arie](https://github.com/orgurar), *MIT License*
- [Python AES-256](https://github.com/boppreh/aes) by [BoppreH](https://github.com/boppreh), *MIT License*
- [Curve25519.py](https://gist.github.com/nickovs/cc3c22d15f239a2640c185035c06f8a3#file-curve25519-py) by [Nicko van Someren](https://gist.github.com/nickovs), *Public Domain*
- [I2Plib](https://github.com/l-n-s/i2plib) by [Viktor Villainov](https://github.com/l-n-s)
- [PySerial](https://github.com/pyserial/pyserial) by Chris Liechti, *BSD License*
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors.
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors.
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+55 -12
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -24,23 +32,57 @@ import RNS.Cryptography.Provider as cp
import RNS.vendor.platformutils as pu
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
from .aes import AES
from .aes import AES128
from .aes import AES256
elif cp.PROVIDER == cp.PROVIDER_PYCA:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
if pu.cryptography_old_api():
from cryptography.hazmat.backends import default_backend
if pu.cryptography_old_api(): from cryptography.hazmat.backends import default_backend
class AES_128_CBC:
@staticmethod
def encrypt(plaintext, key, iv):
if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
cipher = AES(key)
cipher = AES128(key)
return cipher.encrypt(plaintext, iv)
elif cp.PROVIDER == cp.PROVIDER_PYCA:
if not pu.cryptography_old_api():
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
else:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
return ciphertext
@staticmethod
def decrypt(ciphertext, key, iv):
if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
cipher = AES128(key)
return cipher.decrypt(ciphertext, iv)
elif cp.PROVIDER == cp.PROVIDER_PYCA:
if not pu.cryptography_old_api():
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
else:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
return plaintext
class AES_256_CBC:
@staticmethod
def encrypt(plaintext, key, iv):
if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
cipher = AES256(key)
return cipher.encrypt_cbc(plaintext, iv)
elif cp.PROVIDER == cp.PROVIDER_PYCA:
if not pu.cryptography_old_api():
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
@@ -53,9 +95,10 @@ class AES_128_CBC:
@staticmethod
def decrypt(ciphertext, key, iv):
if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
cipher = AES(key)
return cipher.decrypt(ciphertext, iv)
cipher = AES256(key)
return cipher.decrypt_cbc(ciphertext, iv)
elif cp.PROVIDER == cp.PROVIDER_PYCA:
if not pu.cryptography_old_api():
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+48 -44
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -25,7 +33,9 @@ import time
from RNS.Cryptography import HMAC
from RNS.Cryptography import PKCS7
from RNS.Cryptography import AES
from RNS.Cryptography.AES import AES_128_CBC
from RNS.Cryptography.AES import AES_256_CBC
class Token():
"""
@@ -40,71 +50,65 @@ class Token():
TOKEN_OVERHEAD = 48 # Bytes
@staticmethod
def generate_key():
return os.urandom(32)
def generate_key(mode=AES_256_CBC):
if mode == AES_128_CBC: return os.urandom(32)
elif mode == AES_256_CBC: return os.urandom(64)
else: raise TypeError(f"Invalid token mode: {mode}")
def __init__(self, key = None):
if key == None:
raise ValueError("Token key cannot be None")
def __init__(self, key=None, mode=AES):
if key == None: raise ValueError("Token key cannot be None")
if len(key) != 32:
raise ValueError("Token key must be 32 bytes, not "+str(len(key)))
self._signing_key = key[:16]
self._encryption_key = key[16:]
if mode == AES:
if len(key) == 32:
self.mode = AES_128_CBC
self._signing_key = key[:16]
self._encryption_key = key[16:]
elif len(key) == 64:
self.mode = AES_256_CBC
self._signing_key = key[:32]
self._encryption_key = key[32:]
else: raise ValueError("Token key must be 128 or 256 bits, not "+str(len(key)*8))
else: raise TypeError(f"Invalid token mode: {mode}")
def verify_hmac(self, token):
if len(token) <= 32:
raise ValueError("Cannot verify HMAC on token of only "+str(len(token))+" bytes")
if len(token) <= 32: raise ValueError("Cannot verify HMAC on token of only "+str(len(token))+" bytes")
else:
received_hmac = token[-32:]
expected_hmac = HMAC.new(self._signing_key, token[:-32]).digest()
if received_hmac == expected_hmac:
return True
else:
return False
if received_hmac == expected_hmac: return True
else: return False
def encrypt(self, data = None):
if not isinstance(data, bytes): raise TypeError("Token plaintext input must be bytes")
iv = os.urandom(16)
current_time = int(time.time())
if not isinstance(data, bytes):
raise TypeError("Token plaintext input must be bytes")
ciphertext = AES_128_CBC.encrypt(
ciphertext = self.mode.encrypt(
plaintext = PKCS7.pad(data),
key = self._encryption_key,
iv = iv,
)
iv = iv)
signed_parts = iv+ciphertext
return signed_parts + HMAC.new(self._signing_key, signed_parts).digest()
def decrypt(self, token = None):
if not isinstance(token, bytes):
raise TypeError("Token must be bytes")
if not self.verify_hmac(token):
raise ValueError("Token HMAC was invalid")
if not isinstance(token, bytes): raise TypeError("Token must be bytes")
if not self.verify_hmac(token): raise ValueError("Token HMAC was invalid")
iv = token[:16]
ciphertext = token[16:-32]
try:
plaintext = PKCS7.unpad(
AES_128_CBC.decrypt(
ciphertext,
self._encryption_key,
iv,
)
)
return PKCS7.unpad(
self.mode.decrypt(
ciphertext = ciphertext,
key = self._encryption_key,
iv = iv))
return plaintext
except Exception as e:
raise ValueError("Could not decrypt token")
except Exception as e: raise ValueError(f"Could not decrypt token: {e}")
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+2 -1
View File
@@ -1 +1,2 @@
from .aes import AES
from .aes128 import AES128
from .aes256 import AES256
-271
View File
@@ -1,271 +0,0 @@
# MIT License
# Copyright (c) 2021 Or Gur Arie
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .utils import *
class AES:
# AES-128 block size
block_size = 16
# AES-128 encrypts messages with 10 rounds
_rounds = 10
# initiate the AES objecy
def __init__(self, key):
"""
Initializes the object with a given key.
"""
# make sure key length is right
assert len(key) == AES.block_size
# ExpandKey
self._round_keys = self._expand_key(key)
# will perform the AES ExpandKey phase
def _expand_key(self, master_key):
"""
Expands and returns a list of key matrices for the given master_key.
"""
# Initialize round keys with raw key material.
key_columns = bytes2matrix(master_key)
iteration_size = len(master_key) // 4
# Each iteration has exactly as many columns as the key material.
i = 1
while len(key_columns) < (self._rounds + 1) * 4:
# Copy previous word.
word = list(key_columns[-1])
# Perform schedule_core once every "row".
if len(key_columns) % iteration_size == 0:
# Circular shift.
word.append(word.pop(0))
# Map to S-BOX.
word = [s_box[b] for b in word]
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
word[0] ^= r_con[i]
i += 1
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
# Run word through S-box in the fourth iteration when using a
# 256-bit key.
word = [s_box[b] for b in word]
# XOR with equivalent word from previous iteration.
word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size]))
key_columns.append(word)
# Group key words in 4x4 byte matrices.
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
# encrypt a single block of data with AES
def _encrypt_block(self, plaintext):
"""
Encrypts a single block of 16 byte long plaintext.
"""
# length of a single block
assert len(plaintext) == AES.block_size
# perform on a matrix
state = bytes2matrix(plaintext)
# AddRoundKey
add_round_key(state, self._round_keys[0])
# 9 main rounds
for i in range(1, self._rounds):
# SubBytes
sub_bytes(state)
# ShiftRows
shift_rows(state)
# MixCols
mix_columns(state)
# AddRoundKey
add_round_key(state, self._round_keys[i])
# last round, w/t AddRoundKey step
sub_bytes(state)
shift_rows(state)
add_round_key(state, self._round_keys[-1])
# return the encrypted matrix as bytes
return matrix2bytes(state)
# decrypt a single block of data with AES
def _decrypt_block(self, ciphertext):
"""
Decrypts a single block of 16 byte long ciphertext.
"""
# length of a single block
assert len(ciphertext) == AES.block_size
# perform on a matrix
state = bytes2matrix(ciphertext)
# in reverse order, last round is first
add_round_key(state, self._round_keys[-1])
inv_shift_rows(state)
inv_sub_bytes(state)
for i in range(self._rounds - 1, 0, -1):
# nain rounds
add_round_key(state, self._round_keys[i])
inv_mix_columns(state)
inv_shift_rows(state)
inv_sub_bytes(state)
# initial AddRoundKey phase
add_round_key(state, self._round_keys[0])
# return bytes
return matrix2bytes(state)
# will encrypt the entire data
def encrypt(self, plaintext, iv):
"""
Encrypts `plaintext` using CBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
# iv length must be same as block size
assert len(iv) == AES.block_size
assert len(plaintext) % AES.block_size == 0
ciphertext_blocks = []
previous = iv
for plaintext_block in split_blocks(plaintext):
# in CBC mode every block is XOR'd with the previous block
xorred = xor_bytes(plaintext_block, previous)
# encrypt current block
block = self._encrypt_block(xorred)
previous = block
# append to ciphertext
ciphertext_blocks.append(block)
# return as bytes
return b''.join(ciphertext_blocks)
# will decrypt the entire data
def decrypt(self, ciphertext, iv):
"""
Decrypts `ciphertext` using CBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
# iv length must be same as block size
assert len(iv) == AES.block_size
plaintext_blocks = []
previous = iv
for ciphertext_block in split_blocks(ciphertext):
# in CBC mode every block is XOR'd with the previous block
xorred = xor_bytes(previous, self._decrypt_block(ciphertext_block))
# append plaintext
plaintext_blocks.append(xorred)
previous = ciphertext_block
return b''.join(plaintext_blocks)
def test():
# modules and classes requiered for test only
import os
class bcolors:
OK = '\033[92m' #GREEN
WARNING = '\033[93m' #YELLOW
FAIL = '\033[91m' #RED
RESET = '\033[0m' #RESET COLOR
# will test AES class by performing an encryption / decryption
print("AES Tests")
print("=========")
# generate a secret key and print details
key = os.urandom(AES.block_size)
_aes = AES(key)
print(f"Algorithm: AES-CBC-{AES.block_size*8}")
print(f"Secret Key: {key.hex()}")
print()
# test single block encryption / decryption
iv = os.urandom(AES.block_size)
single_block_text = b"SingleBlock Text"
print("Single Block Tests")
print("------------------")
print(f"iv: {iv.hex()}")
print(f"plain text: '{single_block_text.decode()}'")
ciphertext_block = _aes._encrypt_block(single_block_text)
plaintext_block = _aes._decrypt_block(ciphertext_block)
print(f"Ciphertext Hex: {ciphertext_block.hex()}")
print(f"Plaintext: {plaintext_block.decode()}")
assert plaintext_block == single_block_text
print(bcolors.OK + "Single Block Test Passed Successfully" + bcolors.RESET)
print()
# test a less than a block length phrase
iv = os.urandom(AES.block_size)
short_text = b"Just Text"
print("Short Text Tests")
print("----------------")
print(f"iv: {iv.hex()}")
print(f"plain text: '{short_text.decode()}'")
ciphertext_short = _aes.encrypt(short_text, iv)
plaintext_short = _aes.decrypt(ciphertext_short, iv)
print(f"Ciphertext Hex: {ciphertext_short.hex()}")
print(f"Plaintext: {plaintext_short.decode()}")
assert short_text == plaintext_short
print(bcolors.OK + "Short Text Test Passed Successfully" + bcolors.RESET)
print()
# test an arbitrary length phrase
iv = os.urandom(AES.block_size)
text = b"This Text is longer than one block"
print("Arbitrary Length Tests")
print("----------------------")
print(f"iv: {iv.hex()}")
print(f"plain text: '{text.decode()}'")
ciphertext = _aes.encrypt(text, iv)
plaintext = _aes.decrypt(ciphertext, iv)
print(f"Ciphertext Hex: {ciphertext.hex()}")
print(f"Plaintext: {plaintext.decode()}")
assert text == plaintext
print(bcolors.OK + "Arbitrary Length Text Test Passed Successfully" + bcolors.RESET)
print()
if __name__ == "__main__":
# test AES class
test()
+326
View File
@@ -0,0 +1,326 @@
# MIT License
# Copyright (c) 2021 Or Gur Arie
# 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.
## AES lookup tables
# resource: https://en.wikipedia.org/wiki/Rijndael_S-box
s_box = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)
inv_s_box = (
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)
## AES AddRoundKey
# Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants
r_con = (
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)
def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]
## AES SubBytes
def sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = s_box[s[i][j]]
def inv_sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = inv_s_box[s[i][j]]
## AES ShiftRows
def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
def inv_shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
## AES MixColumns
# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
def mix_single_column(a):
# see Sec 4.1.2 in The Design of Rijndael
t = a[0] ^ a[1] ^ a[2] ^ a[3]
u = a[0]
a[0] ^= t ^ xtime(a[0] ^ a[1])
a[1] ^= t ^ xtime(a[1] ^ a[2])
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)
def mix_columns(s):
for i in range(4):
mix_single_column(s[i])
def inv_mix_columns(s):
# see Sec 4.1.3 in The Design of Rijndael
for i in range(4):
u = xtime(xtime(s[i][0] ^ s[i][2]))
v = xtime(xtime(s[i][1] ^ s[i][3]))
s[i][0] ^= u
s[i][1] ^= v
s[i][2] ^= u
s[i][3] ^= v
mix_columns(s)
## AES Bytes
def bytes2matrix(text):
""" Converts a 16-byte array into a 4x4 matrix. """
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
return bytes(sum(matrix, []))
def xor_bytes(a, b):
""" Returns a new byte array with the elements xor'ed. """
return bytes(i^j for i, j in zip(a, b))
def split_blocks(message, block_size=16, require_padding=True):
assert len(message) % block_size == 0 or not require_padding
return [message[i:i+16] for i in range(0, len(message), block_size)]
class AES128:
# AES-128 block size
block_size = 16
# AES-128 encrypts messages with 10 rounds
_rounds = 10
# initiate the AES objecy
def __init__(self, key):
"""
Initializes the object with a given key.
"""
# make sure key length is right
assert len(key) == AES128.block_size
# ExpandKey
self._round_keys = self._expand_key(key)
# will perform the AES ExpandKey phase
def _expand_key(self, master_key):
"""
Expands and returns a list of key matrices for the given master_key.
"""
# Initialize round keys with raw key material.
key_columns = bytes2matrix(master_key)
iteration_size = len(master_key) // 4
# Each iteration has exactly as many columns as the key material.
i = 1
while len(key_columns) < (self._rounds + 1) * 4:
# Copy previous word.
word = list(key_columns[-1])
# Perform schedule_core once every "row".
if len(key_columns) % iteration_size == 0:
# Circular shift.
word.append(word.pop(0))
# Map to S-BOX.
word = [s_box[b] for b in word]
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
word[0] ^= r_con[i]
i += 1
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
# Run word through S-box in the fourth iteration when using a
# 256-bit key.
word = [s_box[b] for b in word]
# XOR with equivalent word from previous iteration.
word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size]))
key_columns.append(word)
# Group key words in 4x4 byte matrices.
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
# encrypt a single block of data with AES
def _encrypt_block(self, plaintext):
"""
Encrypts a single block of 16 byte long plaintext.
"""
# length of a single block
assert len(plaintext) == AES128.block_size
# perform on a matrix
state = bytes2matrix(plaintext)
# AddRoundKey
add_round_key(state, self._round_keys[0])
# 9 main rounds
for i in range(1, self._rounds):
# SubBytes
sub_bytes(state)
# ShiftRows
shift_rows(state)
# MixCols
mix_columns(state)
# AddRoundKey
add_round_key(state, self._round_keys[i])
# last round, w/t AddRoundKey step
sub_bytes(state)
shift_rows(state)
add_round_key(state, self._round_keys[-1])
# return the encrypted matrix as bytes
return matrix2bytes(state)
# decrypt a single block of data with AES
def _decrypt_block(self, ciphertext):
"""
Decrypts a single block of 16 byte long ciphertext.
"""
# length of a single block
assert len(ciphertext) == AES128.block_size
# perform on a matrix
state = bytes2matrix(ciphertext)
# in reverse order, last round is first
add_round_key(state, self._round_keys[-1])
inv_shift_rows(state)
inv_sub_bytes(state)
for i in range(self._rounds - 1, 0, -1):
# nain rounds
add_round_key(state, self._round_keys[i])
inv_mix_columns(state)
inv_shift_rows(state)
inv_sub_bytes(state)
# initial AddRoundKey phase
add_round_key(state, self._round_keys[0])
# return bytes
return matrix2bytes(state)
# will encrypt the entire data
def encrypt(self, plaintext, iv):
"""
Encrypts `plaintext` using CBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
# iv length must be same as block size
assert len(iv) == AES128.block_size
assert len(plaintext) % AES128.block_size == 0
ciphertext_blocks = []
previous = iv
for plaintext_block in split_blocks(plaintext):
# in CBC mode every block is XOR'd with the previous block
xorred = xor_bytes(plaintext_block, previous)
# encrypt current block
block = self._encrypt_block(xorred)
previous = block
# append to ciphertext
ciphertext_blocks.append(block)
# return as bytes
return b''.join(ciphertext_blocks)
# will decrypt the entire data
def decrypt(self, ciphertext, iv):
"""
Decrypts `ciphertext` using CBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
# iv length must be same as block size
assert len(iv) == AES128.block_size
plaintext_blocks = []
previous = iv
for ciphertext_block in split_blocks(ciphertext):
# in CBC mode every block is XOR'd with the previous block
xorred = xor_bytes(previous, self._decrypt_block(ciphertext_block))
# append plaintext
plaintext_blocks.append(xorred)
previous = ciphertext_block
return b''.join(plaintext_blocks)
@@ -1,17 +1,17 @@
# MIT License
# Copyright (c) 2021 Or Gur Arie
#
# Copyright (c) 2024 BoppreH
#
# 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
@@ -20,12 +20,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
'''
Utils class for AES encryption / decryption
'''
## AES lookup tables
# resource: https://en.wikipedia.org/wiki/Rijndael_S-box
s_box = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
@@ -64,53 +58,33 @@ inv_s_box = (
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)
## AES AddRoundKey
# Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants
r_con = (
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)
def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]
## AES SubBytes
def sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = s_box[s[i][j]]
def inv_sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = inv_s_box[s[i][j]]
## AES ShiftRows
def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
def inv_shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]
## AES MixColumns
# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
def mix_single_column(a):
# see Sec 4.1.2 in The Design of Rijndael
t = a[0] ^ a[1] ^ a[2] ^ a[3]
@@ -120,12 +94,10 @@ def mix_single_column(a):
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)
def mix_columns(s):
for i in range(4):
mix_single_column(s[i])
def inv_mix_columns(s):
# see Sec 4.1.3 in The Design of Rijndael
for i in range(4):
@@ -138,22 +110,127 @@ def inv_mix_columns(s):
mix_columns(s)
## AES Bytes
def bytes2matrix(text):
""" Converts a 16-byte array into a 4x4 matrix. """
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
return bytes(sum(matrix, []))
r_con = (
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)
def xor_bytes(a, b):
""" Returns a new byte array with the elements xor'ed. """
return bytes(i^j for i, j in zip(a, b))
def bytes2matrix(text): return [list(text[i:i+4]) for i in range(0, len(text), 4)]
def matrix2bytes(matrix): return bytes(sum(matrix, []))
def xor_bytes(a, b): return bytes(i^j for i, j in zip(a, b))
def inc_bytes(a):
out = list(a)
for i in reversed(range(len(out))):
if out[i] == 0xFF:
out[i] = 0
else:
out[i] += 1
break
return bytes(out)
def split_blocks(message, block_size=16, require_padding=True):
assert len(message) % block_size == 0 or not require_padding
return [message[i:i+16] for i in range(0, len(message), block_size)]
assert len(message) % block_size == 0 or not require_padding
return [message[i:i+16] for i in range(0, len(message), block_size)]
class AES256:
rounds_by_key_size = {32: 14}
def __init__(self, master_key):
assert len(master_key) in AES256.rounds_by_key_size
self.n_rounds = AES256.rounds_by_key_size[len(master_key)]
self._key_matrices = self._expand_key(master_key)
def _expand_key(self, master_key):
# Initialize round keys with raw key material.
key_columns = bytes2matrix(master_key)
iteration_size = len(master_key) // 4
i = 1
while len(key_columns) < (self.n_rounds + 1) * 4:
# Copy previous word.
word = list(key_columns[-1])
# Perform schedule_core once every "row".
if len(key_columns) % iteration_size == 0:
# Circular shift.
word.append(word.pop(0))
# Map to S-BOX.
word = [s_box[b] for b in word]
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
word[0] ^= r_con[i]
i += 1
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
# Run word through S-box in the fourth iteration when using a
# 256-bit key.
word = [s_box[b] for b in word]
# XOR with equivalent word from previous iteration.
word = xor_bytes(word, key_columns[-iteration_size])
key_columns.append(word)
# Group key words in 4x4 byte matrices.
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
def encrypt_block(self, plaintext):
assert len(plaintext) == 16
plain_state = bytes2matrix(plaintext)
add_round_key(plain_state, self._key_matrices[0])
for i in range(1, self.n_rounds):
sub_bytes(plain_state)
shift_rows(plain_state)
mix_columns(plain_state)
add_round_key(plain_state, self._key_matrices[i])
sub_bytes(plain_state)
shift_rows(plain_state)
add_round_key(plain_state, self._key_matrices[-1])
return matrix2bytes(plain_state)
def decrypt_block(self, ciphertext):
assert len(ciphertext) == 16
cipher_state = bytes2matrix(ciphertext)
add_round_key(cipher_state, self._key_matrices[-1])
inv_shift_rows(cipher_state)
inv_sub_bytes(cipher_state)
for i in range(self.n_rounds - 1, 0, -1):
add_round_key(cipher_state, self._key_matrices[i])
inv_mix_columns(cipher_state)
inv_shift_rows(cipher_state)
inv_sub_bytes(cipher_state)
add_round_key(cipher_state, self._key_matrices[0])
return matrix2bytes(cipher_state)
def encrypt_cbc(self, plaintext, iv):
if len(iv) != 16: raise ValueError(f"Invalid IV length: {len(iv)}")
blocks = []
previous = iv
for plaintext_block in split_blocks(plaintext):
block = self.encrypt_block(xor_bytes(plaintext_block, previous))
blocks.append(block)
previous = block
return b''.join(blocks)
def decrypt_cbc(self, ciphertext, iv):
if len(iv) != 16: raise ValueError(f"Invalid IV length: {len(iv)}")
blocks = []
previous = iv
for ciphertext_block in split_blocks(ciphertext):
blocks.append(xor_bytes(previous, self.decrypt_block(ciphertext_block)))
previous = ciphertext_block
return b''.join(blocks)
__all__ = ["AES256"]
+29 -16
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -202,12 +210,16 @@ class Destination:
def _persist_ratchets(self):
try:
with self.ratchet_file_lock:
temp_write_path = self.ratchets_path+".tmp"
packed_ratchets = umsgpack.packb(self.ratchets)
persisted_data = {"signature": self.sign(packed_ratchets), "ratchets": packed_ratchets}
ratchets_file = open(self.ratchets_path, "wb")
ratchets_file = open(temp_write_path, "wb")
ratchets_file.write(umsgpack.packb(persisted_data))
ratchets_file.close()
if os.path.isfile(self.ratchets_path): os.unlink(self.ratchets_path)
os.rename(temp_write_path, self.ratchets_path)
except Exception as e:
RNS.trace_exception(e)
self.ratchets = None
self.ratchets_path = None
raise OSError("Could not write ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
@@ -365,7 +377,7 @@ class Destination:
else:
self.proof_strategy = proof_strategy
def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None):
def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None, auto_compress = True):
"""
Registers a request handler.
@@ -373,17 +385,15 @@ class Destination:
:param response_generator: A function or method with the signature *response_generator(path, data, request_id, link_id, remote_identity, requested_at)* to be called. Whatever this funcion returns will be sent as a response to the requester. If the function returns ``None``, no response will be sent.
:param allow: One of ``RNS.Destination.ALLOW_NONE``, ``RNS.Destination.ALLOW_ALL`` or ``RNS.Destination.ALLOW_LIST``. If ``RNS.Destination.ALLOW_LIST`` is set, the request handler will only respond to requests for identified peers in the supplied list.
:param allowed_list: A list of *bytes-like* :ref:`RNS.Identity<api-identity>` hashes.
:param auto_compress: If ``True`` or ``False``, determines whether automatic compression of responses should be carried out. If set to an integer value, responses will only be auto-compressed if under this size in bytes. If omitted, the default compression settings will be followed.
:raises: ``ValueError`` if any of the supplied arguments are invalid.
"""
if path == None or path == "":
raise ValueError("Invalid path specified")
elif not callable(response_generator):
raise ValueError("Invalid response generator specified")
elif not allow in Destination.request_policies:
raise ValueError("Invalid request policy")
if path == None or path == "": raise ValueError("Invalid path specified")
elif not callable(response_generator): raise ValueError("Invalid response generator specified")
elif not allow in Destination.request_policies: raise ValueError("Invalid request policy")
else:
path_hash = RNS.Identity.truncated_hash(path.encode("utf-8"))
request_handler = [path, response_generator, allow, allowed_list]
request_handler = [path, response_generator, allow, allowed_list, auto_compress]
self.request_handlers[path_hash] = request_handler
def deregister_request_handler(self, path):
@@ -407,7 +417,8 @@ class Destination:
else:
plaintext = self.decrypt(packet.data)
packet.ratchet_id = self.latest_ratchet_id
if plaintext != None:
if plaintext == None: return False
else:
if packet.packet_type == RNS.Packet.DATA:
if self.callbacks.packet != None:
try:
@@ -415,6 +426,8 @@ class Destination:
except Exception as e:
RNS.log("Error while executing receive callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
return True
def incoming_link_request(self, data, packet):
if self.accept_link_requests:
link = RNS.Link.validate_request(self, data, packet)
@@ -450,6 +463,7 @@ class Destination:
self.ratchets_path = None
RNS.trace_exception(e)
raise OSError("Could not read ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
RNS.log("No existing ratchet data found, initialising new ratchet file for "+str(self), RNS.LOG_DEBUG)
self.ratchets = []
@@ -475,7 +489,6 @@ class Destination:
self.latest_ratchet_time = 0
self._reload_ratchets(ratchets_path)
# TODO: Remove at some point
RNS.log("Ratchets enabled on "+str(self), RNS.LOG_DEBUG)
return True
@@ -629,7 +642,7 @@ class Destination:
RNS.log(f"Decryption still failing after ratchet reload. The contained exception was: {e}", RNS.LOG_ERROR)
raise e
RNS.log("Decryption succeeded after ratchet reload", RNS.LOG_NOTICE)
if decrypted: RNS.log("Decryption succeeded after ratchet reload", RNS.LOG_NOTICE)
return decrypted
+33 -25
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors.
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -71,13 +79,16 @@ class Identity:
HASHLENGTH = 256 # In bits
SIGLENGTH = KEYSIZE # In bits
NAME_HASH_LENGTH = 80
TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH
NAME_HASH_LENGTH = 80
TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH
"""
Constant specifying the truncated hash length (in bits) used by Reticulum
for addressable hashes and other purposes. Non-configurable.
"""
DERIVED_KEY_LENGTH = 512//8
DERIVED_KEY_LENGTH_LEGACY = 256//8
# Storage
known_destinations = {}
known_ratchets = {}
@@ -669,7 +680,7 @@ class Identity:
shared_key = ephemeral_key.exchange(target_public_key)
derived_key = RNS.Cryptography.hkdf(
length=32,
length=Identity.DERIVED_KEY_LENGTH,
derive_from=shared_key,
salt=self.get_salt(),
context=self.get_context(),
@@ -683,6 +694,16 @@ class Identity:
else:
raise KeyError("Encryption failed because identity does not hold a public key")
def __decrypt(self, shared_key, ciphertext):
derived_key = RNS.Cryptography.hkdf(
length=Identity.DERIVED_KEY_LENGTH,
derive_from=shared_key,
salt=self.get_salt(),
context=self.get_context())
token = Token(derived_key)
plaintext = token.decrypt(ciphertext)
return plaintext
def decrypt(self, ciphertext_token, ratchets=None, enforce_ratchets=False, ratchet_id_receiver=None):
"""
@@ -692,6 +713,7 @@ class Identity:
:returns: Plaintext as *bytes*, or *None* if decryption fails.
:raises: *KeyError* if the instance does not hold a private key.
"""
if self.prv != None:
if len(ciphertext_token) > Identity.KEYSIZE//8//2:
plaintext = None
@@ -706,15 +728,7 @@ class Identity:
ratchet_prv = X25519PrivateKey.from_private_bytes(ratchet)
ratchet_id = Identity._get_ratchet_id(ratchet_prv.public_key().public_bytes())
shared_key = ratchet_prv.exchange(peer_pub)
derived_key = RNS.Cryptography.hkdf(
length=32,
derive_from=shared_key,
salt=self.get_salt(),
context=self.get_context(),
)
token = Token(derived_key)
plaintext = token.decrypt(ciphertext)
plaintext = self.__decrypt(shared_key, ciphertext)
if ratchet_id_receiver:
ratchet_id_receiver.latest_ratchet_id = ratchet_id
@@ -731,15 +745,8 @@ class Identity:
if plaintext == None:
shared_key = self.prv.exchange(peer_pub)
derived_key = RNS.Cryptography.hkdf(
length=32,
derive_from=shared_key,
salt=self.get_salt(),
context=self.get_context(),
)
plaintext = self.__decrypt(shared_key, ciphertext)
token = Token(derived_key)
plaintext = token.decrypt(ciphertext)
if ratchet_id_receiver:
ratchet_id_receiver.latest_ratchet_id = None
@@ -748,7 +755,8 @@ class Identity:
if ratchet_id_receiver:
ratchet_id_receiver.latest_ratchet_id = None
return plaintext;
return plaintext
else:
RNS.log("Decryption failed because the token size was invalid.", RNS.LOG_DEBUG)
return None
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+299 -71
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -24,6 +32,7 @@ from RNS.Interfaces.Interface import Interface
from time import sleep
import sys
import threading
import socket
import time
import math
import RNS
@@ -65,6 +74,7 @@ class KISS():
CMD_STAT_PHYPRM = 0x26
CMD_STAT_BAT = 0x27
CMD_STAT_CSMA = 0x28
CMD_STAT_TEMP = 0x29
CMD_BLINK = 0x30
CMD_RANDOM = 0x40
CMD_FB_EXT = 0x41
@@ -355,7 +365,9 @@ class RNodeInterface(Interface):
target_device_address = c["target_device_address"] if "target_device_address" in c else None
ble_name = c["ble_name"] if "ble_name" in c else None
ble_addr = c["ble_addr"] if "ble_addr" in c else None
tcp_host = c["tcp_host"] if "tcp_host" in c else None
force_ble = c["force_ble"] if "force_ble" in c else False
force_tcp = c["force_tcp"] if "force_tcp" in c else False
frequency = int(c["frequency"]) if "frequency" in c else 0
bandwidth = int(c["bandwidth"]) if "bandwidth" in c else 0
txpower = int(c["txpower"]) if "txpower" in c else 0
@@ -427,6 +439,14 @@ class RNodeInterface(Interface):
self.ble_rx_queue= b""
self.ble_tx_queue= b""
self.tcp = None
self.use_tcp = False
self.tcp_host = tcp_host
self.tcp_rx_queue= b""
self.tcp_tx_queue= b""
self.tcp_rx_lock = threading.Lock()
self.tcp_tx_lock = threading.Lock()
self.frequency = frequency
self.bandwidth = bandwidth
self.txpower = txpower
@@ -436,6 +456,7 @@ class RNodeInterface(Interface):
self.bitrate = 0
self.st_alock = st_alock
self.lt_alock = lt_alock
self.cpu_temp = None
self.platform = None
self.display = None
self.mcu = None
@@ -448,6 +469,7 @@ class RNodeInterface(Interface):
self.first_tx = None
self.reconnect_w = RNodeInterface.RECONNECT_WAIT
self.reconnect_lock = threading.Lock()
self.awaiting_ble_reset = False
self.r_frequency = None
self.r_bandwidth = None
@@ -478,6 +500,9 @@ class RNodeInterface(Interface):
self.r_csma_cw_max = None
self.r_current_rssi = None
self.r_noise_floor = None
self.r_interference = None
self.r_interference_l = None
self.r_temperature = None
self.r_battery_state = RNodeInterface.BATTERY_STATE_UNKNOWN
self.r_battery_percent = 0
@@ -499,15 +524,15 @@ class RNodeInterface(Interface):
self.port_io_timeout = RNodeInterface.PORT_IO_TIMEOUT
self.last_imagedata = None
if force_ble or self.ble_addr != None or self.ble_name != None:
self.use_ble = True
if force_ble or self.ble_addr != None or self.ble_name != None: self.use_ble = True
if force_tcp or self.tcp_host != None: self.use_tcp = True
self.validcfg = True
if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
if (self.txpower < 0 or self.txpower > 22):
if (self.txpower < 0 or self.txpower > 37):
RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
@@ -550,10 +575,8 @@ class RNodeInterface(Interface):
self.open_port()
if self.serial != None:
if self.serial.is_open:
self.configure_device()
else:
raise IOError("Could not open serial port")
if self.serial.is_open: self.configure_device()
else: raise IOError("Could not open serial port")
elif self.bt_manager != None:
if self.bt_manager.connected:
self.configure_device()
@@ -571,12 +594,9 @@ class RNodeInterface(Interface):
def read_mux(self, len=None):
if self.serial != None:
return self.serial.read()
elif self.bt_manager != None:
return self.bt_manager.read()
else:
raise IOError("No ports available for reading")
if self.serial != None: return self.serial.read()
elif self.bt_manager != None: return self.bt_manager.read()
else: raise IOError("No ports available for reading")
def write_mux(self, data):
if self.serial != None:
@@ -591,17 +611,19 @@ class RNodeInterface(Interface):
else:
raise IOError("No ports available for writing")
# def reset_ble(self):
# RNS.log(f"Clearing previous connection instance: "+str(self.ble))
# del self.ble
# self.ble = None
# self.serial = None
# self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
# self.serial = self.ble
# RNS.log(f"New connection instance: "+str(self.ble))
def reset_ble(self):
if not self.awaiting_ble_reset: return
else:
RNS.log(f"Clearing previous connection instance: "+str(self.ble), RNS.LOG_DEBUG)
self.ble = None
self.serial = None
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
self.serial = self.ble
self.awaiting_ble_reset = False
RNS.log(f"New connection instance: "+str(self.ble), RNS.LOG_DEBUG)
def open_port(self):
if not self.use_ble:
if not self.use_ble and not self.use_tcp:
if self.port != None:
RNS.log("Opening serial port "+self.port+"...")
# Get device parameters
@@ -669,7 +691,7 @@ class RNodeInterface(Interface):
if self.bt_manager != None:
self.bt_manager.connect_any_device()
else:
elif self.use_ble:
if self.ble == None:
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
self.serial = self.ble
@@ -678,6 +700,24 @@ class RNodeInterface(Interface):
while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
time.sleep(1)
elif self.use_tcp:
RNS.log(f"Opening TCP connection for {self}...")
if self.tcp != None and self.tcp.running == False:
self.tcp.close()
self.tcp.cleanup()
self.tcp = None
if self.tcp == None:
self.tcp = TCPConnection(owner=self, target_host=self.tcp_host)
self.serial = self.tcp
open_time = time.time()
while not self.tcp.connected and time.time() < open_time + self.tcp.CONNECT_TIMEOUT:
time.sleep(1)
else:
raise TypeError("No valid device connection type defined for RNode interface")
def configure_device(self):
self.resetRadioState()
@@ -685,17 +725,19 @@ class RNodeInterface(Interface):
thread = threading.Thread(target=self.readLoop, daemon=True).start()
self.detect()
if not self.use_ble:
sleep(0.5)
else:
if self.use_tcp:
tcp_detect_timeout = 5.0
detect_time = time.time()
while not self.detected and time.time() < detect_time + tcp_detect_timeout: time.sleep(0.1)
if not self.detected: RNS.log(f"RNode detect timed out over TCP", RNS.LOG_ERROR)
elif self.use_ble:
ble_detect_timeout = 5
detect_time = time.time()
while not self.detected and time.time() < detect_time + ble_detect_timeout:
time.sleep(0.1)
if self.detected:
detect_time = RNS.prettytime(time.time()-detect_time)
else:
RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
while not self.detected and time.time() < detect_time + ble_detect_timeout: time.sleep(0.1)
if self.detected: detect_time = RNS.prettytime(time.time()-detect_time)
else: RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
else:
sleep(0.2)
if not self.detected:
raise IOError("Could not detect device")
@@ -708,11 +750,19 @@ class RNodeInterface(Interface):
if self.serial != None and self.port != None:
self.timeout = 200
RNS.log("Serial port "+self.port+" is now open")
RNS.log(f"Serial port {self.port} is now open")
if self.bt_manager != None and self.bt_manager.connected:
self.timeout = 1500
RNS.log("Bluetooth connection to RNode now open")
RNS.log(f"Bluetooth connection to RNode now open")
if self.ble != None and self.ble.connected:
self.timeout = 1500
RNS.log(f"BLE connection {self.port} to RNode now open")
if self.tcp != None and self.tcp.connected:
self.timeout = 1500
RNS.log(f"TCP connection tcp://{self.tcp_host} to RNode now open")
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
self.initRadio()
@@ -964,10 +1014,8 @@ class RNodeInterface(Interface):
def validateRadioState(self):
RNS.log("Waiting for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
if not self.platform == KISS.PLATFORM_ESP32:
sleep(1.00);
else:
sleep(2.00);
if not self.platform == KISS.PLATFORM_ESP32: sleep(1.00);
else: sleep(2.00);
self.validcfg = True
if (self.r_frequency != None and abs(self.frequency - int(self.r_frequency)) > 100):
@@ -1264,10 +1312,12 @@ class RNodeInterface(Interface):
self.r_channel_load_long = cul/100.0
self.r_current_rssi = crs-RNodeInterface.RSSI_OFFSET
self.r_noise_floor = nfl-RNodeInterface.RSSI_OFFSET
if ntf == 0xFF:
self.r_interference = None
else:
self.r_interference = ntf-RNodeInterface.RSSI_OFFSET
self.r_interference_l = [time.time(), self.r_interference]
if self.r_interference != None:
RNS.log(f"{self} Radio detected interference at {self.r_interference} dBm", RNS.LOG_DEBUG)
@@ -1347,6 +1397,22 @@ class RNodeInterface(Interface):
bat_percent = 0
self.r_battery_state = command_buffer[0]
self.r_battery_percent = bat_percent
elif (command == KISS.CMD_STAT_TEMP):
if (byte == KISS.FESC):
escape = True
else:
if (escape):
if (byte == KISS.TFEND):
byte = KISS.FEND
if (byte == KISS.TFESC):
byte = KISS.FESC
escape = False
command_buffer = command_buffer+bytes([byte])
if (len(command_buffer) == 1):
temp = command_buffer[0]-120
if temp >= -30 and temp <= 90: self.r_temperature = temp
else: self.r_temperature = None
self.cpu_temp = self.r_temperature
elif (command == KISS.CMD_RANDOM):
self.r_random = byte
elif (command == KISS.CMD_PLATFORM):
@@ -1417,7 +1483,7 @@ class RNodeInterface(Interface):
if got == 0:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
RNS.log(str(self)+" serial read timeout in command "+str(command), RNS.LOG_WARNING)
RNS.log(f"{self} device read timeout in command {command} after {RNS.prettytime(self.timeout/1000.0)}", RNS.LOG_WARNING)
data_buffer = b""
in_frame = False
command = KISS.CMD_UNKNOWN
@@ -1428,19 +1494,19 @@ class RNodeInterface(Interface):
if time.time() > self.first_tx + self.id_interval:
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.id_callsign.decode("utf-8")), RNS.LOG_DEBUG)
self.process_outgoing(self.id_callsign)
if (time.time() - self.last_port_io > self.port_io_timeout):
self.detect()
if (time.time() - self.last_port_io > self.port_io_timeout*3):
raise IOError("Connected port for "+str(self)+" became unresponsive")
if self.bt_manager != None:
sleep(0.08)
if self.use_tcp:
if self.tcp and self.tcp.connected:
if time.time() > self.tcp.last_write + self.tcp.ACTIVITY_KEEPALIVE:
self.detect()
if (time.time() - self.last_port_io > self.port_io_timeout): self.detect()
if (time.time() - self.last_port_io > self.port_io_timeout*3): raise IOError("Connected port for "+str(self)+" became unresponsive")
if self.bt_manager != None or self.ble != None: sleep(0.08)
except Exception as e:
self.online = False
RNS.log("A serial port occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error:
@@ -1498,12 +1564,18 @@ class RNodeInterface(Interface):
def detach(self):
self.detached = True
self.disable_external_framebuffer()
self.setRadioState(KISS.RADIO_STATE_OFF)
self.leave()
try:
self.disable_external_framebuffer()
self.setRadioState(KISS.RADIO_STATE_OFF)
self.leave()
if self.use_ble:
self.ble.close()
except Exception as e:
RNS.log(f"An error occurred while detaching {self}: {e}", RNS.LOG_ERROR)
if self.use_ble: self.ble.close()
if self.use_tcp:
time.sleep(0.5)
self.tcp.close()
def should_ingress_limit(self):
return False
@@ -1524,6 +1596,17 @@ class RNodeInterface(Interface):
def get_battery_percent(self):
return self.r_battery_percent
def tcp_receive(self, data):
with self.tcp_rx_lock: self.tcp_rx_queue += data
def tcp_waiting(self): return len(self.tcp_tx_queue) > 0
def get_tcp_waiting(self, n):
with self.tcp_tx_lock:
data = self.tcp_tx_queue[:n]
self.tcp_tx_queue = self.tcp_tx_queue[n:]
return data
def ble_receive(self, data):
with self.ble_rx_lock:
self.ble_rx_queue += data
@@ -1538,7 +1621,7 @@ class RNodeInterface(Interface):
return data
def __str__(self):
return "RNodeInterface["+str(self.name)+"]"
return f"RNodeInterface[{self.name}]"
class BLEConnection(BluetoothDispatcher):
UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
@@ -1550,7 +1633,7 @@ class BLEConnection(BluetoothDispatcher):
MTU_TIMEOUT = 4.0
CONNECT_TIMEOUT = 7.0
RECONNECT_WAIT = 1.0
RECONNECT_WAIT = 2.5
@property
def is_open(self):
@@ -1651,13 +1734,17 @@ class BLEConnection(BluetoothDispatcher):
self.write_thread = None
def connection_job(self):
ble_devices = []
while self.should_run:
if self.bt_manager.bt_enabled():
if self.ble_device == None:
self.ble_device = self.find_target_device()
if not self.connected:
if len(ble_devices) == 0:
ble_devices = self.find_target_devices()
if len(ble_devices) > 0: self.ble_device = ble_devices.pop()
else: self.ble_device == None
if self.ble_device != None:
if not self.connected:
if self.ble_device != None:
if self.was_connected:
RNS.log(f"Throttling BLE reconnect for {BLEConnection.RECONNECT_WAIT} seconds", RNS.LOG_DEBUG)
time.sleep(BLEConnection.RECONNECT_WAIT)
@@ -1669,7 +1756,7 @@ class BLEConnection(BluetoothDispatcher):
RNS.log("Bluetooth was disabled, closing active BLE device connection", RNS.LOG_ERROR)
self.close()
time.sleep(2)
time.sleep(1)
def connect_device(self):
if self.ble_device != None and self.bt_manager.bt_enabled():
@@ -1687,12 +1774,15 @@ class BLEConnection(BluetoothDispatcher):
else:
RNS.log(f"BLE device connection timed out for {self.owner}", RNS.LOG_DEBUG)
if self.mtu_requested_time:
RNS.log("MTU update timeout, tearing down connection")
RNS.log("MTU update timeout, tearing down connection and resetting BLE dispatcher")
self.owner.hw_errors.append({"error": KISS.ERROR_INVALID_BLE_MTU, "description": "The Bluetooth Low Energy transfer MTU could not be configured for the connected device, and communication has failed. Restart Reticulum and any connected applications to retry connecting."})
self.close()
self.close_gatt()
self.should_run = False
self.close_gatt()
self.owner.awaiting_ble_reset = True
else:
self.close_gatt()
self.connect_job_running = False
@@ -1702,15 +1792,17 @@ class BLEConnection(BluetoothDispatcher):
self.ble_device = None
self.close_gatt()
def find_target_device(self):
def find_target_devices(self):
found_device = None
potential_devices = self.bt_manager.get_paired_devices()
suitable_devices = []
if self.target_bt_addr != None:
for device in potential_devices:
if (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_LE) or (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_DUAL):
if str(device.getAddress()).replace(":", "").lower() == str(self.target_bt_addr).replace(":", "").lower():
found_device = device
suitable_devices.append(device)
break
if not found_device and self.target_name != None:
@@ -1718,6 +1810,7 @@ class BLEConnection(BluetoothDispatcher):
if (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_LE) or (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_DUAL):
if device.getName().lower() == self.target_name.lower():
found_device = device
suitable_devices.append(device)
break
if not found_device:
@@ -1725,9 +1818,9 @@ class BLEConnection(BluetoothDispatcher):
if (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_LE) or (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_DUAL):
if device.getName().startswith("RNode "):
found_device = device
break
suitable_devices.append(device)
return found_device
return suitable_devices
def on_connection_state_change(self, status, state):
if status == GATT_SUCCESS and state:
@@ -1771,4 +1864,139 @@ class BLEConnection(BluetoothDispatcher):
def on_characteristic_changed(self, characteristic):
if characteristic.getUuid().toString() == BLEConnection.UART_TX_CHAR_UUID:
recvd = bytes(characteristic.getValue())
self.owner.ble_receive(recvd)
self.owner.ble_receive(recvd)
class TCPConnection():
TARGET_PORT = 7633
CONNECT_TIMEOUT = 5.0
INITIAL_CONNECT_TIMEOUT = 5.0
RECONNECT_WAIT = 4.0
ACTIVITY_TIMEOUT = 6.0
ACTIVITY_KEEPALIVE = ACTIVITY_TIMEOUT-2.5
TCP_USER_TIMEOUT = 24
TCP_PROBE_AFTER = 5
TCP_PROBE_INTERVAL = 2
TCP_PROBES = 12
@property
def is_open(self):
return self.connected
@property
def in_waiting(self):
buflen = len(self.owner.tcp_rx_queue)
return buflen > 0
def write(self, data_bytes):
if self.connected and self.socket:
with self.owner.tcp_tx_lock:
if len(self.owner.tcp_tx_queue) > 0:
self.socket.sendall(self.owner.tcp_tx_queue)
self.owner.tcp_tx_queue = b""
self.socket.sendall(data_bytes)
self.last_write = time.time()
else:
with self.owner.tcp_tx_lock: self.owner.tcp_tx_queue += data_bytes
return len(data_bytes)
def read(self, n=4096):
with self.owner.tcp_rx_lock:
data = self.owner.tcp_rx_queue[:n]
self.owner.tcp_rx_queue = self.owner.tcp_rx_queue[n:]
return data
def close(self):
if self.connected:
RNS.log(f"Disconnecting TCP socket for {self.owner}", RNS.LOG_DEBUG)
self.must_disconnect = True
if self.socket: self.socket.close()
def __init__(self, owner=None, target_host=None):
self.owner = owner
self.target_host = target_host
self.connected = False
self.reconnecting = False
self.running = False
self.should_run = False
self.must_disconnect = False
self.connect_job_running = False
self.last_write = time.time()
self.should_run = True
self.connection_thread = threading.Thread(target=self.initial_connect, daemon=True).start()
def set_timeouts_linux(self):
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(self.TCP_USER_TIMEOUT * 1000))
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(self.TCP_PROBE_INTERVAL))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(self.TCP_PROBES))
def set_timeouts_osx(self):
if hasattr(socket, "TCP_KEEPALIVE"): TCP_KEEPIDLE = socket.TCP_KEEPALIVE
else: TCP_KEEPIDLE = 0x10
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
def cleanup(self):
try:
if self.socket: self.socket.close()
except Exception as e:
RNS.log(f"Error while disconnecting TCP socket on cleanup for {self.owner}", RNS.LOG_ERROR)
self.should_run = False
def initial_connect(self):
if self.connect(initial=True): threading.Thread(target=self.read_loop, daemon=True).start()
def connect(self, initial=False):
try:
if initial:
RNS.log(f"Establishing TCP connection to device for {self.owner}...", RNS.LOG_DEBUG)
address_info = socket.getaddrinfo(self.target_host, self.TARGET_PORT, proto=socket.IPPROTO_TCP)[0]
address_family = address_info[0]
target_address = address_info[4]
self.socket = socket.socket(address_family, socket.SOCK_STREAM)
self.socket.settimeout(self.INITIAL_CONNECT_TIMEOUT)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.socket.connect(target_address)
self.socket.settimeout(None)
self.connected = True
self.last_write = time.time()
RNS.log(f"TCP connection to device for {self.owner} established", RNS.LOG_DEBUG)
if RNS.vendor.platformutils.is_linux(): self.set_timeouts_linux()
elif RNS.vendor.platformutils.is_darwin(): self.set_timeouts_osx()
return True
except Exception as e:
if initial:
RNS.log(f"TCP connection to device for {self.owner} could not be established: {e}", RNS.LOG_ERROR)
return False
else: raise e
def read_loop(self):
try:
data_in = b""
while not self.must_disconnect:
if self.socket: data_in = self.socket.recv(4096)
else: data_in = b""
if len(data_in) > 0: self.owner.tcp_receive(data_in)
else:
self.connected = False
RNS.log(f"The TCP socket for {self} was closed", RNS.LOG_WARNING)
break
except Exception as e:
self.connected = False
RNS.log(f"A TCP read error occurred for {self}, the contained exception was: {e}", RNS.LOG_WARNING)
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+41 -20
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -50,7 +58,10 @@ class AutoInterface(Interface):
MULTICAST_PERMANENT_ADDRESS_TYPE = "0"
MULTICAST_TEMPORARY_ADDRESS_TYPE = "1"
PEERING_TIMEOUT = 10.0
PEERING_TIMEOUT = 22.0
ANNOUNCE_INTERVAL = 1.6
PEER_JOB_INTERVAL = 4.0
MCAST_ECHO_TIMEOUT = 6.5
ALL_IGNORE_IFS = ["lo0"]
DARWIN_IGNORE_IFS = ["awdl0", "llw0", "lo0", "en5"]
@@ -111,11 +122,13 @@ class AutoInterface(Interface):
self.name = name
self.owner = owner
self.online = False
self.final_init_done = False
self.peers = {}
self.link_local_addresses = []
self.adopted_interfaces = {}
self.interface_servers = {}
self.multicast_echoes = {}
self.initial_echoes = {}
self.timed_out_interfaces = {}
self.spawned_interfaces = {}
self.write_lock = threading.Lock()
@@ -126,15 +139,15 @@ class AutoInterface(Interface):
self.outbound_udp_socket = None
self.announce_rate_target = None
self.announce_interval = AutoInterface.PEERING_TIMEOUT/6.0
self.peer_job_interval = AutoInterface.PEERING_TIMEOUT*1.1
self.announce_interval = AutoInterface.ANNOUNCE_INTERVAL
self.peer_job_interval = AutoInterface.PEER_JOB_INTERVAL
self.peering_timeout = AutoInterface.PEERING_TIMEOUT
self.multicast_echo_timeout = AutoInterface.PEERING_TIMEOUT/2
self.multicast_echo_timeout = AutoInterface.MCAST_ECHO_TIMEOUT
# Increase peering timeout on Android, due to potential
# low-power modes implemented on many chipsets.
if RNS.vendor.platformutils.is_android():
self.peering_timeout *= 2.5
self.peering_timeout *= 1.25
if allowed_interfaces == None:
self.allowed_interfaces = []
@@ -266,8 +279,7 @@ class AutoInterface(Interface):
discovery_socket.bind(addr_info[0][4])
# Set up thread for discovery packets
def discovery_loop():
self.discovery_handler(discovery_socket, ifname)
def discovery_loop(): self.discovery_handler(discovery_socket, ifname)
thread = threading.Thread(target=discovery_loop)
thread.daemon = True
@@ -317,6 +329,7 @@ class AutoInterface(Interface):
time.sleep(peering_wait)
self.online = True
self.final_init_done = True
def discovery_handler(self, socket, ifname):
def announce_loop():
@@ -328,12 +341,13 @@ class AutoInterface(Interface):
while True:
data, ipv6_src = socket.recvfrom(1024)
peering_hash = data[:RNS.Identity.HASHLENGTH//8]
expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
if peering_hash == expected_hash:
self.add_peer(ipv6_src[0], ifname)
else:
RNS.log(str(self)+" received peering packet on "+str(ifname)+" from "+str(ipv6_src[0])+", but authentication hash was incorrect.", RNS.LOG_DEBUG)
if self.final_init_done:
peering_hash = data[:RNS.Identity.HASHLENGTH//8]
expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
if peering_hash == expected_hash:
self.add_peer(ipv6_src[0], ifname)
else:
RNS.log(str(self)+" received peering packet on "+str(ifname)+" from "+str(ipv6_src[0])+", but authentication hash was incorrect.", RNS.LOG_DEBUG)
def peer_jobs(self):
while True:
@@ -402,9 +416,10 @@ class AutoInterface(Interface):
RNS.log("Could not get device information while updating link-local addresses for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
# Check multicast echo timeouts
last_multicast_echo = 0
if ifname in self.multicast_echoes:
last_multicast_echo = self.multicast_echoes[ifname]
last_multicast_echo = 0
multicast_echo_received = False
if ifname in self.multicast_echoes: last_multicast_echo = self.multicast_echoes[ifname]
if ifname in self.initial_echoes: multicast_echo_received = True
if now - last_multicast_echo > self.multicast_echo_timeout:
if ifname in self.timed_out_interfaces and self.timed_out_interfaces[ifname] == False:
@@ -416,6 +431,11 @@ class AutoInterface(Interface):
self.carrier_changed = True
RNS.log(str(self)+" Carrier recovered on "+str(ifname), RNS.LOG_WARNING)
self.timed_out_interfaces[ifname] = False
if not multicast_echo_received:
RNS.log(f"{self} No multicast echoes received on {ifname}. The networking hardware or a firewall may be blocking multicast traffic.", RNS.LOG_ERROR)
# else:
# RNS.log(f"{self} Initial multicast echo on {ifname} received {RNS.prettytime(time.time()-self.initial_echoes[ifname])} ago.", RNS.LOG_DEBUG)
def announce_handler(self, ifname):
@@ -454,6 +474,7 @@ class AutoInterface(Interface):
if ifname != None:
self.multicast_echoes[ifname] = time.time()
if not ifname in self.initial_echoes: self.initial_echoes[ifname] = time.time()
else:
RNS.log(str(self)+" received multicast echo on unexpected interface "+str(ifname), RNS.LOG_WARNING)
+64 -50
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -311,7 +319,8 @@ class BackboneInterface(Interface):
client_socket, address = server_socket.accept()
client_socket.setblocking(0)
if not owner_interface.incoming_connection(client_socket):
client_socket.close()
try: client_socket.close()
except Exception as e: RNS.log(f"Error while closing socket for failed incoming connection: {e}", RNS.LOG_ERROR)
elif fileno == server_socket.fileno() and (event & select.EPOLLHUP):
try: BackboneInterface.deregister_fileno(fileno)
@@ -329,48 +338,53 @@ class BackboneInterface(Interface):
def incoming_connection(self, socket):
RNS.log("Accepting incoming connection", RNS.LOG_VERBOSE)
spawned_configuration = {"name": "Client on "+self.name, "target_host": None, "target_port": None}
spawned_interface = BackboneClientInterface(self.owner, spawned_configuration, connected_socket=socket)
spawned_interface.OUT = self.OUT
spawned_interface.IN = self.IN
spawned_interface.socket = socket
spawned_interface.target_ip = socket.getpeername()[0]
spawned_interface.target_port = str(socket.getpeername()[1])
spawned_interface.parent_interface = self
spawned_interface.bitrate = self.bitrate
spawned_interface.optimise_mtu()
spawned_interface.ifac_size = self.ifac_size
spawned_interface.ifac_netname = self.ifac_netname
spawned_interface.ifac_netkey = self.ifac_netkey
if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None:
ifac_origin = b""
if spawned_interface.ifac_netname != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
if spawned_interface.ifac_netkey != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
try:
spawned_configuration = {"name": "Client on "+self.name, "target_host": None, "target_port": None}
spawned_interface = BackboneClientInterface(self.owner, spawned_configuration, connected_socket=socket)
spawned_interface.OUT = self.OUT
spawned_interface.IN = self.IN
spawned_interface.socket = socket
spawned_interface.target_ip = socket.getpeername()[0]
spawned_interface.target_port = str(socket.getpeername()[1])
spawned_interface.parent_interface = self
spawned_interface.bitrate = self.bitrate
spawned_interface.optimise_mtu()
spawned_interface.ifac_size = self.ifac_size
spawned_interface.ifac_netname = self.ifac_netname
spawned_interface.ifac_netkey = self.ifac_netkey
if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None:
ifac_origin = b""
if spawned_interface.ifac_netname != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
if spawned_interface.ifac_netkey != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
spawned_interface.ifac_key = RNS.Cryptography.hkdf(
length=64,
derive_from=ifac_origin_hash,
salt=RNS.Reticulum.IFAC_SALT,
context=None
)
spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key)
spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key))
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
spawned_interface.ifac_key = RNS.Cryptography.hkdf(
length=64,
derive_from=ifac_origin_hash,
salt=RNS.Reticulum.IFAC_SALT,
context=None
)
spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key)
spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key))
spawned_interface.announce_rate_target = self.announce_rate_target
spawned_interface.announce_rate_grace = self.announce_rate_grace
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
spawned_interface.mode = self.mode
spawned_interface.HW_MTU = self.HW_MTU
spawned_interface.online = True
RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
RNS.Transport.interfaces.append(spawned_interface)
while spawned_interface in self.spawned_interfaces: self.spawned_interfaces.remove(spawned_interface)
self.spawned_interfaces.append(spawned_interface)
BackboneInterface.add_client_socket(socket, spawned_interface)
spawned_interface.announce_rate_target = self.announce_rate_target
spawned_interface.announce_rate_grace = self.announce_rate_grace
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
spawned_interface.mode = self.mode
spawned_interface.HW_MTU = self.HW_MTU
spawned_interface.online = True
RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
RNS.Transport.interfaces.append(spawned_interface)
while spawned_interface in self.spawned_interfaces: self.spawned_interfaces.remove(spawned_interface)
self.spawned_interfaces.append(spawned_interface)
BackboneInterface.add_client_socket(socket, spawned_interface)
except Exception as e:
RNS.log(f"An error occurred while accepting incoming connection on {self}: {e}", RNS.LOG_ERROR)
return False
return True
@@ -574,9 +588,7 @@ class BackboneClientInterface(Interface):
self.teardown()
break
try:
self.connect()
try: self.connect()
except Exception as e:
RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
@@ -634,7 +646,8 @@ class BackboneClientInterface(Interface):
self.online = False
if self.initiator and not self.detached:
RNS.log("The socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
self.reconnect()
def job(): self.reconnect()
threading.Thread(target=job, daemon=True).start()
else:
RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_VERBOSE)
self.teardown()
@@ -645,7 +658,8 @@ class BackboneClientInterface(Interface):
if self.initiator:
RNS.log("Attempting to reconnect...", RNS.LOG_WARNING)
self.reconnect()
def job(): self.reconnect()
threading.Thread(target=job, daemon=True).start()
else:
self.teardown()
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+21 -13
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -127,23 +135,23 @@ class Interface:
def optimise_mtu(self):
if self.AUTOCONFIGURE_MTU:
if self.bitrate > 500_000_000:
if self.bitrate >= 1_000_000_000:
self.HW_MTU = 524288
elif self.bitrate > 16_000_000:
elif self.bitrate > 750_000_000:
self.HW_MTU = 262144
elif self.bitrate > 8_000_000:
elif self.bitrate > 400_000_000:
self.HW_MTU = 131072
elif self.bitrate > 4_000_000:
elif self.bitrate > 200_000_000:
self.HW_MTU = 65536
elif self.bitrate > 2_000_000:
elif self.bitrate > 100_000_000:
self.HW_MTU = 32768
elif self.bitrate > 1_000_000:
elif self.bitrate > 10_000_000:
self.HW_MTU = 16384
elif self.bitrate > 500_000:
elif self.bitrate > 5_000_000:
self.HW_MTU = 8192
elif self.bitrate > 250_000:
elif self.bitrate > 2_000_000:
self.HW_MTU = 4096
elif self.bitrate > 125_000:
elif self.bitrate > 1_000_000:
self.HW_MTU = 2048
elif self.bitrate > 62_500:
self.HW_MTU = 1024
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+22 -8
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -62,7 +70,7 @@ class LocalClientInterface(Interface):
self.HW_MTU = 262144
self.online = False
if socket_path != None and RNS.vendor.platformutils.use_af_unix(): self.socket_path = f"\0rns/{socket_path}"
if socket_path != None and RNS.Reticulum.get_instance().use_af_unix: self.socket_path = f"\0rns/{socket_path}"
else: self.socket_path = None
self.IN = True
@@ -246,6 +254,9 @@ class LocalClientInterface(Interface):
if self.is_connected_to_shared_instance and not self.detached:
RNS.log("Socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
RNS.Transport.shared_connection_disappeared()
# TODO: Potentially run this in a thread, but since if we get here,
# there's no other connectivity left to block anyway, it might be
# unnecessary.
self.reconnect()
else:
self.teardown(nowarning=True)
@@ -268,6 +279,9 @@ class LocalClientInterface(Interface):
if self.is_connected_to_shared_instance and not self.detached:
RNS.log("Socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
RNS.Transport.shared_connection_disappeared()
# TODO: Potentially run this in a thread, but since if we get here,
# there's no other connectivity left to block anyway, it might be
# unnecessary.
self.reconnect()
else:
self.teardown(nowarning=True)
@@ -329,8 +343,8 @@ class LocalClientInterface(Interface):
def __str__(self):
if self.socket_path: return "Shared Instance["+str(self.socket_path.replace("\0", ""))+"]"
else: return "Shared Instance["+str(self.target_port)+"]"
if self.socket_path: return "LocalInterface["+str(self.socket_path.replace("\0", ""))+"]"
else: return "LocalInterface["+str(self.target_port)+"]"
class LocalServerInterface(Interface):
@@ -342,7 +356,7 @@ class LocalServerInterface(Interface):
self.online = False
self.clients = 0
if socket_path != None and RNS.vendor.platformutils.use_af_unix(): self.socket_path = f"\0rns/{socket_path}"
if socket_path != None and RNS.Reticulum.get_instance().use_af_unix: self.socket_path = f"\0rns/{socket_path}"
else: self.socket_path = None
self.IN = True
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+308 -68
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -24,6 +32,7 @@ from RNS.Interfaces.Interface import Interface
from time import sleep
import sys
import threading
import socket
import time
import math
import RNS
@@ -56,6 +65,7 @@ class KISS():
CMD_STAT_PHYPRM = 0x26
CMD_STAT_BAT = 0x27
CMD_STAT_CSMA = 0x28
CMD_STAT_TEMP = 0x29
CMD_BLINK = 0x30
CMD_RANDOM = 0x40
CMD_FB_EXT = 0x41
@@ -150,8 +160,11 @@ class RNodeInterface(Interface):
lt_alock = float(c["airtime_limit_long"]) if "airtime_limit_long" in c else None
force_ble = False
ble_name = None
ble_addr = None
ble_name = None
ble_addr = None
force_tcp = False
tcp_host = None
port = c["port"] if "port" in c else None
@@ -159,6 +172,7 @@ class RNodeInterface(Interface):
raise ValueError("No port specified for RNode interface")
if port != None:
tcp_uri_scheme = "tcp://"
ble_uri_scheme = "ble://"
if port.lower().startswith(ble_uri_scheme):
force_ble = True
@@ -171,6 +185,13 @@ class RNodeInterface(Interface):
else:
ble_name = ble_string
if port.lower().startswith(tcp_uri_scheme):
force_tcp = True
tcp_string = port[len(tcp_uri_scheme):]
port = None
if len(tcp_string) == 0: pass
else: tcp_host = tcp_string
self.HW_MTU = 508
self.pyserial = serial
@@ -187,6 +208,14 @@ class RNodeInterface(Interface):
self.reconnecting= False
self.hw_errors = []
self.use_tcp = False
self.tcp = None
self.tcp_host = tcp_host
self.tcp_rx_queue= b""
self.tcp_tx_queue= b""
self.tcp_rx_lock = threading.Lock()
self.tcp_tx_lock = threading.Lock()
self.use_ble = False
self.ble_name = ble_name
self.ble_addr = ble_addr
@@ -205,6 +234,7 @@ class RNodeInterface(Interface):
self.bitrate = 0
self.st_alock = st_alock
self.lt_alock = lt_alock
self.cpu_temp = None
self.platform = None
self.display = None
self.mcu = None
@@ -246,9 +276,12 @@ class RNodeInterface(Interface):
self.r_csma_cw_max = None
self.r_current_rssi = None
self.r_noise_floor = None
self.r_interference = None
self.r_interference_l = None
self.r_battery_state = RNodeInterface.BATTERY_STATE_UNKNOWN
self.r_battery_percent = 0
self.r_temperature = None
self.r_framebuffer = b""
self.r_framebuffer_readtime = 0
self.r_framebuffer_latency = 0
@@ -264,15 +297,15 @@ class RNodeInterface(Interface):
self.interface_ready = False
self.announce_rate_target = None
if force_ble or self.ble_addr != None or self.ble_name != None:
self.use_ble = True
if force_ble or self.ble_addr != None or self.ble_name != None: self.use_ble = True
if force_tcp or self.tcp_host != None: self.use_tcp = True
self.validcfg = True
if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
if (self.txpower < 0 or self.txpower > 22):
if (self.txpower < 0 or self.txpower > 37):
RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
@@ -314,10 +347,8 @@ class RNodeInterface(Interface):
try:
self.open_port()
if self.serial.is_open:
self.configure_device()
else:
raise IOError("Could not open serial port")
if self.serial.is_open: self.configure_device()
else: raise IOError("Could not open serial port")
except Exception as e:
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
@@ -330,8 +361,8 @@ class RNodeInterface(Interface):
def open_port(self):
if not self.use_ble:
RNS.log("Opening serial port "+self.port+"...")
if not self.use_ble and not self.use_tcp:
RNS.log(f"Opening serial port {self.port}...")
self.serial = self.pyserial.Serial(
port = self.port,
baudrate = self.speed,
@@ -347,19 +378,37 @@ class RNodeInterface(Interface):
)
else:
RNS.log(f"Opening BLE connection for {self}...")
if self.ble != None and self.ble.running == False:
self.ble.close()
self.ble.cleanup()
self.ble = None
if self.use_ble:
RNS.log(f"Opening BLE connection for {self}...")
self.timeout = 1250
if self.ble != None and self.ble.running == False:
self.ble.close()
self.ble.cleanup()
self.ble = None
if self.ble == None:
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
self.serial = self.ble
if self.ble == None:
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
self.serial = self.ble
open_time = time.time()
while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
time.sleep(1)
open_time = time.time()
while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
time.sleep(1)
if self.use_tcp:
self.timeout = 1500
RNS.log(f"Opening TCP connection for {self}...")
if self.tcp != None and self.tcp.running == False:
self.tcp.close()
self.tcp.cleanup()
self.tcp = None
if self.tcp == None:
self.tcp = TCPConnection(owner=self, target_host=self.tcp_host)
self.serial = self.tcp
open_time = time.time()
while not self.tcp.connected and time.time() < open_time + self.tcp.CONNECT_TIMEOUT:
time.sleep(1)
def reset_radio_state(self):
self.r_frequency = None
@@ -380,38 +429,41 @@ class RNodeInterface(Interface):
thread.start()
self.detect()
if not self.use_ble:
sleep(0.2)
else:
ble_detect_timeout = 5
if self.use_tcp:
tcp_detect_timeout = 5.0
detect_time = time.time()
while not self.detected and time.time() < detect_time + ble_detect_timeout:
time.sleep(0.1)
if self.detected:
detect_time = RNS.prettytime(time.time()-detect_time)
else:
RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
while not self.detected and time.time() < detect_time + tcp_detect_timeout: time.sleep(0.1)
if not self.detected: RNS.log(f"RNode detect timed out over TCP", RNS.LOG_ERROR)
elif self.use_ble:
ble_detect_timeout = 5.0
detect_time = time.time()
while not self.detected and time.time() < detect_time + ble_detect_timeout: time.sleep(0.1)
if not self.detected: RNS.log(f"RNode detect timed out over BLE", RNS.LOG_ERROR)
else:
sleep(0.2)
if not self.detected:
RNS.log("Could not detect device for "+str(self), RNS.LOG_ERROR)
RNS.log(f"Could not detect device for {self}", RNS.LOG_ERROR)
self.serial.close()
else:
if self.platform == KISS.PLATFORM_ESP32 or self.platform == KISS.PLATFORM_NRF52:
self.display = True
if self.platform == KISS.PLATFORM_ESP32 or self.platform == KISS.PLATFORM_NRF52: self.display = True
RNS.log("Serial port "+self.port+" is now open")
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
self.initRadio()
if (self.validateRadioState()):
self.interface_ready = True
RNS.log(str(self)+" is configured and powered up")
sleep(0.3)
self.online = True
else:
RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
self.serial.close()
if self.use_tcp: RNS.log(f"TCP connection to {self.tcp_host} is now open", RNS.LOG_VERBOSE)
elif self.use_ble: RNS.log(f"BLE connection to {self} is now open", RNS.LOG_VERBOSE)
else: RNS.log(f"Serial port {self.port} is now open", RNS.LOG_VERBOSE)
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
self.initRadio()
if (self.validateRadioState()):
self.interface_ready = True
RNS.log(str(self)+" is configured and powered up")
sleep(0.3)
self.online = True
else:
RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
self.serial.close()
def initRadio(self):
@@ -606,10 +658,9 @@ class RNodeInterface(Interface):
def validateRadioState(self):
RNS.log("Waiting for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
if self.use_ble:
sleep(1.00)
else:
sleep(0.25)
if self.use_ble: sleep(1.00)
elif self.use_tcp: sleep(1.5)
else: sleep(0.25)
if self.use_ble and self.ble != None and self.ble.device_disappeared:
RNS.log(f"Device disappeared during radio state validation for {self}", RNS.LOG_ERROR)
@@ -894,16 +945,35 @@ class RNodeInterface(Interface):
self.r_channel_load_long = cul/100.0
self.r_current_rssi = crs-RNodeInterface.RSSI_OFFSET
self.r_noise_floor = nfl-RNodeInterface.RSSI_OFFSET
# TODO: Remove debug
# interference_log_threshold = 10
# if ntf == 0xFF:
# self.r_interference = None
# if self.r_noise_floor != None:
# # Filter potential false interference events due to LNA recalibration
# if self.r_interference_l != None:
# if self.r_interference_l[1] < self.r_noise_floor+interference_log_threshold:
# self.r_interference_l = None
# else:
# if self.r_noise_floor != None:
# interference = ntf-RNodeInterface.RSSI_OFFSET
# # Filter potential false interference events due to LNA recalibration
# if interference > self.r_noise_floor+interference_log_threshold:
# self.r_interference = ntf-RNodeInterface.RSSI_OFFSET
# self.r_interference_l = [time.time(), self.r_interference]
if ntf == 0xFF:
self.r_interference = None
else:
self.r_interference = ntf-RNodeInterface.RSSI_OFFSET
self.r_interference_l = [time.time(), self.r_interference]
if self.r_interference != None:
RNS.log(f"{self} Radio detected interference at {self.r_interference} dBm", RNS.LOG_DEBUG)
# TODO: Remove debug
# RNS.log(f"RSSI: {self.r_current_rssi}, Noise floor: {self.r_noise_floor}, Interference: {self.r_interference}", RNS.LOG_EXTREME)
# RNS.log(f"RSSI: {self.r_current_rssi}, Noise floor: {self.r_noise_floor}, Interference: {self.r_interference}", RNS.LOG_DEBUG)
elif (command == KISS.CMD_STAT_PHYPRM):
if (byte == KISS.FESC):
escape = True
@@ -977,6 +1047,22 @@ class RNodeInterface(Interface):
bat_percent = 0
self.r_battery_state = command_buffer[0]
self.r_battery_percent = bat_percent
elif (command == KISS.CMD_STAT_TEMP):
if (byte == KISS.FESC):
escape = True
else:
if (escape):
if (byte == KISS.TFEND):
byte = KISS.FEND
if (byte == KISS.TFESC):
byte = KISS.FESC
escape = False
command_buffer = command_buffer+bytes([byte])
if (len(command_buffer) == 1):
temp = command_buffer[0]-120
if temp >= -30 and temp <= 90: self.r_temperature = temp
else: self.r_temperature = None
self.cpu_temp = self.r_temperature
elif (command == KISS.CMD_RANDOM):
self.r_random = byte
elif (command == KISS.CMD_PLATFORM):
@@ -1046,7 +1132,7 @@ class RNodeInterface(Interface):
else:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
RNS.log(str(self)+" serial read timeout in command "+str(command), RNS.LOG_WARNING)
RNS.log(f"{self} device read timeout in command {command} after {RNS.prettytime(self.timeout/1000.0)}", RNS.LOG_WARNING)
data_buffer = b""
in_frame = False
command = KISS.CMD_UNKNOWN
@@ -1058,6 +1144,11 @@ class RNodeInterface(Interface):
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.id_callsign.decode("utf-8")), RNS.LOG_DEBUG)
self.process_outgoing(self.id_callsign)
if self.use_tcp:
if self.tcp and self.tcp.connected:
if time.time() > self.tcp.last_write + self.tcp.ACTIVITY_KEEPALIVE:
self.detect()
sleep(0.08)
except Exception as e:
@@ -1086,23 +1177,28 @@ class RNodeInterface(Interface):
time.sleep(5)
RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
self.open_port()
if self.serial.is_open:
self.configure_device()
if self.serial.is_open: self.configure_device()
except Exception as e:
RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
self.reconnecting = False
if self.online:
RNS.log("Reconnected serial port for "+str(self))
if self.online: RNS.log(f"Reconnected port for {self}")
def detach(self):
self.detached = True
self.disable_external_framebuffer()
self.setRadioState(KISS.RADIO_STATE_OFF)
self.leave()
try:
self.disable_external_framebuffer()
self.setRadioState(KISS.RADIO_STATE_OFF)
self.leave()
except Exception as e:
RNS.log(f"An error occurred while detaching {self}: {e}", RNS.LOG_ERROR)
if self.use_ble:
self.ble.close()
if self.use_ble: self.ble.close()
if self.use_tcp:
time.sleep(0.5)
self.tcp.close()
def should_ingress_limit(self):
return False
@@ -1123,6 +1219,17 @@ class RNodeInterface(Interface):
def get_battery_percent(self):
return self.r_battery_percent
def tcp_receive(self, data):
with self.tcp_rx_lock: self.tcp_rx_queue += data
def tcp_waiting(self): return len(self.tcp_tx_queue) > 0
def get_tcp_waiting(self, n):
with self.tcp_tx_lock:
data = self.tcp_tx_queue[:n]
self.tcp_tx_queue = self.tcp_tx_queue[n:]
return data
def ble_receive(self, data):
with self.ble_rx_lock:
self.ble_rx_queue += data
@@ -1137,7 +1244,7 @@ class RNodeInterface(Interface):
return data
def __str__(self):
return "RNodeInterface["+str(self.name)+"]"
return f"RNodeInterface[{self.name}]"
class BLEConnection():
UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
@@ -1316,3 +1423,136 @@ class BLEConnection():
RNS.log(f"Error while determining device bond status for {device}, the contained exception was: {e}", RNS.LOG_ERROR)
return False
class TCPConnection():
TARGET_PORT = 7633
CONNECT_TIMEOUT = 5.0
INITIAL_CONNECT_TIMEOUT = 5.0
RECONNECT_WAIT = 4.0
ACTIVITY_TIMEOUT = 6.0
ACTIVITY_KEEPALIVE = ACTIVITY_TIMEOUT-2.5
TCP_USER_TIMEOUT = 24
TCP_PROBE_AFTER = 5
TCP_PROBE_INTERVAL = 2
TCP_PROBES = 12
@property
def is_open(self):
return self.connected
@property
def in_waiting(self):
buflen = len(self.owner.tcp_rx_queue)
return buflen > 0
def write(self, data_bytes):
if self.connected and self.socket:
with self.owner.tcp_tx_lock:
if len(self.owner.tcp_tx_queue) > 0:
self.socket.sendall(self.owner.tcp_tx_queue)
self.owner.tcp_tx_queue = b""
self.socket.sendall(data_bytes)
self.last_write = time.time()
else:
with self.owner.tcp_tx_lock: self.owner.tcp_tx_queue += data_bytes
return len(data_bytes)
def read(self, n):
with self.owner.tcp_rx_lock:
data = self.owner.tcp_rx_queue[:n]
self.owner.tcp_rx_queue = self.owner.tcp_rx_queue[n:]
return data
def close(self):
if self.connected:
RNS.log(f"Disconnecting TCP socket for {self.owner}", RNS.LOG_DEBUG)
self.must_disconnect = True
if self.socket: self.socket.close()
def __init__(self, owner=None, target_host=None):
self.owner = owner
self.target_host = target_host
self.connected = False
self.reconnecting = False
self.running = False
self.should_run = False
self.must_disconnect = False
self.connect_job_running = False
self.last_write = time.time()
self.should_run = True
self.connection_thread = threading.Thread(target=self.initial_connect, daemon=True).start()
def set_timeouts_linux(self):
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(self.TCP_USER_TIMEOUT * 1000))
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(self.TCP_PROBE_INTERVAL))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(self.TCP_PROBES))
def set_timeouts_osx(self):
if hasattr(socket, "TCP_KEEPALIVE"): TCP_KEEPIDLE = socket.TCP_KEEPALIVE
else: TCP_KEEPIDLE = 0x10
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
def cleanup(self):
try:
if self.socket: self.socket.close()
except Exception as e: RNS.log(f"Error while disconnecting TCP socket on cleanup for {self.owner}", RNS.LOG_ERROR)
self.should_run = False
def initial_connect(self):
if self.connect(initial=True): threading.Thread(target=self.read_loop, daemon=True).start()
def connect(self, initial=False):
try:
if initial:
RNS.log(f"Establishing TCP connection to device for {self.owner}...", RNS.LOG_DEBUG)
address_info = socket.getaddrinfo(self.target_host, self.TARGET_PORT, proto=socket.IPPROTO_TCP)[0]
address_family = address_info[0]
target_address = address_info[4]
self.socket = socket.socket(address_family, socket.SOCK_STREAM)
self.socket.settimeout(self.INITIAL_CONNECT_TIMEOUT)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.socket.connect(target_address)
self.socket.settimeout(None)
self.connected = True
self.last_write = time.time()
RNS.log(f"TCP connection to device for {self.owner} established", RNS.LOG_DEBUG)
if RNS.vendor.platformutils.is_linux(): self.set_timeouts_linux()
elif RNS.vendor.platformutils.is_darwin(): self.set_timeouts_osx()
return True
except Exception as e:
if initial:
RNS.log(f"TCP connection to device for {self.owner} could not be established: {e}", RNS.LOG_ERROR)
return False
else: raise e
def read_loop(self):
try:
data_in = b""
while not self.must_disconnect:
if self.socket: data_in = self.socket.recv(4096)
else: data_in = b""
if len(data_in) > 0: self.owner.tcp_receive(data_in)
else:
self.connected = False
RNS.log(f"The TCP socket for {self} was closed", RNS.LOG_WARNING)
break
except Exception as e:
self.connected = False
RNS.log(f"A TCP read error occurred for {self}, the contained exception was: {e}", RNS.LOG_WARNING)
+14 -7
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2024 Jacob Eva. Adapted from the RNodeInterface by Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -66,7 +74,7 @@ class KISS():
CMD_FW_VERSION = 0x50
CMD_ROM_READ = 0x51
CMD_RESET = 0x55
CMD_INTERFACES = 0x64
CMD_INTERFACES = 0x71
CMD_INT0_DATA = 0x00
CMD_INT1_DATA = 0x10
@@ -985,7 +993,6 @@ class RNodeSubInterface(Interface):
self.announce_rate_target = None
self.mode = None
self.announce_cap = None
self.bitrate = None
self.ifac_size = None
@@ -1005,7 +1012,7 @@ class RNodeSubInterface(Interface):
RNS.log("Invalid interface type configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
if (self.txpower < -9 or self.txpower > 27):
if (self.txpower < -9 or self.txpower > 37):
RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
File diff suppressed because it is too large Load Diff
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+1 -1
View File
@@ -1,7 +1,7 @@
# MIT License
#
# Copyright (c) 2025 Mark Qvist
# Copyright (c) 2014 Stefan C. Mueller
# Copyright (c) 2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
+182 -78
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors.
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -32,6 +40,7 @@ import struct
import math
import time
import RNS
import io
class LinkCallbacks:
def __init__(self):
@@ -71,19 +80,23 @@ class Link:
LINK_MTU_SIZE = 3
TRAFFIC_TIMEOUT_MIN_MS = 5
TRAFFIC_TIMEOUT_FACTOR = 6
KEEPALIVE_MAX_RTT = 1.75
KEEPALIVE_TIMEOUT_FACTOR = 4
"""
RTT timeout factor used in link timeout calculation.
"""
STALE_GRACE = 2
STALE_GRACE = 5
"""
Grace period in seconds used in link timeout calculation.
"""
KEEPALIVE = 360
KEEPALIVE_MAX = 360
KEEPALIVE_MIN = 5
KEEPALIVE = KEEPALIVE_MAX
"""
Interval for sending keep-alive packets on established links in seconds.
Default interval for sending keep-alive packets on established links in seconds.
"""
STALE_TIME = 2*KEEPALIVE
STALE_FACTOR = 2
STALE_TIME = STALE_FACTOR*KEEPALIVE
"""
If no traffic or keep-alive packets are received within this period, the
link will be marked as stale, and a final keep-alive packet will be sent.
@@ -92,39 +105,82 @@ class Link:
and will be torn down.
"""
PENDING = 0x00
HANDSHAKE = 0x01
ACTIVE = 0x02
STALE = 0x03
CLOSED = 0x04
WATCHDOG_MAX_SLEEP = 5
TIMEOUT = 0x01
INITIATOR_CLOSED = 0x02
DESTINATION_CLOSED = 0x03
PENDING = 0x00
HANDSHAKE = 0x01
ACTIVE = 0x02
STALE = 0x03
CLOSED = 0x04
ACCEPT_NONE = 0x00
ACCEPT_APP = 0x01
ACCEPT_ALL = 0x02
TIMEOUT = 0x01
INITIATOR_CLOSED = 0x02
DESTINATION_CLOSED = 0x03
ACCEPT_NONE = 0x00
ACCEPT_APP = 0x01
ACCEPT_ALL = 0x02
resource_strategies = [ACCEPT_NONE, ACCEPT_APP, ACCEPT_ALL]
MODE_AES128_CBC = 0x00
MODE_AES256_CBC = 0x01
MODE_AES256_GCM = 0x02
MODE_OTP_RESERVED = 0x03
MODE_PQ_RESERVED_1 = 0x04
MODE_PQ_RESERVED_2 = 0x05
MODE_PQ_RESERVED_3 = 0x06
MODE_PQ_RESERVED_4 = 0x07
ENABLED_MODES = [MODE_AES256_CBC]
MODE_DEFAULT = MODE_AES256_CBC
MODE_DESCRIPTIONS = {MODE_AES128_CBC: "AES_128_CBC",
MODE_AES256_CBC: "AES_256_CBC",
MODE_AES256_GCM: "MODE_AES256_GCM",
MODE_OTP_RESERVED: "MODE_OTP_RESERVED",
MODE_PQ_RESERVED_1: "MODE_PQ_RESERVED_1",
MODE_PQ_RESERVED_2: "MODE_PQ_RESERVED_2",
MODE_PQ_RESERVED_3: "MODE_PQ_RESERVED_3",
MODE_PQ_RESERVED_4: "MODE_PQ_RESERVED_4"}
MTU_BYTEMASK = 0x1FFFFF
MODE_BYTEMASK = 0xE0
@staticmethod
def mtu_bytes(mtu):
return struct.pack(">I", mtu & 0xFFFFFF)[1:]
def signalling_bytes(mtu, mode):
if not mode in Link.ENABLED_MODES: raise TypeError(f"Requested link mode {Link.MODE_DESCRIPTIONS[mode]} not enabled")
signalling_value = (mtu & Link.MTU_BYTEMASK)+(((mode<<5) & Link.MODE_BYTEMASK)<<16)
return struct.pack(">I", signalling_value)[1:]
@staticmethod
def mtu_from_lr_packet(packet):
if len(packet.data) == Link.ECPUBSIZE+Link.LINK_MTU_SIZE:
return (packet.data[Link.ECPUBSIZE] << 16) + (packet.data[Link.ECPUBSIZE+1] << 8) + (packet.data[Link.ECPUBSIZE+2])
else:
return None
return (packet.data[Link.ECPUBSIZE] << 16) + (packet.data[Link.ECPUBSIZE+1] << 8) + (packet.data[Link.ECPUBSIZE+2]) & Link.MTU_BYTEMASK
else: return None
@staticmethod
def mtu_from_lp_packet(packet):
if len(packet.data) == RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2+Link.LINK_MTU_SIZE:
mtu_bytes = packet.data[RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2:RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2+Link.LINK_MTU_SIZE]
return (mtu_bytes[0] << 16) + (mtu_bytes[1] << 8) + (mtu_bytes[2])
else:
return None
return (mtu_bytes[0] << 16) + (mtu_bytes[1] << 8) + (mtu_bytes[2]) & Link.MTU_BYTEMASK
else: return None
@staticmethod
def mode_byte(mode):
if mode in Link.ENABLED_MODES: return (mode << 5) & Link.MODE_BYTEMASK
else: raise TypeError(f"Requested link mode {mode} not enabled")
@staticmethod
def mode_from_lr_packet(packet):
if len(packet.data) > Link.ECPUBSIZE:
mode = (packet.data[Link.ECPUBSIZE] & Link.MODE_BYTEMASK) >> 5
return mode
else: return Link.MODE_DEFAULT
@staticmethod
def mode_from_lp_packet(packet):
if len(packet.data) > RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2:
mode = packet.data[RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2] >> 5
return mode
else: return Link.MODE_DEFAULT
@staticmethod
def validate_request(owner, data, packet):
@@ -141,6 +197,11 @@ class Link:
RNS.trace_exception(e)
link.mtu = RNS.Reticulum.MTU
link.mode = Link.mode_from_lr_packet(packet)
# TODO: Remove debug
RNS.log(f"Incoming link request with mode {Link.MODE_DESCRIPTIONS[link.mode]}", RNS.LOG_DEBUG)
link.update_mdu()
link.destination = packet.destination
link.establishment_timeout = Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, packet.hops) + Link.KEEPALIVE
@@ -169,9 +230,9 @@ class Link:
return None
def __init__(self, destination=None, established_callback = None, closed_callback = None, owner=None, peer_pub_bytes = None, peer_sig_pub_bytes = None):
if destination != None and destination.type != RNS.Destination.SINGLE:
raise TypeError("Links can only be established to the \"single\" destination type")
def __init__(self, destination=None, established_callback=None, closed_callback=None, owner=None, peer_pub_bytes=None, peer_sig_pub_bytes=None, mode=MODE_DEFAULT):
if destination != None and destination.type != RNS.Destination.SINGLE: raise TypeError("Links can only be established to the \"single\" destination type")
self.mode = mode
self.rtt = None
self.mtu = RNS.Reticulum.MTU
self.establishment_cost = 0
@@ -186,6 +247,7 @@ class Link:
self.pending_requests = []
self.last_inbound = 0
self.last_outbound = 0
self.last_keepalive = 0
self.last_proof = 0
self.last_data = 0
self.tx = 0
@@ -244,12 +306,14 @@ class Link:
self.set_link_closed_callback(closed_callback)
if self.initiator:
link_mtu = b""
signalling_bytes = b""
nh_hw_mtu = RNS.Transport.next_hop_interface_hw_mtu(destination.hash)
if RNS.Reticulum.link_mtu_discovery() and nh_hw_mtu:
link_mtu = Link.mtu_bytes(nh_hw_mtu)
signalling_bytes = Link.signalling_bytes(nh_hw_mtu, self.mode)
RNS.log(f"Signalling link MTU of {RNS.prettysize(nh_hw_mtu)} for link", RNS.LOG_DEBUG) # TODO: Remove debug
self.request_data = self.pub_bytes+self.sig_pub_bytes+link_mtu
else: signalling_bytes = Link.signalling_bytes(RNS.Reticulum.MTU, self.mode)
RNS.log(f"Establishing link with mode {Link.MODE_DESCRIPTIONS[self.mode]}", RNS.LOG_DEBUG) # TODO: Remove debug
self.request_data = self.pub_bytes+self.sig_pub_bytes+signalling_bytes
self.packet = RNS.Packet(destination, self.request_data, packet_type=RNS.Packet.LINKREQUEST)
self.packet.pack()
self.establishment_cost += len(self.packet.raw)
@@ -291,25 +355,25 @@ class Link:
self.status = Link.HANDSHAKE
self.shared_key = self.prv.exchange(self.peer_pub)
if self.mode == Link.MODE_AES128_CBC: derived_key_length = 32
elif self.mode == Link.MODE_AES256_CBC: derived_key_length = 64
else: raise TypeError(f"Invalid link mode {self.mode} on {self}")
self.derived_key = RNS.Cryptography.hkdf(
length=32,
length=derived_key_length,
derive_from=self.shared_key,
salt=self.get_salt(),
context=self.get_context(),
)
else:
RNS.log("Handshake attempt on "+str(self)+" with invalid state "+str(self.status), RNS.LOG_ERROR)
context=self.get_context())
else: RNS.log("Handshake attempt on "+str(self)+" with invalid state "+str(self.status), RNS.LOG_ERROR)
def prove(self):
mtu_bytes = b""
if self.mtu != RNS.Reticulum.MTU:
mtu_bytes = Link.mtu_bytes(self.mtu)
signed_data = self.link_id+self.pub_bytes+self.sig_pub_bytes+mtu_bytes
signalling_bytes = Link.signalling_bytes(self.mtu, self.mode)
signed_data = self.link_id+self.pub_bytes+self.sig_pub_bytes+signalling_bytes
signature = self.owner.identity.sign(signed_data)
proof_data = signature+self.pub_bytes+mtu_bytes
proof_data = signature+self.pub_bytes+signalling_bytes
proof = RNS.Packet(self, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.LRPROOF)
proof.send()
self.establishment_cost += len(proof.raw)
@@ -332,11 +396,14 @@ class Link:
def validate_proof(self, packet):
try:
if self.status == Link.PENDING:
mtu_bytes = b""
signalling_bytes = b""
confirmed_mtu = None
mode = Link.mode_from_lp_packet(packet)
RNS.log(f"Validating link request proof with mode {Link.MODE_DESCRIPTIONS[mode]}", RNS.LOG_DEBUG) # TODO: Remove debug
if mode != self.mode: raise TypeError(f"Invalid link mode {mode} in link request proof")
if len(packet.data) == RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2+Link.LINK_MTU_SIZE:
confirmed_mtu = Link.mtu_from_lp_packet(packet)
mtu_bytes = Link.mtu_bytes(confirmed_mtu)
signalling_bytes = Link.signalling_bytes(confirmed_mtu, mode)
packet.data = packet.data[:RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2]
RNS.log(f"Destination confirmed link MTU of {RNS.prettysize(confirmed_mtu)}", RNS.LOG_DEBUG) # TODO: Remove debug
@@ -347,7 +414,7 @@ class Link:
self.handshake()
self.establishment_cost += len(packet.raw)
signed_data = self.link_id+self.peer_pub_bytes+self.peer_sig_pub_bytes+mtu_bytes
signed_data = self.link_id+self.peer_pub_bytes+self.peer_sig_pub_bytes+signalling_bytes
signature = packet.data[:RNS.Identity.SIGLENGTH//8]
if self.destination.identity.validate(signature, signed_data):
@@ -363,11 +430,13 @@ class Link:
self.activated_at = time.time()
self.last_proof = self.activated_at
RNS.Transport.activate_link(self)
RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+str(round(self.rtt, 3))+"s", RNS.LOG_DEBUG)
RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+RNS.prettyshorttime(self.rtt), RNS.LOG_DEBUG)
if self.rtt != None and self.establishment_cost != None and self.rtt > 0 and self.establishment_cost > 0:
self.establishment_rate = self.establishment_cost/self.rtt
self.__update_keepalive()
rtt_data = umsgpack.packb(self.rtt)
rtt_packet = RNS.Packet(self, rtt_data, context=RNS.Packet.LRRTT)
rtt_packet.send()
@@ -475,6 +544,8 @@ class Link:
if self.rtt != None and self.establishment_cost != None and self.rtt > 0 and self.establishment_cost > 0:
self.establishment_rate = self.establishment_cost/self.rtt
self.__update_keepalive()
try:
if self.owner.callbacks.link_established != None:
self.owner.callbacks.link_established(self)
@@ -562,6 +633,12 @@ class Link:
else:
return None
def get_mode(self):
"""
:returns: The mode of an established link.
"""
return self.mode
def get_salt(self):
return self.link_id
@@ -611,23 +688,23 @@ class Link:
def had_outbound(self, is_keepalive=False):
self.last_outbound = time.time()
if not is_keepalive:
self.last_data = self.last_outbound
if not is_keepalive: self.last_data = self.last_outbound
else: self.last_keepalive = self.last_outbound
def __teardown_packet(self):
teardown_packet = RNS.Packet(self, self.link_id, context=RNS.Packet.LINKCLOSE)
teardown_packet.send()
self.had_outbound()
def teardown(self):
"""
Closes the link and purges encryption keys. New keys will
be used if a new link to the same destination is established.
"""
if self.status != Link.PENDING and self.status != Link.CLOSED:
teardown_packet = RNS.Packet(self, self.link_id, context=RNS.Packet.LINKCLOSE)
teardown_packet.send()
self.had_outbound()
if self.status != Link.PENDING and self.status != Link.CLOSED: self.__teardown_packet()
self.status = Link.CLOSED
if self.initiator:
self.teardown_reason = Link.INITIATOR_CLOSED
else:
self.teardown_reason = Link.DESTINATION_CLOSED
if self.initiator: self.teardown_reason = Link.INITIATOR_CLOSED
else: self.teardown_reason = Link.DESTINATION_CLOSED
self.link_closed()
def teardown_packet(self, packet):
@@ -714,9 +791,10 @@ class Link:
elif self.status == Link.ACTIVE:
activated_at = self.activated_at if self.activated_at != None else 0
last_inbound = max(max(self.last_inbound, self.last_proof), activated_at)
now = time.time()
if time.time() >= last_inbound + self.keepalive:
if self.initiator:
if now >= last_inbound + self.keepalive:
if self.initiator and now >= self.last_keepalive + self.keepalive:
self.send_keepalive()
if time.time() >= last_inbound + self.stale_time:
@@ -730,6 +808,7 @@ class Link:
elif self.status == Link.STALE:
sleep_time = 0.001
self.__teardown_packet()
self.status = Link.CLOSED
self.teardown_reason = Link.TIMEOUT
self.link_closed()
@@ -742,6 +821,7 @@ class Link:
self.teardown()
sleep_time = 0.1
sleep_time = min(sleep_time, Link.WATCHDOG_MAX_SLEEP)
sleep(sleep_time)
if not self.__track_phy_stats:
@@ -764,6 +844,10 @@ class Link:
self.snr = packet.snr
if packet.q != None:
self.q = packet.q
def __update_keepalive(self):
self.keepalive = max(min(self.rtt*(Link.KEEPALIVE_MAX/Link.KEEPALIVE_MAX_RTT), Link.KEEPALIVE_MAX), Link.KEEPALIVE_MIN)
self.stale_time = self.keepalive * Link.STALE_FACTOR
def send_keepalive(self):
keepalive_packet = RNS.Packet(self, bytes([0xFF]), context=RNS.Packet.KEEPALIVE)
@@ -782,6 +866,7 @@ class Link:
response_generator = request_handler[1]
allow = request_handler[2]
allowed_list = request_handler[3]
auto_compress = request_handler[4]
allowed = False
if not allow == RNS.Destination.ALLOW_NONE:
@@ -800,18 +885,29 @@ class Link:
else:
raise TypeError("Invalid signature for response generator callback")
if response != None:
packed_response = umsgpack.packb([request_id, response])
file_response = False
file_handle = None
if type(response) == list or type(response) == tuple:
metadata = None
if len(response) > 0 and type(response[0]) == io.BufferedReader:
if len(response) > 1: metadata = response[1]
file_handle = response[0]
file_response = True
if len(packed_response) <= self.mdu:
RNS.Packet(self, packed_response, RNS.Packet.DATA, context = RNS.Packet.RESPONSE).send()
if response != None:
if file_response:
response_resource = RNS.Resource(file_handle, self, metadata=metadata, request_id = request_id, is_response = True, auto_compress=auto_compress)
else:
response_resource = RNS.Resource(packed_response, self, request_id = request_id, is_response = True)
packed_response = umsgpack.packb([request_id, response])
if len(packed_response) <= self.mdu:
RNS.Packet(self, packed_response, RNS.Packet.DATA, context = RNS.Packet.RESPONSE).send()
else:
response_resource = RNS.Resource(packed_response, self, request_id = request_id, is_response = True, auto_compress=auto_compress)
else:
identity_string = str(self.get_remote_identity()) if self.get_remote_identity() != None else "<Unknown>"
RNS.log("Request "+RNS.prettyhexrep(request_id)+" from "+identity_string+" not allowed for: "+str(path), RNS.LOG_DEBUG)
def handle_response(self, request_id, response_data, response_size, response_transfer_size):
def handle_response(self, request_id, response_data, response_size, response_transfer_size, metadata=None):
if self.status == Link.ACTIVE:
remove = None
for pending_request in self.pending_requests:
@@ -822,7 +918,7 @@ class Link:
if pending_request.response_transfer_size == None:
pending_request.response_transfer_size = 0
pending_request.response_transfer_size += response_transfer_size
pending_request.response_received(response_data)
pending_request.response_received(response_data, metadata)
except Exception as e:
RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
@@ -845,12 +941,21 @@ class Link:
def response_resource_concluded(self, resource):
if resource.status == RNS.Resource.COMPLETE:
packed_response = resource.data.read()
unpacked_response = umsgpack.unpackb(packed_response)
request_id = unpacked_response[0]
response_data = unpacked_response[1]
# If the response resource has metadata, this
# is a file response, and we'll pass the open
# file handle directly.
if resource.has_metadata:
self.handle_response(resource.request_id, resource.data, resource.total_size, resource.size, metadata=resource.metadata)
# If not, we'll unpack the response data and
# pass the unpacked structure to the handler
else:
packed_response = resource.data.read()
unpacked_response = umsgpack.unpackb(packed_response)
request_id = unpacked_response[0]
response_data = unpacked_response[1]
self.handle_response(request_id, response_data, resource.total_size, resource.size)
self.handle_response(request_id, response_data, resource.total_size, resource.size)
else:
RNS.log("Incoming response resource failed with status: "+RNS.hexrep([resource.status]), RNS.LOG_DEBUG)
for pending_request in self.pending_requests:
@@ -1085,8 +1190,7 @@ class Link:
def encrypt(self, plaintext):
try:
if not self.token:
try:
self.token = Token(self.derived_key)
try: self.token = Token(self.derived_key)
except Exception as e:
RNS.log("Could not instantiate token while performing encryption on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
raise e
@@ -1100,9 +1204,7 @@ class Link:
def decrypt(self, ciphertext):
try:
if not self.token:
self.token = Token(self.derived_key)
if not self.token: self.token = Token(self.derived_key)
return self.token.decrypt(ciphertext)
except Exception as e:
@@ -1277,6 +1379,7 @@ class RequestReceipt():
self.response = None
self.response_transfer_size = None
self.response_size = None
self.metadata = None
self.status = RequestReceipt.SENT
self.sent_at = time.time()
self.progress = 0
@@ -1363,10 +1466,11 @@ class RequestReceipt():
resource.cancel()
def response_received(self, response):
def response_received(self, response, metadata=None):
if not self.status == RequestReceipt.FAILED:
self.progress = 1.0
self.response = response
self.metadata = metadata
self.status = RequestReceipt.READY
self.response_concluded_at = time.time()
+14 -6
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors.
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -35,10 +43,10 @@ class Packet:
For ``RNS.Destination.GROUP`` destinations, Reticulum will use the
pre-shared key configured for the destination. All packets to group
destinations are encrypted with the same AES-128 key.
destinations are encrypted with the same AES-256 key.
For ``RNS.Destination.SINGLE`` destinations, Reticulum will use a newly
derived ephemeral AES-128 key for every packet.
derived ephemeral AES-256 key for every packet.
For :ref:`RNS.Link<api-link>` destinations, Reticulum will use per-link
ephemeral keys, and offers **Forward Secrecy**.
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors.
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+150 -71
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors.
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -25,6 +33,7 @@ import os
import bz2
import math
import time
import struct
import tempfile
import threading
from threading import Lock
@@ -99,22 +108,20 @@ class Resource:
# it is to be handled within reasonable
# time constraint, even on small systems.
#
# A small system in this regard is
# defined as a Raspberry Pi, which should
# be able to compress, encrypt and hash-map
# the resource in about 10 seconds.
#
# This constant will be used when determining
# how to sequence the sending of large resources.
#
# Capped at 16777215 (0xFFFFFF) per segment to
# fit in 3 bytes in resource advertisements.
MAX_EFFICIENT_SIZE = 16 * 1024 * 1024 - 1
MAX_EFFICIENT_SIZE = 1 * 1024 * 1024 - 1
RESPONSE_MAX_GRACE_TIME = 10
# Max metadata size is 16777215 (0xFFFFFF) bytes
METADATA_MAX_SIZE = 16 * 1024 * 1024 - 1
# The maximum size to auto-compress with
# bz2 before sending.
AUTO_COMPRESS_MAX_SIZE = MAX_EFFICIENT_SIZE
AUTO_COMPRESS_MAX_SIZE = 64 * 1024 * 1024
PART_TIMEOUT_FACTOR = 4
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
@@ -188,12 +195,15 @@ class Resource:
resource.started_transferring = resource.last_activity
resource.storagepath = RNS.Reticulum.resourcepath+"/"+resource.original_hash.hex()
resource.meta_storagepath = resource.storagepath+".meta"
resource.segment_index = adv.i
resource.total_segments = adv.l
if adv.l > 1:
resource.split = True
else:
resource.split = False
if adv.l > 1: resource.split = True
else: resource.split = False
if adv.x: resource.has_metadata = True
else: resource.has_metadata = False
resource.hashmap = [None] * resource.total_parts
resource.hashmap_height = 0
@@ -219,9 +229,7 @@ class Resource:
RNS.log("Error while executing resource started callback from "+str(resource)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
resource.hashmap_update(0, resource.hashmap_raw)
resource.watchdog_job()
return resource
else:
@@ -235,15 +243,33 @@ class Resource:
# Create a resource for transmission to a remote destination
# The data passed can be either a bytes-array or a file opened
# in binary read mode.
def __init__(self, data, link, advertise=True, auto_compress=True, callback=None, progress_callback=None, timeout = None, segment_index = 1, original_hash = None, request_id = None, is_response = False):
def __init__(self, data, link, metadata=None, advertise=True, auto_compress=True, callback=None, progress_callback=None,
timeout = None, segment_index = 1, original_hash = None, request_id = None, is_response = False, sent_metadata_size=0):
data_size = None
resource_data = None
self.assembly_lock = False
self.preparing_next_segment = False
self.next_segment = None
self.metadata = None
self.has_metadata = False
self.metadata_size = sent_metadata_size
if metadata != None:
packed_metadata = umsgpack.packb(metadata)
metadata_size = len(packed_metadata)
if metadata_size > Resource.METADATA_MAX_SIZE:
raise SystemError("Resource metadata size exceeded")
else:
self.metadata = struct.pack(">I", metadata_size)[1:] + packed_metadata
self.metadata_size = len(self.metadata)
self.has_metadata = True
else:
self.metadata = b""
if sent_metadata_size > 0: self.has_metadata = True
if data != None:
if not hasattr(data, "read") and len(data) > Resource.MAX_EFFICIENT_SIZE:
if not hasattr(data, "read") and self.metadata_size + len(data) > Resource.MAX_EFFICIENT_SIZE:
original_data = data
data_size = len(original_data)
data = tempfile.TemporaryFile()
@@ -251,31 +277,43 @@ class Resource:
del original_data
if hasattr(data, "read"):
if data_size == None:
data_size = os.stat(data.name).st_size
if data_size == None: data_size = os.stat(data.name).st_size
self.total_size = data_size + self.metadata_size
self.total_size = data_size
if data_size <= Resource.MAX_EFFICIENT_SIZE:
if self.total_size <= Resource.MAX_EFFICIENT_SIZE:
self.total_segments = 1
self.segment_index = 1
self.split = False
resource_data = data.read()
data.close()
else:
self.total_segments = ((data_size-1)//Resource.MAX_EFFICIENT_SIZE)+1
# self.total_segments = ((data_size-1)//Resource.MAX_EFFICIENT_SIZE)+1
# self.segment_index = segment_index
# self.split = True
# seek_index = segment_index-1
# seek_position = seek_index*Resource.MAX_EFFICIENT_SIZE
self.total_segments = ((self.total_size-1)//Resource.MAX_EFFICIENT_SIZE)+1
self.segment_index = segment_index
self.split = True
seek_index = segment_index-1
seek_position = seek_index*Resource.MAX_EFFICIENT_SIZE
first_read_size = Resource.MAX_EFFICIENT_SIZE - self.metadata_size
if segment_index == 1:
seek_position = 0
segment_read_size = first_read_size
else:
seek_position = first_read_size + ((seek_index-1)*Resource.MAX_EFFICIENT_SIZE)
segment_read_size = Resource.MAX_EFFICIENT_SIZE
data.seek(seek_position)
resource_data = data.read(Resource.MAX_EFFICIENT_SIZE)
resource_data = data.read(segment_read_size)
self.input_file = data
elif isinstance(data, bytes):
data_size = len(data)
self.total_size = data_size
self.total_size = data_size + self.metadata_size
resource_data = data
self.total_segments = 1
@@ -288,7 +326,9 @@ class Resource:
else:
raise TypeError("Invalid data instance type passed to resource initialisation")
data = resource_data
if resource_data:
if self.has_metadata: data = self.metadata + resource_data
else: data = resource_data
self.status = Resource.NONE
self.link = link
@@ -319,7 +359,16 @@ class Resource:
self.request_id = request_id
self.started_transferring = None
self.is_response = is_response
self.auto_compress = auto_compress
self.auto_compress_limit = Resource.AUTO_COMPRESS_MAX_SIZE
self.auto_compress_option = auto_compress
if type(auto_compress) == bool:
self.auto_compress = auto_compress
elif type(auto_compress) == int:
self.auto_compress = True
self.auto_compress_limit = auto_compress
else:
raise TypeError(f"Invalid type {type(auto_compress)} for auto_compress option")
self.req_hashlist = []
self.receiver_min_consecutive_height = 0
@@ -335,7 +384,7 @@ class Resource:
self.uncompressed_data = data
compression_began = time.time()
if (auto_compress and len(self.uncompressed_data) <= Resource.AUTO_COMPRESS_MAX_SIZE):
if self.auto_compress and data_size <= self.auto_compress_limit:
RNS.log("Compressing resource data...", RNS.LOG_EXTREME)
self.compressed_data = bz2.compress(self.uncompressed_data)
RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_EXTREME)
@@ -354,19 +403,20 @@ class Resource:
self.data += self.compressed_data
self.compressed = True
self.uncompressed_data = None
else:
self.data = b""
self.data += RNS.Identity.get_random_hash()[:Resource.RANDOM_HASH_SIZE]
self.data += self.uncompressed_data
self.uncompressed_data = self.data
self.compressed = False
self.compressed_data = None
if auto_compress:
if self.auto_compress and data_size <= self.auto_compress_limit:
RNS.log("Compression did not decrease size, sending uncompressed", RNS.LOG_EXTREME)
self.compressed_data = None
self.uncompressed_data = None
# Resources handle encryption directly to
# make optimal use of packet MTU on an entire
# encrypted stream. The Resource instance will
@@ -419,7 +469,8 @@ class Resource:
self.parts.append(part)
RNS.log("Hashmap computation concluded in "+str(round(time.time()-hashmap_computation_began, 3))+" seconds", RNS.LOG_EXTREME)
self.data = None
if advertise:
self.advertise()
else:
@@ -504,8 +555,7 @@ class Resource:
if self.link: self.link.expected_rate = self.eifr
def watchdog_job(self):
thread = threading.Thread(target=self.__watchdog_job)
thread.daemon = True
thread = threading.Thread(target=self.__watchdog_job, daemon=True)
thread.start()
def __watchdog_job(self):
@@ -551,13 +601,14 @@ class Resource:
else:
sleep_time = self.last_activity + self.part_timeout_factor*((3*self.sdu)/self.eifr) + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
# TODO: Remove debug at some point
# RNS.log(f"EIFR {RNS.prettyspeed(self.eifr)}, ETOF {RNS.prettyshorttime(expected_tof_remaining)} ", RNS.LOG_DEBUG, pt=True)
# RNS.log(f"Resource ST {RNS.prettyshorttime(sleep_time)}, RTT {RNS.prettyshorttime(self.rtt or self.link.rtt)}, {self.outstanding_parts} left", RNS.LOG_DEBUG, pt=True)
if sleep_time < 0:
if self.retries_left > 0:
ms = "" if self.outstanding_parts == 1 else "s"
RNS.log("Timed out waiting for "+str(self.outstanding_parts)+" part"+ms+", requesting retry", RNS.LOG_DEBUG)
RNS.log(f"Timed out waiting for {self.outstanding_parts} part{ms}, requesting retry on {self}", RNS.LOG_DEBUG)
if self.window > self.window_min:
self.window -= 1
if self.window_max > self.window_min:
@@ -620,29 +671,37 @@ class Resource:
self.status = Resource.ASSEMBLING
stream = b"".join(self.parts)
if self.encrypted:
data = self.link.decrypt(stream)
else:
data = stream
if self.encrypted: data = self.link.decrypt(stream)
else: data = stream
# Strip off random hash
data = data[Resource.RANDOM_HASH_SIZE:]
if self.compressed:
self.data = bz2.decompress(data)
else:
self.data = data
if self.compressed: self.data = bz2.decompress(data)
else: self.data = data
calculated_hash = RNS.Identity.full_hash(self.data+self.random_hash)
if calculated_hash == self.hash:
if self.has_metadata and self.segment_index == 1:
# TODO: Add early metadata_ready callback
metadata_size = self.data[0] << 16 | self.data[1] << 8 | self.data[2]
packed_metadata = self.data[3:3+metadata_size]
metadata_file = open(self.meta_storagepath, "wb")
metadata_file.write(packed_metadata)
metadata_file.close()
del packed_metadata
data = self.data[3+metadata_size:]
else:
data = self.data
self.file = open(self.storagepath, "ab")
self.file.write(self.data)
self.file.write(data)
self.file.close()
self.status = Resource.COMPLETE
del data
self.prove()
else:
self.status = Resource.CORRUPT
else: self.status = Resource.CORRUPT
except Exception as e:
@@ -654,21 +713,27 @@ class Resource:
if self.segment_index == self.total_segments:
if self.callback != None:
if not os.path.isfile(self.meta_storagepath):
self.metadata = None
else:
metadata_file = open(self.meta_storagepath, "rb")
self.metadata = umsgpack.unpackb(metadata_file.read())
metadata_file.close()
try: os.unlink(self.meta_storagepath)
except Exception as e:
RNS.log(f"Error while cleaning up resource metadata file, the contained exception was: {e}", RNS.LOG_ERROR)
self.data = open(self.storagepath, "rb")
try:
self.callback(self)
try: self.callback(self)
except Exception as e:
RNS.log("Error while executing resource assembled callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
try:
if hasattr(self.data, "close") and callable(self.data.close):
self.data.close()
os.unlink(self.storagepath)
if hasattr(self.data, "close") and callable(self.data.close): self.data.close()
if os.path.isfile(self.storagepath): os.unlink(self.storagepath)
except Exception as e:
RNS.log("Error while cleaning up resource files, the contained exception was:", RNS.LOG_ERROR)
RNS.log(str(e))
RNS.log(f"Error while cleaning up resource files, the contained exception was: {e}", RNS.LOG_ERROR)
else:
RNS.log("Resource segment "+str(self.segment_index)+" of "+str(self.total_segments)+" received, waiting for next segment to be announced", RNS.LOG_DEBUG)
@@ -699,7 +764,8 @@ class Resource:
request_id = self.request_id,
is_response = self.is_response,
advertise = False,
auto_compress = self.auto_compress,
auto_compress = self.auto_compress_option,
sent_metadata_size = self.metadata_size,
)
def validate_proof(self, proof_data):
@@ -712,18 +778,18 @@ class Resource:
# If all segments were processed, we'll
# signal that the resource sending concluded
if self.callback != None:
try:
self.callback(self)
except Exception as e:
RNS.log("Error while executing resource concluded callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
try: self.callback(self)
except Exception as e: RNS.log("Error while executing resource concluded callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
finally:
try:
if hasattr(self, "input_file"):
if hasattr(self.input_file, "close") and callable(self.input_file.close):
self.input_file.close()
except Exception as e:
RNS.log("Error while closing resource input file: "+str(e), RNS.LOG_ERROR)
if hasattr(self.input_file, "close") and callable(self.input_file.close): self.input_file.close()
except Exception as e: RNS.log("Error while closing resource input file: "+str(e), RNS.LOG_ERROR)
else:
try:
if hasattr(self, "input_file"):
if hasattr(self.input_file, "close") and callable(self.input_file.close): self.input_file.close()
except Exception as e: RNS.log("Error while closing resource input file: "+str(e), RNS.LOG_ERROR)
else:
# Otherwise we'll recursively create the
# next segment of the resource
@@ -731,8 +797,16 @@ class Resource:
RNS.log(f"Next segment preparation for resource {self} was not started yet, manually preparing now. This will cause transfer slowdown.", RNS.LOG_WARNING)
self.__prepare_next_segment()
while self.next_segment == None:
time.sleep(0.05)
while self.next_segment == None: time.sleep(0.05)
self.data = None
self.metadata = None
self.parts = None
self.input_file = None
self.link = None
self.req_hashlist = None
self.hashmap = None
self.next_segment.advertise()
else:
pass
@@ -1194,6 +1268,7 @@ class ResourceAdvertisement:
self.c = resource.compressed # Compression flag
self.e = resource.encrypted # Encryption flag
self.s = resource.split # Split flag
self.x = resource.has_metadata # Metadata flag
self.i = resource.segment_index # Segment index
self.l = resource.total_segments # Total segments
self.q = resource.request_id # ID of associated request
@@ -1209,7 +1284,7 @@ class ResourceAdvertisement:
self.p = True
# Flags
self.f = 0x00 | self.p << 4 | self.u << 3 | self.s << 2 | self.c << 1 | self.e
self.f = 0x00 | self.x << 5 | self.p << 4 | self.u << 3 | self.s << 2 | self.c << 1 | self.e
def get_transfer_size(self):
return self.t
@@ -1229,6 +1304,9 @@ class ResourceAdvertisement:
def is_compressed(self):
return self.c
def has_metadata(self):
return self.x
def get_link(self):
return self.link
@@ -1278,5 +1356,6 @@ class ResourceAdvertisement:
adv.s = True if ((adv.f >> 2) & 0x01) == 0x01 else False
adv.u = True if ((adv.f >> 3) & 0x01) == 0x01 else False
adv.p = True if ((adv.f >> 4) & 0x01) == 0x01 else False
adv.x = True if ((adv.f >> 5) & 0x01) == 0x01 else False
return adv
+90 -21
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors.
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -31,6 +39,7 @@ if get_platform() == "android":
from .Interfaces import UDPInterface
from .Interfaces import I2PInterface
from .Interfaces import RNodeMultiInterface
from .Interfaces import WeaveInterface
from .Interfaces.Android import RNodeInterface
from .Interfaces.Android import SerialInterface
from .Interfaces.Android import KISSInterface
@@ -203,7 +212,8 @@ class Reticulum:
"""
return Reticulum.__instance
def __init__(self,configdir=None, loglevel=None, logdest=None, verbosity=None, require_shared_instance=False):
def __init__(self,configdir=None, loglevel=None, logdest=None, verbosity=None,
require_shared_instance=False, shared_instance_type=None):
"""
Initialises and starts a Reticulum instance. This must be
done before any other operations, and Reticulum will not
@@ -255,9 +265,11 @@ class Reticulum:
self.local_control_port = 37429
self.local_socket_path = None
self.share_instance = True
self.shared_instance_type = shared_instance_type
self.rpc_listener = None
self.rpc_key = None
self.rpc_type = "AF_INET"
self.use_af_unix = False
self.ifac_salt = Reticulum.IFAC_SALT
@@ -314,12 +326,11 @@ class Reticulum:
self.__apply_config()
RNS.log(f"Utilising cryptography backend \"{RNS.Cryptography.Provider.backend()}\"", RNS.LOG_DEBUG)
RNS.log(f"Configuration loaded from {self.configpath}", RNS.LOG_VERBOSE)
RNS.Identity.load_known_destinations()
RNS.Identity.load_known_destinations()
RNS.Transport.start(self)
if RNS.vendor.platformutils.use_af_unix():
if self.use_af_unix:
self.rpc_addr = f"\0rns/{self.local_socket_path}/rpc"
self.rpc_type = "AF_UNIX"
else:
@@ -447,7 +458,11 @@ class Reticulum:
if option == "instance_name":
value = self.config["reticulum"][option]
self.local_socket_path = value
else: self.local_socket_path = "default"
if option == "shared_instance_type":
if self.shared_instance_type == None:
value = self.config["reticulum"][option].lower()
if value in ["tcp", "unix"]:
self.shared_instance_type = value
if option == "shared_instance_port":
value = int(self.config["reticulum"][option])
self.local_interface_port = value
@@ -506,6 +521,17 @@ class Reticulum:
if RNS.compiled: RNS.log("Reticulum running in compiled mode", RNS.LOG_DEBUG)
else: RNS.log("Reticulum running in interpreted mode", RNS.LOG_DEBUG)
if RNS.vendor.platformutils.use_af_unix():
if self.shared_instance_type == "tcp": self.use_af_unix = False
else: self.use_af_unix = True
else:
self.shared_instance_type = "tcp"
self.use_af_unix = False
if self.local_socket_path == None and self.use_af_unix:
self.local_socket_path = "default"
self.__start_local_interface()
if self.is_shared_instance or self.is_standalone_instance:
@@ -626,13 +652,11 @@ class Reticulum:
interface.mode = interface_mode
interface.announce_cap = announce_cap
if configured_bitrate:
interface.bitrate = configured_bitrate
if configured_bitrate: interface.bitrate = configured_bitrate
interface.optimise_mtu()
if ifac_size != None:
interface.ifac_size = ifac_size
else:
interface.ifac_size = interface.DEFAULT_IFAC_SIZE
if ifac_size != None: interface.ifac_size = ifac_size
else: interface.ifac_size = interface.DEFAULT_IFAC_SIZE
interface.announce_rate_target = announce_rate_target
interface.announce_rate_grace = announce_rate_grace
@@ -744,6 +768,10 @@ class Reticulum:
interface_post_init(interface)
interface.start()
if c["type"] == "WeaveInterface":
interface = WeaveInterface.WeaveInterface(RNS.Transport, interface_config)
interface_post_init(interface)
if interface == None:
# Interface was not handled by any internal interface types,
# attempt to load and initialise it from user-supplied modules
@@ -778,6 +806,7 @@ class Reticulum:
except Exception as e:
RNS.log("The interface \""+name+"\" could not be created. Check your configuration file for errors!", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
RNS.panic()
else:
RNS.log("The interface name \""+name+"\" was already used. Check your configuration file for errors!", RNS.LOG_ERROR)
@@ -999,6 +1028,34 @@ class Reticulum:
if hasattr(interface, "r_noise_floor"):
ifstats["noise_floor"] = interface.r_noise_floor
if hasattr(interface, "r_interference"):
ifstats["interference"] = interface.r_interference
if hasattr(interface, "r_interference_l") and type(interface.r_interference_l) == list:
ifstats["interference_last_ts"] = interface.r_interference_l[0]
ifstats["interference_last_dbm"] = interface.r_interference_l[1]
if hasattr(interface, "cpu_temp"):
ifstats["cpu_temp"] = interface.cpu_temp
if hasattr(interface, "cpu_load"):
ifstats["cpu_load"] = interface.cpu_load
if hasattr(interface, "mem_load"):
ifstats["mem_load"] = interface.mem_load
if hasattr(interface, "switch_id"):
if interface.switch_id != None: ifstats["switch_id"] = RNS.hexrep(interface.switch_id)
else: ifstats["switch_id"] = None
if hasattr(interface, "via_switch_id"):
if interface.via_switch_id != None: ifstats["via_switch_id"] = RNS.hexrep(interface.via_switch_id)
else: ifstats["via_switch_id"] = None
if hasattr(interface, "endpoint_id"):
if interface.endpoint_id != None: ifstats["endpoint_id"] = RNS.hexrep(interface.endpoint_id)
else: ifstats["endpoint_id"] = None
if hasattr(interface, "r_battery_state"):
if interface.r_battery_state != 0x00:
ifstats["battery_state"] = interface.get_battery_state_string()
@@ -1360,12 +1417,24 @@ share_instance = Yes
# If you want to run multiple *different* shared instances
# on the same system, you will need to specify different
# shared instance ports for each. The defaults are given
# below, and again, these options can be left out if you
# don't need them.
# instance names for each. On platforms supporting domain
# sockets, this can be done with the instance_name option:
shared_instance_port = 37428
instance_control_port = 37429
instance_name = default
# Some platforms don't support domain sockets, and if that
# is the case, you can isolate different instances by
# specifying a unique set of ports for each:
# shared_instance_port = 37428
# instance_control_port = 37429
# If you want to explicitly use TCP for shared instance
# communication, instead of domain sockets, this is also
# possible, by using the following option:
# shared_instance_type = tcp
# You can configure Reticulum to panic and forcibly close
@@ -1374,7 +1443,7 @@ instance_control_port = 37429
# an optional directive, and can be left out for brevity.
# This behaviour is disabled by default.
panic_on_interface_error = No
# panic_on_interface_error = No
[logging]
+72 -40
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors.
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -1062,7 +1070,7 @@ class Transport:
if should_transmit:
if not stored_hash:
Transport.packet_hashlist.add(packet.packet_hash)
Transport.add_packet_hash(packet.packet_hash)
stored_hash = True
Transport.transmit(interface, packet.raw)
@@ -1074,11 +1082,16 @@ class Transport:
Transport.jobs_locked = False
return sent
@staticmethod
def add_packet_hash(packet_hash):
if not Transport.owner.is_connected_to_shared_instance:
Transport.packet_hashlist.add(packet_hash)
@staticmethod
def packet_filter(packet):
# TODO: Think long and hard about this.
# Is it even strictly necessary with the current
# transport rules?
# If connected to a shared instance, it will handle
# packet filtering
if Transport.owner.is_connected_to_shared_instance: return True
# Filter packets intended for other transport instances
if packet.transport_id != None and packet.packet_type != RNS.Packet.ANNOUNCE:
@@ -1102,7 +1115,7 @@ class Transport:
if packet.destination_type == RNS.Destination.PLAIN:
if packet.packet_type != RNS.Packet.ANNOUNCE:
if packet.hops > 1:
RNS.log("Dropped PLAIN packet "+RNS.prettyhexrep(packet.hash)+" with "+str(packet.hops)+" hops", RNS.LOG_DEBUG)
RNS.log("Dropped PLAIN packet "+RNS.prettyhexrep(packet.packet_hash)+" with "+str(packet.hops)+" hops", RNS.LOG_DEBUG)
return False
else:
return True
@@ -1113,7 +1126,7 @@ class Transport:
if packet.destination_type == RNS.Destination.GROUP:
if packet.packet_type != RNS.Packet.ANNOUNCE:
if packet.hops > 1:
RNS.log("Dropped GROUP packet "+RNS.prettyhexrep(packet.hash)+" with "+str(packet.hops)+" hops", RNS.LOG_DEBUG)
RNS.log("Dropped GROUP packet "+RNS.prettyhexrep(packet.packet_hash)+" with "+str(packet.hops)+" hops", RNS.LOG_DEBUG)
return False
else:
return True
@@ -1266,7 +1279,7 @@ class Transport:
remember_packet_hash = False
if remember_packet_hash:
Transport.packet_hashlist.add(packet.packet_hash)
Transport.add_packet_hash(packet.packet_hash)
# TODO: Enable when caching has been redesigned
# Transport.cache(packet)
@@ -1353,6 +1366,7 @@ class Transport:
proof_timeout += now + RNS.Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, remaining_hops)
path_mtu = RNS.Link.mtu_from_lr_packet(packet)
mode = RNS.Link.mode_from_lr_packet(packet)
nh_mtu = outbound_interface.HW_MTU
if path_mtu:
if outbound_interface.HW_MTU == None:
@@ -1365,10 +1379,14 @@ class Transport:
new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE]
else:
if nh_mtu < path_mtu:
path_mtu = nh_mtu
clamped_mtu = RNS.Link.mtu_bytes(path_mtu)
RNS.log(f"Clamping link MTU to {RNS.prettysize(nh_mtu)}", RNS.LOG_DEBUG) # TODO: Remove debug
new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu
try:
path_mtu = nh_mtu
clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode)
RNS.log(f"Clamping link MTU to {RNS.prettysize(nh_mtu)}", RNS.LOG_DEBUG) # TODO: Remove debug
new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu
except Exception as e:
RNS.log(f"Dropping link request packet. The contained exception was: {e}", RNS.LOG_WARNING)
return
# Entry format is
link_entry = [ now, # 0: Timestamp,
@@ -1431,7 +1449,7 @@ class Transport:
# Add this packet to the filter hashlist if we
# have determined that it's actually our turn
# to process it.
Transport.packet_hashlist.add(packet.packet_hash)
Transport.add_packet_hash(packet.packet_hash)
new_raw = packet.raw[0:1]
new_raw += struct.pack("!B", packet.hops)
@@ -1806,6 +1824,13 @@ class Transport:
announced_identity=announce_identity,
app_data=RNS.Identity.recall_app_data(packet.destination_hash),
announce_packet_hash = packet.packet_hash)
elif len(inspect.signature(handler.received_announce).parameters) == 5:
handler.received_announce(destination_hash=packet.destination_hash,
announced_identity=announce_identity,
app_data=RNS.Identity.recall_app_data(packet.destination_hash),
announce_packet_hash = packet.packet_hash,
is_path_response = packet.context == RNS.Packet.PATH_RESPONSE)
else:
raise TypeError("Invalid signature for announce handler callback")
@@ -1820,6 +1845,7 @@ class Transport:
for destination in Transport.destinations:
if destination.hash == packet.destination_hash and destination.type == packet.destination_type:
path_mtu = RNS.Link.mtu_from_lr_packet(packet)
mode = RNS.Link.mode_from_lr_packet(packet)
if packet.receiving_interface.AUTOCONFIGURE_MTU or packet.receiving_interface.FIXED_MTU:
nh_mtu = packet.receiving_interface.HW_MTU
else:
@@ -1832,10 +1858,14 @@ class Transport:
packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE]
else:
if nh_mtu < path_mtu:
path_mtu = nh_mtu
clamped_mtu = RNS.Link.mtu_bytes(path_mtu)
RNS.log(f"Clamping link MTU to {RNS.prettysize(nh_mtu)}", RNS.LOG_DEBUG) # TODO: Remove debug
packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu
try:
path_mtu = nh_mtu
clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode)
RNS.log(f"Clamping link MTU to {RNS.prettysize(nh_mtu)}", RNS.LOG_DEBUG) # TODO: Remove debug
packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu
except Exception as e:
RNS.log(f"Dropping link request packet to local destination. The contained exception was: {e}", RNS.LOG_WARNING)
return
packet.destination = destination
destination.receive(packet)
@@ -1871,18 +1901,17 @@ class Transport:
for destination in Transport.destinations:
if destination.hash == packet.destination_hash and destination.type == packet.destination_type:
packet.destination = destination
destination.receive(packet)
if destination.receive(packet):
if destination.proof_strategy == RNS.Destination.PROVE_ALL:
packet.prove()
if destination.proof_strategy == RNS.Destination.PROVE_ALL:
packet.prove()
elif destination.proof_strategy == RNS.Destination.PROVE_APP:
if destination.callbacks.proof_requested:
try:
if destination.callbacks.proof_requested(packet):
packet.prove()
except Exception as e:
RNS.log("Error while executing proof request callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
elif destination.proof_strategy == RNS.Destination.PROVE_APP:
if destination.callbacks.proof_requested:
try:
if destination.callbacks.proof_requested(packet):
packet.prove()
except Exception as e:
RNS.log("Error while executing proof request callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
# Handling for proofs and link-request proofs
elif packet.packet_type == RNS.Packet.PROOF:
@@ -1895,15 +1924,15 @@ class Transport:
if packet.receiving_interface == link_entry[IDX_LT_NH_IF]:
try:
if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2 or len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE:
mtu_bytes = b""
signalling_bytes = b""
if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE:
mtu_bytes = RNS.Link.mtu_bytes(RNS.Link.mtu_from_lp_packet(packet))
signalling_bytes = RNS.Link.signalling_bytes(RNS.Link.mtu_from_lp_packet(packet), RNS.Link.mode_from_lp_packet(packet))
peer_pub_bytes = packet.data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2]
peer_identity = RNS.Identity.recall(link_entry[IDX_LT_DSTHASH])
peer_sig_pub_bytes = peer_identity.get_public_key()[RNS.Link.ECPUBSIZE//2:RNS.Link.ECPUBSIZE]
signed_data = packet.destination_hash+peer_pub_bytes+peer_sig_pub_bytes+mtu_bytes
signed_data = packet.destination_hash+peer_pub_bytes+peer_sig_pub_bytes+signalling_bytes
signature = packet.data[:RNS.Identity.SIGLENGTH//8]
if peer_identity.validate(signature, signed_data):
@@ -1945,7 +1974,7 @@ class Transport:
# Add this packet to the filter hashlist if we
# have determined that it's actually destined
# for this system, and then validate the proof
Transport.packet_hashlist.add(packet.packet_hash)
Transport.add_packet_hash(packet.packet_hash)
link.validate_proof(packet)
elif packet.context == RNS.Packet.RESOURCE_PRF:
@@ -2104,7 +2133,10 @@ class Transport:
if Transport.owner.is_connected_to_shared_instance:
if destination.type == RNS.Destination.SINGLE:
destination.announce(path_response=True)
def job():
time.sleep(0.25)
destination.announce(path_response=True)
threading.Thread(target=job, daemon=True).start()
@staticmethod
def deregister_destination(destination):
@@ -2137,9 +2169,10 @@ class Transport:
Registers an announce handler.
:param handler: Must be an object with an *aspect_filter* attribute and a *received_announce(destination_hash, announced_identity, app_data)*
or *received_announce(destination_hash, announced_identity, app_data, announce_packet_hash)* callable. Can optionally have a
*receive_path_responses* attribute set to ``True``, to also receive all path responses, in addition to live announces. See
the :ref:`Announce Example<example-announce>` for more info.
or *received_announce(destination_hash, announced_identity, app_data, announce_packet_hash)* or
*received_announce(destination_hash, announced_identity, app_data, announce_packet_hash, is_path_response)* callable. Can
optionally have a *receive_path_responses* attribute set to ``True``, to also receive all path responses, in addition to live
announces. See the :ref:`Announce Example<example-announce>` for more info.
"""
if hasattr(handler, "received_announce") and callable(handler.received_announce):
if hasattr(handler, "aspect_filter"):
@@ -2763,7 +2796,6 @@ class Transport:
Transport.reverse_table = {}
Transport.link_table = {}
Transport.held_announces = {}
Transport.announce_handlers = []
Transport.tunnels = {}
@staticmethod
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+101 -92
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -25,6 +33,7 @@
import RNS
import argparse
import threading
import shutil
import time
import sys
import os
@@ -34,6 +43,7 @@ from RNS._version import __version__
APP_NAME = "rncp"
allow_all = False
allow_fetch = False
allow_overwrite_on_receive = False
fetch_auto_compress = True
fetch_jail = None
save_path = None
@@ -47,13 +57,14 @@ erase_str = "\33[2K\r"
def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identity = False,
limit = None, disable_auth = None, fetch_allowed = False, no_compress=False,
jail = None, save = None, announce = False):
jail = None, save = None, announce = False, allow_overwrite=False):
global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path, fetch_auto_compress
from tempfile import TemporaryFile
global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path
global fetch_auto_compress, allow_overwrite_on_receive
allow_fetch = fetch_allowed
fetch_auto_compress = not no_compress
allow_overwrite_on_receive = allow_overwrite
identity = None
if announce < 0:
announce = False
@@ -175,22 +186,15 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi
if target_link != None:
RNS.log("Sending file "+str(file_path)+" to client", RNS.LOG_VERBOSE)
temp_file = TemporaryFile()
real_file = open(file_path, "rb")
filename_bytes = os.path.basename(file_path).encode("utf-8")
filename_len = len(filename_bytes)
try:
metadata = {"name": os.path.basename(file_path).encode("utf-8") }
fetch_resource = RNS.Resource(open(file_path, "rb"), target_link, metadata=metadata, auto_compress=fetch_auto_compress)
return True
if filename_len > 0xFFFF:
print("Filename exceeds max size, cannot send")
RNS.exit(1)
except Exception as e:
RNS.log(f"Could not send file to client. The contained exception was: {e}", RNS.LOG_ERROR)
return False
temp_file.write(filename_len.to_bytes(2, "big"))
temp_file.write(filename_bytes)
temp_file.write(real_file.read())
temp_file.seek(0)
fetch_resource = RNS.Resource(temp_file, target_link, auto_compress=fetch_auto_compress)
return True
else:
return None
@@ -215,8 +219,7 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi
threading.Thread(target=job, daemon=True).start()
while True:
time.sleep(1)
while True: time.sleep(1)
def client_link_established(link):
RNS.log("Incoming link established", RNS.LOG_VERBOSE)
@@ -261,34 +264,42 @@ def receive_resource_started(resource):
print("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str)
def receive_resource_concluded(resource):
global save_path
global save_path, allow_overwrite_on_receive
if resource.status == RNS.Resource.COMPLETE:
print(str(resource)+" completed")
if resource.total_size > 4:
filename_len = int.from_bytes(resource.data.read(2), "big")
filename = resource.data.read(filename_len).decode("utf-8")
counter = 0
if save_path:
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
if not saved_filename.startswith(save_path+"/"):
RNS.log(f"Invalid save path {saved_filename}, ignoring", RNS.LOG_ERROR)
return
else:
saved_filename = filename
full_save_path = saved_filename
while os.path.isfile(full_save_path):
counter += 1
full_save_path = saved_filename+"."+str(counter)
file = open(full_save_path, "wb")
file.write(resource.data.read())
file.close()
if resource.metadata == None:
print("Invalid data received, ignoring resource")
return
else:
print("Invalid data received, ignoring resource")
try:
filename = os.path.basename(resource.metadata["name"].decode("utf-8"))
counter = 0
if save_path:
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
if not saved_filename.startswith(save_path+"/"):
RNS.log(f"Invalid save path {saved_filename}, ignoring", RNS.LOG_ERROR)
return
else:
saved_filename = filename
full_save_path = saved_filename
if allow_overwrite_on_receive:
if os.path.isfile(full_save_path):
try: os.unlink(full_save_path)
except Exception as e:
RNS.log(f"Could not overwrite existing file {full_save_path}, renaming instead", RNS.LOG_ERROR)
while os.path.isfile(full_save_path):
counter += 1
full_save_path = saved_filename+"."+str(counter)
shutil.move(resource.data.name, full_save_path)
except Exception as e:
RNS.log(f"An error occurred while saving received resource: {e}", RNS.LOG_ERROR)
return
else:
print("Resource failed")
@@ -334,10 +345,11 @@ def sender_progress(resource):
resource_done = True
link = None
def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, save=None):
global current_resource, resource_done, link, speed, show_phy_rates, save_path
def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, save=None, allow_overwrite=False):
global current_resource, resource_done, link, speed, show_phy_rates, save_path, allow_overwrite_on_receive
targetloglevel = 3+verbosity-quietness
show_phy_rates = phy_rates
allow_overwrite_on_receive = allow_overwrite
if save:
sp = os.path.abspath(os.path.expanduser(save))
@@ -473,32 +485,40 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
def fetch_resource_concluded(resource):
nonlocal resource_resolved, resource_status
global save_path
global save_path, allow_overwrite_on_receive
if resource.status == RNS.Resource.COMPLETE:
if resource.total_size > 4:
filename_len = int.from_bytes(resource.data.read(2), "big")
filename = resource.data.read(filename_len).decode("utf-8")
counter = 0
if save_path:
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
else:
saved_filename = filename
full_save_path = saved_filename
while os.path.isfile(full_save_path):
counter += 1
full_save_path = saved_filename+"."+str(counter)
file = open(full_save_path, "wb")
file.write(resource.data.read())
file.close()
resource_status = "completed"
if resource.metadata == None:
print("Invalid data received, ignoring resource")
return
else:
print("Invalid data received, ignoring resource")
resource_status = "invalid_data"
try:
filename = os.path.basename(resource.metadata["name"].decode("utf-8"))
counter = 0
if save_path:
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
if not saved_filename.startswith(save_path+"/"):
print(f"Invalid save path {saved_filename}, ignoring")
return
else:
saved_filename = filename
full_save_path = saved_filename
if allow_overwrite_on_receive:
if os.path.isfile(full_save_path):
try: os.unlink(full_save_path)
except Exception as e:
print(f"Could not overwrite existing file {full_save_path}, renaming instead")
while os.path.isfile(full_save_path):
counter += 1
full_save_path = saved_filename+"."+str(counter)
shutil.move(resource.data.name, full_save_path)
except Exception as e:
print(f"An error occurred while saving received resource: {e}")
return
else:
print("Resource failed")
@@ -596,7 +616,6 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, no_compress=False):
global current_resource, resource_done, link, speed, show_phy_rates, phy_got_total, phy_speed
from tempfile import TemporaryFile
targetloglevel = 3+verbosity-quietness
show_phy_rates = phy_rates
@@ -618,21 +637,7 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
print("File not found")
sys.exit(1)
temp_file = TemporaryFile()
real_file = open(file_path, "rb")
filename_bytes = os.path.basename(file_path).encode("utf-8")
filename_len = len(filename_bytes)
if filename_len > 0xFFFF:
print("Filename exceeds max size, cannot send")
RNS.exit(1)
else:
print("Preparing file...", end=es)
temp_file.write(filename_len.to_bytes(2, "big"))
temp_file.write(filename_bytes)
temp_file.write(real_file.read())
temp_file.seek(0)
metadata = {"name": os.path.basename(file_path).encode("utf-8") }
print(f"{erase_str}", end="")
@@ -719,9 +724,12 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
link.identify(identity)
auto_compress = True
if no_compress:
auto_compress = False
resource = RNS.Resource(temp_file, link, callback = sender_progress, progress_callback = sender_progress, auto_compress = auto_compress)
if no_compress: auto_compress = False
try: resource = RNS.Resource(open(file_path, "rb"), link, metadata=metadata, callback = sender_progress, progress_callback = sender_progress, auto_compress = auto_compress)
except Exception as e:
print(f"Could not start transfer: {e}")
RNS.exit(1)
current_resource = resource
while resource.status < RNS.Resource.TRANSFERRING:
@@ -792,8 +800,6 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
print("\n"+str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
link.teardown()
time.sleep(0.25)
real_file.close()
temp_file.close()
RNS.exit(0)
def main():
@@ -811,6 +817,7 @@ def main():
parser.add_argument("-f", '--fetch', action='store_true', default=False, help="fetch file from remote listener instead of sending")
parser.add_argument("-j", "--jail", metavar="path", action="store", default=None, help="restrict fetch requests to specified path", type=str)
parser.add_argument("-s", "--save", metavar="path", action="store", default=None, help="save received files in specified path", type=str)
parser.add_argument('-O', '--overwrite', action='store_true', default=False, help="Allow overwriting received files, instead of adding postfix")
parser.add_argument("-b", action='store', metavar="seconds", default=-1, help="announce interval, 0 to only announce at startup", type=int)
parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="allow this identity (or add in ~/.rncp/allowed_identities)", type=str)
parser.add_argument('-n', '--no-auth', action='store_true', default=False, help="accept requests from anyone")
@@ -836,6 +843,7 @@ def main():
# limit=args.limit,
disable_auth=args.no_auth,
announce=args.b,
allow_overwrite=args.overwrite,
)
elif args.fetch:
@@ -850,6 +858,7 @@ def main():
silent = args.silent,
phy_rates = args.phy_rates,
save = args.save,
allow_overwrite=args.overwrite,
)
else:
print("")
+13 -5
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2023-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -194,7 +202,7 @@ def main():
else:
try:
identity.to_file(args.generate)
RNS.log("New identity written to "+str(args.generate))
RNS.log(f"New identity {identity} written to {args.generate}")
exit(0)
except Exception as e:
RNS.log("An error ocurred while saving the generated Identity.", RNS.LOG_ERROR)
+12 -4
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2023-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+390 -72
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2018-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -89,10 +97,17 @@ class KISS():
CMD_BT_CTRL = 0x46
CMD_BT_PIN = 0x62
CMD_DIS_IA = 0x69
CMD_WIFI_MODE = 0x6A
CMD_WIFI_SSID = 0x6B
CMD_WIFI_PSK = 0x6C
CMD_WIFI_CHN = 0x6E
CMD_WIFI_IP = 0x84
CMD_WIFI_NM = 0x85
CMD_BOARD = 0x47
CMD_PLATFORM = 0x48
CMD_MCU = 0x49
CMD_FW_VERSION = 0x50
CMD_CFG_READ = 0x6D
CMD_ROM_READ = 0x51
CMD_ROM_WRITE = 0x52
CMD_ROM_WIPE = 0x59
@@ -159,6 +174,7 @@ class ROM():
MODEL_B9 = 0xB9
MODEL_B4_TCXO = 0x04 # The TCXO model codes are only used here to select the correct firmware,
MODEL_B9_TCXO = 0x09 # actual model codes in firmware is still 0xB4 and 0xB9.
PRODUCT_H32_V2 = 0xC0
MODEL_C4 = 0xC4
MODEL_C9 = 0xC9
@@ -167,6 +183,9 @@ class ROM():
MODEL_C5 = 0xC5
MODEL_CA = 0xCA
PRODUCT_H32_V4 = 0xC3
MODEL_C8 = 0xC8 # 868/915/923 MHz with PA
PRODUCT_TBEAM = 0xE0
MODEL_E4 = 0xE4
MODEL_E9 = 0xE9
@@ -233,6 +252,12 @@ class ROM():
ADDR_CONF_PINT = 0xB6
ADDR_CONF_BSET = 0xB7
ADDR_CONF_DIA = 0xB9
ADDR_CONF_WIFI = 0xBA
ADDR_CONF_WCHN = 0xBB
ADDR_CONF_SSID = 0x00
ADDR_CONF_PSK = 0x21
ADDR_CONF_IP = 0x42
ADDR_CONF_NM = 0x46
INFO_LOCK_BYTE = 0x73
CONF_OK_BYTE = 0x73
@@ -262,6 +287,7 @@ products = {
ROM.PRODUCT_T32_21: "LilyGO LoRa32 v2.1",
ROM.PRODUCT_H32_V2: "Heltec LoRa32 v2",
ROM.PRODUCT_H32_V3: "Heltec LoRa32 v3",
ROM.PRODUCT_H32_V4: "Heltec LoRa32 v4",
ROM.PRODUCT_TECHO: "LilyGO T-Echo",
ROM.PRODUCT_RAK4631: "RAK4631",
ROM.PRODUCT_OPENCOM_XL: "openCom XL",
@@ -306,6 +332,7 @@ models = {
0xC9: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_heltec32v2.zip", "SX1276"],
0xC5: [420000000, 520000000, 22, "420 - 520 MHz", "rnode_firmware_heltec32v3.zip", "SX1268"],
0xCA: [850000000, 950000000, 22, "850 - 950 MHz", "rnode_firmware_heltec32v3.zip", "SX1262"],
0xC8: [860000000, 930000000, 28, "850 - 950 MHz", "rnode_firmware_heltec32v4pa.zip", "SX1262"],
0xC6: [420000000, 520000000, 22, "420 - 520 MHz", "rnode_firmware_heltec_t114.zip", "SX1268"],
0xC7: [850000000, 950000000, 22, "850 - 950 MHz", "rnode_firmware_heltec_t114.zip", "SX1262"],
0xE4: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_tbeam.zip", "SX1278"],
@@ -388,6 +415,7 @@ class RNode():
self.platform = None
self.mcu = None
self.eeprom = None
self.cfg_sector = None
self.major_version = None
self.minor_version = None
self.version = None
@@ -447,12 +475,17 @@ class RNode():
in_frame = False
data_buffer = b""
command_buffer = b""
elif (in_frame and byte == KISS.FEND and command == KISS.CMD_CFG_READ):
self.cfg_sector = data_buffer
in_frame = False
data_buffer = b""
command_buffer = b""
elif (byte == KISS.FEND):
in_frame = True
command = KISS.CMD_UNKNOWN
data_buffer = b""
command_buffer = b""
elif (in_frame and len(data_buffer) < 512):
elif (in_frame and len(data_buffer) < 1024):
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
command = byte
elif (command == KISS.CMD_ROM_READ):
@@ -466,6 +499,17 @@ class RNode():
byte = KISS.FESC
escape = False
data_buffer = data_buffer+bytes([byte])
elif (command == KISS.CMD_CFG_READ):
if (byte == KISS.FESC):
escape = True
else:
if (escape):
if (byte == KISS.TFEND):
byte = KISS.FEND
if (byte == KISS.TFESC):
byte = KISS.FESC
escape = False
data_buffer = data_buffer+bytes([byte])
elif (command == KISS.CMD_DATA):
if (byte == KISS.FESC):
escape = True
@@ -774,6 +818,92 @@ class RNode():
if written != len(kiss_command):
raise IOError("An IO error occurred while sending firmware update command to device")
def set_wifi_mode(self, mode):
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_MODE, mode])+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while sending wifi mode command to device")
def set_wifi_channel(self, channel):
try: ch = int(channel)
except: raise ValueError("Invalid WiFi channel")
if ch < 1 or ch > 14: raise ValueError("Invalid WiFi channel")
ch_data = bytes([ch])
data = KISS.escape(ch_data)
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_CHN])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while sending wifi channel to device")
def set_wifi_ip(self, ip):
if ip == None: ip_data = bytes([0x00, 0x00, 0x00, 0x00])
else:
ip_data = b""
if not type(ip) == str: raise TypeError("Invalid IP address")
octets = ip.split(".")
if not len(octets) == 4: raise ValueError("Invalid IP address length")
try:
for i in range(0, 4):
octet = int(octets[i])
if octet < 0 or octet > 255: raise ValueError("Invalid IP octet value")
else: ip_data += bytes([octet])
except Exception as e:
raise ValueError(f"Could not decode IP address octet: {e}")
data = KISS.escape(ip_data)
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_IP])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command): raise IOError("An IO error occurred while sending wifi IP address to device")
def set_wifi_nm(self, nm):
if nm == None: nm_data = bytes([0x00, 0x00, 0x00, 0x00])
else:
nm_data = b""
if not type(nm) == str: raise TypeError("Invalid IP address")
octets = nm.split(".")
if not len(octets) == 4: raise ValueError("Invalid IP address length")
try:
for i in range(0, 4):
octet = int(octets[i])
if octet < 0 or octet > 255: raise ValueError("Invalid IP octet value")
else: nm_data += bytes([octet])
except Exception as e:
raise ValueError(f"Could not decode IP address octet: {e}")
data = KISS.escape(nm_data)
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_NM])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command): raise IOError("An IO error occurred while sending wifi netmask to device")
def set_wifi_ssid(self, ssid):
if ssid == None: data = bytes([0x00])
else:
ssid_data = ssid.encode("utf-8")+bytes([0x00])
if len(ssid_data) < 0 or len(ssid_data) > 33: raise ValueError("Invalid SSID length")
data = KISS.escape(ssid_data)
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_SSID])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while sending wifi SSID to device")
def set_wifi_psk(self, psk):
if psk == None: data = bytes([0x00])
else:
psk_data = psk.encode("utf-8")+bytes([0x00])
if len(psk_data) < 8 or len(psk_data) > 33: raise ValueError("Invalid psk length")
data = KISS.escape(psk_data)
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_PSK])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while sending wifi SSID to device")
def initRadio(self):
self.setFrequency()
self.setBandwidth()
@@ -880,7 +1010,7 @@ class RNode():
kiss_command = bytes([KISS.FEND, KISS.CMD_ROM_READ, 0x00, KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while configuring radio state")
raise IOError("An IO error occurred while downloading EEPROM")
sleep(0.6)
if self.eeprom == None:
@@ -889,6 +1019,15 @@ class RNode():
else:
self.parse_eeprom()
def download_cfg_sector(self):
self.cfg_sector = None
kiss_command = bytes([KISS.FEND, KISS.CMD_CFG_READ, 0x00, KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while downloading config sector")
sleep(0.6)
def parse_eeprom(self):
global squashvw;
try:
@@ -1351,6 +1490,14 @@ def main():
parser.add_argument("-B", "--bluetooth-off", action="store_true", help="Turn device bluetooth off")
parser.add_argument("-p", "--bluetooth-pair", action="store_true", help="Put device into bluetooth pairing mode")
parser.add_argument("-w", "--wifi", action="store", metavar="mode", default=None, help="Set WiFi mode (OFF, AP or STATION)")
parser.add_argument("--channel", action="store", metavar="channel", default=None, help="Set WiFi channel")
parser.add_argument("--ssid", action="store", metavar="ssid", default=None, help="Set WiFi SSID (NONE to delete)")
parser.add_argument("--psk", action="store", metavar="psk", default=None, help="Set WiFi PSK (NONE to delete)")
parser.add_argument("--show-psk", action="store_true", default=False, help="Display stored WiFi PSK")
parser.add_argument("--ip", action="store", metavar="ip", default=None, help="Set static WiFi IP address (NONE for DHCP)")
parser.add_argument("--nm", action="store", metavar="nm", default=None, help="Set static WiFi network mask (NONE for DHCP)")
parser.add_argument("-D", "--display", action="store", metavar="i", type=int, default=None, help="Set display intensity (0-255)")
parser.add_argument("-t", "--timeout", action="store", metavar="s", type=int, default=None, help="Set display timeout in seconds, 0 to disable")
parser.add_argument("-R", "--rotation", action="store", metavar="rotation", type=int, default=None, help="Set display rotation, valid values are 0 through 3")
@@ -1708,7 +1855,7 @@ def main():
print(" '")
print("[1] A specific kind of RNode")
print("")
print(" | Select this option if you have put toghether an RNode")
print(" | Select this option if you have put together an RNode")
print(" \\ / of your own design, or if you are prototyping one.")
print(" '")
print("[2] Homebrew RNode")
@@ -1722,13 +1869,14 @@ def main():
print("[6] LilyGO T-Beam")
print("[7] Heltec LoRa32 v2")
print("[8] Heltec LoRa32 v3")
print("[9] LilyGO LoRa T3S3")
print("[10] RAK4631")
print("[11] LilyGo T-Echo")
print("[12] LilyGO T-Beam Supreme")
print("[13] LilyGO T-Deck")
print("[14] Heltec T114")
print("[15] Seeed XIAO ESP32S3 Wio-SX1262")
print("[9] Heltec LoRa32 v4")
print("[10] LilyGO LoRa T3S3")
print("[11] RAK4631")
print("[12] LilyGo T-Echo")
print("[13] LilyGO T-Beam Supreme")
print("[14] LilyGO T-Deck")
print("[15] Heltec T114")
print("[16] Seeed XIAO ESP32S3 Wio-SX1262")
print("")
print("---------------------------------------------------------------------------")
print("\nEnter the number that matches your device type:\n? ", end="")
@@ -1737,7 +1885,7 @@ def main():
try:
c_dev = int(input())
c_mod = False
if c_dev < 1 or c_dev > 15:
if c_dev < 1 or c_dev > 16:
raise ValueError()
elif c_dev == 1:
selected_product = ROM.PRODUCT_RNODE
@@ -1754,7 +1902,7 @@ def main():
print("")
print("Important! Using RNode firmware on homebrew devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
@@ -1770,11 +1918,11 @@ def main():
print("")
print("Important! Using RNode firmware on T-Beam devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
elif c_dev == 12:
elif c_dev == 13:
selected_product = ROM.PRODUCT_TBEAM_S_V1
clear()
print("")
@@ -1786,11 +1934,11 @@ def main():
print("")
print("Important! Using RNode firmware on T-Beam devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
elif c_dev == 13:
elif c_dev == 14:
selected_product = ROM.PRODUCT_TDECK
clear()
print("")
@@ -1802,7 +1950,7 @@ def main():
print("")
print("Important! Using RNode firmware on T-Beam devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
@@ -1815,7 +1963,7 @@ def main():
print("")
print("Important! Using RNode firmware on LoRa32 devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
@@ -1828,7 +1976,7 @@ def main():
print("")
print("Important! Using RNode firmware on LoRa32 devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it.")
print("")
print("Please Note! This device is known to have a faulty battery charging circuit,")
@@ -1847,7 +1995,7 @@ def main():
print("")
print("Important! Using RNode firmware on LoRa32 devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
@@ -1861,11 +2009,11 @@ def main():
print("Important! Using RNode firmware on Heltec devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
elif c_dev == 9:
elif c_dev == 10:
selected_product = ROM.PRODUCT_RNODE
c_mod = True
clear()
@@ -1876,7 +2024,7 @@ def main():
print("Important! Using RNode firmware on T3S3 devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
@@ -1890,11 +2038,25 @@ def main():
print("Important! Using RNode firmware on Heltec devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
elif c_dev == 10:
elif c_dev == 9:
selected_product = ROM.PRODUCT_H32_V4
clear()
print("")
print("---------------------------------------------------------------------------")
print(" Heltec LoRa32 v4 RNode Installer")
print("")
print("Important! Using RNode firmware on Heltec devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
elif c_dev == 11:
selected_product = ROM.PRODUCT_RAK4631
clear()
print("")
@@ -1903,11 +2065,11 @@ def main():
print("")
print("Important! Using RNode firmware on RAKwireless devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
elif c_dev == 11:
elif c_dev == 12:
selected_product = ROM.PRODUCT_TECHO
clear()
print("")
@@ -1916,11 +2078,11 @@ def main():
print("")
print("Important! Using RNode firmware on LilyGo T-Echo devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
elif c_dev == 14:
elif c_dev == 15:
selected_product = ROM.PRODUCT_HELTEC_T114
clear()
print("")
@@ -1929,11 +2091,11 @@ def main():
print("")
print("Important! Using RNode firmware on Heltec T114 devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
elif c_dev == 15:
elif c_dev == 16:
selected_product = ROM.PRODUCT_XIAO_S3
clear()
print("")
@@ -1942,7 +2104,7 @@ def main():
print("")
print("Important! Using RNode firmware on SeeedStudio XIAO/wio devices should currently be")
print("considered experimental. It is not intended for production or critical use.")
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
print("who would like to experiment with it. Hit enter to continue.")
print("---------------------------------------------------------------------------")
input()
@@ -2256,6 +2418,7 @@ def main():
print("[2] 868 MHz")
print("[3] 915 MHz")
print("[4] 923 MHz")
print("\n? ", end="")
try:
c_model = int(input())
if c_model < 1 or c_model > 4:
@@ -2270,6 +2433,24 @@ def main():
print("That band does not exist, exiting now.")
exit()
elif selected_product == ROM.PRODUCT_H32_V4:
selected_mcu = ROM.MCU_ESP32
print("\nWhat band is this Heltec LoRa32 V4 for?\n")
print("[1] 868 MHz (28 dBm output)")
print("[2] 915 MHz (28 dBm output)")
print("[3] 923 MHz (28 dBm output)")
print("\n? ", end="")
try:
c_model = int(input())
if c_model < 1 or c_model > 3:
raise ValueError()
else:
selected_model = ROM.MODEL_C8
selected_platform = ROM.PLATFORM_ESP32
except Exception as e:
print("That band does not exist, exiting now.")
exit()
elif selected_product == ROM.PRODUCT_HELTEC_T114:
selected_mcu = ROM.MCU_NRF52
print("\nWhat band is this Heltec T114 for?\n")
@@ -2277,6 +2458,7 @@ def main():
print("[2] 868 MHz")
print("[3] 915 MHz")
print("[4] 923 MHz")
print("\n? ", end="")
try:
c_model = int(input())
if c_model < 1 or c_model > 4:
@@ -2296,6 +2478,7 @@ def main():
print("\nWhat band is this XIAO esp32s3 wio module for?\n")
print("[1] 433 MHz")
print("[2] 868 MHz")
print("\n? ", end="")
try:
c_model = int(input())
if c_model < 1 or c_model > 2:
@@ -2867,6 +3050,24 @@ def main():
"0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
"0x8000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v3.partitions",
]
elif fw_filename == "rnode_firmware_heltec32v4pa.zip":
return [
sys.executable, flasher,
"--chip", "esp32-s3",
"--port", args.port,
"--baud", args.baud_flash,
"--before", "default_reset",
"--after", "hard_reset",
"write_flash", "-z",
"--flash_mode", "dio",
"--flash_freq", "80m",
"--flash_size", "16MB",
"0xe000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.boot_app0",
"0x0", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.bootloader",
"0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.bin",
"0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
"0x8000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.partitions",
]
elif fw_filename == "rnode_firmware_featheresp32.zip":
if numeric_version >= 1.55:
return [
@@ -3490,17 +3691,14 @@ def main():
graceful_exit()
if args.config:
rnode.download_cfg_sector()
eeprom_reserved = 200
if rnode.platform == ROM.PLATFORM_ESP32:
eeprom_size = 296
elif rnode.platform == ROM.PLATFORM_NRF52:
eeprom_size = 296
else:
eeprom_size = 4096
if rnode.platform == ROM.PLATFORM_ESP32: eeprom_size = 296
elif rnode.platform == ROM.PLATFORM_NRF52: eeprom_size = 296
else: eeprom_size = 4096
eeprom_offset = eeprom_size-eeprom_reserved
def ea(a):
return a+eeprom_offset
def ea(a): return a+eeprom_offset
ec_bt = rnode.eeprom[ROM.ADDR_CONF_BT]
ec_dint = rnode.eeprom[ROM.ADDR_CONF_DINT]
ec_dadr = rnode.eeprom[ROM.ADDR_CONF_DADR]
@@ -3510,40 +3708,89 @@ def main():
ec_pint = rnode.eeprom[ROM.ADDR_CONF_PINT]
ec_bset = rnode.eeprom[ROM.ADDR_CONF_BSET]
ec_dia = rnode.eeprom[ROM.ADDR_CONF_DIA]
ec_wifi = rnode.eeprom[ROM.ADDR_CONF_WIFI]
ec_wchn = rnode.eeprom[ROM.ADDR_CONF_WCHN]
ec_ssid = None
ec_psk = None
ec_ip = None
ec_nm = None
if ec_wchn < 1 or ec_wchn > 14: ec_wchn = 1
if rnode.cfg_sector:
ssid_bytes = b""
for i in range(0, 32):
byte = rnode.cfg_sector[ROM.ADDR_CONF_SSID+i]
if byte == 0xFF: byte = 0x00
if byte == 0x00: break
else: ssid_bytes += bytes([byte])
try: ec_ssid = ssid_bytes.decode("utf-8")
except Exception as e: print(f"Error: Could not decode WiFi SSID read from device")
psk_bytes = b""
for i in range(0, 32):
byte = rnode.cfg_sector[ROM.ADDR_CONF_PSK+i]
if byte == 0xFF: byte = 0x00
if byte == 0x00: break
else: psk_bytes += bytes([byte])
ip_bytes = b""
for i in range(0, 4):
byte = rnode.cfg_sector[ROM.ADDR_CONF_IP+i]
ip_bytes += bytes([byte])
if len(ip_bytes) == 4: ec_ip = f"{int(ip_bytes[0])}.{int(ip_bytes[1])}.{int(ip_bytes[2])}.{int(ip_bytes[3])}"
if ec_ip == "255.255.255.255" or ec_ip == "0.0.0.0": ec_ip = None
nm_bytes = b""
for i in range(0, 4):
byte = rnode.cfg_sector[ROM.ADDR_CONF_NM+i]
nm_bytes += bytes([byte])
if len(nm_bytes) == 4: ec_nm = f"{int(nm_bytes[0])}.{int(nm_bytes[1])}.{int(nm_bytes[2])}.{int(nm_bytes[3])}"
if ec_nm == "255.255.255.255" or ec_nm == "0.0.0.0": ec_nm = None
if ec_wifi == 0x02:
ec_ip = "10.0.0.1"
ec_nm = "255.255.255.0"
try: ec_psk = psk_bytes.decode("utf-8")
except Exception as e: print(f"Error: Could not decode WiFi PSK read from device")
if not args.show_psk and ec_psk: ec_psk = "*"*len(ec_psk)
print("\nDevice configuration:")
if ec_bt == 0x73:
print(f" Bluetooth : Enabled")
else:
print(f" Bluetooth : Disabled")
if ec_dia == 0x00:
print(f" Interference avoidance : Enabled")
else:
print(f" Interference avoidance : Disabled")
if ec_bt == 0x73: print(f" Bluetooth : Enabled")
else: print(f" Bluetooth : Disabled")
if ec_wifi == 0x01: print(f" WiFi : Enabled (Station)")
if ec_wifi == 0x02: print(f" WiFi : Enabled (AP)")
else: print(f" WiFi : Disabled")
if ec_wifi == 0x01 or ec_wifi == 0x02:
if not ec_wchn: print(f" Channel : Unknown")
else: print(f" Channel : {ec_wchn}")
if not ec_ssid: print(f" SSID : Not set")
else: print(f" SSID : {ec_ssid}")
if not ec_psk: print(f" PSK : Not set")
else: print(f" PSK : {ec_psk}")
if not ec_ip: print(f" IP Address : DHCP")
else: print(f" IP Address : {ec_ip}")
if ec_ip and ec_nm: print(f" Network Mask : {ec_nm}")
if ec_dia == 0x00: print(f" Interference avoidance : Enabled")
else: print(f" Interference avoidance : Disabled")
print( f" Display brightness : {ec_dint}")
if ec_dadr == 0xFF:
print(f" Display address : Default")
else:
print(f" Display address : {RNS.hexrep(ec_dadr, delimit=False)}")
if ec_bset == 0x73 and ec_dblk != 0x00:
print(f" Display blanking : {ec_dblk}s")
else:
print(f" Display blanking : Disabled")
if ec_dadr == 0xFF: print(f" Display address : Default")
else: print(f" Display address : {RNS.hexrep(ec_dadr, delimit=False)}")
if ec_bset == 0x73 and ec_dblk != 0x00: print(f" Display blanking : {ec_dblk}s")
else: print(f" Display blanking : Disabled")
if ec_drot != 0xFF:
if ec_drot == 0x00:
rstr = "Landscape"
if ec_drot == 0x01:
rstr = "Portrait"
if ec_drot == 0x02:
rstr = "Landscape 180"
if ec_drot == 0x03:
rstr = "Portrait 180"
if ec_drot == 0x00: rstr = "Landscape"
if ec_drot == 0x01: rstr = "Portrait"
if ec_drot == 0x02: rstr = "Landscape 180"
if ec_drot == 0x03: rstr = "Portrait 180"
print(f" Display rotation : {rstr}")
else:
print(f" Display rotation : Default")
if ec_pset == 0x73:
print(f" Neopixel Intensity : {ec_pint}")
if ec_pset == 0x73: print(f" Neopixel Intensity : {ec_pint}")
print("")
rnode.leave()
graceful_exit()
if args.eeprom_dump:
@@ -3654,6 +3901,77 @@ def main():
input()
rnode.leave()
if args.channel:
try:
RNS.log(f"Setting WiFi channel to {args.channel}")
rnode.set_wifi_channel(args.channel)
except Exception as e:
print(f"Could not set WiFi channel: {e}")
graceful_exit()
if args.ssid:
try:
if args.ssid.lower() == "none":
ssid_str = None
RNS.log(f"Deleting WiFi SSID")
else:
ssid_str = str(args.ssid)
RNS.log(f"Setting WiFi SSID to: {ssid_str}")
rnode.set_wifi_ssid(ssid_str)
except Exception as e:
print(f"Could not set WiFi SSID: {e}")
graceful_exit()
if args.psk:
try:
if args.psk.lower() == "none":
psk_str = None
RNS.log(f"Deleting WiFi PSK")
else:
psk_str = str(args.psk)
RNS.log(f"Setting WiFi PSK")
rnode.set_wifi_psk(psk_str)
except Exception as e:
print(f"Could not set WiFi PSK: {e}")
graceful_exit()
if args.ip:
try:
if args.ip.lower() == "none":
RNS.log(f"Setting WiFi IP to DHCP...")
rnode.set_wifi_ip(None)
else:
RNS.log(f"Setting WiFi static IP to: {args.ip}")
rnode.set_wifi_ip(args.ip)
except Exception as e:
print(f"Could not set WiFi IP: {e}")
graceful_exit()
if args.nm:
try:
if args.nm.lower() == "none":
RNS.log(f"Deleting WiFi static netmask configuration...")
rnode.set_wifi_nm(None)
else:
RNS.log(f"Setting WiFi static netmask to: {args.nm}")
rnode.set_wifi_nm(args.nm)
except Exception as e:
print(f"Could not set WiFi netmask: {e}")
graceful_exit()
if args.wifi:
try:
mode = 0x00
if str(args.wifi).lower().startswith("sta"): mode = 0x01
elif str(args.wifi).lower().startswith("ap"): mode = 0x02
if mode == 0x00: RNS.log(f"Disabling WiFi...")
elif mode == 0x01: RNS.log(f"Setting WiFi to station mode")
elif mode == 0x02: RNS.log(f"Setting WiFi to AP mode")
rnode.set_wifi_mode(mode)
except Exception as e:
print(f"Could not set WiFi mode: {e}")
graceful_exit()
if args.info:
if rnode.provisioned:
timestamp = struct.unpack(">I", rnode.made)[0]
@@ -4028,8 +4346,8 @@ def main():
print("cases, and copies of the source code for both the RNode Firmware,")
print("Reticulum and other utilities.")
print("")
print("To activate the RNode Bootstrap Console, power up your RNode and press")
print("the reset button twice with a one second interval. The RNode will now")
print("To activate the RNode Bootstrap Console, power up your RNode and hold")
print("down the user button for 10+ seconds, then release. The RNode will now")
print("reboot into console mode, and activate a WiFi access point for you to")
print("connect to. The console is then reachable at: http://10.0.0.1")
print("")
+12 -4
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+12 -4
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+31 -11
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -109,12 +117,24 @@ share_instance = Yes
# If you want to run multiple *different* shared instances
# on the same system, you will need to specify different
# shared instance ports for each. The defaults are given
# below, and again, these options can be left out if you
# don't need them.
# instance names for each. On platforms supporting domain
# sockets, this can be done with the instance_name option:
shared_instance_port = 37428
instance_control_port = 37429
instance_name = default
# Some platforms don't support domain sockets, and if that
# is the case, you can isolate different instances by
# specifying a unique set of ports for each:
# shared_instance_port = 37428
# instance_control_port = 37429
# If you want to explicitly use TCP for shared instance
# communication, instead of domain sockets, this is also
# possible, by using the following option:
# shared_instance_type = tcp
# On systems where running instances may not have access
@@ -146,7 +166,7 @@ instance_control_port = 37429
# an optional directive, and can be left out for brevity.
# This behaviour is disabled by default.
panic_on_interface_error = No
# panic_on_interface_error = No
# When Transport is enabled, it is possible to allow the
@@ -157,7 +177,7 @@ panic_on_interface_error = No
# Transport Instance, and printed to the log at startup.
# Optional, and disabled by default.
respond_to_probes = No
# respond_to_probes = No
[logging]
+61 -23
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -160,6 +168,9 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
stats = None
if remote:
try:
if management_identity is None:
raise ValueError("Remote management requires an identity file. Use -i to specify the path to a management identity.")
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(remote) != dest_len:
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
@@ -252,6 +263,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
name.startswith("TCPInterface[Client") or
name.startswith("BackboneInterface[Client on") or
name.startswith("AutoInterfacePeer[") or
name.startswith("WeaveInterfacePeer[") or
name.startswith("I2PInterfacePeer[Connected peer") or
(name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False))
):
@@ -260,23 +272,15 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
if name_filter == None or name_filter.lower() in name.lower():
print("")
if ifstat["status"]:
ss = "Up"
else:
ss = "Down"
if ifstat["status"]: ss = "Up"
else: ss = "Down"
if ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ACCESS_POINT:
modestr = "Access Point"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_POINT_TO_POINT:
modestr = "Point-to-Point"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ROAMING:
modestr = "Roaming"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_BOUNDARY:
modestr = "Boundary"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_GATEWAY:
modestr = "Gateway"
else:
modestr = "Full"
if ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ACCESS_POINT: modestr = "Access Point"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_POINT_TO_POINT: modestr = "Point-to-Point"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ROAMING: modestr = "Roaming"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_BOUNDARY: modestr = "Boundary"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_GATEWAY: modestr = "Gateway"
else: modestr = "Full"
if ifstat["clients"] != None:
@@ -323,10 +327,32 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
print(" Rate : {ss}".format(ss=speed_str(ifstat["bitrate"])))
if "noise_floor" in ifstat:
if ifstat["noise_floor"] != None:
print(" Noise Fl. : {nfl} dBm".format(nfl=str(ifstat["noise_floor"])))
if not "interference" in ifstat: nstr = ""
else:
print(" Noise Fl. : Unknown")
nf = ifstat["interference"]
lstr = ", no interference"
if "interference_last_ts" in ifstat and "interference_last_dbm" in ifstat:
lago = time.time()-ifstat["interference_last_ts"]
ldbm = ifstat["interference_last_dbm"]
lstr = f"\n Intrfrnc. : {ldbm} dBm {RNS.prettytime(lago, compact=True)} ago"
nstr = f"\n Intrfrnc. : {nf} dBm" if nf else lstr
if ifstat["noise_floor"] != None: print(" Noise Fl. : {nfl} dBm{ntr}".format(nfl=str(ifstat["noise_floor"]), ntr=nstr))
else: print(" Noise Fl. : Unknown")
if "cpu_load" in ifstat:
if ifstat["cpu_load"] != None: print(" CPU load : {v} %".format(v=str(ifstat["cpu_load"])))
else: print(" CPU load : Unknown")
if "cpu_temp" in ifstat:
if ifstat["cpu_temp"] != None: print(" CPU temp : {v}°C".format(v=str(ifstat["cpu_temp"])))
else: print(" CPU load : Unknown")
if "mem_load" in ifstat:
if ifstat["cpu_load"] != None: print(" Mem usage : {v} %".format(v=str(ifstat["mem_load"])))
else: print(" Mem usage : Unknown")
if "battery_percent" in ifstat and ifstat["battery_percent"] != None:
try:
@@ -342,6 +368,18 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
if "channel_load_short" in ifstat and "channel_load_long" in ifstat:
print(" Ch. Load : {ats}% (15s), {atl}% (1h)".format(ats=str(ifstat["channel_load_short"]),atl=str(ifstat["channel_load_long"])))
if "switch_id" in ifstat:
if ifstat["switch_id"] != None: print(" Switch ID : {v}".format(v=str(ifstat["switch_id"])))
else: print(" Switch ID : Unknown")
if "endpoint_id" in ifstat:
if ifstat["endpoint_id"] != None: print(" Endpoint : {v}".format(v=str(ifstat["endpoint_id"])))
else: print(" Endpoint : Unknown")
if "via_switch_id" in ifstat:
if ifstat["via_switch_id"] != None: print(" Via : {v}".format(v=str(ifstat["via_switch_id"])))
else: print(" Via : Unknown")
if "peers" in ifstat and ifstat["peers"] != None:
print(" Peers : {np} reachable".format(np=ifstat["peers"]))
+31 -5
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
@@ -83,7 +91,25 @@ def listen(configdir, identitypath = None, verbosity = 0, quietness = 0, allowed
except Exception as e:
print(str(e))
exit(1)
try:
allowed_file_name = "allowed_identities"
allowed_file = None
if os.path.isfile(os.path.expanduser("/etc/rnx/"+allowed_file_name)):
allowed_file = os.path.expanduser("/etc/rnx/"+allowed_file_name)
elif os.path.isfile(os.path.expanduser("~/.config/rnx/"+allowed_file_name)):
allowed_file = os.path.expanduser("~/.config/rnx/"+allowed_file_name)
elif os.path.isfile(os.path.expanduser("~/.rnx/"+allowed_file_name)):
allowed_file = os.path.expanduser("~/.rnx/"+allowed_file_name)
if allowed_file != None:
with open(allowed_file, "r") as af_handle:
allowed_by_file = af_handle.read().replace("\r", "").split("\n")
for allowed_ID in allowed_by_file:
if len(allowed_ID) == (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2:
allowed_identity_hashes.append(bytes.fromhex(allowed_ID))
except Exception as e:
print(str(e))
exit(1)
if len(allowed_identity_hashes) < 1 and not disable_auth:
print("Warning: No allowed identities configured, rncx will not accept any commands!")
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# 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 shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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,
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.9.4"
__version__ = "1.0.4"
+30
View File
@@ -1,3 +1,33 @@
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# 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 Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - 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.
def get_platform():
from os import environ
if "ANDROID_ARGUMENT" in environ: return "android"
+3 -18
View File
@@ -14,18 +14,6 @@ This document outlines the currently established development roadmap for Reticul
## Currently Active Work Areas
For each release cycle of Reticulum, improvements and additions from the five [Primary Efforts](#primary-efforts) are selected as active work areas, and can be expected to be included in the upcoming releases within that cycle. While not entirely set in stone for each release cycle, they serve as a pointer of what to expect in the near future.
- The current `0.8.x` release cycle aims at completing
- [ ] Hot-pluggable interface system
- [ ] External interface plugins
- [ ] Network-wide path balancing and multi-pathing
- [ ] Expanded hardware support
- [ ] Overhauling and updating the documentation
- [ ] Distributed Destination Naming System
- [ ] A standalone RNS Daemon app for Android
- [ ] Addding automatic retries to all use cases of the `Request` API
- [ ] Performance and memory optimisations of the Python reference implementation
- [ ] Fixing bugs discovered while operating Reticulum systems and applications
## Primary Efforts
The development path for Reticulum is currently laid out in five distinct areas: *Comprehensibility*, *Universality*, *Functionality*, *Usability & Utility* and *Interfaceability*. Conceptualising the development of Reticulum into these areas serves to advance the implementation and work towards the Foundational Goals & Values of Reticulum.
@@ -50,17 +38,14 @@ These efforts are aimed at improving the ease of which Reticulum is understood,
### Universality
These efforts seek to broaden the universality of the Reticulum software and hardware ecosystem by continously diversifying platform support, and by improving the overall availability and ease of deployment of the Reticulum stack.
- OpenWRT support
- Create a standalone RNS Daemon app for Android
- A lightweight and portable C implementation for microcontrollers, µRNS
- A portable, high-performance Reticulum implementation in C/C++, see [#21](https://github.com/markqvist/Reticulum/discussions/21)
- Performance and memory optimisations of the Python implementation
- Bindings for other programming languages
### Functionality
These efforts aim to expand and improve the core functionality and reliability of Reticulum.
- Add support for user-supplied external interface drivers
- Add interface hot-plug and live up/down control to running instances
- Add automatic retries to all use cases of the `Request` API
- Network-wide path balancing
@@ -70,11 +55,11 @@ These efforts aim to expand and improve the core functionality and reliability o
- [Metric-based path selection and multiple paths](https://github.com/markqvist/Reticulum/discussions/86)
### Usability & Utility
These effors seek to make Reticulum easier to use and operate, and to expand the utility of the stack on deployed systems.
These efforts seek to make Reticulum easier to use and operate, and to expand the utility of the stack on deployed systems.
- Easy way to share interface configurations, see [#19](https://github.com/markqvist/Reticulum/discussions/19)
- Transit traffic display in rnstatus
- rnsconfig utility
- Transit traffic display in `rnstatus`
- `rnsconfig` utility
### Interfaceability
These efforts aim to expand the types of physical and virtual interfaces that Reticulum can natively use to transport data.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
config: b499af51edc22529181ef3b25973fa2b
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
config: b259ec0dae0fa8339c3fe8ae4c431f72
tags: 645f666f9bcd5a90fca523b33c5a78b7
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

+104 -40
View File
@@ -12,7 +12,7 @@ Standalone Reticulum Installation
If you simply want to install Reticulum and related utilities on a system,
the easiest way is via the ``pip`` package manager:
.. code::
.. code:: shell
pip install rns
@@ -23,9 +23,18 @@ of your system with a command like ``sudo apt install python3-pip``,
You can also dowload the Reticulum release wheels from GitHub, or other release channels,
and install them offline using ``pip``:
.. code::
.. code:: shell
pip install ./rns-0.5.1-py3-none-any.whl
pip install ./rns-1.0.2-py3-none-any.whl
On platforms that limit user package installation via ``pip``, you may need to manually
allow this using the ``--break-system-packages`` command line flag when installing. This
will not actually break any packages, unless you have installed Reticulum directly via
your operating system's package manager.
.. code:: shell
pip install rns --break-system-packages
For more detailed installation instructions, please see the
:ref:`Platform-Specific Install Notes<install-guides>` section.
@@ -39,7 +48,7 @@ On some platforms, there may not be binary packages available for all dependenci
``pip`` installation may fail with an error message. In these cases, the issue can usually
be resolved by installing the development essentials packages for your platform:
.. code::
.. code:: shell
# Debian / Ubuntu / Derivatives
sudo apt install build-essential
@@ -140,8 +149,8 @@ MeshChat
^^^^^^^^
The `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ application
is a user-friendly LXMF client for macOS and Windows, that also includes voice call
functionality, and a range of other interesting functions.
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
page browser and other interesting functionality.
.. only:: html
@@ -209,7 +218,7 @@ and :ref:`Interfaces<interfaces-main>` chapters of this manual.
Connecting Reticulum Instances Over the Internet
================================================
Reticulum currently offers two interfaces suitable for connecting instances over the Internet: :ref:`TCP<interfaces-tcps>`
Reticulum currently offers three interfaces suitable for connecting instances over the Internet: :ref:`Backbone<interfaces-backbone>`, :ref:`TCP<interfaces-tcps>`
and :ref:`I2P<interfaces-i2p>`. Each interface offers a different set of features, and Reticulum
users should carefully choose the interface which best suites their needs.
@@ -217,6 +226,10 @@ The ``TCPServerInterface`` allows users to host an instance accessible over TCP/
method is generally faster, lower latency, and more energy efficient than using ``I2PInterface``,
however it also leaks more data about the server host.
The ``BackboneInterface`` is a very fast and efficient interface type available on POSIX operating
systems, designed to handle many hundreds of connections simultaneously with low memory, processing
and I/O overhead. It is fully compatible with the TCP-based interface types.
TCP connections reveal the IP address of both your instance and the server to anyone who can
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
inspecting your packets may be able to record packet metadata like time of transmission and packet size.
@@ -245,17 +258,16 @@ easier setup, use TCP.
Connect to the Public Testnet
===========================================
An experimental public testnet has been made accessible over both I2P and TCP. You can join it
by adding one of the following interfaces to your ``.reticulum/config`` file:
An experimental public testnet has been made accessible by volunteers in the community. You
can find interface definitions for adding to your ``.reticulum/config`` file on the
`Reticulum Website <https://reticulum.network/connect.html>`_ or the
`Community Wiki <https://github.com/markqvist/Reticulum/wiki/Community-Node-List>`_
.. code::
You can connect your devices or instances to one or more of these to gain access to any
Reticulum networks they are physically connected to. Simply add one or more interface
snippets to your config file in the ``[interface]`` section, like in the example below:
# TCP/IP interface to the RNS Amsterdam Hub
[[RNS Testnet Amsterdam]]
type = TCPClientInterface
enabled = yes
target_host = amsterdam.connect.reticulum.network
target_port = 4965
.. code:: ini
# TCP/IP interface to the BetweenTheBorders Hub (community-provided)
[[RNS Testnet BetweenTheBorders]]
@@ -264,11 +276,11 @@ by adding one of the following interfaces to your ``.reticulum/config`` file:
target_host = reticulum.betweentheborders.com
target_port = 4242
# Interface to Testnet I2P Hub
[[RNS Testnet I2P Hub]]
type = I2PInterface
enabled = yes
peers = g3br23bvx3lq5uddcsjii74xgmn6y5q325ovrkq2zw2wbzbqgbuq.b32.i2p
.. tip::
Ideally, set up a Reticulum Transport Node that your own devices can reach locally, and then
connect that transport node to a couple of public entrypoints. This will provide efficient
connections and redundancy in case any of them go down.
Many other Reticulum instances are connecting to this testnet, and you can also join it
via other entry points if you know them. There is absolutely no control over the network
@@ -276,13 +288,65 @@ topography, usage or what types of instances connect. It will also occasionally
to test various failure scenarios, and there are no availability or service guarantees.
Expect weird things to happen on this network, as people experiment and try out things.
It probably goes without saying, but *don't use the testnet entry-points as
hardcoded or default interfaces in any applications you ship to users*. When
shipping applications, the best practice is to provide your own default
connectivity solutions, if needed and applicable, or in most cases, simply
leave it up to the user which networks to connect to, and how.
.. warning::
It probably goes without saying, but *don't use the testnet entry-points as
hardcoded or default interfaces in any applications you ship to users*. When
shipping applications, the best practice is to provide your own default
connectivity solutions, if needed and applicable, or in most cases, simply
leave it up to the user which networks to connect to, and how.
Hosting Public Entrypoints
===========================================
If you want to host a public (or private) entry-point to a Reticulum network over the
Internet, this section offers some helpful pointers. You will need a machine, physical or
virtual with a public IP address, that can be reached by other devices on the Internet.
The most efficient and performant way to host a connectable entry-point supporting many
users is to use the ``BackboneInterface``. This interface type is fully compatible with
the ``TCPClientInterface`` and ``TCPServerInterface`` types, but much faster and uses
less system resources, allowing your device to handle thousands of connections even on
small systems.
It is also important to set your connectable interface to ``gateway`` mode, since this
will greatly improve network convergence time and path resolution for anyone connecting
to your entry-point.
.. code:: ini
# This example demonstrates a backbone interface
# configured for acting as a gateway for users to
# connect to either a public or private network
[[Public Gateway]]
type = BackboneInterface
enabled = yes
mode = gateway
listen_on = 0.0.0.0
port = 4242
If instead you want to make a private entry-point from the Internet, you can use the
:ref:`IFAC name and passphrase options<interfaces-options>` to secure your interface with a network name and passphrase.
.. code:: ini
# A private entry-point requiring a pre-shared
# network name and passphrase to connect to.
[[Private Gateway]]
type = BackboneInterface
enabled = yes
mode = gateway
listen_on = 0.0.0.0
port = 4242
network_name = private_ret
passphrase = 2owjajquafIanPecAc
If you are hosting an entry-point on an operating system that does not support
``BackboneInterface``, you can use ``TCPServerInterface`` instead, although it will
not be as performant.
Adding Radio Interfaces
==============================================
Once you have Reticulum installed and working, you can add radio interfaces with
@@ -349,7 +413,7 @@ If you want to participate in the development of Reticulum and associated
utilities, you'll want to get the latest source from GitHub. In that case,
don't use pip, but try this recipe:
.. code::
.. code:: shell
# Install dependencies
pip install cryptography pyserial
@@ -415,7 +479,7 @@ build into Termux. After that, you can use ``pip`` to install Reticulum.
From within Termux, execute the following:
.. code::
.. code:: shell
# First, make sure indexes and packages are up to date.
pkg update
@@ -434,7 +498,7 @@ If for some reason the ``python-cryptography`` package is not available for
your platform via the Termux package manager, you can attempt to build it
locally on your device using the following command:
.. code::
.. code:: shell
# First, make sure indexes and packages are up to date.
pkg update
@@ -470,7 +534,7 @@ On some architectures, including ARM64, not all dependencies have precompiled
binaries. On such systems, you may need to install ``python3-dev`` (or similar) before
installing Reticulum or programs that depend on Reticulum.
.. code::
.. code:: shell
# Install Python and development packages
sudo apt update
@@ -491,7 +555,7 @@ use the replacement ``pipx`` command instead, which places installed packages in
isolated environment. This should not negatively affect Reticulum, but will not work
for including and using Reticulum in your own scripts and programs.
.. code::
.. code:: shell
# Install pipx
sudo apt install pipx
@@ -506,7 +570,7 @@ Alternatively, you can restore normal behaviour to ``pip`` by creating or editin
the configuration file located at ``~/.config/pip/pip.conf``, and adding the
following section:
.. code:: text
.. code:: ini
[global]
break-system-packages = true
@@ -514,7 +578,7 @@ following section:
For a one-shot installation of Reticulum, without globally enabling the ``break-system-packages``
option, you can use the following command:
.. code:: text
.. code:: shell
pip install rns --break-system-packages
@@ -539,7 +603,7 @@ Python manually.
When Python and ``pip`` is available on your system, simply open a terminal window
and use one of the following commands:
.. code::
.. code:: shell
# Install Reticulum and utilities with pip:
pip3 install rns
@@ -560,7 +624,7 @@ manually add your installed ``pip`` packages directory to your `PATH` environmen
variable, before you can use installed commands in your terminal. Usually, adding
the following line to your shell init script (for example ``~/.zshrc``) will be enough:
.. code::
.. code:: shell
export PATH=$PATH:~/Library/Python/3.9/bin
@@ -583,7 +647,7 @@ Reticulum and related utilities using the `opkg` package manager and `pip`.
To install Reticulum on OpenWRT, first log into a command line session, and
then use the following instructions:
.. code::
.. code:: shell
# Install dependencies
opkg install python3 python3-pip python3-cryptography python3-pyserial
@@ -620,7 +684,7 @@ don't always have packages available for some dependencies. If Python and the
`pip` package manager is not already installed, do that first, and then
install Reticulum using `pip`.
.. code::
.. code:: shell
# Install dependencies
sudo apt install python3 python3-pip python3-cryptography python3-pyserial
@@ -646,7 +710,7 @@ On some architectures, including RISC-V, not all dependencies have precompiled
binaries. On such systems, you may need to install ``python3-dev`` (or similar) before
installing Reticulum or programs that depend on Reticulum.
.. code::
.. code:: shell
# Install Python and development packages
sudo apt update
@@ -667,7 +731,7 @@ use the replacement ``pipx`` command instead, which places installed packages in
isolated environment. This should not negatively affect Reticulum, but will not work
for including and using Reticulum in your own scripts and programs.
.. code::
.. code:: shell
# Install pipx
sudo apt install pipx
@@ -717,7 +781,7 @@ use the ``pip`` installer, or run the included Reticulum utility programs (such
After installing Python, open the command prompt or Windows Powershell, and type:
.. code::
.. code:: shell
pip install rns
+16 -1
View File
@@ -17,6 +17,9 @@ for example the :ref:`PipeInterface<interfaces-pipe>` or the :ref:`TCPClientInte
in combination with code like `TCP KISS Server <https://github.com/simplyequipped/tcpkissserver>`_
by `simplyequipped <https://github.com/simplyequipped>`_.
It is also very easy to write and load :ref:`custom interface modules<interfaces-custom>`
into Reticulum, allowing you to communicate with more or less anything you can think of.
While this broad support and flexibility is very useful, an abundance of options
can sometimes make it difficult to know where to begin, especially when you are
starting from scratch.
@@ -149,7 +152,7 @@ OpenCom XL
""""""""""""""""""""
- **Transceiver ICs** Semtech SX1262 and SX1280 (dual transceiver)
- **Device Platform** nRF52
- **Manufacturer** `RAK Wireless <https://liberatedsystems.co.uk/>`_
- **Manufacturer** `Liberated Embedded Systems <https://liberatedsystems.co.uk/>`_
------------
@@ -232,6 +235,18 @@ LilyGO T-Echo
Heltec T114
"""""""""""
- **Transceiver IC** Semtech SX1262 or SX1268
- **Device Platform** nRF52
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
------------
.. image:: graphics/board_heltec32v4.png
:width: 58%
:align: center
Heltec LoRa32 v4.0
""""""""""""""""""
- **Transceiver IC** Semtech SX1262
- **Device Platform** ESP32
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
+1 -1
View File
@@ -25,8 +25,8 @@ to participate in the development of Reticulum itself.
hardware
interfaces
networks
examples
support
examples
.. toctree::
:maxdepth: 2
+38 -24
View File
@@ -39,6 +39,11 @@ nodes over any kind of local Ethernet or WiFi-based medium. Even though it uses
discovery, and UDP for packet transport, it **does not** need any functional IP
infrastructure like routers or DHCP servers, on your physical network.
.. warning::
If you have **firewall** software running on your computer, it may block traffic
required for ``AutoInterface`` to work. If this is the case, you will have to
allow UDP traffic on port ``29716`` and ``42671``.
As long as there is at least some sort of switching medium present between peers (a
wired switch, a hub, a WiFi access point or similar, or simply two devices connected
directly by Ethernet cable), it will work without any configuration, setup or intermediary devices.
@@ -55,7 +60,7 @@ current operating systems, both desktop and mobile.
most commonly seen on very cheap, ISP-supplied WiFi routers, and can sometimes
be turned off in the router configuration.
.. code::
.. code:: ini
# This example demonstrates a bare-minimum setup
# of an Auto Interface. It will allow communica-
@@ -99,7 +104,7 @@ autodiscover other Reticulum nodes within your selected Group ID. You can specif
the discovery scope by setting it to one of ``link``, ``admin``, ``site``,
``organisation`` or ``global``.
.. code::
.. code:: ini
[[Default Interface]]
type = AutoInterface
@@ -145,7 +150,7 @@ Listeners
The following examples illustrates various ways to set up ``BackboneInterface`` listeners.
.. code::
.. code:: ini
# This example demonstrates a backbone interface
# that listens for incoming connections on the
@@ -173,7 +178,7 @@ The following examples illustrates various ways to set up ``BackboneInterface``
If you are using the interface on a device which has both IPv4 and IPv6 addresses available,
you can use the ``prefer_ipv6`` option to bind to the IPv6 address:
.. code::
.. code:: ini
# This example demonstrates a backbone interface
# listening on the IPv6 address of a specified
@@ -188,7 +193,7 @@ you can use the ``prefer_ipv6`` option to bind to the IPv6 address:
To use the ``BackboneInterface`` over `Yggdrasil <https://yggdrasil-network.github.io/>`_, you
can simply specify the Yggdrasil ``tun`` device and a listening port, like so:
.. code::
.. code:: ini
# This example demonstrates a backbone interface
# listening for connections over Yggdrasil.
@@ -204,7 +209,7 @@ The following examples illustrates various ways to connect to remote ``BackboneI
As noted above, ``BackboneInterface`` interfaces can also connect to remote ``TCPServerInterface``,
and as such these interface types can be used interchangably.
.. code::
.. code:: ini
# Here's an example of a backbone interface that
# connects to a remote listener.
@@ -217,7 +222,7 @@ and as such these interface types can be used interchangably.
To connect to remotes over `Yggdrasil <https://yggdrasil-network.github.io/>`_, simply
specify the target Yggdrasil IPv6 address and port, like so:
.. code::
.. code:: ini
[[Yggdrasil Remote]]
type = BackboneInterface
@@ -234,7 +239,7 @@ The TCP Server interface is suitable for allowing other peers to connect over
the Internet or private IPv4 and IPv6 networks. When a TCP server interface has been
configured, other Reticulum peers can connect to it with a TCP Client interface.
.. code::
.. code:: ini
# This example demonstrates a TCP server interface.
# It will listen for incoming connections on all IP
@@ -262,7 +267,7 @@ configured, other Reticulum peers can connect to it with a TCP Client interface.
If you are using the interface on a device which has both IPv4 and IPv6 addresses available,
you can use the ``prefer_ipv6`` option to bind to the IPv6 address:
.. code::
.. code:: ini
# This example demonstrates a TCP server interface.
# It will listen for incoming connections on the
@@ -278,7 +283,7 @@ you can use the ``prefer_ipv6`` option to bind to the IPv6 address:
To use the TCP Server Interface over `Yggdrasil <https://yggdrasil-network.github.io/>`_, you
can simply specify the Yggdrasil ``tun`` device and a listening port, like so:
.. code::
.. code:: ini
[[Yggdrasil TCP Server Interface]]
type = TCPServerInterface
@@ -315,7 +320,7 @@ The TCP interface types can also tolerate intermittency in the IP link layer.
This means that Reticulum will gracefully handle IP links that go up and down,
and restore connectivity after a failure, once the other end of a TCP interface reappears.
.. code::
.. code:: ini
# Here's an example of a TCP Client interface. The
# target_host can be a hostname or an IPv4 or IPv6 address.
@@ -328,7 +333,7 @@ and restore connectivity after a failure, once the other end of a TCP interface
To use the TCP Client Interface over `Yggdrasil <https://yggdrasil-network.github.io/>`_, simply
specify the target Yggdrasil IPv6 address and port, like so:
.. code::
.. code:: ini
[[Yggdrasil TCP Client Interface]]
type = TCPClientInterface
@@ -340,7 +345,7 @@ It is also possible to use this interface type to connect via other programs
or hardware devices that expose a KISS interface on a TCP port, for example
software-based soundmodems. To do this, use the ``kiss_framing`` option:
.. code::
.. code:: ini
# Here's an example of a TCP Client interface that connects
# to a software TNC soundmodem on a KISS over TCP port.
@@ -363,7 +368,7 @@ intermittent TCP links.
The TCP interfaces support tunneling over I2P, but to do so reliably,
you must use the i2p_tunneled option:
.. code::
.. code:: ini
[[TCP Client over I2P]]
type = TCPClientInterface
@@ -387,10 +392,10 @@ with all other peers on a local area network.
Using broadcast UDP traffic has performance implications,
especially on WiFi. If your goal is simply to enable easy communication
with all peers in your local Ethernet broadcast domain, the
:ref:`Auto Interface<interfaces-auto>` performs better, and is even
:ref:`Auto Interface<interfaces-auto>` performs *much* better, and is even
easier to use.
.. code::
.. code:: ini
# This example enables communication with other
# local Reticulum peers over UDP.
@@ -460,7 +465,7 @@ of the ``i2pd`` package. For more details about I2P, see the
When an I2P router is running on your system, you can simply add
an I2P interface to Reticulum:
.. code::
.. code:: ini
[[I2P]]
type = I2PInterface
@@ -477,7 +482,7 @@ also inspect the status of the interface using the ``rnstatus`` utility.
To connect to other Reticulum instances over I2P, just add a comma-separated
list of I2P base32 addresses to the ``peers`` option of the interface:
.. code::
.. code:: ini
[[I2P]]
type = I2PInterface
@@ -516,7 +521,7 @@ can be used, and offers full control over LoRa parameters.
varies widely around the world. It is your responsibility to be aware of any
relevant regulation for your location, and to make decisions accordingly.
.. code::
.. code:: ini
# Here's an example of how to add a LoRa interface
# using the RNode LoRa transceiver.
@@ -530,6 +535,15 @@ can be used, and offers full control over LoRa parameters.
# Serial port for the device
port = /dev/ttyUSB0
# You can connect wirelessly to the
# RNode device if it supports WiFi.
# Connect by IP address
# port = tcp://10.0.0.1
# Or, connect by hostname
# port = tcp://rnodef3b9.local
# It is also possible to use BLE devices
# instead of wired serial ports. The
# target RNode must be paired with the
@@ -608,7 +622,7 @@ Multi interface can be used to configure sub-interfaces individually.
varies widely around the world. It is your responsibility to be aware of any
relevant regulation for your location, and to make decisions accordingly.
.. code::
.. code:: ini
# Here's an example of how to add an RNode Multi interface
# using the RNode LoRa transceiver.
@@ -724,7 +738,7 @@ Reticulum can be used over serial ports directly, or over any device with a
serial port, that will transparently pass data. Useful for communicating
directly over a wire-pair, or for using devices such as data radios and lasers.
.. code::
.. code:: ini
[[Serial Interface]]
type = SerialInterface
@@ -749,7 +763,7 @@ Using this interface, Reticulum can use any program as an interface via `stdin`
`stdout`. This can be used to easily create virtual interfaces, or to interface with
custom hardware or other systems.
.. code::
.. code:: ini
[[Pipe Interface]]
type = PipeInterface
@@ -780,7 +794,7 @@ for station identification purposes.
varies widely around the world. It is your responsibility to be aware of any
relevant regulation for your location, and to make decisions accordingly.
.. code::
.. code:: ini
[[Packet Radio KISS Interface]]
type = KISSInterface
@@ -848,7 +862,7 @@ beaconing functionality described above.
varies widely around the world. It is your responsibility to be aware of any
relevant regulation for your location, and to make decisions accordingly.
.. code::
.. code:: ini
[[Packet Radio AX.25 KISS Interface]]
type = AX25KISSInterface
+3
View File
@@ -22,6 +22,9 @@ Donations are gratefully accepted via the following channels:
Bitcoin:
3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq
Liberapay:
https://liberapay.com/Reticulum/
Ko-Fi:
https://ko-fi.com/markqvist
+60 -34
View File
@@ -117,7 +117,7 @@ Reticulum uses the singular concept of *destinations*. Any application using Ret
networking stack will need to create one or more destinations to receive data, and know the
destinations it needs to send data to.
All destinations in Reticulum are _represented_ as a 16 byte hash. This hash is derived from truncating a full
All destinations in Reticulum are *represented* as a 16 byte hash. This hash is derived from truncating a full
SHA-256 hash of identifying characteristics of the destination. To users, the destination addresses
will be displayed as 16 hexadecimal bytes, like this example: ``<13425ec15b621c1d928589718000d814>``.
@@ -141,7 +141,7 @@ ratchets on a per-destination basis. The multi-hop transport, coordination, veri
layers are fully autonomous and also based on elliptic curve cryptography.
Reticulum also offers symmetric key encryption for group-oriented communications, as well as
unencrypted packets for local broadcast purposes.
unencrypted packets (for local broadcast purposes **only**).
Reticulum can connect to a variety of interfaces such as radio modems, data radios and serial ports,
and offers the possibility to easily tunnel Reticulum traffic over IP links such as the Internet or
@@ -401,11 +401,10 @@ any transport node receiving it, but according to some specific rules:
to be transmitted, the newest announce is discarded. If the newest announce contains different
application specific data, it will replace the old announce.
Once an announce has reached a node in the network, any other node in direct contact with that
node will be able to reach the destination the announce originated from, simply by sending a packet
addressed to that destination. Any node with knowledge of the announce will be able to direct the
packet towards the destination by looking up the next node with the shortest amount of hops to the
destination.
Once an announce has reached a transport node in the network, any other node in direct contact with that
transport node will be able to reach the destination the announce originated from, simply by sending a packet
addressed to that destination. Any transport node with knowledge of the announce will be able to direct the
packet towards the destination by looking up the most efficient next node to the destination.
According to these rules, an announce will propagate throughout the network in a predictable way,
and make the announced destination reachable in a short amount of time. Fast networks that have the
@@ -414,6 +413,17 @@ new destinations. Slower segments of such networks might take a bit longer to ga
the wide and fast networks they are connected to, but can still do so over time, while prioritising full
and quickly converging end-to-end connectivity for their local, slower segments.
.. tip::
Even very slow networks, that simply don't have the capacity to ever reach *full* convergence
will generally still be able to reach **any other destination on any connected segments**, since
interconnecting transport nodes will prioritize announces into the slower segments that are
actually requested by nodes on these.
This means that slow, low-capacity or low-resource segments **don't** need to have full network
knowledge, since paths can always be recursively resolved from other segments that do have
knowledge about them.
In general, even extremely complex networks, that utilize the maximum 128 hops will converge to full
end-to-end connectivity in about one minute, given there is enough bandwidth available to process
the required amount of announces.
@@ -424,7 +434,7 @@ Reaching the Destination
------------------------
In networks with changing topology and trustless connectivity, nodes need a way to establish
*verified connectivity* with each other. Since the network is assumed to be trustless, Reticulum
*verified connectivity* with each other. Since the underlying network mediums are assumed to be trustless, Reticulum
must provide a way to guarantee that the peer you are communicating with is actually who you
expect. Reticulum offers two ways to do this.
@@ -435,7 +445,7 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
an ECDH key exchange with the destination's public key (or ratchet key, if available), and encrypt the information.
* | It is important to note that this key exchange does not require any network traffic. The sender already
knows the public key of the destination from an earlier received *announce*, and can thus perform the ECDH
knows the public key of the destination from an earlier received announce, and can thus perform the ECDH
key exchange locally, before sending the packet.
* | The public part of the newly generated ephemeral key-pair is included with the encrypted token, and sent
@@ -453,7 +463,7 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
public signing key.
* | In case the packet is addressed to a *group* destination type, the packet will be encrypted with the
pre-shared AES-128 key associated with the destination. In case the packet is addressed to a *plain*
pre-shared AES-256 key associated with the destination. In case the packet is addressed to a *plain*
destination type, the payload data will not be encrypted. Neither of these two destination types can offer
forward secrecy. In general, it is recommended to always use the *single* destination type, unless it is
strictly necessary to use one of the others.
@@ -461,14 +471,14 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
For exchanges of larger amounts of data, or when longer sessions of bidirectional communication is desired, Reticulum offers the *Link* API. To establish a *link*, the following process is employed:
* | First, the node that wishes to establish a link will send out a special packet, that
* | First, the node that wishes to establish a link will send out a *link request* packet, that
traverses the network and locates the desired destination. Along the way, the Transport Nodes that
forward the packet will take note of this *link request*.
forward the packet will take note of this *link request*, and mark it as pending.
* | Second, if the destination accepts the *link request* , it will send back a packet that proves the
authenticity of its identity (and the receipt of the link request) to the initiating node. All
nodes that initially forwarded the packet will also be able to verify this proof, and thus
accept the validity of the *link* throughout the network.
accept the validity of the *link* throughout the network. The link is now marked as *established*.
* | When the validity of the *link* has been accepted by forwarding nodes, these nodes will
remember the *link* , and it can subsequently be used by referring to a hash representing it.
@@ -560,9 +570,10 @@ an arbitrary number of hops, where information will be exchanged between two nod
*link proof* to perform it's own Diffie Hellman Key Exchange and derive the symmetric key
that is used to encrypt the channel. Information can now be exchanged reliably and securely.
.. note::
Its important to note that this methodology ensures that the source of the request does not need to
reveal any identifying information about itself. The link initiator remains completely anonymous.
Its important to note that this methodology ensures that the source of the request does not need to
reveal any identifying information about itself. **The link initiator remains completely anonymous**.
When using *links*, Reticulum will automatically verify all data sent over the link, and can also
automate retransmissions if *Resources* are used.
@@ -624,18 +635,20 @@ into the future. The current Reference System Setup is as follows:
* **Interface Device**
A data radio consisting of a LoRa radio module, and a microcontroller with open source
firmware, that can connect to host devices via USB. It operates in either the 430, 868 or 900
MHz frequency bands. More details can be found on the `RNode Page <https://unsigned.io/rnode>`_.
MHz frequency bands. More details can be found on the `RNode Page <https://github.com/markqvist/rnode_firmware>`_.
* **Host Device**
Any computer device running Linux and Python. A Raspberry Pi with a Debian based OS is
recommended.
a good place to start, but anything can be used.
* **Software Stack**
The most recently released Python Implementation of Reticulum, running on a Debian based
The most recently released Python Implementation of Reticulum, running on a Linux-based
operating system.
To avoid confusion, it is very important to note, that the reference interface device **does not**
use the LoRaWAN standard, but uses a custom MAC layer on top of the plain LoRa modulation! As such, you will
need a plain LoRa radio module connected to an controller with the correct firmware. Full details on how to
get or make such a device is available on the `RNode Page <https://unsigned.io/rnode>`_.
.. note::
To avoid confusion, it is very important to note, that the reference interface device **does not**
use the LoRaWAN standard, but uses a custom MAC layer on top of the plain LoRa modulation! As such, you will
need a plain LoRa radio module connected to a controller with the correct firmware. Full details on how to
get or make such a device is available on the `RNode Page <https://github.com/markqvist/rnode_firmware>`_.
With the current reference setup, it should be possible to get on a Reticulum network for around 100$
even if you have none of the hardware already, and need to purchase everything.
@@ -649,16 +662,16 @@ Protocol Specifics
==================
This chapter will detail protocol specific information that is essential to the implementation of
Reticulum, but non critical in understanding how the protocol works on a general level. It should be
Reticulum, but non-critical in understanding how the protocol works on a general level. It should be
treated more as a reference than as essential reading.
Packet Prioritisation
---------------------
Currently, Reticulum is completely priority-agnostic regarding general traffic. All traffic is handled
on a first-come, first-serve basis. Announce re-transmission are handled according to the re-transmission
times and priorities described earlier in this chapter.
Currently, Reticulum is completely priority-agnostic regarding *general* traffic. All traffic is handled
on a first-come, first-serve basis. Announce re-transmission and other maintenance traffic is handled
according to the re-transmission times and priorities described earlier in this chapter.
Interface Access Codes
@@ -666,8 +679,8 @@ Interface Access Codes
Reticulum can create named virtual networks, and networks that are only accessible by knowing a preshared
passphrase. The configuration of this is detailed in the :ref:`Common Interface Options<interfaces-options>`
section. To implement these feature, Reticulum uses the concept of Interface Access Codes, that are calculated
and verified per packet.
section. To implement this feature, Reticulum uses the concept of Interface Access Codes, that are calculated
and verified per-packet.
An interface with a named virtual network or passphrase authentication enabled will derive a shared Ed25519
signing identity, and for every outbound packet generate a signature of the entire packet. This signature is
@@ -858,9 +871,17 @@ of the different interface modes, and how they are configured.
Cryptographic Primitives
------------------------
Reticulum has been designed to use a simple suite of efficient, strong and modern
cryptographic primitives, with widely available implementations that can be used
both on general-purpose CPUs and on microcontrollers. The necessary primitives are:
Reticulum uses a simple suite of efficient, strong and well-tested cryptographic
primitives, with widely available implementations that can be used both on
general-purpose CPUs and on microcontrollers.
One of the primary considerations for choosing this particular set of primitives is
that they can be implemented *safely* with relatively few pitfalls, on practically
all current computing platforms.
The primitives listed here **are authoritative**. Anything claiming to be Reticulum,
but not using these exact primitives **is not** Reticulum, and possibly an
intentionally compromised or weakened clone. The utilised primitives are:
* Ed25519 for signatures
@@ -872,11 +893,11 @@ both on general-purpose CPUs and on microcontrollers. The necessary primitives a
* Ephemeral keys derived from an ECDH key exchange on Curve25519
* AES-128 in CBC mode with PKCS7 padding
* AES-256 in CBC mode with PKCS7 padding
* HMAC using SHA256 for message authentication
* IVs are generated through os.urandom()
* IVs must be generated through ``os.urandom()`` or better
* No Fernet version and timestamp metadata fields
@@ -884,7 +905,7 @@ both on general-purpose CPUs and on microcontrollers. The necessary primitives a
* SHA-512
In the default installation configuration, the ``X25519``, ``Ed25519`` and ``AES-128-CBC``
In the default installation configuration, the ``X25519``, ``Ed25519`` and ``AES-256-CBC``
primitives are provided by `OpenSSL <https://www.openssl.org/>`_ (via the `PyCA/cryptography <https://github.com/pyca/cryptography>`_
package). The hashing functions ``SHA-256`` and ``SHA-512`` are provided by the standard
Python `hashlib <https://docs.python.org/3/library/hashlib.html>`_. The ``HKDF``, ``HMAC``,
@@ -904,6 +925,11 @@ with the OpenSSL backend being *much* faster. The most important consequence how
potential loss of security by using primitives that has not seen the same amount of scrutiny,
testing and review as those from OpenSSL.
Using the normal RNS installation procedures, it is not possible to install Reticulum on a
system without the required OpenSSL primitives being available, and if they are not, they will
be resolved and installed as a dependency. It is only possible to use the pure-python primitives
by manually specifying this, for example by using the ``rnspure`` package.
.. warning::
If you want to use the internal pure-python primitives, it is **highly advisable** that you
have a good understanding of the risks that this pose, and make an informed decision on whether
+83 -47
View File
@@ -53,7 +53,7 @@ The entire configuration of Reticulum is found in the ``~/.reticulum/config``
file. When Reticulum is first started on a new system, a basic, but fully functional
configuration file is created. The default configuration looks like this:
.. code::
.. code:: ini
# This is the default Reticulum config file.
# You should probably edit it to include any additional,
@@ -69,12 +69,12 @@ configuration file is created. The default configuration looks like this:
# If you enable Transport, your system will route traffic
# for other peers, pass announces and serve path requests.
# This should only be done for systems that are suited to
# act as transport nodes, ie. if they are stationary and
# This should be done for systems that are suited to act
# as transport nodes, ie. if they are stationary and
# always-on. This directive is optional and can be removed
# for brevity.
enable_transport = False
enable_transport = No
# By default, the first program to launch the Reticulum
@@ -91,12 +91,24 @@ configuration file is created. The default configuration looks like this:
# If you want to run multiple *different* shared instances
# on the same system, you will need to specify different
# shared instance ports for each. The defaults are given
# below, and again, these options can be left out if you
# don't need them.
# instance names for each. On platforms supporting domain
# sockets, this can be done with the instance_name option:
shared_instance_port = 37428
instance_control_port = 37429
instance_name = default
# Some platforms don't support domain sockets, and if that
# is the case, you can isolate different instances by
# specifying a unique set of ports for each:
# shared_instance_port = 37428
# instance_control_port = 37429
# If you want to explicitly use TCP for shared instance
# communication, instead of domain sockets, this is also
# possible, by using the following option:
# shared_instance_type = tcp
# On systems where running instances may not have access
@@ -110,13 +122,25 @@ configuration file is created. The default configuration looks like this:
# rpc_key = e5c032d3ec4e64a6aca9927ba8ab73336780f6d71790
# It is possible to allow remote management of Reticulum
# systems using the various built-in utilities, such as
# rnstatus and rnpath. You will need to specify one or
# more Reticulum Identity hashes for authenticating the
# queries from client programs. For this purpose, you can
# use existing identity files, or generate new ones with
# the rnid utility.
# enable_remote_management = yes
# remote_management_allowed = 9fb6d773498fb3feda407ed8ef2c3229, 2d882c5586e548d79b5af27bca1776dc
# You can configure Reticulum to panic and forcibly close
# if an unrecoverable interface error occurs, such as the
# hardware device for an interface disappearing. This is
# an optional directive, and can be left out for brevity.
# This behaviour is disabled by default.
panic_on_interface_error = No
# panic_on_interface_error = No
# When Transport is enabled, it is possible to allow the
@@ -127,7 +151,7 @@ configuration file is created. The default configuration looks like this:
# Transport Instance, and printed to the log at startup.
# Optional, and disabled by default.
respond_to_probes = No
# respond_to_probes = No
[logging]
@@ -236,13 +260,14 @@ various configuration options, and interface configuration examples:
Reticulum Network Stack Daemon
options:
-h, --help show this help message and exit
--config CONFIG path to alternative Reticulum config directory
-h, --help show this help message and exit
--config CONFIG path to alternative Reticulum config directory
-v, --verbose
-q, --quiet
-s, --service rnsd is running as a service and should log to file
--exampleconfig print verbose configuration example to stdout and exit
--version show program's version number and exit
-s, --service rnsd is running as a service and should log to file
-i, --interactive drop into interactive shell after initialisation
--exampleconfig print verbose configuration example to stdout and exit
--version show program's version number and exit
You can easily add ``rnsd`` as an always-on service by :ref:`configuring a service<using-systemd>`.
@@ -331,12 +356,15 @@ Filter output to only show some interfaces:
-s SORT, --sort SORT sort interfaces by [rate, traffic, rx, tx, announces, arx, atx, held]
-r, --reverse reverse sorting
-j, --json output in JSON format
-R hash transport identity hash of remote instance to get status from
-R hash transport identity hash of remote instance to get status from (requires -i)
-i path path to identity used for remote management
-w seconds timeout before giving up on remote queries
-v, --verbose
.. note::
When using ``-R`` to query a remote transport instance, you must also specify ``-i`` with the path to a management identity file that is authorized for remote management on the target system.
The rnid Utility
====================
@@ -409,31 +437,30 @@ Decrypt a file using the Reticulum Identity it was encrypted for:
options:
-h, --help show this help message and exit
--config path path to alternative Reticulum config directory
-i identity, --identity identity
hexadecimal Reticulum Destination hash or path to Identity file
-g path, --generate path
generate a new Identity
-i, --identity identity
hexadecimal Reticulum identity or destination hash, or path to Identity file
-g, --generate file generate a new Identity
-m, --import identity_data
import Reticulum identity in hex, base32 or base64 format
-x, --export export identity to hex, base32 or base64 format
-v, --verbose increase verbosity
-q, --quiet decrease verbosity
-a aspects, --announce aspects
-a, --announce aspects
announce a destination based on this Identity
-H aspects, --hash aspects
show destination hashes for other aspects for this Identity
-e path, --encrypt path
encrypt file
-d path, --decrypt path
decrypt file
-s path, --sign path sign file
-V path, --validate path
validate signature
-r path, --read path input file path
-w path, --write path
output file path
-H, --hash aspects show destination hashes for other aspects for this Identity
-e, --encrypt file encrypt file
-d, --decrypt file decrypt file
-s, --sign path sign file
-V, --validate path validate signature
-r, --read file input file path
-w, --write file output file path
-f, --force write output even if it overwrites existing files
-R, --request request unknown Identities from the network
-t seconds identity request timeout before giving up
-p, --print-identity print identity info and exit
-P, --print-private allow displaying private keys
-b, --base64 Use base64-encoded input and output
-B, --base32 Use base32-encoded input and output
--version show program's version number and exit
@@ -613,14 +640,18 @@ Or fetch a file from the remote system:
-q, --quiet decrease verbosity
-S, --silent disable transfer progress output
-l, --listen listen for incoming transfer requests
-C, --no-compress disable automatic compression
-F, --allow-fetch allow authenticated clients to fetch files
-f, --fetch fetch file from remote listener instead of sending
-j path, --jail path restrict fetch requests to specified path
-j, --jail path restrict fetch requests to specified path
-s, --save path save received files in specified path
-O, --overwrite Allow overwriting received files, instead of adding postfix
-b seconds announce interval, 0 to only announce at startup
-a allowed_hash allow this identity
-a allowed_hash allow this identity (or add in ~/.rncp/allowed_identities)
-n, --no-auth accept requests from anyone
-p, --print-identity print identity and destination info and exit
-w seconds sender timeout before giving up
-P, --phy-rates display physical layer transfer rates
--version show program's version number and exit
@@ -728,31 +759,36 @@ to create and provision new :ref:`RNodes<rnode-main>` from any supported hardwar
-i, --info Show device info
-a, --autoinstall Automatic installation on various supported devices
-u, --update Update firmware to the latest version
-U, --force-update Update to specified firmware even if version matches
or is older than installed version
--fw-version version Use a specific firmware version for update or
autoinstall
-U, --force-update Update to specified firmware even if version matches or is older than installed version
--fw-version version Use a specific firmware version for update or autoinstall
--fw-url url Use an alternate firmware download URL
--nocheck Don't check for firmware updates online
-e, --extract Extract firmware from connected RNode for later use
-E, --use-extracted Use the extracted firmware for autoinstallation or
update
-E, --use-extracted Use the extracted firmware for autoinstallation or update
-C, --clear-cache Clear locally cached firmware files
--baud-flash baud_flash
Set specific baud rate when flashing device. Default
is 921600
Set specific baud rate when flashing device. Default is 921600
-N, --normal Switch device to normal mode
-T, --tnc Switch device to TNC mode
-b, --bluetooth-on Turn device bluetooth on
-B, --bluetooth-off Turn device bluetooth off
-p, --bluetooth-pair Put device into bluetooth pairing mode
-D i, --display i Set display intensity (0-255)
-D, --display i Set display intensity (0-255)
-t, --timeout s Set display timeout in seconds, 0 to disable
-R, --rotation rotation
Set display rotation, valid values are 0 through 3
--display-addr byte Set display address as hex byte (00 - FF)
--recondition-display
Start display reconditioning
--np i Set NeoPixel intensity (0-255)
--freq Hz Frequency in Hz for TNC mode
--bw Hz Bandwidth in Hz for TNC mode
--txp dBm TX power in dBm for TNC mode
--sf factor Spreading factor for TNC mode (7 - 12)
--cr rate Coding rate for TNC mode (5 - 8)
-x, --ia-enable Enable interference avoidance
-X, --ia-disable Disable interference avoidance
-c, --config Print device configuration
--eeprom-backup Backup EEPROM to file
--eeprom-dump Dump EEPROM to console
--eeprom-wipe Unlock and wipe EEPROM
@@ -763,8 +799,8 @@ to create and provision new :ref:`RNodes<rnode-main>` from any supported hardwar
-r, --rom Bootstrap EEPROM without flashing firmware
-k, --key Generate a new signing key and exit
-S, --sign Display public part of signing key
-H FIRMWARE_HASH, --firmware-hash FIRMWARE_HASH
Display installed firmware hash
-H, --firmware-hash FIRMWARE_HASH
Set installed firmware hash
--platform platform Platform specification for device bootstrap
--product product Product specification for device bootstrap
--model model Model code for device bootstrap
+4 -5
View File
@@ -35,10 +35,9 @@ runs well even on small single-board computers like the Pi Zero.
Current Status
==============
**Please know!** Reticulum should currently be considered beta software. All core protocol
features are implemented and functioning, but additions will probably occur as
real-world use is explored. *There will be bugs*. The API and wire-format can be
considered complete and stable at the moment, but could change if absolutely warranted.
All core protocol features are implemented and functioning, but additions will probably occur as
real-world use is explored. The API and wire-format can be considered complete and stable, but
could change if absolutely warranted.
What does Reticulum Offer?
@@ -68,7 +67,7 @@ What does Reticulum Offer?
* Ephemeral per-packet and link keys and derived from an ECDH key exchange on Curve25519
* AES-128 in CBC mode with PKCS7 padding
* AES-256 in CBC mode with PKCS7 padding
* HMAC using SHA256 for authentication
@@ -1,134 +0,0 @@
/*
* _sphinx_javascript_frameworks_compat.js
* ~~~~~~~~~~
*
* Compatability shim for jQuery and underscores.js.
*
* WILL BE REMOVED IN Sphinx 6.0
* xref RemovedInSphinx60Warning
*
*/
/**
* select a different prefix for underscore
*/
$u = _.noConflict();
/**
* small helper function to urldecode strings
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
*/
jQuery.urldecode = function(x) {
if (!x) {
return x
}
return decodeURIComponent(x.replace(/\+/g, ' '));
};
/**
* small helper function to urlencode strings
*/
jQuery.urlencode = encodeURIComponent;
/**
* This function returns the parsed url parameters of the
* current request. Multiple values per key are supported,
* it will always return arrays of strings for the value parts.
*/
jQuery.getQueryParameters = function(s) {
if (typeof s === 'undefined')
s = document.location.search;
var parts = s.substr(s.indexOf('?') + 1).split('&');
var result = {};
for (var i = 0; i < parts.length; i++) {
var tmp = parts[i].split('=', 2);
var key = jQuery.urldecode(tmp[0]);
var value = jQuery.urldecode(tmp[1]);
if (key in result)
result[key].push(value);
else
result[key] = [value];
}
return result;
};
/**
* highlight a given string on a jquery object by wrapping it in
* span elements with the given class name.
*/
jQuery.fn.highlightText = function(text, className) {
function highlight(node, addItems) {
if (node.nodeType === 3) {
var val = node.nodeValue;
var pos = val.toLowerCase().indexOf(text);
if (pos >= 0 &&
!jQuery(node.parentNode).hasClass(className) &&
!jQuery(node.parentNode).hasClass("nohighlight")) {
var span;
var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
if (isInSVG) {
span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
} else {
span = document.createElement("span");
span.className = className;
}
span.appendChild(document.createTextNode(val.substr(pos, text.length)));
node.parentNode.insertBefore(span, node.parentNode.insertBefore(
document.createTextNode(val.substr(pos + text.length)),
node.nextSibling));
node.nodeValue = val.substr(0, pos);
if (isInSVG) {
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
var bbox = node.parentElement.getBBox();
rect.x.baseVal.value = bbox.x;
rect.y.baseVal.value = bbox.y;
rect.width.baseVal.value = bbox.width;
rect.height.baseVal.value = bbox.height;
rect.setAttribute('class', className);
addItems.push({
"parent": node.parentNode,
"target": rect});
}
}
}
else if (!jQuery(node).is("button, select, textarea")) {
jQuery.each(node.childNodes, function() {
highlight(this, addItems);
});
}
}
var addItems = [];
var result = this.each(function() {
highlight(this, addItems);
});
for (var i = 0; i < addItems.length; ++i) {
jQuery(addItems[i].parent).before(addItems[i].target);
}
return result;
};
/*
* backward compatibility for jQuery.browser
* This will be supported until firefox bug is fixed.
*/
if (!jQuery.browser) {
jQuery.uaMatch = function(ua) {
ua = ua.toLowerCase();
var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
/(msie) ([\w.]+)/.exec(ua) ||
ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
[];
return {
browser: match[ 1 ] || "",
version: match[ 2 ] || "0"
};
};
jQuery.browser = {};
jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
}
+19 -13
View File
@@ -1,12 +1,5 @@
/*
* basic.css
* ~~~~~~~~~
*
* Sphinx stylesheet -- basic theme.
*
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
/* -- main layout ----------------------------------------------------------- */
@@ -115,15 +108,11 @@ img {
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
margin-top: 10px;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
padding: 5px 0;
}
ul.search li a {
@@ -237,6 +226,10 @@ a.headerlink {
visibility: hidden;
}
a:visited {
color: #551A8B;
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
@@ -324,6 +317,7 @@ aside.sidebar {
p.sidebar-title {
font-weight: bold;
}
nav.contents,
aside.topic,
div.admonition, div.topic, blockquote {
@@ -331,6 +325,7 @@ div.admonition, div.topic, blockquote {
}
/* -- topics ---------------------------------------------------------------- */
nav.contents,
aside.topic,
div.topic {
@@ -606,6 +601,7 @@ ol.simple p,
ul.simple p {
margin-bottom: 0;
}
aside.footnote > span,
div.citation > span {
float: left;
@@ -667,6 +663,16 @@ dd {
margin-left: 30px;
}
.sig dd {
margin-top: 0px;
margin-bottom: 0px;
}
.sig dl {
margin-top: 0px;
margin-bottom: 0px;
}
dl > dd:last-child,
dl > dd:last-child > :last-child {
margin-bottom: 0;
-7
View File
@@ -1,12 +1,5 @@
/*
* doctools.js
* ~~~~~~~~~~~
*
* Base JavaScript utilities for all Sphinx HTML documentation.
*
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
"use strict";
+2 -3
View File
@@ -1,6 +1,5 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '0.9.4 beta',
const DOCUMENTATION_OPTIONS = {
VERSION: '1.0.4',
LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',
-10881
View File
File diff suppressed because it is too large Load Diff
-2
View File
File diff suppressed because one or more lines are too long
+1 -8
View File
@@ -1,19 +1,12 @@
/*
* language_data.js
* ~~~~~~~~~~~~~~~~
*
* This script contains the language-specific data used by searchtools.js,
* namely the list of stopwords, stemmer, scorer and splitter.
*
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"];
/* Non-minified version is copied as a separate JS file, is available */
/* Non-minified version is copied as a separate JS file, if available */
/**
* Porter Stemmer
+71 -79
View File
@@ -3,85 +3,77 @@
.highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
.highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight { background: #f8f8f8; }
.highlight .c { color: #8F5902; font-style: italic } /* Comment */
.highlight .err { color: #A40000; border: 1px solid #EF2929 } /* Error */
.highlight .g { color: #000 } /* Generic */
.highlight .k { color: #204A87; font-weight: bold } /* Keyword */
.highlight .l { color: #000 } /* Literal */
.highlight .n { color: #000 } /* Name */
.highlight .o { color: #CE5C00; font-weight: bold } /* Operator */
.highlight .x { color: #000 } /* Other */
.highlight .p { color: #000; font-weight: bold } /* Punctuation */
.highlight .ch { color: #8F5902; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #8F5902; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #8F5902; font-style: italic } /* Comment.Preproc */
.highlight .cpf { color: #8F5902; font-style: italic } /* Comment.PreprocFile */
.highlight .c1 { color: #8F5902; font-style: italic } /* Comment.Single */
.highlight .cs { color: #8F5902; font-style: italic } /* Comment.Special */
.highlight .gd { color: #A40000 } /* Generic.Deleted */
.highlight .ge { color: #000; font-style: italic } /* Generic.Emph */
.highlight .ges { color: #000; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #EF2929 } /* Generic.Error */
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.highlight .gi { color: #00A000 } /* Generic.Inserted */
.highlight .go { color: #000; font-style: italic } /* Generic.Output */
.highlight .gp { color: #8F5902 } /* Generic.Prompt */
.highlight .gs { color: #000; font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.highlight .gt { color: #A40000; font-weight: bold } /* Generic.Traceback */
.highlight .kc { color: #204A87; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #204A87; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #204A87; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #204A87; font-weight: bold } /* Keyword.Pseudo */
.highlight .kr { color: #204A87; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #204A87; font-weight: bold } /* Keyword.Type */
.highlight .ld { color: #000 } /* Literal.Date */
.highlight .m { color: #0000CF; font-weight: bold } /* Literal.Number */
.highlight .s { color: #4E9A06 } /* Literal.String */
.highlight .na { color: #C4A000 } /* Name.Attribute */
.highlight .nb { color: #204A87 } /* Name.Builtin */
.highlight .nc { color: #000 } /* Name.Class */
.highlight .no { color: #000 } /* Name.Constant */
.highlight .nd { color: #5C35CC; font-weight: bold } /* Name.Decorator */
.highlight .ni { color: #CE5C00 } /* Name.Entity */
.highlight .ne { color: #C00; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #000 } /* Name.Function */
.highlight .nl { color: #F57900 } /* Name.Label */
.highlight .nn { color: #000 } /* Name.Namespace */
.highlight .nx { color: #000 } /* Name.Other */
.highlight .py { color: #000 } /* Name.Property */
.highlight .nt { color: #204A87; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #000 } /* Name.Variable */
.highlight .ow { color: #204A87; font-weight: bold } /* Operator.Word */
.highlight .pm { color: #000; font-weight: bold } /* Punctuation.Marker */
.highlight .w { color: #F8F8F8 } /* Text.Whitespace */
.highlight .mb { color: #0000CF; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000CF; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000CF; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000CF; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000CF; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #4E9A06 } /* Literal.String.Affix */
.highlight .sb { color: #4E9A06 } /* Literal.String.Backtick */
.highlight .sc { color: #4E9A06 } /* Literal.String.Char */
.highlight .dl { color: #4E9A06 } /* Literal.String.Delimiter */
.highlight .sd { color: #8F5902; font-style: italic } /* Literal.String.Doc */
.highlight .s2 { color: #4E9A06 } /* Literal.String.Double */
.highlight .se { color: #4E9A06 } /* Literal.String.Escape */
.highlight .sh { color: #4E9A06 } /* Literal.String.Heredoc */
.highlight .si { color: #4E9A06 } /* Literal.String.Interpol */
.highlight .sx { color: #4E9A06 } /* Literal.String.Other */
.highlight .sr { color: #4E9A06 } /* Literal.String.Regex */
.highlight .s1 { color: #4E9A06 } /* Literal.String.Single */
.highlight .ss { color: #4E9A06 } /* Literal.String.Symbol */
.highlight .bp { color: #3465A4 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #000 } /* Name.Function.Magic */
.highlight .vc { color: #000 } /* Name.Variable.Class */
.highlight .vg { color: #000 } /* Name.Variable.Global */
.highlight .vi { color: #000 } /* Name.Variable.Instance */
.highlight .vm { color: #000 } /* Name.Variable.Magic */
.highlight .il { color: #0000CF; font-weight: bold } /* Literal.Number.Integer.Long */
.highlight .hll { background-color: #fdf2e2 }
.highlight { background: #f2f2f2; color: #1E1E1E }
.highlight .c { color: #515151 } /* Comment */
.highlight .err { color: #D71835 } /* Error */
.highlight .k { color: #8045E5 } /* Keyword */
.highlight .l { color: #7F4707 } /* Literal */
.highlight .n { color: #1E1E1E } /* Name */
.highlight .o { color: #163 } /* Operator */
.highlight .p { color: #1E1E1E } /* Punctuation */
.highlight .ch { color: #515151 } /* Comment.Hashbang */
.highlight .cm { color: #515151 } /* Comment.Multiline */
.highlight .cp { color: #515151 } /* Comment.Preproc */
.highlight .cpf { color: #515151 } /* Comment.PreprocFile */
.highlight .c1 { color: #515151 } /* Comment.Single */
.highlight .cs { color: #515151 } /* Comment.Special */
.highlight .gd { color: #00749C } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gh { color: #00749C } /* Generic.Heading */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #00749C } /* Generic.Subheading */
.highlight .kc { color: #8045E5 } /* Keyword.Constant */
.highlight .kd { color: #8045E5 } /* Keyword.Declaration */
.highlight .kn { color: #8045E5 } /* Keyword.Namespace */
.highlight .kp { color: #8045E5 } /* Keyword.Pseudo */
.highlight .kr { color: #8045E5 } /* Keyword.Reserved */
.highlight .kt { color: #7F4707 } /* Keyword.Type */
.highlight .ld { color: #7F4707 } /* Literal.Date */
.highlight .m { color: #7F4707 } /* Literal.Number */
.highlight .s { color: #163 } /* Literal.String */
.highlight .na { color: #7F4707 } /* Name.Attribute */
.highlight .nb { color: #7F4707 } /* Name.Builtin */
.highlight .nc { color: #00749C } /* Name.Class */
.highlight .no { color: #00749C } /* Name.Constant */
.highlight .nd { color: #7F4707 } /* Name.Decorator */
.highlight .ni { color: #163 } /* Name.Entity */
.highlight .ne { color: #8045E5 } /* Name.Exception */
.highlight .nf { color: #00749C } /* Name.Function */
.highlight .nl { color: #7F4707 } /* Name.Label */
.highlight .nn { color: #1E1E1E } /* Name.Namespace */
.highlight .nx { color: #1E1E1E } /* Name.Other */
.highlight .py { color: #00749C } /* Name.Property */
.highlight .nt { color: #00749C } /* Name.Tag */
.highlight .nv { color: #D71835 } /* Name.Variable */
.highlight .ow { color: #8045E5 } /* Operator.Word */
.highlight .pm { color: #1E1E1E } /* Punctuation.Marker */
.highlight .w { color: #1E1E1E } /* Text.Whitespace */
.highlight .mb { color: #7F4707 } /* Literal.Number.Bin */
.highlight .mf { color: #7F4707 } /* Literal.Number.Float */
.highlight .mh { color: #7F4707 } /* Literal.Number.Hex */
.highlight .mi { color: #7F4707 } /* Literal.Number.Integer */
.highlight .mo { color: #7F4707 } /* Literal.Number.Oct */
.highlight .sa { color: #163 } /* Literal.String.Affix */
.highlight .sb { color: #163 } /* Literal.String.Backtick */
.highlight .sc { color: #163 } /* Literal.String.Char */
.highlight .dl { color: #163 } /* Literal.String.Delimiter */
.highlight .sd { color: #163 } /* Literal.String.Doc */
.highlight .s2 { color: #163 } /* Literal.String.Double */
.highlight .se { color: #163 } /* Literal.String.Escape */
.highlight .sh { color: #163 } /* Literal.String.Heredoc */
.highlight .si { color: #163 } /* Literal.String.Interpol */
.highlight .sx { color: #163 } /* Literal.String.Other */
.highlight .sr { color: #D71835 } /* Literal.String.Regex */
.highlight .s1 { color: #163 } /* Literal.String.Single */
.highlight .ss { color: #00749C } /* Literal.String.Symbol */
.highlight .bp { color: #7F4707 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #00749C } /* Name.Function.Magic */
.highlight .vc { color: #D71835 } /* Name.Variable.Class */
.highlight .vg { color: #D71835 } /* Name.Variable.Global */
.highlight .vi { color: #D71835 } /* Name.Variable.Instance */
.highlight .vm { color: #7F4707 } /* Name.Variable.Magic */
.highlight .il { color: #7F4707 } /* Literal.Number.Integer.Long */
@media not print {
body[data-theme="dark"] .highlight pre { line-height: 125%; }
body[data-theme="dark"] .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+153 -84
View File
@@ -1,12 +1,5 @@
/*
* searchtools.js
* ~~~~~~~~~~~~~~~~
*
* Sphinx JavaScript utilities for the full-text search.
*
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
"use strict";
@@ -20,7 +13,7 @@ if (typeof Scorer === "undefined") {
// and returns the new score.
/*
score: result => {
const [docname, title, anchor, descr, score, filename] = result
const [docname, title, anchor, descr, score, filename, kind] = result
return score
},
*/
@@ -47,6 +40,14 @@ if (typeof Scorer === "undefined") {
};
}
// Global search result kind enum, used by themes to style search results.
class SearchResultKind {
static get index() { return "index"; }
static get object() { return "object"; }
static get text() { return "text"; }
static get title() { return "title"; }
}
const _removeChildren = (element) => {
while (element && element.lastChild) element.removeChild(element.lastChild);
};
@@ -57,16 +58,20 @@ const _removeChildren = (element) => {
const _escapeRegExp = (string) =>
string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
const _displayItem = (item, searchTerms) => {
const _displayItem = (item, searchTerms, highlightTerms) => {
const docBuilder = DOCUMENTATION_OPTIONS.BUILDER;
const docUrlRoot = DOCUMENTATION_OPTIONS.URL_ROOT;
const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;
const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;
const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;
const contentRoot = document.documentElement.dataset.content_root;
const [docName, title, anchor, descr, score, _filename] = item;
const [docName, title, anchor, descr, score, _filename, kind] = item;
let listItem = document.createElement("li");
// Add a class representing the item's type:
// can be used by a theme's CSS selector for styling
// See SearchResultKind for the class names.
listItem.classList.add(`kind-${kind}`);
let requestUrl;
let linkUrl;
if (docBuilder === "dirhtml") {
@@ -75,28 +80,35 @@ const _displayItem = (item, searchTerms) => {
if (dirname.match(/\/index\/$/))
dirname = dirname.substring(0, dirname.length - 6);
else if (dirname === "index/") dirname = "";
requestUrl = docUrlRoot + dirname;
requestUrl = contentRoot + dirname;
linkUrl = requestUrl;
} else {
// normal html builders
requestUrl = docUrlRoot + docName + docFileSuffix;
requestUrl = contentRoot + docName + docFileSuffix;
linkUrl = docName + docLinkSuffix;
}
let linkEl = listItem.appendChild(document.createElement("a"));
linkEl.href = linkUrl + anchor;
linkEl.dataset.score = score;
linkEl.innerHTML = title;
if (descr)
if (descr) {
listItem.appendChild(document.createElement("span")).innerHTML =
" (" + descr + ")";
// highlight search terms in the description
if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js
highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted"));
}
else if (showSearchSummary)
fetch(requestUrl)
.then((responseData) => responseData.text())
.then((data) => {
if (data)
listItem.appendChild(
Search.makeSearchSummary(data, searchTerms)
Search.makeSearchSummary(data, searchTerms, anchor)
);
// highlight search terms in the summary
if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js
highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted"));
});
Search.output.appendChild(listItem);
};
@@ -108,27 +120,46 @@ const _finishSearch = (resultCount) => {
"Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories."
);
else
Search.status.innerText = _(
`Search finished, found ${resultCount} page(s) matching the search query.`
);
Search.status.innerText = Documentation.ngettext(
"Search finished, found one page matching the search query.",
"Search finished, found ${resultCount} pages matching the search query.",
resultCount,
).replace('${resultCount}', resultCount);
};
const _displayNextItem = (
results,
resultCount,
searchTerms
searchTerms,
highlightTerms,
) => {
// results left, load the summary and display it
// this is intended to be dynamic (don't sub resultsCount)
if (results.length) {
_displayItem(results.pop(), searchTerms);
_displayItem(results.pop(), searchTerms, highlightTerms);
setTimeout(
() => _displayNextItem(results, resultCount, searchTerms),
() => _displayNextItem(results, resultCount, searchTerms, highlightTerms),
5
);
}
// search finished, update title and status message
else _finishSearch(resultCount);
};
// Helper function used by query() to order search results.
// Each input is an array of [docname, title, anchor, descr, score, filename, kind].
// Order the results by score (in opposite order of appearance, since the
// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically.
const _orderResultsByScoreThenName = (a, b) => {
const leftScore = a[4];
const rightScore = b[4];
if (leftScore === rightScore) {
// same score: sort alphabetically
const leftTitle = a[1].toLowerCase();
const rightTitle = b[1].toLowerCase();
if (leftTitle === rightTitle) return 0;
return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
}
return leftScore > rightScore ? 1 : -1;
};
/**
* Default splitQuery function. Can be overridden in ``sphinx.search`` with a
@@ -152,13 +183,26 @@ const Search = {
_queued_query: null,
_pulse_status: -1,
htmlToText: (htmlString) => {
htmlToText: (htmlString, anchor) => {
const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html');
htmlElement.querySelectorAll(".headerlink").forEach((el) => { el.remove() });
for (const removalQuery of [".headerlink", "script", "style"]) {
htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() });
}
if (anchor) {
const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`);
if (anchorContent) return anchorContent.textContent;
console.warn(
`Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`
);
}
// if anchor not specified or not found, fall back to main content
const docContent = htmlElement.querySelector('[role="main"]');
if (docContent !== undefined) return docContent.textContent;
if (docContent) return docContent.textContent;
console.warn(
"Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template."
"Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template."
);
return "";
},
@@ -211,6 +255,7 @@ const Search = {
searchSummary.classList.add("search-summary");
searchSummary.innerText = "";
const searchList = document.createElement("ul");
searchList.setAttribute("role", "list");
searchList.classList.add("search");
const out = document.getElementById("search-results");
@@ -231,16 +276,7 @@ const Search = {
else Search.deferQuery(query);
},
/**
* execute search (requires search index to be loaded)
*/
query: (query) => {
const filenames = Search._index.filenames;
const docNames = Search._index.docnames;
const titles = Search._index.titles;
const allTitles = Search._index.alltitles;
const indexEntries = Search._index.indexentries;
_parseQuery: (query) => {
// stem the search terms and add them to the correct list
const stemmer = new Stemmer();
const searchTerms = new Set();
@@ -276,22 +312,40 @@ const Search = {
// console.info("required: ", [...searchTerms]);
// console.info("excluded: ", [...excludedTerms]);
// array of [docname, title, anchor, descr, score, filename]
let results = [];
return [query, searchTerms, excludedTerms, highlightTerms, objectTerms];
},
/**
* execute search (requires search index to be loaded)
*/
_performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => {
const filenames = Search._index.filenames;
const docNames = Search._index.docnames;
const titles = Search._index.titles;
const allTitles = Search._index.alltitles;
const indexEntries = Search._index.indexentries;
// Collect multiple result groups to be sorted separately and then ordered.
// Each is an array of [docname, title, anchor, descr, score, filename, kind].
const normalResults = [];
const nonMainIndexResults = [];
_removeChildren(document.getElementById("search-progress"));
const queryLower = query.toLowerCase();
const queryLower = query.toLowerCase().trim();
for (const [title, foundTitles] of Object.entries(allTitles)) {
if (title.toLowerCase().includes(queryLower) && (queryLower.length >= title.length/2)) {
if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) {
for (const [file, id] of foundTitles) {
let score = Math.round(100 * queryLower.length / title.length)
results.push([
const score = Math.round(Scorer.title * queryLower.length / title.length);
const boost = titles[file] === title ? 1 : 0; // add a boost for document titles
normalResults.push([
docNames[file],
titles[file] !== title ? `${titles[file]} > ${title}` : title,
id !== null ? "#" + id : "",
null,
score,
score + boost,
filenames[file],
SearchResultKind.title,
]);
}
}
@@ -300,46 +354,48 @@ const Search = {
// search for explicit entries in index directives
for (const [entry, foundEntries] of Object.entries(indexEntries)) {
if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) {
for (const [file, id] of foundEntries) {
let score = Math.round(100 * queryLower.length / entry.length)
results.push([
for (const [file, id, isMain] of foundEntries) {
const score = Math.round(100 * queryLower.length / entry.length);
const result = [
docNames[file],
titles[file],
id ? "#" + id : "",
null,
score,
filenames[file],
]);
SearchResultKind.index,
];
if (isMain) {
normalResults.push(result);
} else {
nonMainIndexResults.push(result);
}
}
}
}
// lookup as object
objectTerms.forEach((term) =>
results.push(...Search.performObjectSearch(term, objectTerms))
normalResults.push(...Search.performObjectSearch(term, objectTerms))
);
// lookup as search terms in fulltext
results.push(...Search.performTermsSearch(searchTerms, excludedTerms));
normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms));
// let the scorer override scores with a custom scoring function
if (Scorer.score) results.forEach((item) => (item[4] = Scorer.score(item)));
if (Scorer.score) {
normalResults.forEach((item) => (item[4] = Scorer.score(item)));
nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item)));
}
// now sort the results by score (in opposite order of appearance, since the
// display function below uses pop() to retrieve items) and then
// alphabetically
results.sort((a, b) => {
const leftScore = a[4];
const rightScore = b[4];
if (leftScore === rightScore) {
// same score: sort alphabetically
const leftTitle = a[1].toLowerCase();
const rightTitle = b[1].toLowerCase();
if (leftTitle === rightTitle) return 0;
return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
}
return leftScore > rightScore ? 1 : -1;
});
// Sort each group of results by score and then alphabetically by name.
normalResults.sort(_orderResultsByScoreThenName);
nonMainIndexResults.sort(_orderResultsByScoreThenName);
// Combine the result groups in (reverse) order.
// Non-main index entries are typically arbitrary cross-references,
// so display them after other results.
let results = [...nonMainIndexResults, ...normalResults];
// remove duplicate search results
// note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
@@ -353,14 +409,19 @@ const Search = {
return acc;
}, []);
results = results.reverse();
return results.reverse();
},
query: (query) => {
const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query);
const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms);
// for debugging
//Search.lastresults = results.slice(); // a copy
// console.info("search results:", Search.lastresults);
// print the results
_displayNextItem(results, results.length, searchTerms);
_displayNextItem(results, results.length, searchTerms, highlightTerms);
},
/**
@@ -424,6 +485,7 @@ const Search = {
descr,
score,
filenames[match[0]],
SearchResultKind.object,
]);
};
Object.keys(objects).forEach((prefix) =>
@@ -451,21 +513,27 @@ const Search = {
// perform the search on the required terms
searchTerms.forEach((word) => {
const files = [];
// find documents, if any, containing the query word in their text/title term indices
// use Object.hasOwnProperty to avoid mismatching against prototype properties
const arr = [
{ files: terms[word], score: Scorer.term },
{ files: titleTerms[word], score: Scorer.title },
{ files: terms.hasOwnProperty(word) ? terms[word] : undefined, score: Scorer.term },
{ files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, score: Scorer.title },
];
// add support for partial matches
if (word.length > 2) {
const escapedWord = _escapeRegExp(word);
Object.keys(terms).forEach((term) => {
if (term.match(escapedWord) && !terms[word])
arr.push({ files: terms[term], score: Scorer.partialTerm });
});
Object.keys(titleTerms).forEach((term) => {
if (term.match(escapedWord) && !titleTerms[word])
arr.push({ files: titleTerms[word], score: Scorer.partialTitle });
});
if (!terms.hasOwnProperty(word)) {
Object.keys(terms).forEach((term) => {
if (term.match(escapedWord))
arr.push({ files: terms[term], score: Scorer.partialTerm });
});
}
if (!titleTerms.hasOwnProperty(word)) {
Object.keys(titleTerms).forEach((term) => {
if (term.match(escapedWord))
arr.push({ files: titleTerms[term], score: Scorer.partialTitle });
});
}
}
// no match but word was a required one
@@ -481,16 +549,16 @@ const Search = {
// set score for the word in each file
recordFiles.forEach((file) => {
if (!scoreMap.has(file)) scoreMap.set(file, {});
scoreMap.get(file)[word] = record.score;
if (!scoreMap.has(file)) scoreMap.set(file, new Map());
const fileScores = scoreMap.get(file);
fileScores.set(word, record.score);
});
});
// create the mapping
files.forEach((file) => {
if (fileMap.has(file) && fileMap.get(file).indexOf(word) === -1)
fileMap.get(file).push(word);
else fileMap.set(file, [word]);
if (!fileMap.has(file)) fileMap.set(file, [word]);
else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word);
});
});
@@ -522,7 +590,7 @@ const Search = {
break;
// select one (max) score for the file.
const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w]));
const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w)));
// add result to the result list
results.push([
docNames[file],
@@ -531,6 +599,7 @@ const Search = {
null,
score,
filenames[file],
SearchResultKind.text,
]);
}
return results;
@@ -541,8 +610,8 @@ const Search = {
* search summary for a given text. keywords is a list
* of stemmed words.
*/
makeSearchSummary: (htmlText, keywords) => {
const text = Search.htmlToText(htmlText);
makeSearchSummary: (htmlText, keywords, anchor) => {
const text = Search.htmlToText(htmlText, anchor);
if (text === "") return null;
const textLower = text.toLowerCase();
+13 -3
View File
@@ -29,14 +29,19 @@ const _highlight = (node, addItems, text, className) => {
}
span.appendChild(document.createTextNode(val.substr(pos, text.length)));
const rest = document.createTextNode(val.substr(pos + text.length));
parent.insertBefore(
span,
parent.insertBefore(
document.createTextNode(val.substr(pos + text.length)),
rest,
node.nextSibling
)
);
node.nodeValue = val.substr(0, pos);
/* There may be more occurrences of search term in this node. So call this
* function recursively on the remaining fragment.
*/
_highlight(rest, addItems, text, className);
if (isInSVG) {
const rect = document.createElementNS(
@@ -140,5 +145,10 @@ const SphinxHighlight = {
},
};
_ready(SphinxHighlight.highlightSearchWords);
_ready(SphinxHighlight.initEscapeListener);
_ready(() => {
/* Do not call highlightSearchWords() when we are on the search page.
* It will highlight words from the *previous* search query.
*/
if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords();
SphinxHighlight.initEscapeListener();
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+114 -85
View File
@@ -1,25 +1,26 @@
<!doctype html>
<html class="no-js" lang="en">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<html class="no-js" lang="en" data-content_root="./">
<head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="Support Reticulum" href="support.html" /><link rel="prev" title="Building Networks" href="networks.html" />
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html"><link rel="next" title="API Reference" href="reference.html"><link rel="prev" title="Support Reticulum" href="support.html">
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/>
<title>Code Examples - Reticulum Network Stack 0.9.4 beta documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=189ec851f9bb375a2509b67be1f64f0cf212b702" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<link rel="stylesheet" type="text/css" href="_static/custom.css" />
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Code Examples - Reticulum Network Stack 1.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
--color-code-background: #f2f2f2;
--color-code-foreground: #1e1e1e;
}
@media not print {
@@ -86,7 +87,7 @@
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
@@ -101,34 +102,73 @@
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<symbol id="svg-sun-with-moon" viewBox="0 0 24 24">
<title>Auto light/dark, in light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
class="icon-custom-derived-from-feather-sun-and-tabler-moon">
<path style="opacity: 50%" d="M 5.411 14.504 C 5.471 14.504 5.532 14.504 5.591 14.504 C 3.639 16.319 4.383 19.569 6.931 20.352 C 7.693 20.586 8.512 20.551 9.25 20.252 C 8.023 23.207 4.056 23.725 2.11 21.184 C 0.166 18.642 1.702 14.949 4.874 14.536 C 5.051 14.512 5.231 14.5 5.411 14.5 L 5.411 14.504 Z"/>
<line x1="14.5" y1="3.25" x2="14.5" y2="1.25"/>
<line x1="14.5" y1="15.85" x2="14.5" y2="17.85"/>
<line x1="10.044" y1="5.094" x2="8.63" y2="3.68"/>
<line x1="19" y1="14.05" x2="20.414" y2="15.464"/>
<line x1="8.2" y1="9.55" x2="6.2" y2="9.55"/>
<line x1="20.8" y1="9.55" x2="22.8" y2="9.55"/>
<line x1="10.044" y1="14.006" x2="8.63" y2="15.42"/>
<line x1="19" y1="5.05" x2="20.414" y2="3.636"/>
<circle cx="14.5" cy="9.55" r="3.6"/>
</svg>
</symbol>
<symbol id="svg-moon-with-sun" viewBox="0 0 24 24">
<title>Auto light/dark, in dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
class="icon-custom-derived-from-feather-sun-and-tabler-moon">
<path d="M 8.282 7.007 C 8.385 7.007 8.494 7.007 8.595 7.007 C 5.18 10.184 6.481 15.869 10.942 17.24 C 12.275 17.648 13.706 17.589 15 17.066 C 12.851 22.236 5.91 23.143 2.505 18.696 C -0.897 14.249 1.791 7.786 7.342 7.063 C 7.652 7.021 7.965 7 8.282 7 L 8.282 7.007 Z"/>
<line style="opacity: 50%" x1="18" y1="3.705" x2="18" y2="2.5"/>
<line style="opacity: 50%" x1="18" y1="11.295" x2="18" y2="12.5"/>
<line style="opacity: 50%" x1="15.316" y1="4.816" x2="14.464" y2="3.964"/>
<line style="opacity: 50%" x1="20.711" y1="10.212" x2="21.563" y2="11.063"/>
<line style="opacity: 50%" x1="14.205" y1="7.5" x2="13.001" y2="7.5"/>
<line style="opacity: 50%" x1="21.795" y1="7.5" x2="23" y2="7.5"/>
<line style="opacity: 50%" x1="15.316" y1="10.184" x2="14.464" y2="11.036"/>
<line style="opacity: 50%" x1="20.711" y1="4.789" x2="21.563" y2="3.937"/>
<circle style="opacity: 50%" cx="18" cy="7.5" r="2.169"/>
</svg>
</symbol>
<symbol id="svg-pencil" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-pencil-code">
<path d="M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4" />
<path d="M13.5 6.5l4 4" />
<path d="M20 21l2 -2l-2 -2" />
<path d="M17 17l-2 2l2 2" />
</svg>
</symbol>
<symbol id="svg-eye" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-eye-code">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path
d="M11.11 17.958c-3.209 -.307 -5.91 -2.293 -8.11 -5.958c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6c-.21 .352 -.427 .688 -.647 1.008" />
<path d="M20 21l2 -2l-2 -2" />
<path d="M17 17l-2 2l2 2" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation" aria-label="Toggle site navigation sidebar">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc" aria-label="Toggle table of contents sidebar">
<label class="overlay sidebar-overlay" for="__navigation"></label>
<label class="overlay toc-overlay" for="__toc"></label>
<a class="skip-to-content muted-link" href="#furo-main-content">Skip to content</a>
@@ -136,38 +176,35 @@
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
<span class="icon"><svg><use href="#svg-menu"></use></svg></span>
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.4 beta documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<button class="theme-toggle" aria-label="Toggle Light / Dark / Auto color theme">
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
<span class="icon"><svg><use href="#svg-toc"></use></svg></span>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand centered" href="index.html">
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<div class="sidebar-logo-container">
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.4 beta documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -183,8 +220,8 @@
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Code Examples</a></li>
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Code Examples</a></li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a></li>
@@ -208,25 +245,24 @@
</a>
<div class="content-icon-container">
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<button class="theme-toggle" aria-label="Toggle Light / Dark / Auto color theme">
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
<span class="icon"><svg><use href="#svg-toc"></use></svg></span>
</label>
</div>
<article role="main">
<article role="main" id="furo-main-content">
<section id="code-examples">
<span id="examples-main"></span><h1>Code Examples<a class="headerlink" href="#code-examples" title="Permalink to this heading">#</a></h1>
<span id="examples-main"></span><h1>Code Examples<a class="headerlink" href="#code-examples" title="Link to this heading"></a></h1>
<p>A number of examples are included in the source distribution of Reticulum.
You can use these examples to learn how to write your own programs.</p>
<section id="minimal">
<span id="example-minimal"></span><h2>Minimal<a class="headerlink" href="#minimal" title="Permalink to this heading">#</a></h2>
<span id="example-minimal"></span><h2>Minimal<a class="headerlink" href="#minimal" title="Link to this heading"></a></h2>
<p>The <em>Minimal</em> example demonstrates the bare-minimum setup required to connect to
a Reticulum network from your program. In about five lines of code, you will
have the Reticulum Network Stack initialised, and ready to pass traffic in your
@@ -338,7 +374,7 @@ program.</p>
<p>This example can also be found at <a class="reference external" href="https://github.com/markqvist/Reticulum/blob/master/Examples/Minimal.py">https://github.com/markqvist/Reticulum/blob/master/Examples/Minimal.py</a>.</p>
</section>
<section id="announce">
<span id="example-announce"></span><h2>Announce<a class="headerlink" href="#announce" title="Permalink to this heading">#</a></h2>
<span id="example-announce"></span><h2>Announce<a class="headerlink" href="#announce" title="Link to this heading"></a></h2>
<p>The <em>Announce</em> example builds upon the previous example by exploring how to
announce a destination on the network, and how to let your program receive
notifications about announces from relevant destinations.</p>
@@ -519,7 +555,7 @@ notifications about announces from relevant destinations.</p>
<p>This example can also be found at <a class="reference external" href="https://github.com/markqvist/Reticulum/blob/master/Examples/Announce.py">https://github.com/markqvist/Reticulum/blob/master/Examples/Announce.py</a>.</p>
</section>
<section id="broadcast">
<span id="example-broadcast"></span><h2>Broadcast<a class="headerlink" href="#broadcast" title="Permalink to this heading">#</a></h2>
<span id="example-broadcast"></span><h2>Broadcast<a class="headerlink" href="#broadcast" title="Link to this heading"></a></h2>
<p>The <em>Broadcast</em> example explores how to transmit plaintext broadcast messages
over the network.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
@@ -648,7 +684,7 @@ over the network.</p>
<p>This example can also be found at <a class="reference external" href="https://github.com/markqvist/Reticulum/blob/master/Examples/Broadcast.py">https://github.com/markqvist/Reticulum/blob/master/Examples/Broadcast.py</a>.</p>
</section>
<section id="echo">
<span id="example-echo"></span><h2>Echo<a class="headerlink" href="#echo" title="Permalink to this heading">#</a></h2>
<span id="example-echo"></span><h2>Echo<a class="headerlink" href="#echo" title="Link to this heading"></a></h2>
<p>The <em>Echo</em> example demonstrates communication between two destinations using
the Packet interface.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
@@ -988,7 +1024,7 @@ the Packet interface.</p>
<p>This example can also be found at <a class="reference external" href="https://github.com/markqvist/Reticulum/blob/master/Examples/Echo.py">https://github.com/markqvist/Reticulum/blob/master/Examples/Echo.py</a>.</p>
</section>
<section id="link">
<span id="example-link"></span><h2>Link<a class="headerlink" href="#link" title="Permalink to this heading">#</a></h2>
<span id="example-link"></span><h2>Link<a class="headerlink" href="#link" title="Link to this heading"></a></h2>
<p>The <em>Link</em> example explores establishing an encrypted link to a remote
destination, and passing traffic back and forth over the link.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
@@ -1286,7 +1322,7 @@ destination, and passing traffic back and forth over the link.</p>
<p>This example can also be found at <a class="reference external" href="https://github.com/markqvist/Reticulum/blob/master/Examples/Link.py">https://github.com/markqvist/Reticulum/blob/master/Examples/Link.py</a>.</p>
</section>
<section id="example-identify">
<span id="identification"></span><h2>Identification<a class="headerlink" href="#example-identify" title="Permalink to this heading">#</a></h2>
<span id="identification"></span><h2>Identification<a class="headerlink" href="#example-identify" title="Link to this heading"></a></h2>
<p>The <em>Identify</em> example explores identifying an intiator of a link, once
the link has been established.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
@@ -1607,11 +1643,11 @@ the link has been established.</p>
<p>This example can also be found at <a class="reference external" href="https://github.com/markqvist/Reticulum/blob/master/Examples/Identify.py">https://github.com/markqvist/Reticulum/blob/master/Examples/Identify.py</a>.</p>
</section>
<section id="requests-responses">
<span id="example-request"></span><h2>Requests &amp; Responses<a class="headerlink" href="#requests-responses" title="Permalink to this heading">#</a></h2>
<span id="example-request"></span><h2>Requests &amp; Responses<a class="headerlink" href="#requests-responses" title="Link to this heading"></a></h2>
<p>The <em>Request</em> example explores sending requests and receiving responses.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
<span class="c1"># This RNS example demonstrates how to set perform #</span>
<span class="c1"># requests and receive responses over a link. #</span>
<span class="c1"># This RNS example demonstrates how to perform requests #</span>
<span class="c1"># and receive responses over a link. #</span>
<span class="c1">##########################################################</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">os</span>
@@ -1900,7 +1936,7 @@ the link has been established.</p>
<p>This example can also be found at <a class="reference external" href="https://github.com/markqvist/Reticulum/blob/master/Examples/Request.py">https://github.com/markqvist/Reticulum/blob/master/Examples/Request.py</a>.</p>
</section>
<section id="channel">
<span id="example-channel"></span><h2>Channel<a class="headerlink" href="#channel" title="Permalink to this heading">#</a></h2>
<span id="example-channel"></span><h2>Channel<a class="headerlink" href="#channel" title="Link to this heading"></a></h2>
<p>The <em>Channel</em> example explores using a <code class="docutils literal notranslate"><span class="pre">Channel</span></code> to send structured
data between peers of a <code class="docutils literal notranslate"><span class="pre">Link</span></code>.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
@@ -2297,7 +2333,7 @@ data between peers of a <code class="docutils literal notranslate"><span class="
<p>This example can also be found at <a class="reference external" href="https://github.com/markqvist/Reticulum/blob/master/Examples/Channel.py">https://github.com/markqvist/Reticulum/blob/master/Examples/Channel.py</a>.</p>
</section>
<section id="buffer">
<h2>Buffer<a class="headerlink" href="#buffer" title="Permalink to this heading">#</a></h2>
<h2>Buffer<a class="headerlink" href="#buffer" title="Link to this heading"></a></h2>
<p>The <em>Buffer</em> example explores using buffered readers and writers to send
binary data between peers of a <code class="docutils literal notranslate"><span class="pre">Link</span></code>.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
@@ -2627,7 +2663,7 @@ binary data between peers of a <code class="docutils literal notranslate"><span
<p>This example can also be found at <a class="reference external" href="https://github.com/markqvist/Reticulum/blob/master/Examples/Buffer.py">https://github.com/markqvist/Reticulum/blob/master/Examples/Buffer.py</a>.</p>
</section>
<section id="filetransfer">
<span id="example-filetransfer"></span><h2>Filetransfer<a class="headerlink" href="#filetransfer" title="Permalink to this heading">#</a></h2>
<span id="example-filetransfer"></span><h2>Filetransfer<a class="headerlink" href="#filetransfer" title="Link to this heading"></a></h2>
<p>The <em>Filetransfer</em> example implements a basic file-server program that
allow clients to connect and download files. The program uses the Resource
interface to efficiently pass files of any size over a Reticulum <a class="reference internal" href="reference.html#api-link"><span class="std std-ref">Link</span></a>.</p>
@@ -3239,14 +3275,12 @@ interface to efficiently pass files of any size over a Reticulum <a class="refer
<p>This example can also be found at <a class="reference external" href="https://github.com/markqvist/Reticulum/blob/master/Examples/Filetransfer.py">https://github.com/markqvist/Reticulum/blob/master/Examples/Filetransfer.py</a>.</p>
</section>
<section id="custom-interfaces">
<span id="example-custominterface"></span><h2>Custom Interfaces<a class="headerlink" href="#custom-interfaces" title="Permalink to this heading">#</a></h2>
<span id="example-custominterface"></span><h2>Custom Interfaces<a class="headerlink" href="#custom-interfaces" title="Link to this heading"></a></h2>
<p>The <em>ExampleInterface</em> demonstrates creating custom interfaces for Reticulum.
Any number of custom interfaces can be loaded and utilised by Reticulum, and
will be fully on-par with natively included interfaces, including all supported
<a class="reference internal" href="interfaces.html#interfaces-modes"><span class="std std-ref">interface modes</span></a> and <a class="reference internal" href="interfaces.html#interfaces-options"><span class="std std-ref">common configuration options</span></a>.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># MIT License - Copyright (c) 2024 Mark Qvist / unsigned.io</span>
<span class="c1"># This example illustrates creating a custom interface</span>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># This example illustrates creating a custom interface</span>
<span class="c1"># definition, that can be loaded and used by Reticulum at</span>
<span class="c1"># runtime. Any number of custom interfaces can be created</span>
<span class="c1"># and loaded. To use the interface place it in the folder</span>
@@ -3554,23 +3588,23 @@ will be fully on-par with natively included interfaces, including all supported
<footer>
<div class="related-pages">
<a class="next-page" href="support.html">
<a class="next-page" href="reference.html">
<div class="page-info">
<div class="context">
<span>Next</span>
</div>
<div class="title">Support Reticulum</div>
<div class="title">API Reference</div>
</div>
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
</a>
<a class="prev-page" href="networks.html">
<a class="prev-page" href="support.html">
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
<div class="page-info">
<div class="context">
<span>Previous</span>
</div>
<div class="title">Building Networks</div>
<div class="title">Support Reticulum</div>
</div>
</a>
@@ -3578,16 +3612,14 @@ will be fully on-par with natively included interfaces, including all supported
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, Mark Qvist
Copyright &#169; 2025, Mark Qvist
</div>
Generated with <a href="https://www.sphinx-doc.org/">Sphinx</a> and
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
<div class="icons">
</div>
</div>
</div>
@@ -3628,14 +3660,11 @@ will be fully on-par with natively included interfaces, including all supported
</aside>
</div>
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/sphinx_highlight.js"></script>
<script src="_static/scripts/furo.js"></script>
<script src="_static/clipboard.min.js"></script>
<script src="_static/copybutton.js"></script>
</div><script src="_static/documentation_options.js?v=71272d9f"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
<script src="_static/clipboard.min.js?v=a7894cd8"></script>
<script src="_static/copybutton.js?v=f281be69"></script>
</body>
</html>
+96 -65
View File
@@ -1,25 +1,26 @@
<!doctype html>
<html class="no-js" lang="en">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<html class="no-js" lang="en" data-content_root="./">
<head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" />
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html">
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/>
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 0.9.4 beta documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=189ec851f9bb375a2509b67be1f64f0cf212b702" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<link rel="stylesheet" type="text/css" href="_static/custom.css" />
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
--color-code-background: #f2f2f2;
--color-code-foreground: #1e1e1e;
}
@media not print {
@@ -86,7 +87,7 @@
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
@@ -101,34 +102,73 @@
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<symbol id="svg-sun-with-moon" viewBox="0 0 24 24">
<title>Auto light/dark, in light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
class="icon-custom-derived-from-feather-sun-and-tabler-moon">
<path style="opacity: 50%" d="M 5.411 14.504 C 5.471 14.504 5.532 14.504 5.591 14.504 C 3.639 16.319 4.383 19.569 6.931 20.352 C 7.693 20.586 8.512 20.551 9.25 20.252 C 8.023 23.207 4.056 23.725 2.11 21.184 C 0.166 18.642 1.702 14.949 4.874 14.536 C 5.051 14.512 5.231 14.5 5.411 14.5 L 5.411 14.504 Z"/>
<line x1="14.5" y1="3.25" x2="14.5" y2="1.25"/>
<line x1="14.5" y1="15.85" x2="14.5" y2="17.85"/>
<line x1="10.044" y1="5.094" x2="8.63" y2="3.68"/>
<line x1="19" y1="14.05" x2="20.414" y2="15.464"/>
<line x1="8.2" y1="9.55" x2="6.2" y2="9.55"/>
<line x1="20.8" y1="9.55" x2="22.8" y2="9.55"/>
<line x1="10.044" y1="14.006" x2="8.63" y2="15.42"/>
<line x1="19" y1="5.05" x2="20.414" y2="3.636"/>
<circle cx="14.5" cy="9.55" r="3.6"/>
</svg>
</symbol>
<symbol id="svg-moon-with-sun" viewBox="0 0 24 24">
<title>Auto light/dark, in dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
class="icon-custom-derived-from-feather-sun-and-tabler-moon">
<path d="M 8.282 7.007 C 8.385 7.007 8.494 7.007 8.595 7.007 C 5.18 10.184 6.481 15.869 10.942 17.24 C 12.275 17.648 13.706 17.589 15 17.066 C 12.851 22.236 5.91 23.143 2.505 18.696 C -0.897 14.249 1.791 7.786 7.342 7.063 C 7.652 7.021 7.965 7 8.282 7 L 8.282 7.007 Z"/>
<line style="opacity: 50%" x1="18" y1="3.705" x2="18" y2="2.5"/>
<line style="opacity: 50%" x1="18" y1="11.295" x2="18" y2="12.5"/>
<line style="opacity: 50%" x1="15.316" y1="4.816" x2="14.464" y2="3.964"/>
<line style="opacity: 50%" x1="20.711" y1="10.212" x2="21.563" y2="11.063"/>
<line style="opacity: 50%" x1="14.205" y1="7.5" x2="13.001" y2="7.5"/>
<line style="opacity: 50%" x1="21.795" y1="7.5" x2="23" y2="7.5"/>
<line style="opacity: 50%" x1="15.316" y1="10.184" x2="14.464" y2="11.036"/>
<line style="opacity: 50%" x1="20.711" y1="4.789" x2="21.563" y2="3.937"/>
<circle style="opacity: 50%" cx="18" cy="7.5" r="2.169"/>
</svg>
</symbol>
<symbol id="svg-pencil" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-pencil-code">
<path d="M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4" />
<path d="M13.5 6.5l4 4" />
<path d="M20 21l2 -2l-2 -2" />
<path d="M17 17l-2 2l2 2" />
</svg>
</symbol>
<symbol id="svg-eye" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-eye-code">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path
d="M11.11 17.958c-3.209 -.307 -5.91 -2.293 -8.11 -5.958c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6c-.21 .352 -.427 .688 -.647 1.008" />
<path d="M20 21l2 -2l-2 -2" />
<path d="M17 17l-2 2l2 2" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation" aria-label="Toggle site navigation sidebar">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc" aria-label="Toggle table of contents sidebar">
<label class="overlay sidebar-overlay" for="__navigation"></label>
<label class="overlay toc-overlay" for="__toc"></label>
<a class="skip-to-content muted-link" href="#furo-main-content">Skip to content</a>
@@ -136,38 +176,35 @@
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
<span class="icon"><svg><use href="#svg-menu"></use></svg></span>
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.4 beta documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<button class="theme-toggle" aria-label="Toggle Light / Dark / Auto color theme">
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
<span class="icon"><svg><use href="#svg-toc"></use></svg></span>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand centered" href="index.html">
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<div class="sidebar-logo-container">
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.4 beta documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -183,8 +220,8 @@
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a></li>
@@ -208,21 +245,20 @@
</a>
<div class="content-icon-container">
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<button class="theme-toggle" aria-label="Toggle Light / Dark / Auto color theme">
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
<span class="icon"><svg><use href="#svg-toc"></use></svg></span>
</label>
</div>
<article role="main">
<article role="main" id="furo-main-content">
<section id="an-explanation-of-reticulum-for-human-beings">
<h1>An Explanation of Reticulum for Human Beings<a class="headerlink" href="#an-explanation-of-reticulum-for-human-beings" title="Permalink to this heading">#</a></h1>
<h1>An Explanation of Reticulum for Human Beings<a class="headerlink" href="#an-explanation-of-reticulum-for-human-beings" title="Link to this heading"></a></h1>
</section>
</article>
@@ -236,16 +272,14 @@
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, Mark Qvist
Copyright &#169; 2025, Mark Qvist
</div>
Generated with <a href="https://www.sphinx-doc.org/">Sphinx</a> and
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
<div class="icons">
</div>
</div>
</div>
@@ -257,14 +291,11 @@
</aside>
</div>
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/sphinx_highlight.js"></script>
<script src="_static/scripts/furo.js"></script>
<script src="_static/clipboard.min.js"></script>
<script src="_static/copybutton.js"></script>
</div><script src="_static/documentation_options.js?v=71272d9f"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
<script src="_static/clipboard.min.js?v=a7894cd8"></script>
<script src="_static/copybutton.js?v=f281be69"></script>
</body>
</html>
+96 -63
View File
@@ -1,23 +1,24 @@
<!doctype html>
<html class="no-js" lang="en">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#" /><link rel="search" title="Search" href="search.html" />
<html class="no-js" lang="en" data-content_root="./">
<head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#"><link rel="search" title="Search" href="search.html">
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/><title>Index - Reticulum Network Stack 0.9.4 beta documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=189ec851f9bb375a2509b67be1f64f0cf212b702" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<link rel="stylesheet" type="text/css" href="_static/custom.css" />
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.0.4 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
--color-code-background: #f2f2f2;
--color-code-foreground: #1e1e1e;
}
@media not print {
@@ -84,7 +85,7 @@
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
@@ -99,34 +100,73 @@
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<symbol id="svg-sun-with-moon" viewBox="0 0 24 24">
<title>Auto light/dark, in light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
class="icon-custom-derived-from-feather-sun-and-tabler-moon">
<path style="opacity: 50%" d="M 5.411 14.504 C 5.471 14.504 5.532 14.504 5.591 14.504 C 3.639 16.319 4.383 19.569 6.931 20.352 C 7.693 20.586 8.512 20.551 9.25 20.252 C 8.023 23.207 4.056 23.725 2.11 21.184 C 0.166 18.642 1.702 14.949 4.874 14.536 C 5.051 14.512 5.231 14.5 5.411 14.5 L 5.411 14.504 Z"/>
<line x1="14.5" y1="3.25" x2="14.5" y2="1.25"/>
<line x1="14.5" y1="15.85" x2="14.5" y2="17.85"/>
<line x1="10.044" y1="5.094" x2="8.63" y2="3.68"/>
<line x1="19" y1="14.05" x2="20.414" y2="15.464"/>
<line x1="8.2" y1="9.55" x2="6.2" y2="9.55"/>
<line x1="20.8" y1="9.55" x2="22.8" y2="9.55"/>
<line x1="10.044" y1="14.006" x2="8.63" y2="15.42"/>
<line x1="19" y1="5.05" x2="20.414" y2="3.636"/>
<circle cx="14.5" cy="9.55" r="3.6"/>
</svg>
</symbol>
<symbol id="svg-moon-with-sun" viewBox="0 0 24 24">
<title>Auto light/dark, in dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
class="icon-custom-derived-from-feather-sun-and-tabler-moon">
<path d="M 8.282 7.007 C 8.385 7.007 8.494 7.007 8.595 7.007 C 5.18 10.184 6.481 15.869 10.942 17.24 C 12.275 17.648 13.706 17.589 15 17.066 C 12.851 22.236 5.91 23.143 2.505 18.696 C -0.897 14.249 1.791 7.786 7.342 7.063 C 7.652 7.021 7.965 7 8.282 7 L 8.282 7.007 Z"/>
<line style="opacity: 50%" x1="18" y1="3.705" x2="18" y2="2.5"/>
<line style="opacity: 50%" x1="18" y1="11.295" x2="18" y2="12.5"/>
<line style="opacity: 50%" x1="15.316" y1="4.816" x2="14.464" y2="3.964"/>
<line style="opacity: 50%" x1="20.711" y1="10.212" x2="21.563" y2="11.063"/>
<line style="opacity: 50%" x1="14.205" y1="7.5" x2="13.001" y2="7.5"/>
<line style="opacity: 50%" x1="21.795" y1="7.5" x2="23" y2="7.5"/>
<line style="opacity: 50%" x1="15.316" y1="10.184" x2="14.464" y2="11.036"/>
<line style="opacity: 50%" x1="20.711" y1="4.789" x2="21.563" y2="3.937"/>
<circle style="opacity: 50%" cx="18" cy="7.5" r="2.169"/>
</svg>
</symbol>
<symbol id="svg-pencil" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-pencil-code">
<path d="M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4" />
<path d="M13.5 6.5l4 4" />
<path d="M20 21l2 -2l-2 -2" />
<path d="M17 17l-2 2l2 2" />
</svg>
</symbol>
<symbol id="svg-eye" viewBox="0 0 24 24">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-eye-code">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path
d="M11.11 17.958c-3.209 -.307 -5.91 -2.293 -8.11 -5.958c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6c-.21 .352 -.427 .688 -.647 1.008" />
<path d="M20 21l2 -2l-2 -2" />
<path d="M17 17l-2 2l2 2" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation" aria-label="Toggle site navigation sidebar">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc" aria-label="Toggle table of contents sidebar">
<label class="overlay sidebar-overlay" for="__navigation"></label>
<label class="overlay toc-overlay" for="__toc"></label>
<a class="skip-to-content muted-link" href="#furo-main-content">Skip to content</a>
@@ -134,38 +174,35 @@
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
<span class="icon"><svg><use href="#svg-menu"></use></svg></span>
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.4 beta documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.4 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<button class="theme-toggle" aria-label="Toggle Light / Dark / Auto color theme">
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
<span class="icon"><svg><use href="#svg-toc"></use></svg></span>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand centered" href="index.html">
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<div class="sidebar-logo-container">
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.4 beta documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.4 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -181,8 +218,8 @@
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
</ul>
<ul>
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a></li>
@@ -206,19 +243,18 @@
</a>
<div class="content-icon-container">
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<button class="theme-toggle" aria-label="Toggle Light / Dark / Auto color theme">
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
<span class="icon"><svg><use href="#svg-toc"></use></svg></span>
</label>
</div>
<article role="main">
<article role="main" id="furo-main-content">
<section class="genindex-section">
<h1 id="index">Index</h1>
@@ -389,6 +425,8 @@
<li><a href="reference.html#RNS.Reticulum.get_instance">get_instance() (RNS.Reticulum static method)</a>
</li>
<li><a href="reference.html#RNS.Link.get_mdu">get_mdu() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.Link.get_mode">get_mode() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.Link.get_mtu">get_mtu() (RNS.Link method)</a>
</li>
@@ -762,16 +800,14 @@
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, Mark Qvist
Copyright &#169; 2025, Mark Qvist
</div>
Generated with <a href="https://www.sphinx-doc.org/">Sphinx</a> and
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
<div class="icons">
</div>
</div>
</div>
@@ -783,14 +819,11 @@
</aside>
</div>
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/_sphinx_javascript_frameworks_compat.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/sphinx_highlight.js"></script>
<script src="_static/scripts/furo.js"></script>
<script src="_static/clipboard.min.js"></script>
<script src="_static/copybutton.js"></script>
</div><script src="_static/documentation_options.js?v=71272d9f"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
<script src="_static/clipboard.min.js?v=a7894cd8"></script>
<script src="_static/copybutton.js?v=f281be69"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More